Skip to content

This content is for v5.5. Switch to the latest version for up-to-date documentation.

What's New in V5

V5 represents a near-complete rewrite of the Laravel-Elasticsearch package. This update delivers a more seamless integration with Laravel’s Models, Eloquent, and Query Builders while providing greater access to Elasticsearch’s advanced features.

New elastic:re-index command that automates the entire re-indexing process when your field mappings change. Pass a model name, and the command handles the rest — creating a temp index, copying data, verifying counts, swapping, and cleaning up across 9 interactive phases. - Docs

Terminal window
php artisan elastic:re-index UserLog

The command compares your model’s mappingDefinition() against the live index and detects both type changes and sub-field changes (e.g., adding hasKeyword: true to a text field). Built-in resume capability means interrupted runs pick up where they left off.

New elastic:make command to scaffold Elasticsearch models with the correct base class, connection, and a starter mappingDefinition(). - Docs

Terminal window
php artisan elastic:make UserLog
php artisan elastic:make ES/UserLog # subdirectory support

Define your index field mappings directly on the model with mappingDefinition(). Uses the same Blueprint syntax as migrations, but keeps the schema close to the model that uses it. Powers the elastic:re-index command’s mapping analysis. - Docs

public static function mappingDefinition(Blueprint $index): void
{
$index->keyword('status');
$index->text('title', hasKeyword: true);
$index->geoPoint('location');
}

All five Artisan commands are now documented on a dedicated page: elastic:status, elastic:indices, elastic:show, elastic:make, and elastic:re-index. - Docs


When a model queries an index that doesn’t exist yet, the index is now created automatically instead of throwing an index_not_found_exception. This matches Elasticsearch’s own auto-create behavior for writes, extended to reads.

// No migration needed - index is created on first query
$products = Product::where('status', 'active')->get(); // returns empty collection

This is enabled by default. To disable, set in your config/database.php:

'options' => [
'auto_create_index' => false,
],

New upsert() method matching Laravel’s native signature. Insert or update records by unique key in a single bulk operation. - Docs

Product::upsert(
[
['sku' => 'ABC', 'name' => 'Widget', 'price' => 10],
['sku' => 'DEF', 'name' => 'Gadget', 'price' => 20],
],
['sku'], // unique key
['name', 'price'] // columns to update if exists
);

Supports single documents, batch operations, and composite unique keys.

New GeneratesTimeOrderedIds trait for sortable, chronologically-ordered IDs. 20 characters, URL-safe, lexicographic sort matches creation order across processes. - Docs

use PDPhilip\Elasticsearch\Eloquent\Model;
use PDPhilip\Elasticsearch\Eloquent\GeneratesTimeOrderedIds;
class TrackingEvent extends Model
{
use GeneratesTimeOrderedIds;
protected $connection = 'elasticsearch';
$event->id; // "0B3kF5XRABCDE_fghijk"
$event->getRecordTimestamp(); // 1771160093773 (ms)
$event->getRecordDate(); // Carbon instance

Three new Artisan commands for managing your Elasticsearch connection:

  • php artisan elastic:status — Connection health check with cluster info and license details
  • php artisan elastic:indices — List all indices with health, doc count, and store size
  • php artisan elastic:show {index} — Inspect an index: mappings, settings, and analysis config

All commands support --connection= for non-default connections.


distinct() queries now return ElasticCollections;

If a model relation exists and the aggregation is done on the foreign key, you can load the related model

UserLog::where('created_at', '>=', Carbon::now()->subDays(30))
->with('user')
->orderByDesc('_count')
->select('user_id')
->distinct(true);

Why: You can now treat distinct aggregations like real Eloquent results, including relationships.

New query method bulkDistinct(array $fields, $includeDocCount = false) - Docs

Run multiple distinct aggregations in parallel within a single Elasticsearch query.

$top3 = UserSession::where('created_at', '>=', Carbon::now()->subDays(30))
->limit(3)
->bulkDistinct(['country', 'device', 'browser_name'], true);

Why: Massive performance gains vs running sequential distinct queries.

groupByRanges() performs a range aggregation on the specified field. - Docs

groupByRanges()->get() — return bucketed results - Docs

groupByRanges()->agg() - apply metric aggregations per bucket -Docs

groupByDateRanges() performs a date range aggregation on the specified field. - Docs

groupByDateRanges()->get() — bucketed date ranges

groupByDateRanges()->agg() — metrics per date bucket

New model method getMetaValue($key) - Docs

Convenience method to get a specific meta value from the model instance.

$product = Product::where('color', 'green')->first();
$score = $product->getMetaValue('score');

When a bucketed query is executed, the raw bucket data is now stored in model meta. -Docs

$products = Product::distinct('price');
$buckets = $products->map(function ($product) {
return $product->getMetaValue('bucket');
});
  • New query methods for advanced full-text and structured searches using Elasticsearch’s query_string syntax.
  • Method: searchQueryString($query, $fields = null, $options = []) - Docs

New query method withTrackTotalHits(bool|int|null $val = true) - Docs

Appends the track_total_hits parameter to the DSL query, setting value to true will count all the hits embedded in the query meta not capping to Elasticsearch default of 10k hits

New query model method createOrFail(array $attributes) - Docs

  • By default, when using create($attributes) where $attributes has an id that exists, the operation will upsert. createOrFail will throw a BulkInsertQueryException with status code 409 if the id exists
  • New query method withRefresh(bool|string $refresh) - Docs

By default, inserting documents will wait for the shards to refresh, ie: withRefresh(true), you can set the refresh flag with the following (as per ES docs):

  1. true (default): Refresh the relevant primary and replica shards (not the whole index) immediately after the operation occurs, so that the updated document appears in search results immediately.
  2. wait_for: Wait for the changes made by the request to be made visible by a refresh before replying. This doesn’t force an immediate refresh, rather, it waits for a refresh to happen.
  3. false: Take no refresh-related actions. The changes made by this request will be made visible at some point after the request returns.
  • Ids can be generated on the Laravel side by using the GeneratesUuids or the GeneratesElasticIds trait. Docs
  • Field Maps can be defined in the model to map text to keyword fields, boosting performance Docs
  • All clauses in the query builder now accept an optional callback of Elasticsearch options to be applied to the clause. Docs
  • BelongsToMany relationships are now supported. Docs

Three powerful new query methods have been added to simplify expressive search:

  • Boxplot Aggregations Docs
  • Stats Aggregations Docs
  • Extended Stats Aggregations - Docs
  • Cardinality Aggregations - Docs
  • Median Absolute Deviation Aggregations - Docs
  • Percentiles Aggregations - Docs
  • String Stats Aggregations - Docs
  • Normalizers can now be defined in migrations. Docs

For low-level operations, you can access the native Elasticsearch PHP client directly:

Connection::on('elasticsearch')->elastic()->{clientMethod}();

This release is a complete architectural overhaul designed to mirror Laravel’s Eloquent ORM more closely.
The internals have been refactored for better testability, performance, and future extensibility.

Diagram