Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Scout Laravel Package

laravel/scout

Laravel Scout adds driver-based full-text search to Eloquent models, automatically syncing model changes to indexes. Supports Algolia, Meilisearch, and Typesense.

View on GitHub
Deep Wiki
Context7

Getting Started

Scout is a thin, driver-based abstraction over an external search engine. You install it, tell it which engine to use, and then treat Model::search() as an Eloquent-like builder. The package itself does not store an index — it syncs your models to Algolia, Meilisearch, Typesense, the database, or an in-memory collection.

Minimal path to first search:

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

Set the driver in .env:

SCOUT_DRIVER=collection   # or database, meilisearch, typesense, algolia

Then opt a model in:

use Laravel\Scout\Searchable;

class Post extends Model
{
    use Searchable;

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

Backfill existing rows and query:

php artisan scout:import "App\Models\Post"
$posts = Post::search('laravel scout')
    ->where('author', 'Taylor')
    ->orderBy('published_at', 'desc')
    ->paginate(20);

Where to look first: config/scout.php (driver, queue, chunk sizes, prefix), the Searchable trait, Laravel\Scout\Builder (the query surface), and src/Engines/* when an engine behaves differently than you expect. The README is intentionally thin; the real reference is the Laravel docs plus the changelog, because engine-specific fixes land frequently.

Start with collection in local/testing and one real engine in staging. Use the database engine only if you need zero extra infrastructure and can live without typo tolerance, facets, and relevance tuning — it relies on MySQL/PostgreSQL full-text indexes and gives you the least search-like behaviour.

Implementation Patterns

Treat toSearchableArray() as a denormalized view, not a serialization of the model. Index flat scalars: IDs, titles, slugs, display names, timestamps as integers, booleans as booleans. Relationships are the main source of N+1 pain — either eager-load them or compute values in a dedicated transformer/accessor that does not lazy-load.

Control what gets indexed. shouldBeSearchable() is the gate for drafts, unpublished, disabled, or tenant-scoped records. searchableAs() controls the index/collection name — use it for per-tenant indices or environment prefixes rather than sharing one index across apps.

Querying looks like Eloquent but is not Eloquent. where(), whereIn(), whereNotIn(), orderBy(), take(), paginate() are translated into engine filters/sorts. Anything the engine cannot express is not available; there is no whereHas, no joins, no LIKE. Design your index so every filter and sort field is present in toSearchableArray().

Use the callback for engine-specific features (facets, highlights, geo, custom ranking):

Post::search('scout', function ($engine, string $query, array $options) {
    $options['highlightPreTag'] = '<mark>';
    return $engine->search($query, $options);
})->raw();

->raw() returns the engine response untouched — use it for facets, highlight snippets, and relevance scores. ->get() re-queries the database and hydrates models, preserving engine order.

Keep the sync pipeline deliberate. Enable SCOUT_QUEUE=true for production writes, and confirm after_commit behavior in config/scout.php so jobs are not dispatched before the transaction commits. For request-critical updates (e.g. the user just edited a record and immediately searches for it), use searchableSync() / unsearchableSync() (v10.14.1+) to index inline instead of queueing. For bulk seeding or imports, wrap in Model::withoutSyncingToSearch(fn () => ...) so you do not enqueue thousands of redundant jobs.

Backfill with intent. scout:import uses chunkById; add --chunk=1000 for large tables, and implement makeAllSearchableUsing($query) to eager-load the relations your toSearchableArray() touches. This is the single biggest lever on import time and memory.

Test against the collection driver. Set SCOUT_DRIVER=collection in phpunit.xml; it behaves like a real engine for search, where, orderBy, and paginate without external services. Alternatively, Scout::withoutSyncingToSearch() around factories prevents index churn.

Wrap custom engines behind Scout::extend() when you need an internal search service or a shim around an unsupported engine. Bind a custom MakeSearchable / RemoveFromSearch job if you need different queue names, retries, or backoff — the jobs are overridable via the container.

Aggregate multi-model search yourself. Scout searches one index at a time; there is no native cross-index query. For a global search box, query each model and merge results in PHP, or use ->raw() to call the engine directly against multiple indices.

Gotchas and Tips

  • Search results are fresh DB reads, not index payloads. Scout takes keys from the engine and re-queries your database. Rows deleted between indexing and search silently disappear, and the model you get back may differ from what was indexed. This is usually good, but it means toSearchableArray() is not a source of truth.
  • Filters require engine configuration. Meilisearch needs filterableAttributes / sortableAttributes; Algolia needs attributesForFaceting; Typesense needs the fields in its schema. Until configured, where() and orderBy() fail or silently return nothing — this is the most common "Scout is broken" report. Sync index settings as part of deployment.
  • paginate() plus custom callbacks has historically produced wrong totals. Several engines have had fixes here (Typesense pagination fixes in 10.11.3/10.11.4/10.14.2). If your total count looks wrong, call ->raw() and compare against the engine's reported count before blaming your query.
  • Empty queries can return everything. Model::search('') is engine-dependent; guard the entry point with filled($request->q) and fall back to a normal Eloquent listing.
  • Queued sync plus transactions is a classic stale-index bug. Events fire before commit unless after_commit is enabled; enable it. The flip side is that a queued RemoveFromSearch may run after the model is already gone — Scout handles it, but your custom engine might not.
  • Soft deletes need a decision. Scout removes documents when the model is deleted (including soft deletes). If trashed records must remain searchable for admin views, express that via shouldBeSearchable() and verify restore behavior — restoring a model does not always re-trigger indexing; a re-save or scout:import may be needed.
  • Custom keys are supported but easy to get wrong. Override getScoutKey() and getScoutKeyName(); ensure the key is a stable string. UUID models should be explicit about key handling, and queued removal jobs must carry the same key.
  • Index naming collisions across environments are a recurring footgun. Use searchableAs() with an app/env prefix, or the config-level prefix, so staging never writes into production's index.
  • Debugging order: Model::search(...)->raw() to see exactly what the engine returned, then ->keys() to see the IDs Scout will hydrate, then the query log to see what SQL re-query ran. If the engine returns results but your page is empty, it is almost always the DB re-query (soft deletes, global scopes, or a tenant constraint that excludes those IDs).
  • Extension points to know: Scout::extend(), Scout::engine(), makeAllSearchableUsing(), searchableAs(), shouldBeSearchable(), the Laravel\Scout\Jobs\MakeSearchable / RemoveFromSearch bindings, and afterRawSearch (v10.13) for post-processing engine responses.
  • Track the changelog per engine. Scout's releases are frequently engine-specific fixes (Algolia client v4 support, Typesense boolean filters, Meilisearch null filtering, whereIn accepting Arrayable, orderByDesc, newScoutQuery() for the database engine). Pin a known-good minor and read the notes before upgrading.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections