Search
Setup
- Laravel Scout with Meilisearch driver.
- In
.env:SCOUT_DRIVER=meilisearch,MEILISEARCH_HOST,MEILISEARCH_KEY(optional). - Queue and transactions (recommended for production):
SCOUT_QUEUE=true— Indexing runs in the queue so requests are not blocked. Ensure Horizon or queue workers are running. UseSCOUT_QUEUE=falsefor local sync (indexing runs in the same request).SCOUT_AFTER_COMMIT=true— Index sync runs only after the database transaction commits, so rolled-back data is never indexed.
- Mark searchable models with the Scout
Searchabletrait and implementtoSearchableArray()(and optionallyshouldBeSearchable()).
Nav search (top bar)
The Search domain (app/Domains/Search/) exposes a JSON endpoint consumed by the NavSearch component in the public header. Results include pages, blog posts, FAQ (question snippet + link to /faq), and testimonials (quote snippet + link to /testimonials). Each result set is gated by its Pennant feature flag: page, blog, faq, and testimonials. When a feature is inactive, that type is omitted from the response and from the nav search UI. Search uses Scout (Meilisearch or fallback) per model; see PageSearch, BlogSearch, FaqSearch, and TestimonialSearch in their respective domains.
Adding a searchable model
- Add an entry to
config/scout.phpundermeilisearch.index-settingswith key = full model class name (e.g.\App\Domains\Blog\Models\BlogPost::class). Value is an array of Meilisearch options (e.g.filterableAttributes). - Implement
toSearchableArray()on the model. ImplementshouldBeSearchable()if only some records should be searchable (e.g. active pages, published posts). - Run
php artisan scout:sync-index-settingsafter changing filterable attributes so Meilisearch gets the new settings. - Run
php artisan scout:import "App\Domains\Blog\Models\BlogPost"(or flush then import) after schema or searchable-data changes.
Flush and re-import after changing searchable fields to avoid stale index state.
Keeping the index in sync
- Normal create/update/delete/restore/force delete: Scout’s model observer syncs the index automatically (queued or sync depending on
SCOUT_QUEUE). - Bulk updates:
Model::where(...)->update([...])does not fire model events. After bulk updates that affect searchable state (e.g. deactivating pages), runphp artisan scout:import "ModelClass"for each affected model, or usescout:sync-allif implemented.
Migrations
- After schema or data migrations that affect searchable data or filterable attributes:
- Run
scout:sync-index-settingsif you changedfilterableAttributes(or other index settings) inconfig/scout.php. - Run
scout:import "ModelClass"for each searchable model whose data or schema changed.
- Run
- Deployment order: (1) Run migrations, (2) run
scout:sync-index-settingsif index settings changed, (3) runscout:importfor affected models (orscout:sync-all). - Data migrations / backfills (e.g.
DB::table()->update(), raw SQL, orModel::withoutEvents()): No model events fire. Runscout:importfor the affected model(s) after the migration or backfill. - Edge cases:
- New column in toSearchableArray(): Add to config if filterable, run
scout:sync-index-settings, thenscout:import. - Column removed/renamed: Update model and config, run
scout:sync-index-settings; thenscout:flushandscout:importfor a clean index. - Zero-downtime backfill: Run
scout:importafter the backfill completes. - Migration rollback: After rollback, run
scout:flushfor the model and thenscout:importto repopulate from the current schema, or accept a temporary mismatch until the next full import.
- New column in toSearchableArray(): Add to config if filterable, run
Seeding
| Seeder writes with | Model events | Scout syncs automatically? | Action after seed |
|---|---|---|---|
create() / save() | Yes | Yes (sync if queue off) | If queue on: run workers or scout:import |
insert() / upsert() / raw | No | No | Run scout:import (or scout:sync-all) |
migrate:fresh --seed: If seeders use onlycreate()/save()andSCOUT_QUEUE=false, the index is in sync after the command. If seeders useinsert()/upsert()or you use queued indexing, runscout:import(orscout:sync-all) after the command.- New developer / fresh clone: After
migrate --seedormigrate:fresh --seed, runscout:importfor Page, BlogPost, Faq, and Testimonial (orscout:sync-all) unless seeders use onlycreate()/save()withSCOUT_QUEUE=false. - CI: Use
SCOUT_DRIVER=collectionin tests so no Meilisearch is required and no post-seed import is needed; or runscout:importafter seeding when using Meilisearch.
Commands
scout:import "ModelClass"— Import all searchable records for the model into the index (e.g. after schema changes, bulk updates, or seeding with insert/upsert).scout:flush "ModelClass"— Remove all records for the model from the index. Follow withscout:importto rebuild.scout:sync-index-settings— Push index settings fromconfig/scout.php(e.g.filterableAttributes) to Meilisearch. Run after changing filterable or other index settings.scout:sync-all(optional, custom) — Runscout:importfor all configured searchable models in one command.
Queue and transactions
- SCOUT_QUEUE: When
true, indexing is queued (recommended in production so requests are fast). Whenfalse, indexing runs synchronously (useful for local dev or tests). - SCOUT_AFTER_COMMIT: When
true, indexing runs only after the DB transaction commits, so data from rolled-back transactions is never indexed. Recommended for production.