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 Elasticsearch Driver Laravel Package

lavatech/scout-elasticsearch-driver

A Laravel Scout driver that connects Elasticsearch to your Eloquent models for fast full-text search, filtering, and aggregations. Index and query large datasets through a fluent, Scout-compatible API, making it a solid fit for apps that need scalable, relevant search.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and wire it up

composer require lavatech/scout-elasticsearch-driver
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
php artisan vendor:publish --tag=scout_elasticsearch # verify the tag from the service provider

Set the driver in .env and point at your cluster (check vendor/lavatech/scout-elasticsearch-driver/src first — the fork may rename the config file, the config key, and the Babenkoivan\Scout\... namespace):

SCOUT_DRIVER=elasticsearch
ELASTICSEARCH_HOST=localhost:9200

2. Where to look first

In this order, in the vendor directory, so you know exactly what you're building on:

  • src/ElasticsearchEngine.php — implements Laravel\Scout\Engines\Engine; this is where search(), update(), delete(), and paginate() are translated into ES calls.
  • src/ServiceProvider.php — the Artisan commands, config key, and default index name.
  • The config file — hosts, index prefix, index suffix, queue.

Then read the upstream project (babenkoivan/scout-elasticsearch-driver). If this fork tracks it, the upstream README and issues are your real documentation; treat the fork diff (if any) as the source of truth for deltas.

3. First use case: a searchable Post

php artisan make:index-configurator PostIndexConfigurator
namespace App\IndexConfigurators;

use Lavatech\Scout\Contracts\IndexConfigurator; // verify namespace

class PostIndexConfigurator extends IndexConfigurator
{
    protected $name = 'posts';

    protected $settings = [
        'analysis' => [
            'analyzer' => [
                'post_analyzer' => ['type' => 'custom', 'tokenizer' => 'standard', 'filter' => ['lowercase']],
            ],
        ],
    ];

    protected $defaultMapping = [
        'properties' => [
            'title'   => ['type' => 'text', 'analyzer' => 'post_analyzer',
                          'fields' => ['keyword' => ['type' => 'keyword']]],
            'body'    => ['type' => 'text'],
            'user_id' => ['type' => 'integer'],
            'published_at' => ['type' => 'date'],
        ],
    ];
}
class Post extends Model
{
    use Searchable;

    protected $indexConfigurator = PostIndexConfigurator::class;

    protected $searchRules = [PostSearchRule::class]; // optional

    public function searchableAs(): string
    {
        return $this->indexConfigurator()->name(); // or however the fork exposes it
    }

    public function toSearchableArray(): array
    {
        return [
            'title'        => $this->title,
            'body'         => $this->body,
            'user_id'      => $this->user_id,
            'published_at' => optional($this->published_at)->toIso8601String(),
        ];
    }
}
php artisan elastic:create-index "App\IndexConfigurators\PostIndexConfigurator"
php artisan scout:import "App\Models\Post"
Post::search('laravel elasticsearch')->paginate(15);

If that returns results, the rest of the driver is a matter of shaping queries and mappings.

Implementation Patterns

Index configurators are the contract. One configurator per index, holding $settings (analyzers, shards, replicas) and $defaultMapping. Everything else — model trait, search rules, Artisan commands — keys off this class name. Keep mappings in code, not in a wiki; the configurator is what elastic:create-index and elastic:update-mapping read.

Search rules for reusable, composable queries. Generate one per model:

namespace App\SearchRules;

use Lavatech\Scout\SearchRules\SearchRule;

class PostSearchRule extends SearchRule
{
    public function builder(): array
    {
        return SearchRule::where('published_at', '<=', now()->toIso8601String())
            ->must(SearchRule::whereMatch('title', $this->query, ['operator' => 'and']));
    }
}

Then Post::search($q)->rule(PostSearchRule::class)->get(). For ad-hoc filtering, the fluent helpers (where, whereIn, whereNot, whereFuzzy, wherePhrase, whereRegexp, whereGeoDistance) cover most needs; treat SearchRule subclasses as the extension point when you outgrow them. Aggregations typically require either the driver's aggregation support or a ->raw() escape hatch — verify what the fork ships before assuming.

Zero-downtime reindexing. Never elastic:drop-index against production. Instead: create posts_v2 with the new mapping, scout:import into it, smoke-test, then flip an alias (posts → posts_v2) and drop the old index. Model searchableAs() should return the alias, not the concrete index name, so the flip is invisible to the app.

Always queue imports. SCOUT_QUEUE=true in .env plus 'queue' => true in the driver config. Full imports go through scout:import, model-save indexing goes through the queue. This is the difference between a responsive app and a slow one under load.

Bulk operations bypass the trait. For seeds, backfills, and imports, use Post::withoutSyncingToSearch(fn () => ...) or ->searchable() in explicit chunks. The trait's save hook fires per-model; a 100k-row seed will hammer the cluster one document at a time.

Index only what you query. toSearchableArray() is a projection, not a serialization of the row. Include filterable/sortable fields, exclude large blobs and secrets. Less _source means faster responses and cheaper storage.

Testing. Do not stub the trait; run an Elasticsearch container (Testcontainers or a docker-compose service) in CI, create the index once, import fixtures with withoutSyncingToSearch. Add a thin SearchInterface around Model::search(...) if you want to unit-test business logic without an ES dependency.

Gotchas and Tips

The fork is the risk. 0 stars, 0 dependents, and a low score means you're effectively maintaining this yourself. Pin the version, commit the lockfile, and diff against upstream before every bump. If the fork lags a major elasticsearch/elasticsearch release, you will be pinned to older ES clients while your cluster upgrades — plan a migration path back to upstream or to a maintained alternative (Meilisearch, Typesense, or the official ES/Laravel integrations).

Client/server version skew. The elasticsearch/elasticsearch client, the driver's assumed client API, and your cluster version must be compatible. An ES 8 cluster with a v7 client (or vice versa) produces confusing 400s. Check composer show elasticsearch/elasticsearch against GET / on the cluster.

Filtering on text fields silently fails. ->where('status', 'published') against a text field won't filter as expected. Either map that field as keyword, or add a .keyword subfield and query that. Same for sorting and aggregations.

Nested vs object. Arrays of objects require type: nested in the mapping and whereNested in the query. Plain object mappings flatten and produce nonsensical matching across array elements.

Mapping changes need a reindex, not a restart. elastic:update-mapping only adds fields; changing an existing field's type is rejected. The alias-flip pattern above is the only safe path.

Soft deletes. The default searchable() includes trashed rows unless you override it. Implement shouldBeSearchable() (Scout) or override the sync to remove trashed models explicitly, and decide whether the ES document should be deleted or just flagged.

Pagination is a Laravel paginator over ES hits. ->paginate() works, but deep pagination hits the 10k from/size ceiling. For infinite scroll, switch to search_after via a raw query rather than page 500.

Debug queries, not results. When a search returns nothing, call ->raw() and inspect the generated body, then run it directly:

curl -XPOST 'localhost:9200/posts/_search?pretty' -H 'Content-Type: application/json' -d @query.json

Also check the index actually has documents (/_count) before blaming the query — a failed scout:import is the most common cause of "search is broken."

Fail open, log loud. Wrap search() in a try/catch that falls back to a database LIKE query and logs the ES exception. A search outage should degrade the app, not take down every page that has a search box.

Custom rules are the escape hatch. When the fluent builders can't express what you need (script scores, function scores, complex bool nesting), extend SearchRule, override builder(), and return a raw array body. This is cleaner than string-concatenating JSON in a controller.

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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core