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

Elasticquent Laravel Package

daltcore/elasticquent

Elasticsearch integration for Laravel's Eloquent models: define indexes and mappings per model, bulk-index records, and run fluent search queries that return hydrated models. A maintained fork of elasticquent with config-driven client setup and artisan helpers.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and wire the provider

composer require daltcore/elasticquent
php artisan vendor:publish --provider="Elasticquent\ElasticquentServiceProvider"

This drops config/elasticquent.php and registers the client in the container. Set the essentials in .env (names vary slightly by fork — check the published config before assuming):

ELASTICQUENT_HOST=127.0.0.1
ELASTICQUENT_PORT=9200
ELASTICQUENT_DEFAULT_INDEX=app

Resolve the client directly when you need raw access:

$client = app(\Elasticquent\ElasticquentClientTrait::class); // or ElasticquentTrait::getElasticSearchClient()

2. Attach the trait to a model

use Elasticquent\ElasticquentTrait;

class Article extends Model
{
    use ElasticquentTrait;

    protected $fillable = ['title', 'body', 'published_at'];

    // Document body is built from toArray(); shape it here.
    protected $hidden = ['internal_notes'];

    protected $mappingProperties = [
        'title'        => ['type' => 'text', 'analyzer' => 'standard'],
        'body'         => ['type' => 'text'],
        'published_at' => ['type' => 'date', 'format' => 'yyyy-MM-dd HH:mm:ss'],
    ];
}

Override getIndexName() and getTypeName() on the model if you need per-model or per-tenant indices rather than the global default.

3. First end-to-end loop

Article::createIndex();      // create index + push mapping
Article::putMapping(Article::getMapping()); // if the index already existed
$article = Article::first();
$article->addToIndex();      // single doc, _id = primary key

sleep(1); // see "refresh interval" gotcha below

$results = Article::search('laravel');
foreach ($results as $hit) {
    echo $hit->title . ' (' . $hit->_score . ')';
}

Where to look first

  • src/ElasticquentTrait.php — every public method you'll actually call (addToIndex, removeFromIndex, search, searchByQuery, complexSearch, reindex, createIndex, putMapping).
  • src/ElasticquentResultCollection.php — what search() returns and which metadata accessors exist (total(), took(), getHits(), aggregations).
  • The published config — connection array passed straight to elasticsearch/elasticsearch; confirm which client major version the fork pins before you write queries (ES 6 vs 7 vs 8 query DSL differs).

Implementation Patterns

Keep the index in sync via model events, not controller code

The package does no automatic syncing. Wire it once in a ModelObserver or booted() hook and every write path is covered:

class Article extends Model
{
    use ElasticquentTrait;

    protected static function booted(): void
    {
        static::saved(fn (self $m) => IndexArticle::dispatch($m)->afterCommit());
        static::deleted(fn (self $m) => RemoveArticle::dispatch($m)->afterCommit());
    }
}

Queue the jobs. Indexing inline in a request adds ES latency to every save, and ES failures should never roll back a database write. ->afterCommit() (Laravel 8+) prevents indexing rows from transactions that later roll back.

Custom document shaping

The document body comes from the model's toArray(). For anything richer than a table row — computed fields, related names, denormalized counters — override toArray() or (if this fork exposes it) toSearchableArray():

public function toArray(): array
{
    return [
        'title'        => $this->title,
        'body'         => strip_tags($this->body),
        'author'       => $this->author?->name,
        'tags'         => $this->tags->pluck('name')->all(),
        'published_at' => optional($this->published_at)->toDateTimeString(),
    ];
}

Eager-load relations ($with = ['author', 'tags']) or you'll fire N+1 queries inside the index job.

Bulk reindex in chunks, never in one shot

Article::query()->with(['author', 'tags'])->chunk(500, function ($articles) {
    $articles->each->addToIndex();
});

// or let the package do it if reindex() in your version chunks internally
Article::reindex();

Wrap migrations of mappings in a maintenance command: deleteIndex() → createIndex() → chunked reindex. Do not attempt in-place mapping changes for field types — ES rejects most of them.

Search endpoint pattern with graceful degradation

public function index(Request $request)
{
    $query = $request->string('q');

    if ($query->isEmpty()) {
        return Article::latest()->paginate();
    }

    try {
        return Article::search($query)
            ->paginate($request->integer('per_page', 20));
    } catch (\Throwable $e) {
        report($e);

        return Article::where('title', 'like', "%{$query}%")->paginate();
    }
}

