All notable changes to this laravel-elasticsearch package will be documented in this file.
- PHP client constraint: Updated to
^8.17|^9.0to support both Elasticsearch 8.x and 9.x PHP clients - CI matrix: GitHub Actions now tests against both ES 8.18.0 and 9.0.0 (using official
docker.elastic.coimages) - Docker: Local development uses ES 9.0.0 with official
docker.elastic.coimages - Verified compatibility: Full test suite passes against ES 9.0.0 without code changes
force_sourcehighlighting parameter is deprecated since ES 8.11 and removed in ES 9.x. The parameter is still accepted for backward compatibility with ES 8.x, but will be ignored by ES 9.x servers
_idno longer leaks into serialized output (toArray(),toJson()). The internal_idmetadata field was being exposed alongsideid, resulting in duplicate ID fields in model serialization.
This release is compatible with Laravel 10, 11 & 12
When a model queries an index that doesn't exist yet, the index is created automatically instead of throwing index_not_found_exception. 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 collectionControlled via options.auto_create_index config (default: true). - Docs
Why: New models shouldn't crash before the first write. Elasticsearch already auto-creates on insert; this extends the same behavior to reads.
First-class CLI tools for managing your Elasticsearch connection and indices:
php artisan elastic:status- Connection health check with cluster info and license detailsphp artisan elastic:indices- List all indices with health, doc count, and store sizephp artisan elastic:show {index}- Inspect an index: overview, mappings, settings, and analysis config
php artisan elastic:status
php artisan elastic:indices --all
php artisan elastic:show productsAll commands support --connection= for non-default connections.
Why: Until now, inspecting your Elasticsearch setup meant leaving Laravel for curl or Kibana. These commands bring that visibility into Artisan where it belongs.
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.
Why: Elasticsearch has no native upsert-by-field. This queries for existing documents first, then issues a single bulk request mixing index and update actions.
New GeneratesTimeOrderedIds trait for sortable, chronologically-ordered IDs. 20 characters, URL-safe, lexicographic sort matches creation order across processes. - Docs
use PDPhilip\Elasticsearch\Eloquent\GeneratesTimeOrderedIds;
class TrackingEvent extends Model
{
use GeneratesTimeOrderedIds;
}
$event->id; // "0B3kF5XRABCDE_fghijk"
$event->getRecordTimestamp(); // 1771160093773 (ms)
$event->getRecordDate(); // Carbon instanceSafe for mixed datasets; returns null for pre-existing IDs not generated by this trait.
Why: When you need IDs that sort chronologically across multiple processes/workers, ideal for high-volume event tracking and time-sequenced analytics.
- Refactored Query Builder into focused concerns:
BuildsAggregations,BuildsSearchQueries,BuildsFieldQueries,BuildsGeoQueries,BuildsNestedQueries,HandlesScripts,ManagesPit - Refactored Grammar into concerns:
CompilesAggregations,CompilesOrders,CompilesWheres,FieldUtilities - Decomposed
ElasticsearchModeltrait into focused traits for clarity - Consolidated ES PHP client usage into
ElasticClientwrapper - Consolidated metadata handling into single
MetaDTO - Simplified
ManagesOptionsparameter inference - Extracted
addFieldQuery()dispatcher inBuildsFieldQueriesto deduplicate field query methods - Refactored Relations for readability: early returns, named variables, simplified loops
- Schema Builder: added
getIndexes(),getForeignKeys(),getViews()for Laravel compatibility - CI updated to Elasticsearch 8.18.0
- Test suite expanded from 379 to 422 tests (2,548 assertions), all passing
idis now always present in serialized model output (toArray(),toJson())_idis no longer exposed in serialized output (internal metadata stays internal)- Removed dead debug code from Connection.php
Full Changelog: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.3.0...v5.4.0
This release is compatible with Laravel 10, 11 & 12
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');
});Full Changelog: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.2.0...v5.3.0
This release is compatible with Laravel 10, 11 & 12
This release introduces Query String Queries, bringing full Elasticsearch query_string syntax support directly into your Eloquent-style queries.
- Method:
searchQueryString(query, $fields = null, $options = [])and related methods (orSearchQueryString,searchNotQueryString, etc.) - Supports all
query_stringfeatures — logical operators, wildcards, fuzziness, ranges, regex, boosting, field scoping, and more - Includes a dedicated
QueryStringOptionsclass for fluent option configuration or array-based parameters - See Tests
- Full documentation
Example:
Product::searchQueryString('status:(active OR pending) name:(full text search)^2')->get();
Product::searchQueryString('price:[5 TO 19}')->get();
// vanilla optional, +pizza required, -ice forbidden
Product::searchQueryString('vanilla +pizza -ice', function (QueryStringOptions $options) {
$options->type('cross_fields')->fuzziness(2);
})->get();
//etc
- You can now add an
unmapped_typeflag to your ordering query #88
Product::query()->orderBy('name', 'desc', ['unmapped_type' => 'keyword'])->get();
- Fixed issue where limit values were being reset on bucket aggregations #84
Full Changelog: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.1.0...v5.2.0
This release is compatible with Laravel 10, 11 & 12
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
$products = Product::limit(5)->withTrackTotalHits(true)->get();
$totalHits = $products->getQueryMeta()->getTotalHits();
This can be set by default for all queries by updating the connection config in database.php:
'elasticsearch' => [
'driver' => 'elasticsearch',
.....
'options' => [
'track_total_hits' => env('ES_TRACK_TOTAL_HITS', null),
....
],
],
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
Product::createOrFail([
'id' => 'some-existing-id',
'name' => 'Blender',
'price' => 30,
]);
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):
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.wait_forWait 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.falseTake no refresh-related actions. The changes made by this request will be made visible at some point after the request returns.
Product::withRefresh('wait_for')->create([
'name' => 'Blender',
'price' => 30,
]);
- Add withTrackTotalHits method to Builder class to add track_total_hits by @caufab in pdphilip#76
- feat(query): add op_type=create support and dedupe helpers by @abkrim in pdphilip#79
- Laravel ^12.23 Compatibility - close #81
- @caufab made their first contribution in pdphilip#76
Full Changelog: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.0.7...v5.1.0
This release is compatible with Laravel 10, 11 & 12
- Connection bug fix by @pdphilip in pdphilip#75 - close #70
Full Changelog: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.0.6...v5.0.7
This release is compatible with Laravel 10, 11 & 12
- Bug fix: Chunking
$countvalue fixed for setting query limit correctly, via #68
Full Changelog: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.0.5...v5.0.6
This release is compatible with Laravel 10, 11 & 12
- Merging in bug fixes by @use-the-fork in pdphilip#65
- Updated outstanding tests
- Fixed bug in relations
has()method
Full Changelog: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.0.4...v5.0.5
This release is compatible with Laravel 10, 11 & 12
- Connection
disconnect()resets connection - removing connection is unnecessary in the context of Elasticsearch. Issue #64 - Added
getTotalHits()helper method from query meta - Bug fix:
searchFuzzy()parses options as a closure - Minor code reorganising
Full Changelog: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.0.3...v5.0.4
This release is compatible with Laravel 10, 11 & 12
- Bug fix: Internal model attribute
metarenamed to_metato avoid the issue where a model could have a field calledmeta - Bug fix:
highlight()passed without fields did not highlight all hits - Bug fix: Hybrid
BelongsToin some SQL cases used ES connection - Bug fix:
orderBy('_score')was not parsing correctly - Bug fix: Edge case where a string value was being seen as callable
Full Changelog: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.0.2...v5.0.3
This release is compatible with Laravel 10, 11 & 12
bulkInsert() is identical to insert() but will continue on errors and return an array of the results.
People::bulkInsert([
[
'id' => '_edo3ZUBnJmuNNwymfhJ', // Will update (if id exists)
'name' => 'Jane Doe',
'status' => 1,
],
[
'name' => 'John Doe', // Will Create
'status' => 2,
],
[
'name' => 'John Dope',
'status' => 3,
'created_at' => 'xxxxxx', // Will fail
],
]);
Returns:
{
"hasErrors": true,
"total": 3,
"took": 0,
"success": 2,
"created": 1,
"modified": 1,
"failed": 1,
"errors": [
{
"id": "Y-dp3ZUBnJmuNNwy7vkF",
"type": "document_parsing_exception",
"reason": "[1:45] failed to parse field [created_at] of type [date] in document with id 'Y-dp3ZUBnJmuNNwy7vkF'. Preview of field's value: 'xxxxxx'"
}
]
}
Full Changelog: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.0.1...v5.0.2
This release is compatible with Laravel 10, 11 & 12
- Updated model docs for comprehensive IDE support when building queries
- Added
orderByNestedDesc() - Removed & replaced compatibility-loader that depended on
class_aliasto set the correct traits for the given Laravel version
Full Changelog: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.0.0...v5.0.1
We’re excited to announce v5 of the laravel-elasticsearch package - compatible with Laravel 10, 11, and 12.
V5 is the brainchild of @use-the-fork and is a near-complete rewrite of the package; packed with powerful new features, deep integration with Elasticsearch’s full capabilities, and a much tighter alignment with Laravel’s Eloquent. It lays a solid, future-proof foundation for everything that comes next.
-
Please take a look at the upgrade guide carefully, as there are several significant breaking changes.
"pdphilip/elasticsearch": "^5",
- Index Prefix Handling
The
ES_INDEX_PREFIXno longer auto-appends an underscore (_). Old behavior:ES_INDEX_PREFIX=my_prefix→my_prefix_New: set explicitly if needed →ES_INDEX_PREFIX=my_prefix_
-
Model ID Field
$model->_idis deprecated. Use$model->idinstead. If your model had a separateidfield, you must rename it. -
Default Limit Constant
MAX_SIZEconstant is removed. Use$defaultLimitproperty:use PDPhilip\Elasticsearch\Eloquent\Model; class Product extends Model { protected $defaultLimit = 10000; protected $connection = 'elasticsearch'; }
-
where()Behavior ChangedNow uses term query instead of match.
// Old: Product::where('name', 'John')->get(); // match query // New: Product::whereMatch('name', 'John')->get(); // match query Product::where('name', 'John')->get(); // term query
-
orderByRandom()RemovedReplace with
functionScore()Docs -
Full-text Search Options Updated Methods like
asFuzzy(),setMinShouldMatch(),setBoost()removed. Use callback-based SearchOptions instead:Product::searchTerm('espresso time', function (SearchOptions $options) { $options->searchFuzzy(); $options->boost(2); $options->minimumShouldMatch(2); })->get();
-
Legacy Search Methods Removed All
{xx}->search()methods been removed. Use{multi_match}->get()instead.
-
distinct()andgroupBy()behavior updated. DocsReview queries using them and refactor accordingly.
-
IndexBlueprintandAnalyzerBlueprinthas been removed and replaced with a singleBlueprintclass- use PDPhilip\Elasticsearch\Schema\IndexBlueprint; - use PDPhilip\Elasticsearch\Schema\AnalyzerBlueprint; use PDPhilip\Elasticsearch\Schema\Blueprint;
-
Schema::hasIndexhas been removed. UseSchema::hasTableorSchema::indexExistsinstead. -
geo($field)field property has been replaced withgeoPoint($field) -
{field}->index($bool)field property has been replaced with{field}->indexField($bool); -
alias()field type has been removed. UsealiasField()instead. -
settings()method has been replaced withwithSetting() -
map()method has been replaced withwithMapping() -
analyzer()method has been replaced withaddAnalyzer() -
tokenizer()method has been replaced withaddTokenizer() -
charFilter()method has been replaced withaddCharFilter() -
filter()method has been replaced withaddFilter()
- Dynamic indices are now managed by the
DynamicIndextrait. upgrade guide
- You can now generate Elasticsearch ids in Laravel Docs
- All clauses in the query builder now accept an optional callback of Elasticsearch options to be applied to the clause. Docs
- Belongs to many relationships are now supported. Docs
- 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
Connection::on('elasticsearch')->elastic()->{clientMethod}();
- V5.0.0 by @use-the-fork in pdphilip#54
- Small Bug Fixes found in RC1 by @use-the-fork in pdphilip#60
Full Changelog: https://github.com/pdphilip/laravel-elasticsearch/compare/v4.5.3...v5.0.0