Laravel Magazine

Add Full-Text Search to Your Laravel App with Scout and Meilisearch

Eric Van Johnson · Tutorials
Add Full-Text Search to Your Laravel App with Scout and Meilisearch

Database LIKE queries will carry you a long way, but eventually users notice they have to spell things exactly right, cannot search across multiple fields at once, and get no results ranking to help them find what they are looking for. That is when it is time for proper full-text search.

Laravel Scout makes the integration surprisingly painless, and Meilisearch is the best open source engine to pair with it for most Laravel apps. This tutorial walks from zero to a working search with filters, relevance, and typo tolerance.

What You Are Building

By the end of this tutorial you will have:

  • Eloquent models that are automatically indexed when created or updated
  • A search endpoint that returns ranked, typo-tolerant results
  • Filters to narrow results by category and status
  • A working local development setup with Laravel Sail

Installing Scout and Meilisearch

composer require laravel/scout
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
composer require meilisearch/meilisearch-php http-interop/http-factory-guzzle

Add the Meilisearch service to your docker-compose.yml (or sail services if you use Sail):

meilisearch:
    image: 'getmeili/meilisearch:latest'
    ports:
        - '${FORWARD_MEILISEARCH_PORT:-7700}:7700'
    volumes:
        - 'sail-meilisearch:/meili_data'
    healthcheck:
        test: ["CMD", "wget", "--no-verbose", "--spider", "http://localhost:7700/health"]

Update your .env:

SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://localhost:7700
MEILISEARCH_KEY=  # Leave blank for local dev

Making a Model Searchable

Add the Searchable trait to any Eloquent model you want to index:

use Laravel\Scout\Searchable;

class Article extends Model
{
    use Searchable;

    public function toSearchableArray(): array
    {
        return [
            'id'         => $this->id,
            'title'      => $this->title,
            'body'       => $this->body,
            'excerpt'    => $this->excerpt,
            'author'     => $this->author->name,
            'category'   => $this->category,
            'status'     => $this->status,
            'created_at' => $this->created_at->timestamp,
        ];
    }
}

toSearchableArray() controls what gets indexed. Keep it lean — you do not need every column, just the fields users search on and the filters you will use. Sending fewer fields keeps your index smaller and searches faster.

Import existing records into the index:

php artisan scout:import "App\Models\Article"

From this point on, Scout automatically keeps the index in sync when models are created, updated, or deleted.

Searching

The basic search call:

$results = Article::search('laravel queues')->get();

That is it. Meilisearch handles the heavy lifting: typo tolerance, multi-field search, and relevance ranking are all on by default.

For paginated results (which you almost always want in a web interface):

$results = Article::search($query)->paginate(15);

The returned collection is a standard Laravel paginator, so it plugs directly into your existing Blade pagination or JSON API responses.

Adding Filters

Meilisearch separates search (full-text) from filtering (structured conditions). To filter on a field, you first need to declare it as a filterable attribute. Do this in your AppServiceProvider::boot():

use Laravel\Scout\EngineManager;
use Meilisearch\Client;

app(EngineManager::class)->engine()->index('articles')->updateFilterableAttributes([
    'category',
    'status',
    'created_at',
]);

Then apply filters in your search query using Scout's where():

$results = Article::search('eloquent')
    ->where('status', 'published')
    ->where('category', 'tutorials')
    ->get();

For range filters on numeric fields like timestamps:

$results = Article::search('laravel')
    ->whereIn('category', ['tutorials', 'tips'])
    ->get();

Configuring Relevance and Sortable Attributes

Meilisearch lets you define which attributes matter more for relevance ranking. You can also define sortable attributes for explicit ordering (newest first, etc.):

app(EngineManager::class)->engine()
    ->index('articles')
    ->updateRankingRules([
        'words',
        'typo',
        'proximity',
        'attribute',
        'sort',
        'exactness',
    ])
    ->updateSortableAttributes(['created_at']);

Then use orderBy() in your query:

$results = Article::search('php')
    ->orderBy('created_at', 'desc')
    ->get();

Scout and Queues

By default, Scout syncs models to the index synchronously, which adds latency to every model save. In production, enable queued indexing:

SCOUT_QUEUE=true

With this set, index updates are dispatched as background jobs. Saves are fast, the index update happens asynchronously, and you get the normal queue retry behavior for any indexing failures.

Deploying Meilisearch

For production, you have a few options:

Self-hosted on a VPS or your own infrastructure. Meilisearch is a single binary with minimal requirements. For most apps under a few hundred thousand records, a $10-20/month VPS is plenty.

Meilisearch Cloud (their managed service) if you do not want to manage the infrastructure yourself.

Algolia is Scout's other first-class driver if you prefer a fully managed SaaS with a generous free tier. The Scout API is identical — swap the driver in .env and you are done.

Typo tolerance, instant results, and relevance ranking out of the box — once you have added Scout to an app it is hard to imagine going back to LIKE '%search%' queries.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.