Laravel Scout is Laravel's first party package for full text search. It keeps your Eloquent models in sync with a dedicated search engine and gives you one fluent API, Post::search('...'), whichever engine sits behind it. Meilisearch is the fastest driver to get running. Typesense is the better fit when you need typed schemas and heavy faceting.
Most Laravel search bugs are not search bugs. They are indexing bugs.
You have probably shipped the LIKE query at some point. It behaves on a seeded table, crawls once the data is real, and never handles typos, stemming, or relevance ranking. This guide walks the whole path, installing Laravel Scout, wiring Meilisearch and Typesense, shaping what actually gets indexed, filtering and sorting without silent empty results, and keeping the index honest in production. Every snippet is standard Laravel that runs on Laravel 9 through 12.
Key Takeaways
What Laravel Scout actually does
Scout is a driver based abstraction. Add the Laravel\Scout\Searchable trait to a model and Scout registers an observer that pushes every create, update, and delete to the configured engine. Your controllers keep calling the same search() method no matter what runs underneath.
That abstraction is the real value here. Moving from Meilisearch to Typesense becomes a config change plus a reindex instead of a rewrite of every search endpoint.
Three moving parts explain nearly every problem you will hit.
The index is a copy of your data shaped for search, stored outside your database.
The searchable array decides what gets copied, and you control it with
toSearchableArray().The sync is the observer, or queued job, that keeps the copy current.
Keep the official Laravel Scout documentation open alongside this guide. Driver support shifts between major versions, and the docs are the source of truth for the current API.
The Scout drivers and when to use each
Driver | Best for | Trade off |
|---|---|---|
| Fast setup, typo tolerance, self hosting | Filterable and sortable fields must be declared and synced |
| Faceting, typed data, catalog search | You define the collection schema before the first import |
| Zero operations, managed scaling | Proprietary, and pricing scales with records and operations |
| Small datasets with no extra service to run | Runs on SQL, so relevance tuning is limited |
| Local development and test suites | Filters in PHP, so it is not a production option |
Start with the database or collection driver if you are undecided. Swapping to Meilisearch later costs one env change and one import command. Curious how the whole feature looks when it is generated for you? Try StackMivo Now.
How to set up Laravel Scout with Meilisearch
Install Scout, publish its config, and pull in the Meilisearch client.
composer require laravel/scout
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
composer require meilisearch/meilisearch-php http-interop/http-factory-guzzlePoint your environment at a running Meilisearch instance. A local binary or a Docker container is plenty for development.
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=masterKey
SCOUT_QUEUE=trueNow make the model searchable. The trait alone works, but toSearchableArray() is where the real decisions happen. Send the fields users search on and filter by, not the entire row.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Post extends Model
{
use Searchable;
// Only what search needs. Sending the whole row bloats the index
// and leaks fields you never meant to expose.
public function toSearchableArray(): array
{
return [
'id' => (string) $this->id,
'title' => $this->title,
'excerpt' => $this->excerpt,
'author_name' => $this->author->name,
'status' => $this->status,
'published_at' => $this->published_at?->timestamp,
];
}
}Index the records you already have, then search them.
php artisan scout:import "App\Models\Post"$posts = Post::search('laravel queue')->paginate(15);Scaffolding the model at the same time? The php artisan make:model cheatsheet covers the flags that save the most typing.
Declaring filterable and sortable attributes
Most tutorials skip this step, and it is exactly why where() quietly returns an empty collection. Meilisearch refuses to filter or sort on a field until you declare it. Scout keeps those declarations in config/scout.php.
'meilisearch' => [
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
'key' => env('MEILISEARCH_KEY'),
'index-settings' => [
Post::class => [
'filterableAttributes' => ['status', 'author_id'],
'sortableAttributes' => ['published_at'],
'searchableAttributes' => ['title', 'excerpt', 'author_name'],
],
],
],Push those settings to the engine with one command. Run it in your deploy pipeline whenever the settings change, or the engine keeps the old rules.
php artisan scout:sync-index-settingsAfter that, filters and ordering behave the way you expect them to.
Post::search('laravel queue')
->where('status', 'published')
->orderBy('published_at', 'desc')
->paginate(15);Order matters inside searchableAttributes. Meilisearch weights earlier attributes more heavily, so a title match outranks an excerpt match without any custom tuning. The Meilisearch documentation goes deeper on ranking rules when the defaults stop being enough.
How to set up Typesense as your Scout driver
Typesense takes a stricter line. You declare the collection schema, and it enforces types on every document you send. More work up front, fewer surprises later.
composer require typesense/typesense-phpSCOUT_DRIVER=typesense
TYPESENSE_API_KEY=your-api-key
TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=httpThe schema lives under model-settings in config/scout.php. Any field you do not declare is not searchable, filterable, or sortable.
'typesense' => [
'model-settings' => [
Post::class => [
'collection-schema' => [
'fields' => [
['name' => 'id', 'type' => 'string'],
['name' => 'title', 'type' => 'string'],
['name' => 'excerpt', 'type' => 'string'],
['name' => 'status', 'type' => 'string', 'facet' => true],
['name' => 'published_at', 'type' => 'int64'],
],
'default_sorting_field' => 'published_at',
],
'search-parameters' => [
'query_by' => 'title,excerpt',
],
],
],
],The two Typesense gotchas that break imports
First, the document id must be a string. Eloquent hands you an integer, so cast it inside toSearchableArray() or the import fails on the very first record.
Second, dates must be integers. Typesense has no native date type, so store timestamps as int64 and format them in Blade when you render the results.
public function toSearchableArray(): array
{
return array_merge($this->toArray(), [
'id' => (string) $this->id,
'published_at' => $this->published_at?->timestamp,
]);
}Set query_by deliberately. It controls which fields get searched and in what priority order, and leaving it too wide is the usual reason results feel noisy. The Typesense documentation lists every parameter you can set under search-parameters.
Meilisearch vs Typesense for Laravel search
Both engines are open source, both self host, both offer a managed cloud, and both ship typo tolerance, synonyms, and faceting in current releases. The practical difference is how much structure each one asks for before it will index anything.
Question | Meilisearch | Typesense |
|---|---|---|
Time to first search | Minutes | Longer, schema comes first |
Schema | Schemaless with index settings | Explicit typed fields |
Filtering setup | Declare filterableAttributes, then sync | Declare the field in the collection schema |
Field priority | searchableAttributes order | query_by order |
Date handling | Timestamps or strings | int64 timestamps only |
Strong fit | Blogs, docs, dashboards, MVPs | Catalogs, marketplaces, faceted search |
The short verdict. Pick Meilisearch when you want Laravel search working this afternoon and the data is mostly text. Pick Typesense when the data is a catalog with many facets and you want the engine to reject bad documents instead of quietly indexing them. Faceted product search on a multi vendor marketplace is where that schema discipline pays for itself.
When plain Laravel full text search is enough
Adding a search engine adds a service to run, monitor, back up, and keep in sync. Sometimes your database already does the job. Laravel supports native full text indexes on MySQL and PostgreSQL with no extra infrastructure at all.
Schema::table('posts', function (Blueprint $table) {
$table->fullText(['title', 'body']);
});Post::whereFullText(['title', 'body'], 'laravel queue')->paginate(15);The full text where clauses in the query builder handle relevance reasonably on modest datasets. What you give up is typo tolerance, instant search latency, faceting, and fine grained ranking control.
A two person team at a B2B SaaS hit this from the other direction. They stood up a search service for a 12,000 row internal products table, burned a sprint on Docker and deploy plumbing, then found out the actual complaint was that admins could not filter by supplier. A full text index and a filter dropdown fixed it in an afternoon. They moved to Meilisearch a year later, when the public catalog launched and typo tolerance genuinely started to matter.
A simple rule of thumb works well. Reach for Scout when users expect typo tolerance and instant results, when you need faceted filters, or when the searchable table is large and still growing. Scout's own database driver sits in the middle if you want the Scout API without the extra service.
Keeping the Laravel Scout index in sync in production
Indexing correctly is a launch problem. Staying indexed is a production problem, and that is where most teams get burned.
Queue every index update. With SCOUT_QUEUE=false, saving a model fires an HTTP call to your search host inside the request. One agency team discovered this when their search host had a slow evening and every admin save in a client CMS began timing out. Flipping SCOUT_QUEUE=true and running a worker moved that write off the request cycle, and the timeouts stopped the same day.
Index only what should be public. The shouldBeSearchable() method keeps drafts and archived rows out of the index without extra conditionals scattered through your controllers.
public function shouldBeSearchable(): bool
{
return $this->status === 'published';
}Eager load during imports. If toSearchableArray() touches a relationship, a bulk import fires one extra query per record. Three lines fix it.
// use Illuminate\Database\Eloquent\Collection;
public function makeSearchableUsing(Collection $models): Collection
{
return $models->load('author');
}Silence the observer for bulk work. Seeders, backfills, and data migrations should not trigger thousands of individual index writes.
Post::withoutSyncingToSearch(function () {
Post::factory()->count(5000)->create();
});Test against the collection driver. Set SCOUT_DRIVER=collection in phpunit.xml so your Pest suite runs without a search engine on the machine. Feature tests then cover the query path instead of the network.
Plan the reindex too. Running scout:import over a live index leaves a window where results are incomplete, so run it off peak or import into a fresh index name and switch searchableAs() once it finishes. Wiring all of this by hand, the trait, the searchable array, the config, the queue setup, and the tests, is the kind of Laravel boilerplate that AI code generation handles well.
Where to go from here
Laravel Scout earns its place the moment your users expect search to forgive a typo and answer instantly. Install it, shape toSearchableArray() with intent, declare your filterable and sortable fields, and queue the sync before you launch. Choose Meilisearch for speed of setup, Typesense for typed catalog data, and your database when the dataset is small enough that an extra service would be the bigger risk.
Generic assistants suggest plausible PHP and still miss the Laravel specifics, that Meilisearch needs its filterable attributes synced, that Typesense wants a string id, that the observer belongs behind a queue. StackMivo's Laravel-native intelligence is built on the Laravel ecosystem, so it produces production-ready Laravel apps with models, migrations, controllers, CRUD, auth, and tests that follow the conventions your team already reviews for. Describe the search feature, review the output, and keep what you ship. Get started free and take the search work off your backlog this week.
Build your Laravel app with AI
Skip the boilerplate. StackMivo turns a plain-English prompt into production-ready Laravel apps, with models, migrations, controllers, CRUD, auth, and tests.
Get started free