aschaeffer/meilisearch-search-bundle
Install the Bundle
composer require aschaeffer/meilisearch-search-bundle
Enable the bundle in config/bundles.php:
return [
// ...
ArnaudSchaeffer\MeilisearchSearchBundle\MeilisearchSearchBundle::class => ['all' => true],
];
Configure the Bundle
Add MeiliSearch connection details to config/packages/meilisearch_search.yaml:
meilisearch_search:
clients:
default:
host: 'http://localhost:7700'
api_key: 'your-api-key'
First Use Case: Indexing a Doctrine Entity
Annotate your entity with @Meilisearch\Index and define a searchable index:
use ArnaudSchaeffer\MeilisearchSearchBundle\Annotation\Index;
/**
* @Index(indexName="products")
*/
class Product {}
Sync the index with your database:
php bin/console meilisearch:sync
Searching
Inject the MeilisearchSearchClient and perform a search:
use ArnaudSchaeffer\MeilisearchSearchBundle\Client\MeilisearchSearchClient;
class ProductController
{
public function search(MeilisearchSearchClient $client)
{
$results = $client->search('products', 'query');
return $this->json($results);
}
}
Index Management
meilisearch:index:create command to generate indexes for annotated entities.postPersist, postUpdate, postRemove) to sync changes:
// src/EventListener/MeilisearchSyncListener.php
use ArnaudSchaeffer\MeilisearchSearchBundle\EventListener\MeilisearchSyncListener;
class MeilisearchSyncListener extends MeilisearchSyncListener
{
protected function getIndexName(object $entity): string
{
return 'products'; // Custom logic if needed
}
}
Search Integration
# config/services.yaml
services:
App\Service\ProductSearchService:
arguments:
$client: '@meilisearch_search.client.default'
class ProductSearchService
{
public function __construct(private MeilisearchSearchClient $client) {}
public function searchProducts(string $query, int $limit = 10): array
{
return $this->client->search('products', $query, [
'limit' => $limit,
'attributesToRetrieve' => ['id', 'name', 'price'],
]);
}
}
Hybrid Search (Database + MeiliSearch)
// Fallback to DB if MeiliSearch returns no results
$meiliResults = $searchService->searchProducts($query);
if (empty($meiliResults)) {
$products = $entityManager->getRepository(Product::class)
->createQueryBuilder('p')
->where('p.name LIKE :query')
->setParameter('query', "%$query%")
->getQuery()
->getResult();
}
Bulk Operations
MeilisearchIndexManager for bulk indexing:
$manager = $container->get('meilisearch_search.index_manager');
$manager->addDocuments('products', $productsArray);
$manager->updateIndex('products');
MeilisearchSyncListener in services.yaml to auto-sync entities:
services:
ArnaudSchaeffer\MeilisearchSearchBundle\EventListener\MeilisearchSyncListener:
tags:
- { name: 'doctrine.event_listener', event: 'postPersist' }
- { name: 'doctrine.event_listener', event: 'postUpdate' }
- { name: 'doctrine.event_listener', event: 'postRemove' }
use ArnaudSchaeffer\MeilisearchSearchBundle\Mapper\EntityMapperInterface;
class CustomEntityMapper implements EntityMapperInterface
{
public function toArray(object $entity): array
{
return [
'id' => $entity->getId(),
'name' => $entity->getName(),
'custom_field' => $this->mapCustomField($entity),
];
}
}
Register it in services.yaml:
services:
App\Mapper\CustomEntityMapper:
tags:
- { name: 'meilisearch_search.entity_mapper', alias: 'product' }
Index Naming Conflicts
@Index annotations use unique indexName values. Overlapping names will cause sync failures.{entity_class}_index) or validate in a custom MeilisearchSyncListener.Circular References in Entities
Product ↔ Category).#[Groups] from Symfony Serializer or implement JsonSerializable:
use JsonSerializable;
class Product implements JsonSerializable
{
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'name' => $this->name,
// Exclude circular references
];
}
}
Rate Limiting
MeilisearchIndexManager::addDocuments() with chunking:
$manager->addDocuments('products', array_chunk($products, 1000));
$manager->updateIndex('products');
Doctrine Proxy Classes
getId() to trigger loading:
$entity->getId(); // Force proxy initialization
Case Sensitivity in Search
$client->updateIndexSettings('products', [
'searchableAttributes' => ['name', 'description'],
'searchSettings' => [
'typoTolerance' => 'lenient',
],
]);
meilisearch_search.yaml to log requests:
meilisearch_search:
clients:
default:
host: 'http://localhost:7700'
api_key: 'your-api-key'
debug: true # Logs all API calls
php bin/console meilisearch:index:status
docker run -p 7700:7700 -it meilisearch/meilisearch:v0.22.0
Custom Search Parameters
Extend the MeilisearchSearchClient to add domain-specific search logic:
class ExtendedSearchClient extends MeilisearchSearchClient
{
public function advancedSearch(string $index, string $query, array $params = []): array
{
$params['filter'] = $this->buildCustomFilter($query);
return parent::search($index, $query, $params);
}
}
Override the service in services.yaml:
services:
meilisearch_search.client.default:
class: App\Client\ExtendedSearchClient
Event-Driven Indexing Dispatch events before/after index updates:
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class MeilisearchSyncListener
{
public function __construct(private EventDispatcherInterface $dispatcher) {}
public function postPersist(object $entity)
{
$this->dispatcher->dispatch(new MeilisearchIndexEvent($entity, 'persist'));
// Sync logic...
}
}
Async Indexing Use Symfony Messenger to queue index updates:
# config/packages/messenger.yaml
framework:
messenger:
transports:
meilisearch: '%env(MEILISE
How can I help you explore Laravel packages today?