addons/elasticsearch
A Laravel/PHP addon that integrates Elasticsearch into your application, making it easy to index Eloquent models and run fast, flexible full-text searches through a simple, configurable bridge between Laravel and Elasticsearch.
Install the helper alongside Scout and the official Elasticsearch PHP client:
composer require fly-studio/laravel-scout-elasticsearch
php artisan vendor:publish --provider="FlyStudio\Elasticsearch\ElasticsearchServiceProvider"
Look first at:
config/elasticsearch.php (or the published config) — hosts, index prefix, and whether logging is on.src/ directory — specifically the Paginator, Collection, and Logger classes, since the public API is thin and you will mostly be using those three directly.config/scout.php — confirm whether this package is used as a Scout engine or as a standalone query helper layered on top of the official client. In most installs it is the latter.Minimal first use case — run a search and hand the result to a paginator:
use FlyStudio\Elasticsearch\ElasticsearchManager;
$hits = app(ElasticsearchManager::class)->search('posts', [
'body' => [
'query' => ['multi_match' => [
'query' => $request->q,
'fields' => ['title^3', 'body'],
]],
],
]);
return $hits->paginate(15); // LengthAwarePaginator backed by ES hits
If you are driving this through Scout models instead, confirm the driver name in config/scout.php and run php artisan scout:import "App\Models\Post" before your first query. The collection helper is the natural place to map _source documents into Eloquent models or API resources.
Repository wrapper. The helpers are most useful when hidden behind a repository, so controllers never touch ES query DSL:
abstract class ElasticsearchRepository
{
public function __construct(protected ElasticsearchManager $es) {}
abstract protected function index(): string;
abstract protected function query(array $filters): array;
public function paginate(array $filters, int $perPage = 15)
{
return $this->es
->search($this->index(), ['body' => $this->query($filters)])
->paginate($perPage);
}
public function collect(array $filters): \Illuminate\Support\Collection
{
return $this->es
->search($this->index(), ['body' => $this->query($filters)])
->collect();
}
}
Paginator + API Resource. Because the paginator implements Laravel's pagination contract, you can hand it straight to Resource::collection():
return PostResource::collection(
$repo->paginate($request->only('q', 'category'))
);
Keep from/size conversion inside the repository — do not let controllers compute offsets, or you will end up with ES max_result_window errors leaking into your HTTP layer.
Collection + hydration. The collection helper returns a plain Illuminate\Support\Collection, which is the right tool for aggregations and non-model results (facets, suggesters, analytics dashboards). Use it for anything where you do not need Eloquent models:
$facets = $repo->collect($filters)
->pluck('_source.category')
->countBy();
Logger as a dev-time query inspector. Enable the package's logger in local/staging, dump the DSL to the elasticsearch log channel, and disable it in production. This is the fastest way to diff the query you think you built against the query that was actually sent:
// config/elasticsearch.php
'logging' => [
'enabled' => env('ES_LOG_QUERIES', false),
'channel' => env('ES_LOG_CHANNEL', 'stack'),
],
Pair it with php artisan tail while writing DSL — the logger is the package's real day-to-day value.
Sync discipline. If Scout is the write path (Model::searchable()), keep ES as a derived store: index on model events, reindex with scout:import, and never treat ES as authoritative. Add a queued job or nightly scout:import for drift repair.
index.max_result_window defaults to 10,000. The paginator will happily construct a from beyond that and throw a search_phase_execution_exception. Guard by returning the paginator's last valid page or switching to search_after for deep scrolling.total shape differs by ES version. ES 7+ returns hits.total as an object ({ value, relation }) and only counts up to 10,000 unless you set track_total_hits. If page counts look wrong, that is usually why — set 'track_total_hits' => true on searches where exact totals matter.text fields fails silently or loudly. You must sort on .keyword subfields (title.keyword) or you will get a fielddata is disabled error. The logger makes this obvious; the exception message alone often does not.toSearchArray() does not alter the ES mapping. Delete and recreate the index (scout:flush + scout:import) during development; in production, use aliases and reindex, then swap._id unless you handle it. _source typically does not contain the primary key. If the collection helper maps to models, map _id back to the model key, or store id in the document explicitly.php artisan config:clear (or config:cache in the deploy script). A stale cached config is the single most common cause of "it works locally" ES failures.composer.json and add an integration test around your repository that hits a real ES container (or testcontainers), because the package itself has no meaningful test harness for downstream users.track_total_hits globally, or a custom logger format), extend the manager/logger class and rebind it in a service provider rather than editing vendor code — the package has no extend() hook of its own.How can I help you explore Laravel packages today?