Keep a database fallback. Every ES-backed feature is one cluster restart away from being a 500.

Structured queries and aggregations

search() is fine for a quick string. For filters, sorting, and facets use searchByQuery() / complexSearch():

$results = Article::complexSearch([
    'query' => [
        'bool' => [
            'must'   => [['match' => ['title' => $term]]],
            'filter' => [
                ['term'  => ['tenant_id' => tenant()->id]],
                ['range' => ['published_at' => ['gte' => 'now-30d/d']]],
            ],
        ],
    ],
    'aggs' => [
        'by_tag' => ['terms' => ['field' => 'tags.keyword', 'size' => 20]],
    ],
    'sort' => [['published_at' => 'desc']],
]);

$buckets = $results['aggregations']['by_tag']['buckets'] ?? [];

Filters belong in filter (cacheable, no scoring cost), not must. For term/terms/aggs on string fields you need a keyword sub-field or an explicit keyword mapping — the dynamic default only gives you text.

Per-tenant / per-environment indices

public function getIndexName(): string
{
    return config('elasticquent.prefix') . 'articles_' . tenant()->id;
}

Pair with an Artisan command that provisions/tears down an index per tenant, and make index creation part of your deploy pipeline rather than a lazy first-request side effect.

Testing

Point the config at a dedicated test index and reset it in setUp(). Don't mock the client for search tests — the value of the test is the query DSL actually being valid. If a real cluster isn't available in CI, mark those tests as a separate group and keep the rest of the suite on the DB fallback path.

Gotchas and Tips

Refresh interval will make you think indexing is broken. ES refreshes every ~1s by default; a document indexed in the same request as the search won't appear. Options: sleep(1) in tests, ['refresh' => 'wait_for'] on the index call, or a refresh: true index setting in tests only. This is the single most common "the package isn't indexing" report.

Types are gone in ES 7+. getTypeName() still exists for backward compatibility, but ES 7 deprecated mapping types and ES 8 removed them entirely. Whatever _doc-ish string the fork sends, verify against your cluster version. Pin elasticsearch/elasticsearch to a major that matches your cluster — a 7.x client against an 8.x node (or vice versa) fails with opaque handshake errors.

Results are hydrated from _source, not the database. The returned models are reconstructed from the indexed document, so:

  • Accessors, casts, and appended attributes not present in toArray() are missing.
  • $hidden fields are genuinely absent — good for security, surprising when $article->body is null.
  • Relations aren't loaded; touching $hit->author triggers a fresh query per hit.
  • _score and other metadata land as attributes on the model — don't put them in $fillable and be aware toArray() on a result will include them.

Mapping changes are effectively one-way. Most field type changes require dropping and rebuilding the index. Keep createIndex() + chunked reindex in a deploy command, and version your index names (articles_v2) with an alias if you need zero-downtime rebuilds.

Bulk operations blow memory. addAllToIndex() loads the entire table. On anything nontrivial use chunk(). Each addToIndex() is a separate HTTP round trip — for large backfills, batch with the client's bulk() API directly rather than one call per model.

Transactions and index jobs. Index inside the transaction and you'll index rows that never commit. Use afterCommit() or dispatch from an event listener fired post-commit.

Don't run this alongside Laravel Scout. Both want to own model syncing and search entry points. Pick one. If you're starting fresh, Scout's toSearchableArray/searchable() conventions are better supported; this package's advantage is direct, unabstracted access to the ES query DSL and aggregations.

Connection and error handling. Configure timeouts and retry in the config's client array. Wrap production search paths in try/catch with a DB fallback — a slow or down cluster should degrade, not 500. Log the failing query, not just the exception.

Low adoption is the real risk. Zero dependents and a single-maintainer fork means no community answers when something breaks and no guarantee of updates for new Laravel/ES majors. Before committing:

  • Pin an exact version in composer.json.
  • Read ElasticquentTrait.php end to end — it's small, and knowing it lets you fork or replace it later.
  • Isolate usage behind a thin repository/search service so swapping to Scout or the raw client is a contained change.
  • If the repo goes quiet, the trait is small enough to vendor into your app and maintain in-house; that's often the pragmatic endgame for packages like this.

Extension points worth knowing. getIndexName(), getTypeName(), getMapping(), and toArray() are your override surface. getElasticSearchClient() gives you the raw client for anything the trait doesn't cover — use it rather than fighting the wrapper.

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.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk