guikingone/meili-search-bundle
A Symfony bundle that integrates Meilisearch into your application, offering configuration, index management, and search services. It helps index Doctrine entities and query results through a simple, framework-friendly API, simplifying search setup and index synchronization.
First, the reality check — still true, with a caveat. guikingone/meili-search-bundle remains a Symfony bundle, not a Laravel package. It ships no service provider, no config/scout.php driver, no facade. The 0.1.0 release doesn't change that: the roadmap calls out Api-Platform support in progress, which is more Symfony-ecosystem surface area, not less. You cannot composer require it into a Laravel app and expect anything to bind. If you're on Laravel, the day-to-day answer is unchanged:
composer require meilisearch/meilisearch-php # direct SDK
# or, more idiomatic:
composer require laravel/scout meilisearch/meilisearch-php
Set SCOUT_DRIVER=meilisearch and MEILISEARCH_HOST in .env, add the Laravel\Scout\Searchable trait to models, then php artisan scout:import "App\Models\Post".
What 0.1.0 changes about the project's status. The release is tagged Initial release, which means the package has been rebooted onto a fresh version line rather than continued from the old one. Treat everything you read about it — including older write-ups and the previous version-pinning advice — as superseded and re-verify against the actual tag. Two consequences that matter immediately:
0.1.x, so the public API is not stable. composer require guikingone/meili-search-bundle:^0.1 and expect breaking changes in 0.2, 0.3, and so on — that's what a 0.x line licenses.Command/, Index/, and Search/ is likely the whole of it; don't assume features from blog posts exist.The bundle is worth reading in three situations: (a) you inherited a Symfony app that already uses it, (b) you're on API Platform and want to track the roadmap item, or (c) you want a reference for how to structure index configuration, document serialization, and event-driven sync — patterns you'll reimplement in Laravel.
Minimal Symfony setup (if you're actually in Symfony):
composer require guikingone/meili-search-bundle:^0.1config/bundles.php (Symfony Flex may do this automatically).config/packages/meili_search.yaml — the config root is typically meili_search and covers hosts, api_key, and an indexes map of class → primary key. Verify the exact tree by opening DependencyInjection/Configuration.php in the vendor directory; at 0.1.0 the schema is whatever that file says, and the README may lag it.bin/console list meili to see the actual command names available in your installed version (create index, import, clear, search, etc.). On an initial release, this list is short and authoritative.getUid() + toArray()), index it, and query from a controller.Where to look first in the source:
| Directory | What it tells you |
|---|---|
DependencyInjection/Configuration.php |
The real config schema — trust this over the README, especially at 0.1.0 |
composer.json |
Which meilisearch/meilisearch-php constraint you're actually getting — this decides server compatibility |
Command/ |
Every CLI entry point and its flags |
Index/ |
Index creation, primary key handling, document saving |
Search/ |
Query building, pagination, filters |
EventSubscriber/ or EventListener/ |
How Doctrine entities get auto-synced |
First use case to prove it works: index a single Post entity, curl http://localhost:7700/indexes to confirm the index exists, then curl a search query. If the curl works but the bundle doesn't, the problem is in the bundle's wiring, not Meilisearch.
The architecture is three layers: client (Meilisearch HTTP), index (write path), search (read path). Every pattern below maps cleanly onto Laravel equivalents.
1. Configuration-as-classes (the bundle's best idea). The bundle lets you describe an index (primary key, searchable attributes, filterable attributes, ranking rules) in a dedicated class rather than scattering it in YAML. Do the same in Laravel with a small value object per index:
final class PostIndexSettings
{
public static function apply(Client $client): void
{
$client->index('posts')->updateSettings([
'searchableAttributes' => ['title', 'body'],
'filterableAttributes' => ['status', 'author_id', 'published_at'],
'sortableAttributes' => ['published_at'],
]);
}
}
Call it from a deploy step or an artisan command — not on every request. At 0.1.x this layer is the one most likely to change shape; keep your settings definitions in your own code, not in bundle-specific classes, so you can swap the driver underneath them.
2. Event-driven sync, with a queue.
The bundle hooks Doctrine lifecycle events so entities are pushed to Meilisearch on persist/update/delete. In Laravel the analogue is model observers or Scout's built-in Searchable hooks:
class PostObserver
{
public function saved(Post $post): void
{
dispatch(new SyncPostToSearch($post))->onQueue('search');
}
public function deleted(Post $post): void
{
dispatch(new RemovePostFromSearch($post->id))->onQueue('search');
}
}
Keep it queued. Meilisearch writes are asynchronous tasks anyway — doing them inline in the request just doubles the latency for no benefit.
3. Explicit document serialization.
The bundle's toArray() contract forces you to decide what actually lands in the index. Copy that discipline into Scout's toSearchableArray(): flatten relations, drop nulls, format dates as ISO-8601 or Unix timestamps consistently, and never include the full model.
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'body' => strip_tags($this->body),
'author_id' => $this->author_id,
'status' => $this->status,
'published_at' => $this->published_at?->timestamp,
];
}
4. Bulk import over per-record writes.
The bundle exposes import commands; use them. php artisan scout:import "App\Models\Post" is orders of magnitude faster than saving 50k models one at a time. Reserve per-record sync for user-generated edits after the initial backfill.
5. Two keys, two clients.
Admin/master key for indexing and settings (server-side only), a scoped search-only key for anything the browser touches. The bundle's config has a single api_key, which is a footgun — don't reproduce that in Laravel. Use MEILISEARCH_KEY (admin) in the backend and generate tenant/search keys via the SDK for the frontend.
6. Health check and settings as code.
Wrap a $client->health() call in a /health route or a scheduled command, and keep index settings in version-controlled classes so a fresh environment can be rebuilt with: create index → apply settings → import → verify.
7. Wrap the dependency, don't spread it.
Because 0.1.0 is a new line with an actively moving roadmap (API Platform support in flight), the correct integration posture is a thin adapter: your own interface for "index this document", "remove this document", "apply settings", with the bundle behind it. When 0.2 reshuffles namespaces or the API Platform work changes the entry points, you edit one class rather than every call site.
0.x instability is now the headline gotcha. 0.1.0 is an initial release on a fresh line, with a roadmap that explicitly says API Platform support is in progress. That combination means: the public API will move, the config schema may move with it, and documentation will lag the code. Pin to ^0.1 in composer.json, commit the lockfile, and treat every minor bump as a breaking-change review rather than a routine update.
Version compatibility is a two-sided problem — check it first. MeiliSearch renamed to Meilisearch in 2021, and the HTTP API changed substantially across the v0.x → v1.x line (route restructuring, the task/update-id model, waitForTask semantics, settings endpoint changes). A rebooted 0.1.0 could pin anything. Check composer.json in the vendor directory before doing anything else and pin your Meilisearch server to a version that matches the bundled SDK. Mixing a 2024 Meilisearch container with an SDK from an older constraint will produce 404s on every call and waste a day.
Writes are asynchronous — search results lag. Meilisearch queues every write as a task. Right after save(), a search may not find the document. Production code must either waitForTask() (blocking, use sparingly) or accept eventual consistency and design UX around it. Never write a test that saves and immediately searches without waiting.
Bulk operations bypass Doctrine events. DQL UPDATE, DB::table()->update(), raw SQL imports, and queue workers running raw queries will not fire the bundle's subscribers (or Laravel observers). Your index drifts silently. Rule: any write path that isn't a model save must have explicit reindexing attached, or run a nightly scout:import reconciliation.
Primary key mismatches. Meilisearch infers the primary key on first insert. If your first document lacks the field, the index is poisoned and every subsequent insert fails. Always declare it explicitly (id) at index creation time, and make sure toSearchableArray() always includes it — even for soft-deleted or partial records.
Nulls and empty arrays in documents. Meilisearch rejects documents with inconsistent field types across records, and null in a filterable attribute is a common source of "invalid document" task failures. Normalize: use '' or omit the field rather than null.
Fresh-install traps are likelier on an initial release. A 0.1.0 that nobody has run in anger yet will have rough edges in exactly the places you least want them: container aliases, default config values, and the order in which services get wired. Budget for reading source rather than docs, and check bin/console debug:container | grep -i meili early to confirm the services you expect actually exist and under what aliases.
Config cache in Symfony. After editing meili_search.yaml, run bin/console cache:clear — otherwise the old compiled container is used and your new index definitions are invisible. In Laravel the equivalent trap is php artisan config:clear after touching config/scout.php.
Debugging is a curl exercise, not a PHP exercise. When the bundle/SDK reports an error, drop to HTTP directly:
curl -s http://localhost:7700/indexes | jq
curl -s http://localhost:7700/tasks?status=failed | jq '.results[].error'
The /tasks endpoint is where Meilisearch actually tells you what went wrong — the PHP layer frequently swallows it behind a generic exception. Task failures from bad settings or schema mismatches are the #1 cause of "indexing silently does nothing."
Extension points if you must stay on Symfony. Decorate or override the bundle's client service alias to swap in a custom HTTP client (timeouts, retries, logging) — find the alias via bin/console debug:container | grep -i meili. The search service is typically decoratable too, which is how you'd add multi-tenant filtering or query logging the bundle doesn't provide. On a 0.x release, prefer decoration over forking: you keep the upgrade path open when the next version lands.
Migration advice. For anything greenfield on Laravel: use Scout with the Meilisearch driver — it's maintained, tested against current Meilisearch, and gives you searchableAs(), shouldBeSearchable(), and engine-level settings out of the box. Borrow this bundle's concepts (index config classes, explicit document contracts, event-driven sync), not its code. If you're on Symfony or API Platform, the 0.1.0 reboot makes it defensible to evaluate again — but evaluate behind an adapter, with the lockfile pinned, and with the expectation that the API Platform work will churn the surface. If you're maintaining an existing Symfony app that already depends on it, isolate Meilisearch behind your own thin service interface so a future swap to the official SDK is a single-class change rather than a bundle-wide rewrite.
How can I help you explore Laravel packages today?