ximil/elasticquent
Elasticquent maps Laravel Eloquent models to Elasticsearch, letting you index records, run fuzzy and geo searches, and sync data through simple traits and methods so your app gets fast, flexible full-text search without hand-writing queries.
Install and publish config
composer require ximil/elasticquent
php artisan vendor:publish --provider="Elasticquent\ElasticquentServiceProvider"
This drops config/elasticquent.php with default_index, default_type, prefix, hosts, logging, logPath, logLevel. Set hosts and default_index first — everything else in the package resolves against those two values.
Wire up a model
use Elasticquent\ElasticquentTrait;
class Post extends Model
{
use ElasticquentTrait;
protected $fillable = ['title', 'body', 'published_at'];
// Optional: per-model override of the config defaults
protected $indexName = 'posts';
protected $typeName = 'post';
protected $mappingProperties = [
'title' => ['type' => 'text', 'analyzer' => 'standard'],
'body' => ['type' => 'text'],
];
}
First use case: create the index and search
Post::createIndex(); // create the ES index with your mapping
Post::addAllToIndex(); // push every row into ES
$results = Post::search('laravel elasticsearch');
foreach ($results as $post) {
echo $post->title;
}
echo $results->getTotal();
Where to look first in the repo: composer.json (which elasticsearch/elasticsearch client version the fork pins — this dictates your ES server version), the ElasticquentTrait source (it is the entire public API), and ElasticquentResultCollection (the object search() returns).
1. Keep ES in sync with model events, not manual calls
The trait does not auto-index on save. The standard pattern is an observer:
class PostObserver
{
public function saved(Post $post) { $post->addToIndex(); }
public function deleted(Post $post) { $post->removeFromIndex(); }
}
// AppServiceProvider::boot()
Post::observe(PostObserver::class);
If you need write latency out of the request cycle, dispatch a queued job instead of calling addToIndex() inline.
2. Bulk reindex as an artisan command
Post::reindex() deletes and recreates the index and re-adds everything — fine for small tables, dangerous for large ones. For real workloads, chunk it yourself:
Post::chunkById(500, function ($posts) {
foreach ($posts as $post) { $post->addToIndex(); }
});
For serious volume, skip the trait for writes and use the underlying elasticsearch-php bulk endpoint directly in a scheduled/queued command.
3. Per-tenant or per-environment index naming
Override getIndexName() on the model so you get isolation without touching config:
public function getIndexName()
{
return config('elasticquent.prefix') . 'posts_' . tenant()->id;
}
Then createIndex() / deleteIndex() in your tenant provisioning/teardown jobs.
4. Querying: search() for convenience, searchByQuery() / complexSearch() for control
// Simple query_string
$results = Post::search('title:laravel');
// Explicit query DSL + aggregations
$results = Post::searchByQuery([
'bool' => [
'must' => [['match' => ['body' => 'elasticsearch']]],
'filter' => [['term' => ['status' => 'published']]],
],
], $aggregations = [
'by_tag' => ['terms' => ['field' => 'tags', 'size' => 20]],
]);
$results->getAggregation('by_tag');
complexSearch(array $params) takes the raw request body (index, type, body with from/size/sort/_source), which is the escape hatch when the helpers don't cover a case.
5. Treat search results as models, but re-fetch when you need relations
Results are hydrated Eloquent instances from _source, so $post->title works and $post->relation triggers a lazy load. That means N+1 on listing pages. Two safe patterns:
Post::with('author')->whereIn('id', $ids)->get() and reorder in PHP.6. Wrap it in a repository
interface PostSearch { public function find(array $criteria): Collection; }
Because the trait is static-method-heavy and hard to mock, having one seam around it means you can swap to elasticsearch-php directly later without touching controllers.
addToIndex(), addAllToIndex(), removeFromIndex() are all explicit. Every "why is my search stale" bug starts here.toArray(). $hidden and $visible therefore control what gets indexed — a hidden body column silently disappears from ES. $casts and date serialization also determine whether your mapping (date, numeric types) will accept the document.putMapping() only adds fields; changing an analyzer or type means create a new index, repopulate, then flip an alias.createIndex() fails if the index already exists. Guard with an existence check or catch the exception before calling it in deploy scripts.refresh in your params (or refresh=true) for test suites and for read-after-write flows._type-based API (default_type, getTypeName()) maps onto ES 1.x–6.x semantics. On ES 7 it works only in compatibility mode; on ES 8 types are gone. Confirm the pinned elasticsearch/elasticsearch version in composer.json matches your cluster before adopting the package.hosts format drifts between client majors. The string form ('localhost:9200') used by older configs isn't accepted by newer clients that expect ['host' => ..., 'port' => ...]. Check the generated config against the installed client version.$guarded. Models come back via newInstance-style construction from _source; if you rely on $guarded for security, verify fields aren't being dropped. If anything looks missing, either extend $fillable or handle the raw hits yourself ($results->getHits()).elasticquent.logging = true plus a writable logPath gives you the full request/response, which is far faster than guessing at mapping errors.0 dependents, 0 stars) of the original Elasticquent. Pin the exact version in composer.json, and avoid depending on undocumented internals. The public surface you're actually leaning on is small — trait methods plus the result collection — so keep a fallback plan to call the elasticsearch-php client directly.getIndexName(), getTypeName(), getMappingProperties(), and getIndexSettings() on the model rather than editing the trait; those four cover almost every customization you'll need.addAllToIndex() loads the whole table. Never run it against a large model outside a chunked command or a queue job.How can I help you explore Laravel packages today?