Web Development 5-8 minutes

Full-Text Search in Laravel 13 with Laravel Scout and Meilisearch: Step-by-Step

Diego Cortés
Diego Cortés
Full Stack Developer & SEO Specialist
Share:
Full-Text Search in Laravel 13 with Laravel Scout and Meilisearch: Step-by-Step
Image generated with AI

A search built on WHERE ... LIKE %text% breaks down as the table grows: no indexes, no relevance ranking, no typo forgiveness. With Laravel Scout and Meilisearch in Laravel 13, the same search answers in under 50 ms with typo tolerance, without paying for a third-party API.

Why LIKE Is No Longer Enough

The %text% Problem: Full Scans and Zero Relevance

When you write WHERE title LIKE '%blender%', the database can't use a regular index: the leading wildcard forces it to scan row by row looking for the string. That works with a few hundred records and degrades badly with thousands. On top of that, the engine returns any match without ranking by relevance, and a query with a single misspelled letter finds nothing at all.

What a Dedicated Search Engine Brings

A search engine like Meilisearch maintains an inverted index built for text queries: it tokenizes content, applies stemming, tolerates typos out of the box, and returns results ranked by relevance in milliseconds. It doesn't replace the database, which remains the source of truth: the index is just an optimized copy for text lookups.

Laravel Scout: The Layer That Unifies Search Engines

How the Searchable Trait Works

Scout is Laravel's official package for full-text search. Its core idea is simple: you add the Laravel\Scout\Searchable trait to an Eloquent model and, from that moment on, every time you save or create a record it is automatically sent to the search index. Deleting a model removes it from the index too, so you never hand-write that synchronization.

Meilisearch vs Typesense vs Algolia vs the Database Driver

Scout works with several engines, and it pays to know which one fits. Meilisearch is open source, self-hosted as a binary or container, and the most direct option to get started. Typesense is also open source and adds vector support if you plan to combine textual and semantic search. Algolia is the reference search-as-a-service for teams that don't want to operate infrastructure. And Scout's database driver, powered by whereLike, covers small projects where a full engine would be overkill.

Installation: Meilisearch, Scout, and the PHP Driver

Running Meilisearch Locally (Binary or Docker)

The first step is starting the engine. You can download the binary for your platform and run it, or spin it up with Docker in a single command. Either way the server listens on port 7700 by default, and if you set a master key at startup you'll need it for every request afterwards.

composer require laravel/scout and Publishing the Config

With the engine running, you install Scout and publish its configuration:

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

The second package is Meilisearch's PHP client, which Scout uses internally to talk to the engine. The vendor:publish command generates config/scout.php, where the active driver and credentials are defined.

Host, Key, and Driver in .env

In the project's .env file you declare the driver and the connection:

SCOUT_DRIVER=meilisearch
SCOUT_MEILISEARCH_HOST=http://127.0.0.1:7700
SCOUT_MEILISEARCH_KEY=masterKey

The key must match the master key you set when starting Meilisearch. With this in place, Scout knows which engine to talk to.

Making a Model Searchable

The Searchable Trait and Automatic Sync

The step that activates the magic is adding the trait to your model. From then on, creating, updating, or deleting a record keeps the index up to date with no extra code. If you don't want a model indexed, you simply don't add the trait.

Customizing the Index with toSearchableArray

By default Scout indexes all visible attributes of the model. With toSearchableArray() you decide exactly what goes into the index, which also lets you include data from relationships:

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

That way a search can find articles by their author's name even though that data lives in another table.

Naming the Index with searchableAs

If the default index name doesn't suit you, the searchableAs() method returns the name you want to use. It's handy when several models share an index or when you need a short name in the Meilisearch dashboard.

Indexing Existing Data: scout:import and scout:flush

The trait syncs new changes, but records that existed before you added search are not in the index. The command php artisan scout:import "App\Models\Post" walks the table and sends everything to the engine. Its companion php artisan scout:flush empties the index, which is useful when you change the toSearchableArray structure and want to repopulate from scratch.

Searching in Your Application

The search() Method and Pagination

Searching is as direct as calling the model's search() method:

$posts = Post::search('blender 5.2')->paginate(10);

The result is a regular Laravel paginator, so it works with Blade and with any pagination setup you already use in the app.

Filters with where() and the filterableAttributes Trap

Scout lets you narrow things down with where(), for example Post::search('tutorial')->where('user_id', 1)->paginate(10). Here's the most repeated gotcha in forums: if the attribute isn't declared in the index's filterableAttributes, the filter fails silently or returns unexpected results, even when the field is mapped in toSearchableArray. You must declare filterable attributes in the index settings for where() to work.

Why Results Don't Look Like LIKE

Meilisearch results are not exact substring matches: the engine normalizes, tolerates typos, and scores by relevance. That means a query with spelling errors still returns great results, but also that the ordering can surprise you if you're coming from LIKE. That's the desired behavior: users find what they're looking for even when they type it wrong.

Production: Queues, Soft Deletes, and Index Maintenance

Background Indexing with Queues

In production you don't want an HTTP request to wait while the record travels to the engine. In config/scout.php you can enable queues so synchronization runs in the background. If you work with Laravel 13 and Horizon, the integration is straightforward, as we covered in our article on queues and jobs in Laravel 13.

Soft Deletes and Index Sync

If your model uses soft deletes, logically deleted records must disappear from the index. By configuring the Meilisearch driver in config/scout.php, Scout handles that removal automatically when delete() is called. Even if a physical deletion fails for any reason, the index stays consistent because the database remains the source of truth.

Conclusion

Swapping LIKE %text% for Laravel Scout and Meilisearch isn't a luxury: it's the jump between a search that degrades and one that responds in milliseconds with typo tolerance and real relevance. If you're coming from Laravel 12, first review the Laravel 13 new features to get the version context. Keep reading the blog for more practical Laravel web development guides.

Categories