Skip to content

Commit a4a483a

Browse files
committed
Merge branch 'main' into 5.x-dev
2 parents 12f8078 + fa763d0 commit a4a483a

21 files changed

Lines changed: 1018 additions & 37 deletions

CHANGELOG.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,143 @@
22

33
All notable changes to this `laravel-elasticsearch` package will be documented in this file.
44

5+
## v5.2.0 - 2025-10-24
6+
7+
This release is compatible with Laravel 10, 11 & 12
8+
9+
### New Feature: Query String Queries
10+
11+
This release introduces Query String Queries, bringing full Elasticsearch `query_string` syntax support directly into your Eloquent-style queries.
12+
13+
- Method: `searchQueryString(query, $fields = null, $options = [])` and related methods (`orSearchQueryString`, `searchNotQueryString`, etc.)
14+
- Supports all `query_string` features — logical operators, wildcards, fuzziness, ranges, regex, boosting, field scoping, and more
15+
- Includes a dedicated `QueryStringOptions` class for fluent option configuration or array-based parameters
16+
- [See Tests](https://github.com/pdphilip/laravel-elasticsearch/blob/main/tests/QueryStringTest.php)
17+
- [Full documentation](https://elasticsearch.pdphilip.com/eloquent/query-string-queries/)
18+
19+
Example:
20+
21+
```php
22+
Product::searchQueryString('status:(active OR pending) name:(full text search)^2')->get();
23+
Product::searchQueryString('price:[5 TO 19}')->get();
24+
25+
// vanilla optional, +pizza required, -ice forbidden
26+
Product::searchQueryString('vanilla +pizza -ice', function (QueryStringOptions $options) {
27+
$options->type('cross_fields')->fuzziness(2);
28+
})->get();
29+
30+
//etc
31+
32+
```
33+
### Ordering enhancement: unmapped_type
34+
35+
- You can now add an `unmapped_type` flag to your ordering query #88
36+
37+
```php
38+
Product::query()->orderBy('name', 'desc', ['unmapped_type' => 'keyword'])->get();
39+
40+
```
41+
### Bugfix
42+
43+
- Fixed issue where limit values were being reset on bucket aggregations #84
44+
45+
**Full Changelog**: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.1.0...v5.2.0
46+
47+
## v5.1.0 - 2025-08-20
48+
49+
This release is compatible with Laravel 10, 11 & 12
50+
51+
#### 1. New feature, `withTrackTotalHits(bool|int|null $val = true)`
52+
53+
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
54+
55+
```php
56+
$products = Product::limit(5)->withTrackTotalHits(true)->get();
57+
$totalHits = $products->getQueryMeta()->getTotalHits();
58+
59+
60+
```
61+
This can be set by default for all queries by updating the connection config in `database.php`:
62+
63+
```php
64+
'elasticsearch' => [
65+
'driver' => 'elasticsearch',
66+
.....
67+
'options' => [
68+
'track_total_hits' => env('ES_TRACK_TOTAL_HITS', null),
69+
....
70+
],
71+
],
72+
73+
74+
```
75+
#### 2. New feature, `createOrFail(array $attributes)`
76+
77+
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
78+
79+
```php
80+
Product::createOrFail([
81+
'id' => 'some-existing-id',
82+
'name' => 'Blender',
83+
'price' => 30,
84+
]);
85+
86+
87+
```
88+
#### 3. New feature `withRefresh(bool|string $refresh)`
89+
90+
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):
91+
92+
- `true` (default)
93+
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.
94+
- `wait_for`
95+
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.
96+
- `false`
97+
Take no refresh-related actions. The changes made by this request will be made visible at some point after the request returns.
98+
99+
```php
100+
Product::withRefresh('wait_for')->create([
101+
'name' => 'Blender',
102+
'price' => 30,
103+
]);
104+
105+
106+
```
107+
### PRS
108+
109+
* Add withTrackTotalHits method to Builder class to add track_total_hits by @caufab in https://github.com/pdphilip/laravel-elasticsearch/pull/76
110+
* feat(query): add op_type=create support and dedupe helpers by @abkrim in https://github.com/pdphilip/laravel-elasticsearch/pull/79
111+
112+
### Bugfix
113+
114+
* Laravel ^12.23 Compatibility - close [#81](https://github.com/pdphilip/laravel-elasticsearch/issues/81)
115+
116+
### New Contributors
117+
118+
* @caufab made their first contribution in https://github.com/pdphilip/laravel-elasticsearch/pull/76
119+
120+
**Full Changelog**: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.0.7...v5.1.0
121+
122+
## v5.0.7 - 2025-07-13
123+
124+
This release is compatible with Laravel 10, 11 & 12
125+
126+
### What's Changed
127+
128+
* Connection bug fix by @pdphilip in https://github.com/pdphilip/laravel-elasticsearch/pull/75 - close #70
129+
130+
**Full Changelog**: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.0.6...v5.0.7
131+
132+
## v5.0.6 - 2025-06-04
133+
134+
This release is compatible with Laravel 10, 11 & 12
135+
136+
### What's Changed
137+
138+
* Bug fix: Chunking `$count` value fixed for setting query limit correctly, via #68
139+
140+
**Full Changelog**: https://github.com/pdphilip/laravel-elasticsearch/compare/v5.0.5...v5.0.6
141+
5142
## v5.0.5 - 2025-05-19
6143

7144
This release is compatible with Laravel 10, 11 & 12
@@ -71,6 +208,10 @@ People::bulkInsert([
71208

72209

73210

211+
212+
213+
214+
74215
```
75216
Returns:
76217

@@ -93,6 +234,10 @@ Returns:
93234
}
94235

95236

237+
238+
239+
240+
96241
```
97242
#### 2. Bug fix: `distinct()` aggregation now appends `searchAfter` key in meta
98243

@@ -130,6 +275,10 @@ with Laravel’s Eloquent. It lays a solid, future-proof foundation for everythi
130275
"pdphilip/elasticsearch": "^5",
131276

132277

278+
279+
280+
281+
133282
```
134283
### Breaking Changes
135284

@@ -159,6 +308,10 @@ with Laravel’s Eloquent. It lays a solid, future-proof foundation for everythi
159308
}
160309

161310

311+
312+
313+
314+
162315
```
163316

164317
#### 3. Queries
@@ -175,6 +328,10 @@ with Laravel’s Eloquent. It lays a solid, future-proof foundation for everythi
175328
Product::where('name', 'John')->get(); // term query
176329

177330

331+
332+
333+
334+
178335
```
179336
- `orderByRandom()` Removed
180337

@@ -192,6 +349,10 @@ with Laravel’s Eloquent. It lays a solid, future-proof foundation for everythi
192349
})->get();
193350

194351

352+
353+
354+
355+
195356
```
196357
- Legacy Search Methods Removed
197358
All `{xx}->search()` methods been removed. Use `{multi_match}->get()` instead.
@@ -214,6 +375,10 @@ with Laravel’s Eloquent. It lays a solid, future-proof foundation for everythi
214375
use PDPhilip\Elasticsearch\Schema\Blueprint;
215376

216377

378+
379+
380+
381+
217382
```
218383
- `Schema::hasIndex` has been removed. Use `Schema::hasTable` or `Schema::indexExists` instead.
219384

@@ -280,6 +445,10 @@ with Laravel’s Eloquent. It lays a solid, future-proof foundation for everythi
280445
Connection::on('elasticsearch')->elastic()->{clientMethod}();
281446

282447

448+
449+
450+
451+
283452
```
284453
### What's Changed
285454

src/Connection.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ private function sanitizeConfig(): void
123123
'cert_password' => null,
124124
],
125125
'options' => [
126+
'track_total_hits' => null, // null -> skips - max 10k by default; true -> full values; false -> no hit tracking, returns -1; int -> max hit tracking val, ex 20000
126127
'bypass_map_validation' => false, // This skips the safety checks for Elastic Specific queries.
127128
'logging' => false,
128129
'ssl_verification' => true,
@@ -162,22 +163,24 @@ public function setOptions(): void
162163
{
163164
$this->allowIdSort = $this->config['options']['allow_id_sort'] ?? false;
164165

166+
$this->options()->add('track_total_hits', $this->config['options']['track_total_hits'] ?? null);
167+
165168
$this->options()->add('bypass_map_validation', $this->config['options']['bypass_map_validation'] ?? null);
166169

167170
if (isset($this->config['options']['ssl_verification'])) {
168171
$this->options()->add('ssl_verification', $this->config['options']['ssl_verification']);
169172
}
170173

171174
if (! empty($this->config['options']['retires'])) {
172-
$this->options()->add('retires', $this->config['options']['retires']);
175+
$this->options()->add('retires', (int) $this->config['options']['retires']);
173176
}
174177

175178
if (isset($this->config['options']['meta_header'])) {
176179
$this->options()->add('meta_header', $this->config['options']['meta_header']);
177180
}
178181

179182
if (isset($this->config['options']['default_limit'])) {
180-
$this->defaultQueryLimit = $this->config['options']['default_limit'];
183+
$this->defaultQueryLimit = (int) $this->config['options']['default_limit'];
181184
}
182185
}
183186

src/Eloquent/Builder.php

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class Builder extends BaseEloquentBuilder
7171
'bucketaggregation',
7272
'openpit',
7373
'bulkinsert',
74+
'createonly',
7475
];
7576

7677
/**
@@ -95,7 +96,7 @@ public function setModel($model): static
9596
public function newModelInstance($attributes = [])
9697
{
9798
$model = $this->model->newInstance($attributes)->setConnection(
98-
$this->query->getConnection()->getName()
99+
$this->query->connection->getName()
99100
);
100101

101102
// Merge in our options.
@@ -216,7 +217,7 @@ public function hydrate(array $items)
216217
$instance = $this->newModelInstance();
217218

218219
return $instance->newCollection(array_map(function ($item) use ($instance) {
219-
return $instance->newFromBuilder($item, $this->getConnection()->getName());
220+
return $instance->newFromBuilder($item, $this->query->connection->getName());
220221
}, $items));
221222
}
222223

@@ -275,6 +276,7 @@ public function orderedChunkById($count, callable $callback, $column = null, $al
275276
public function chunkByPit($count, callable $callback, $keepAlive = '1m'): bool
276277
{
277278
$this->query->keepAlive = $keepAlive;
279+
$this->query->limit = $count;
278280
$pitId = $this->query->openPit();
279281

280282
$searchAfter = null;
@@ -386,9 +388,25 @@ public function findOrNew($id, $columns = ['*']): Model
386388
return $model;
387389
}
388390

389-
public function withoutRefresh()
391+
public function withoutRefresh(): Model
390392
{
391-
$this->model->options()->add('refresh', false);
393+
return $this->withRefresh(false);
394+
}
395+
396+
/**
397+
* Explicitly control the Elasticsearch refresh behavior for write ops.
398+
* Accepts: true, false, or 'wait_for'.
399+
*/
400+
public function withRefresh(bool|string $refresh): Model
401+
{
402+
$this->model->options()->add('refresh', $refresh);
403+
404+
return $this->model;
405+
}
406+
407+
public function withOpType(string $value)
408+
{
409+
$this->model->options()->add('op_type', $value);
392410

393411
return $this->model;
394412
}
@@ -643,6 +661,27 @@ public function rawDsl($dsl): array
643661
return $this->query->raw($dsl)->asArray();
644662
}
645663

664+
/**
665+
* Force insert operations to use op_type=create for dedupe semantics.
666+
* When set, attempts to create an existing _id will fail with a 409 from Elasticsearch.
667+
*/
668+
public function createOnly(): Model
669+
{
670+
// mark insert op type on the underlying query options
671+
$this->withOpType('create');
672+
673+
return $this->model;
674+
}
675+
676+
/**
677+
* Convenience method to perform a create-only insert and surface 409s as exceptions.
678+
* Accepts single document attributes or an array of documents.
679+
*/
680+
public function createOrFail(array $attributes)
681+
{
682+
return $this->createOnly()->create($attributes);
683+
}
684+
646685
// ----------------------------------------------------------------------
647686
// Protected
648687
// ----------------------------------------------------------------------

src/Eloquent/Docs/ModelDocs.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
/**
1515
* Query Builder Methods ---------------------------------
1616
*
17-
* @method static Builder query()
17+
* @method static $this query()
1818
*-----------------------------------
1919
* @method static $this where($column, $operator = null, $value = null, $boolean = 'and', $options = [])
2020
* @method static $this whereNot($column, $operator = null, $value = null, $boolean = 'and', $options = [])
@@ -127,6 +127,11 @@
127127
* @method static $this orSearchFuzzyPrefix($term, $fields = ['*'], $options = [])
128128
* @method static $this searchNotFuzzyPrefix($term, $fields = ['*'], $options = [])
129129
* @method static $this orSearchNotFuzzyPrefix($term, $fields = ['*'], $options = [])
130+
* -----------------------------------
131+
* @method static $this searchQueryString($query, $fields = null, $options = [])
132+
* @method static $this orSearchQueryString($query, $fields = null, $options = [])
133+
* @method static $this searchNotQueryString($query, $fields = null, $options = [])
134+
* @method static $this orSearchNotQueryString($query, $fields = null, $options = [])
130135
*===========================================
131136
* Speciality methods
132137
*===========================================
@@ -181,6 +186,8 @@
181186
* @method static array getModels($columns = ['*'])
182187
* @method static ElasticCollection get($columns = ['*'])
183188
* @method static ElasticCollection insert($values, $returnData = null)
189+
* @method static self createOnly()
190+
* @method static self createOrFail(array $attributes)
184191
*-----------------------------------
185192
* @method static array toDsl($columns = ['*'])
186193
* @method static array toSql($columns = ['*'])

0 commit comments

Comments
 (0)