Files
e-ticket/src/Service/MeilisearchService.php
Serreau Jovann e4edc76f58 Add Redis cache for Meilisearch search results and admin dashboard stats
- Configure Redis DB 2 as Symfony cache adapter
- Cache Meilisearch search results for 5 minutes (invalidated on writes)
- Cache admin dashboard stats for 10 minutes
- Add invalidateSearchCache() called after each Meilisearch write
- Update tests to support cache mock injection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:24:35 +01:00

183 lines
5.2 KiB
PHP

<?php
namespace App\Service;
use App\Message\MeilisearchMessage;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class MeilisearchService
{
public function __construct(
private HttpClientInterface $httpClient,
private MessageBusInterface $bus,
#[Autowire(env: 'MEILISEARCH_URL')] private string $url,
#[Autowire(env: 'MEILISEARCH_API_KEY')] private string $apiKey,
#[Autowire(service: 'meilisearch.cache')] private CacheInterface $cache,
) {
}
public function indexExists(string $index): bool
{
try {
$response = $this->httpClient->request('GET', $this->url."/indexes/{$index}", [
'headers' => ['Authorization' => "Bearer {$this->apiKey}"],
]);
return 200 === $response->getStatusCode();
} catch (\Throwable) {
return false;
}
}
public function createIndexIfNotExists(string $index, string $primaryKey = 'id'): void
{
if (!$this->indexExists($index)) {
$this->createIndex($index, $primaryKey);
}
}
public function createIndex(string $index, string $primaryKey = 'id'): void
{
$this->bus->dispatch(new MeilisearchMessage('createIndex', $index, ['primaryKey' => $primaryKey]));
}
public function deleteIndex(string $index): void
{
$this->bus->dispatch(new MeilisearchMessage('deleteIndex', $index));
}
/**
* @param list<array<string, mixed>> $documents
*/
public function addDocuments(string $index, array $documents): void
{
$this->bus->dispatch(new MeilisearchMessage('addDocuments', $index, ['documents' => $documents]));
}
/**
* @param list<array<string, mixed>> $documents
*/
public function updateDocuments(string $index, array $documents): void
{
$this->bus->dispatch(new MeilisearchMessage('updateDocuments', $index, ['documents' => $documents]));
}
public function deleteDocument(string $index, string|int $documentId): void
{
$this->bus->dispatch(new MeilisearchMessage('deleteDocument', $index, ['documentId' => $documentId]));
}
/**
* @param list<string|int> $ids
*/
public function deleteDocuments(string $index, array $ids): void
{
$this->bus->dispatch(new MeilisearchMessage('deleteDocuments', $index, ['ids' => $ids]));
}
/**
* @param array<string, mixed> $settings
*/
public function updateSettings(string $index, array $settings): void
{
$this->bus->dispatch(new MeilisearchMessage('updateSettings', $index, ['settings' => $settings]));
}
/**
* @param array<string, mixed> $options
*
* @return array<string, mixed>
*/
public function search(string $index, string $query, array $options = []): array
{
$cacheKey = 'ms_search_'.md5($index.$query.serialize($options));
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($index, $query, $options) {
$item->expiresAfter(300);
return $this->request('POST', "/indexes/{$index}/search", array_merge([
'q' => $query,
], $options));
});
}
public function invalidateSearchCache(): void
{
$this->cache->clear();
}
/**
* @return array<string, mixed>
*/
public function getDocument(string $index, string|int $documentId): array
{
return $this->request('GET', "/indexes/{$index}/documents/{$documentId}");
}
/**
* @return list<int|string>
*/
public function getAllDocumentIds(string $index): array
{
$ids = [];
$offset = 0;
$limit = 1000;
do {
$response = $this->request('GET', "/indexes/{$index}/documents?offset={$offset}&limit={$limit}&fields=id");
$results = $response['results'] ?? [];
foreach ($results as $doc) {
$ids[] = $doc['id'];
}
$offset += $limit;
} while (\count($results) === $limit);
return $ids;
}
/**
* @return list<string>
*/
public function listIndexes(): array
{
$response = $this->request('GET', '/indexes?limit=1000');
$indexes = [];
foreach ($response['results'] ?? [] as $idx) {
$indexes[] = $idx['uid'];
}
return $indexes;
}
/**
* @param array<string, mixed>|null $body
*
* @return array<string, mixed>
*/
public function request(string $method, string $path, ?array $body = null): array
{
$options = [
'headers' => [
'Authorization' => "Bearer {$this->apiKey}",
'Content-Type' => 'application/json',
],
];
if (null !== $body && 'GET' !== $method && 'DELETE' !== $method) {
$options['json'] = $body;
}
$response = $this->httpClient->request($method, $this->url.$path, $options);
if (204 === $response->getStatusCode()) {
return [];
}
return $response->toArray(false);
}
}