- 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>
183 lines
5.2 KiB
PHP
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);
|
|
}
|
|
}
|