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

Elasticsearch Laravel Package

pdphilip/elasticsearch

An Elasticsearch implementation of Laravel's Eloquent ORM. Use familiar Eloquent methods and models while leveraging full-text search, geo queries, fuzzy matching, highlights, and aggregations through a seamless Laravel-native integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Pick the version that matches your Laravel, not the latest tag. The 5.6 line targets Laravel 11/12/13 on Elasticsearch 8.x; 5.5 is the LTS branch for Laravel 10/11/12. Pin it explicitly:

composer require pdphilip/elasticsearch:~5.6   # Laravel 11/12/13, ES 8.x

Wire up three things before writing any model:

  1. .env — ES_HOSTS (comma-separated for multi-node), ES_AUTH_TYPE (http or cloud), credentials, and ES_INDEX_PREFIX. Note the prefix is applied verbatim in v5 — no auto underscore — so set ES_INDEX_PREFIX=my_app_ if you want my_app_user_logs.
  2. config/database.php — add the elasticsearch connection block exactly as the README shows. The options sub-array matters more than it looks: default_limit, bypass_map_validation, ssl_verification, meta_header.
  3. Register PDPhilip\Elasticsearch\ElasticServiceProvider::class in bootstrap/providers.php (Laravel 11+) if auto-discovery doesn't pick it up.

Where to look first: the docs site (elasticsearch.pdphilip.com), specifically The Base Model, Saving Models, Querying Models, and Index Blueprint. Skip the "Search" chapters until you have documents indexed and flowing.

First use case — the one this package is genuinely good at: an append-heavy, read-by-filter collection you'd otherwise put in a relational table with ugly JSON columns. Application logs, audit trails, clickstream events, user activity.

use PDPhilip\Elasticsearch\Eloquent\Model;

class UserLog extends Model
{
    protected $connection = 'elasticsearch';
    protected $index = 'user_logs';       // prefix is prepended automatically
}

Create the index in a migration, then round-trip a document:

UserLog::create([
    'user_id' => '2936adb0-b10d-11ed-8e03-0b234bda3e12',
    'country_code' => 'US',
    'location' => [40.7185, -74.0025],
    'status' => 1,
]);

UserLog::where('status', 1)->orderByDesc('created_at')->paginate(50);

If that returns a paginator with hydrated models and working ->id, you're set up correctly. Everything after this is Eloquent muscle memory plus ES-specific query builders.

Implementation Patterns

Treat it as a second connection, not a second database. The package's real value is that UserLog behaves like a model — casts, toArray(), relationships, paginate() — so you can drop it into controllers and resources without teaching your team a new ORM. But it is not your source of truth. Keep Postgres/MySQL as the system of record and mirror or dual-write into ES.

Dual-write with an explicit sync path. The cleanest day-to-day pattern is an observer or an event listener that writes to ES after the SQL write commits:

class UserLog extends Model
{
    protected $connection = 'elasticsearch';
    protected $guarded = [];
}

Then in the SQL-side model's created event, push a document. This keeps ES rebuildable — you can truncate the index and replay from SQL, which is the escape hatch you want when a mapping decision turns out wrong.

Use the ES-native query builders deliberately; don't hope where() does full text. In v5, where('name', 'John') is a term query (exact, un-analyzed) — the opposite of what most people expect. Reach for the right method by intent:

UserLog::where('status', 1)->get();                             // term
UserProfile::whereMatch('bio', 'PHP')->get();                   // analyzed match
UserProfile::searchTerm('Laravel')->orSearchTerm('Elasticsearch')->get();
UserProfile::searchPhrasePrefix('loves espressos and t')->highlight()->get();
UserProfile::whereFuzzy('description', 'qick brwn fx')->get();
UserLog::whereGeoDistance('location', '10km', [40.7185, -74.0025])->get();

Tuning happens via a callback on SearchOptions, not chained methods:

Product::searchTerm('espresso time', function (SearchOptions $options) {
    $options->searchFuzzy();
    $options->boost(2);
    $options->minimumShouldMatch(2);
})->get();

Adopt this convention early: one helper method per search intent on the model (scopeFuzzySearch, scopeGeoNearby) so callers never have to remember which builder is term vs match.

Exploit ES→SQL relationships for the join you can't do in ES. UserLog::where('status', 1)->with('user')->get() hydrates the SQL User model in a second query. Use this for display enrichment, not for filtering — you can't whereHas across the boundary and expect ES-side efficiency.

Aggregations are your dashboard shortcut. Instead of pulling 100k rows and grouping in PHP, use the aggregation and distinct/groupBy chapters. This is where the package pays for itself — one request returns counts by country_code or status buckets. Read the Distinct and GroupBy page carefully: v5 changed behavior and it isn't a straight SQL translation.

Chunk when backfilling. chunk() exists for a reason; a naive ->get() over a large backfill will hit default_limit (1000) or blow memory. Set protected $defaultLimit per model if your index legitimately needs more, but prefer chunking.

Use dynamic indices for time-series. For daily/rolling log indices, the Dynamic Indices feature lets one model address date-suffixed indices — the standard alternative to ILM rollover when you want the framework to manage it.

Gotchas and Tips

  • The where() semantics change is the #1 production surprise. Migrating from v4: anything that relied on where() being a match query silently becomes an exact term query and returns near-zero results. Grep for where( on text-ish fields and convert to whereMatch.
  • Index prefix no longer auto-appends _. v4 users get my_prefixuser_logs after upgrading. Audit the env var.
  • $model->_id is gone — use $model->id. If your model had its own id field, it collides and must be renamed.
  • MAX_SIZE is gone — set protected $defaultLimit. A silent 1000-row cap looks like "missing data" rather than a limit.
  • IndexBlueprint/AnalyzerBlueprint collapsed into one Blueprint. Old migration files will fatal. hasIndex → hasTable/indexExists; geo() → geoPoint(); {field}->index() → indexField(); alias() → aliasField().
  • Visibility lag after writes. Elasticsearch is near-real-time (default ~1s refresh). A test that creates a document then immediately queries it will intermittently fail. In tests either force a refresh or assert with a retry.
  • You cannot run your test suite on SQLite. CI needs a real ES container. Budget for docker-compose with elasticsearch:8.x and point ES_HOSTS at it; the package's own test matrix (PHP 8.3/8.4) is a good template.
  • Mapping drift is silent. A changed attribute type won't error until you reindex — ES accepts the document and coerces or rejects fields per its existing mapping. During development, set ES_OPT_BYPASS_MAP_VALIDATION=false to catch mismatches early; turn it on only as a last resort for legacy indices.
  • Set ES_ERROR_INDEX. Failed bulk writes otherwise vanish into logs. This is the single highest-value debugging knob.
  • Deep pagination has a hard ceiling (from + size limits). Use search_after-style patterns or aggregation for anything beyond a few thousand pages — paginate() will not save you.
  • Debugging: enable logging in the connection options to dump the raw DSL, and always compare your generated query against what Kibana Dev Tools would accept. Most "no results" bugs are an analyzer mismatch (keyword vs text), not a PHP bug.
  • Extension points: custom SearchOptions callbacks, per-model $defaultLimit/$index/$connection, the Blueprint for analyzers and custom mappings, and functionScore() (which replaced the removed orderByRandom()).
  • Maturity reality check: effectively no dependents and a single primary maintainer, but recent releases, a real CI matrix, and a full docs site. That's healthy for a driver-shaped package — just contain the blast radius: keep ES derived, rebuildable, and never the only copy of anything.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core