meilisearch/search-bundle
A Symfony bundle that integrates Meilisearch into your application, delivering fast, typo-tolerant search and autocomplete with simple configuration, automatic indexing, and customizable searchable entities.
First, the obvious fork in the road: this is a Symfony bundle, not a Laravel package. meilisearch/search-bundle depends on symfony/framework-bundle, symfony/doctrine-bridge and auto-registers a compiler pass — it will not boot inside a Laravel app. If you're shipping Laravel, the equivalent day-to-day stack is laravel/scout + the meilisearch driver (composer require laravel/scout meilisearch/meilisearch-php), and the mental model maps almost 1:1 (searchable model → toSearchableArray() → Meilisearch index). Read this bundle's docs anyway if you're working in a mixed PHP shop, because the concepts — index prefixing, serializer groups, Doctrine event hooks, explicit filterable attributes — transfer directly. If you genuinely have a Laravel app talking to a Symfony service, the integration boundary is the Meilisearch HTTP API, not this bundle.
Assuming a Symfony context:
composer require meilisearch/search-bundle
Flex wires the bundle. Configure it in config/packages/meili_search.yml:
meili_search:
url: '%env(MEILISEARCH_URL)%'
api_key: '%env(MEILISEARCH_API_KEY)%'
prefix: '%env(MEILI_PREFIX)%' # e.g. 'dev_', 'test_'
batch_size: 500
Mark an entity and its identifier:
use MeiliSearch\Bundle\Attribute\Searchable;
use MeiliSearch\Bundle\Attribute\SearchableId;
#[Searchable]
class Article
{
#[SearchableId]
public function getId(): ?int { return $this->id; }
public function getTitle(): string { return $this->title; }
}
Then hydrate the index once and search:
bin/console meilisearch:import
$result = $this->searchService->search(Article::class, 'meilisearch', ['limit' => 10]);
foreach ($result->getHits() as $hit) { /* ... */ }
Where to look first: config/packages/meili_search.yml (the indices block is where class → index mapping lives), MeiliSearch\Bundle\SearchService (search() / SearchResult::getHits()), the meilisearch:* console commands, and the meilisearch-php client it wraps. First real use case: make one read-heavy listing page (catalog, docs, people directory) searchable, with the Doctrine listener doing incremental sync, then a nightly meilisearch:import as a safety net.
Let Doctrine do the sync. The bundle's selling point is the event listeners on postPersist / postUpdate / postRemove. Write your entity normally and the index follows. This is the "seamless" path and it's the right default for CRUD-shaped admin backends.
Control the payload with serializer groups. Don't let the bundle serialize your whole entity — relations explode, $password leaks, and blobs get shipped to Meilisearch. Either expose a narrow surface of getters or use enable_serializer_groups: true with explicit serializer_groups per index, and annotate only the fields you want indexed. Treat the search document as a read model, not your ORM entity.
Bulk import is the pattern for anything large. For fixtures, migrations, backfills, or a 500k-row table, run meilisearch:import with an increased batch_size rather than relying on per-entity listeners. meilisearch:clear before a full rebuild keeps the index consistent if a run fails halfway.
Pass query parameters, don't post-filter in PHP. search() accepts Meilisearch search params directly — filter, facets, sort, attributesToRetrieve, limit, offset. Pushing filtering into Meilisearch is the difference between 20ms and 2s.
Reach for the raw client past the 80% line. Settings (ranking rules, synonyms, stop words), filterable/sortable attribute declaration, task polling, multi-search — inject MeiliSearch\Client and talk to it directly. Don't fight the bundle's abstraction for configuration work it doesn't model.
Prefix per environment. prefix: '%env(MEILI_PREFIX)%' with dev_ / test_ / nothing-in-prod is the standard way to keep a shared Meilisearch instance from cross-contaminating environments. This one config line saves you from a very confusing afternoon.
Testing: in config/packages/test/, point at a throwaway index prefix, disable the Doctrine listeners (or set the prefix and meilisearch:clear in setUp), and waitForTask() after any write you intend to assert on — Meilisearch is async by default and your test will race the indexer.
filter on attributes listed in filterableAttributes. The bundle doesn't infer this from your entity. Symptom: your query returns everything, no exception thrown. Same for sort and sortableAttributes. Declare them explicitly on the client, once per index.postPersist pushes the document before commit; a rollback leaves a phantom document in Meilisearch, and a relationship loaded lazily at that moment may be stale. For anything correctness-sensitive, index after commit (Symfony's kernel.terminate or Messenger) rather than trusting the listener.batch_size up, disable listeners during bulk work, and re-import.id; make sure your #[SearchableId] maps to something immutable.master_key where current ones use api_key; the attribute API replaced the earlier @Searchable annotation / SearchableObjectInterface approach. Pin the version and check the CHANGELOG before copying snippets from a blog post — most search results online are for a two-year-old release.SearchService, the listener classes) rather than find Stack Overflow answers. Budget for that. Also set MEILI_NO_ANALYTICS=true in CI.SearchService decorator for logging/sanitizing queries, a custom index-name resolver if your naming convention differs, and Messenger for async reindexing. Those three cover most of what you'll want to bend.How can I help you explore Laravel packages today?