baijunyao/laravel-scout-elasticsearch
A Laravel Scout driver that integrates Elasticsearch as the search engine for Eloquent models. Configure indices, index and search records through Scout, and use Elasticsearch full-text capabilities in Laravel applications.
Prerequisites (do these before touching the package):
elasticsearch/elasticsearch PHP client, so pick an ES major version that matches the client constraint in the package's composer.json (7.x is the safest bet for most versions of this package).ik_max_word / ik_smart, and index creation will fail without it:
bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.x.x/elasticsearch-analysis-ik-7.x.x.zip
curl -s localhost:9200/_cat/plugins and curl -s localhost:9200/_cat/indices?v.Install:
composer require laravel/scout baijunyao/laravel-scout-elasticsearch
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider" # config/scout.php
Add the driver block to config/scout.php and switch the default driver:
'driver' => env('SCOUT_DRIVER', 'elasticsearch'),
'elasticsearch' => [
'index' => env('ELASTICSEARCH_INDEX', 'laravel'),
'hosts' => [env('ELASTICSEARCH_HOST', 'http://127.0.0.1:9200')],
'settings' => [
'number_of_shards' => 1,
'number_of_replicas' => 0,
],
],
First use case — index one model and search it:
use Laravel\Scout\Searchable;
class Article extends Model
{
use Searchable;
public function searchableAs(): string { return 'articles'; }
public function toSearchableArray(): array {
return [
'id' => $this->id,
'title' => $this->title,
'content' => strip_tags($this->content),
];
}
}
php artisan scout:import "App\Models\Article"
Article::search('中文 关键词')->paginate(15);
Where to look first: the package's Engine class (it is the driver registration in the service provider — check Scout::extend('elasticsearch', ...)), then config/scout.php. Everything day-to-day routes through toSearchableArray() and searchableAs().
One index per model (searchableAs). This is how the package delivers "multiple tables": each Eloquent model declares its own index name, so articles, products, and users live in separate ES indices rather than Scout's default single index. Keep the name stable — renaming it orphans a whole index.
Index build lifecycle. Treat indexing as a deploy step, not a runtime side effect:
php artisan scout:flush "App\Models\Article"
php artisan scout:import "App\Models\Article" # auto-creates the index using your settings
Run flush + import in a release script when mappings or analyzers change. scout:import chunks through makeAllSearchableUsing() (chunkById, typically), so it is safe on large tables — but it is synchronous, so run it from a queue-backed job or a deploy hook, not a web request.
Incremental sync. With the Searchable trait, saved/deleted model events push to ES automatically. In production set SCOUT_QUEUE=true so writes go through the queue instead of blocking the request; then make sure your workers are running and restarted after any config change. Override shouldBeSearchable() to keep drafts/soft-deleted rows out of the index.
Query building. Scout's fluent API maps onto the driver's ES query builder:
Article::search($q)
->where('category_id', 3)
->where('status', 'published')
->orderBy('created_at', 'desc')
->take(50)
->get();
Chain ->query(fn ($q) => $q->with('author', 'tags')) to eager-load relations — Scout only rehydrates IDs back into Eloquent, so without this you get N+1s on every search page.
Escape hatch for real ES features. Aggregations, highlighters, multi_match with field boosts, and completion suggesters aren't covered by the fluent API. Use ->raw() for the raw response, or grab the underlying client from the engine and issue DSL you write yourself. For anything beyond simple filters, plan on writing DSL.
Analyzer-aware mappings. The package creates indices from the settings block in config/scout.php. Put your analysis (tokenizer = ik_max_word, filters, per-field analyzers) and mappings there so auto-created indices are correct — otherwise you're relying on dynamic mapping, which will happily type your id as a long and your date strings as text.
articles_v2), reindex, swap an alias. Plan the alias from day one — searchableAs() should return an alias, not a physical index, or you'll be forced into downtime later.analyzer [ik_max_word] not found on scout:import means the plugin isn't installed on every node, or ES wasn't restarted after install.refresh lag. Newly written documents aren't visible for ~1s (default refresh interval). Integration tests that write-then-search will flake — either add a sleep, or issue writes with refresh=wait_for, or set refresh_interval to -1 during bulk import and back afterwards for a large scout:import.toSearchableArray() only returns plain scalars/arrays. Returning Eloquent models, Carbon objects without casting, or HTML-laden content will produce garbage or blow up on mapping. Strip tags, flatten relations, and exclude $appends bloat — every field goes over the wire and into the index.where() is not SQL. Scopes, whereIn, whereNull, and relation filters do not translate. Ranges, booleans, and nested fields need raw DSL. Don't assume Laravel semantics.Engine contract changed across v9/v10 (keys(), paginate() signatures). A 79-star community package will lag; pin laravel/scout to the version the package's composer.json targets, and be ready to vendor-patch. The README and config comments are in Chinese — keep a translation handy for the option names._cat/indices?v for shard health (a yellow cluster with number_of_replicas: 1 on a single node means the replica is unassigned), _mapping for what actually got created, and _search with "explain": true for scoring surprises. Log the raw response from ->raw() when a query returns the wrong thing — very often the issue is the mapping, not the query.mysqldump habit; a lost index is a scout:import away only if you can rebuild — make sure the canonical data is still in your database.How can I help you explore Laravel packages today?