aloko/elasticquent5
Elasticquent5 brings Elasticsearch to your Laravel Eloquent models with a simple trait, letting you index, search, and query documents without leaving the familiar Eloquent API.
Install and wire it up
composer require aloko/elasticquent5
This is a Laravel-5-era fork of elasticquent/elasticquent, so it pulls in the elasticsearch/elasticsearch 5.x PHP client and expects an Elasticsearch 5 cluster. Check composer.lock after install — the client version it resolves is the single biggest constraint on your project.
Register the provider. On Laravel 5.5+ package discovery handles Aloko\Elasticquent\ElasticquentServiceProvider automatically; on 5.0–5.4 add it to config/app.php:
'providers' => [
// ...
Aloko\Elasticquent\ElasticquentServiceProvider::class,
],
Publish the config so you can point it at your cluster:
php artisan vendor:publish --provider="Aloko\Elasticquent\ElasticquentServiceProvider"
config/elasticquent.php holds the default index name and the client options (hosts, logging, log path, log level). Put the host in .env rather than hard-coding it, and make sure the log path is writable if you turn logging on.
First use case: index a model and search it
use Elasticquent\ElasticquentTrait;
class Book extends Eloquent
{
use ElasticquentTrait;
protected $fillable = ['title', 'author', 'summary'];
protected $mappingProperties = [
'title' => ['type' => 'text', 'analyzer' => 'standard'],
'author' => ['type' => 'text', 'analyzer' => 'standard'],
'summary' => ['type' => 'text', 'analyzer' => 'english'],
];
}
Then, from php artisan tinker:
Book::createIndex(); // create index + apply mapping
Book::addAllToIndex(); // bulk-load existing rows
$books = Book::search('moby dick');
$books->getTotal(); // ES hit count, not count($books)
Look first at: the trait (to see exactly which methods your fork exposes and how it builds request bodies), config/elasticquent.php, and ElasticquentResultCollection — that result wrapper is where getTotal(), getTook() and aggregations live.
Treat the index as a derived store, never as the source of truth. The trait does not hook into Eloquent events. Wire sync yourself in the model's boot():
protected static function boot()
{
parent::boot();
static::saved(fn ($model) => $model->addToIndex());
static::deleted(fn ($model) => $model->removeFromIndex());
}
For anything with real write volume, push that to a queued job (dispatch(new IndexBook($book->id))) so a slow cluster never fails a user's save. Guard the listener with a config flag (config('elasticquent.sync', true)) so seeders, imports and test runs don't hammer the cluster.
Reindex through a dedicated artisan command, chunked.
addAllToIndex() and reindex() walk the whole table. Write your own command that chunks and builds a fresh index, then flips an alias — that gives you a zero-downtime mapping change instead of an outage:
Book::where('id', '>', $lastId)->orderBy('id')->chunk(500, function ($books) {
foreach ($books as $book) { $book->addToIndex(); }
});
Search deliberately — two entry points. search('free text') is a convenience wrapper (usually a multi_match across your mapping); searchByQuery(array $query) gives you the raw ES query DSL plus aggregations, _source filtering, limit/offset and sort as positional arguments:
$results = Book::searchByQuery(
['bool' => ['must' => [['match' => ['title' => $term]]],
'filter' => [['term' => ['status' => 'published']]]]],
['by_author' => ['terms' => ['field' => 'author']]], // aggregations
['title', 'author'], // _source
20, 0, // limit, offset
['_score' => 'desc']
);
$results->getTotal();
$results->getAggregations();
Results are hydrated from _source into real Eloquent models, so accessors, casts and relation loading all work on them — but attributes come from the index, not the database.
Isolate the package behind a small interface. Because this is a low-traffic fork on a pinned client, put search behind BookSearchRepository with just the methods your app calls (search, index, remove, reindex). Replacing it with Laravel Scout or the official ES client later becomes a one-class change instead of a hunt through controllers.
Model configuration tips. Override getIndexName(), getTypeName(), getMapping() and getIndexSettings() explicitly rather than relying on the table-name default — especially if you have multiple models that could land in the same index. Use one index per environment (books_development, books_production) driven from config.
Pagination. There is no built-in paginator. Build a LengthAwarePaginator from $results and $results->getTotal() yourself; remember ES from + size is capped by index.max_result_window (10 000 by default), so deep pages need search_after.
index/type request shape will not work against a modern cluster. Do not let your infra team upgrade ES underneath this package without migrating the code.type => 'string' is gone in ES 5. Half the Elasticquent tutorials online use it. Use text (analyzed) and keyword (exact/aggregatable).text to keyword in place — you must create a new index, repopulate, and swap an alias. Plan this from day one.addToIndex() then immediate search() can return nothing for up to the refresh interval (~1 s). In tests, index with refresh=true (via the raw client) or sleep(1) before asserting.Book::where(...)->delete()) bypass your deleted hook entirely; schedule a reconciliation job if that's a real code path.addAllToIndex() / reindex() are memory hogs and reindex() is destructive (drops the index before rebuilding). Never run it against production without an alias swap or a snapshot.Book::search(...). Those trait methods are instance methods in most variants; older PHP tolerates the static call (with a deprecation notice), PHP 8 throws. If you're pinned to a modern runtime, use (new Book)->search(...) or add your own static wrapper._source. If the DB is authoritative, whereIn('id', $results->pluck('id')) in the database and re-order by the ES hit order.toArray() leaks into the index. Hidden attributes, appended accessors, Carbon date formatting and relation arrays all end up in _source unless the trait filters them — check what gets stored and override the indexing payload if needed.'logging' => true in config and tail the log while you're bringing this up. Also confirm storage/logs (or wherever logPath points) is writable.default_index in config vs getIndexName() are two different things and a common source of "why are my docs in the wrong index" confusion. Set $indexName on the model and stop guessing.->where() with search results. Filtering the returned ElasticquentResultCollection happens in PHP, after the cluster already did the work — it's not a query.composer.json, keep it isolated behind your repository class, and keep Laravel Scout + an ES driver or the official elasticsearch/elasticsearch 7/8 client on the roadmap for when this blocks a framework or cluster upgrade.getIndexName(), getTypeName(), getMapping(), getIndexSettings() and the indexing payload method are all overridable on the model — they're the cleanest places to inject per-tenant index names or custom analyzers without touching vendor code.How can I help you explore Laravel packages today?