Skip to content

Commit 226c4bf

Browse files
committed
Port v5.1/v5.2 features + fix BulkInsertQueryException
- withRefresh()/withoutRefresh(): control refresh behavior on writes (true/false/'wait_for') - withOpType()/createOnly()/createOrFail(): dedupe semantics via op_type=create, 409 on duplicates - withTrackTotalHits()/withoutTrackTotalHits(): override default 10k hit count cap - searchQueryString() + or/not variants: full query_string DSL support with QueryStringOptions - Fix BulkInsertQueryException: use array_key_first() for action key (supports 'create' not just 'index'), add inferStatusCode for proper 409s - Fix geo bounding box test (wrong field name + double .get()) - Add QueryStringOptions, SimpleQueryStringOptions - Add CreateOpTypeTest (3 tests), QueryStringTest (20 tests)
1 parent 14db09a commit 226c4bf

11 files changed

Lines changed: 683 additions & 32 deletions

src/Eloquent/Builder.php

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,9 +397,25 @@ public function findOrNew($id, $columns = ['*']): Model
397397
return $model;
398398
}
399399

400-
public function withoutRefresh()
400+
public function withoutRefresh(): Model
401401
{
402-
$this->model->options()->add('refresh', false);
402+
return $this->withRefresh(false);
403+
}
404+
405+
/**
406+
* Explicitly control the OpenSearch refresh behavior for write ops.
407+
* Accepts: true, false, or 'wait_for'.
408+
*/
409+
public function withRefresh(bool|string $refresh): Model
410+
{
411+
$this->model->options()->add('refresh', $refresh);
412+
413+
return $this->model;
414+
}
415+
416+
public function withOpType(string $value)
417+
{
418+
$this->model->options()->add('op_type', $value);
403419

404420
return $this->model;
405421
}
@@ -658,6 +674,26 @@ public function rawDsl($dsl): array
658674
// Protected
659675
// ----------------------------------------------------------------------
660676

677+
/**
678+
* Force insert operations to use op_type=create for dedupe semantics.
679+
* When set, attempts to create an existing _id will fail with a 409 from OpenSearch.
680+
*/
681+
public function createOnly(): Model
682+
{
683+
$this->withOpType('create');
684+
685+
return $this->model;
686+
}
687+
688+
/**
689+
* Convenience method to perform a create-only insert and surface 409s as exceptions.
690+
* Accepts single document attributes or an array of documents.
691+
*/
692+
public function createOrFail(array $attributes)
693+
{
694+
return $this->createOnly()->create($attributes);
695+
}
696+
661697
protected function loadRelations($models, $builder)
662698
{
663699
if (count($models) > 0) {

src/Exceptions/BulkInsertQueryException.php

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class BulkInsertQueryException extends LaravelOpenSearchException
1313
*/
1414
public function __construct($queryResult)
1515
{
16-
parent::__construct($this->formatMessage($queryResult), 400);
16+
parent::__construct($this->formatMessage($queryResult), $this->inferStatusCode($queryResult));
1717
}
1818

1919
/**
@@ -28,12 +28,16 @@ private function formatMessage(array $result): string
2828
// Clean that ish up.
2929
$items = collect($result['items'] ?? [])
3030
->filter(function (array $item) {
31-
return $item['index'] && ! empty($item['index']['error']);
31+
$action = array_key_first($item) ?? 'index';
32+
33+
return isset($item[$action]) && ! empty($item[$action]['error']);
3234
})
3335
->map(function (array $item) {
34-
return $item['index'];
36+
$action = array_key_first($item) ?? 'index';
37+
38+
return $item[$action];
3539
})
36-
// reduce to max limit
40+
// reduce to max limit
3741
->slice(0, $this->errorLimit)
3842
->values();
3943

@@ -42,11 +46,28 @@ private function formatMessage(array $result): string
4246
$message->push('Bulk Insert Errors ('.'Showing '.$items->count().' of '.$totalErrors->count().'):');
4347

4448
$items = $items->map(function (array $item) {
45-
return "{$item['_id']}: {$item['error']['reason']}";
49+
$id = $item['_id'] ?? 'unknown';
50+
$reason = $item['error']['reason'] ?? 'unknown error';
51+
$type = $item['error']['type'] ?? 'error';
52+
53+
return "$id: [$type] $reason";
4654
})->values()->toArray();
4755

4856
$message->push(...$items);
4957

5058
return $message->implode(PHP_EOL);
5159
}
60+
61+
private function inferStatusCode(array $result): int
62+
{
63+
foreach ($result['items'] ?? [] as $item) {
64+
$action = array_key_first($item) ?? 'index';
65+
$error = $item[$action]['error'] ?? null;
66+
if (is_array($error) && ($error['type'] ?? '') === 'version_conflict_engine_exception') {
67+
return 409;
68+
}
69+
}
70+
71+
return 400;
72+
}
5273
}

src/Query/Builder.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2411,6 +2411,74 @@ public function withAnalyzer(string $analyzer): self
24112411
return $this;
24122412
}
24132413

2414+
public function withTrackTotalHits(bool|int|null $val = true): self
2415+
{
2416+
if ($val === null) {
2417+
return $this->withoutTrackTotalHits();
2418+
}
2419+
$this->bodyParameters['track_total_hits'] = $val;
2420+
2421+
return $this;
2422+
}
2423+
2424+
public function withoutTrackTotalHits(): self
2425+
{
2426+
unset($this->bodyParameters['track_total_hits']);
2427+
2428+
return $this;
2429+
}
2430+
2431+
// ----------------------------------------------------------------------
2432+
// Query String Queries
2433+
// ----------------------------------------------------------------------
2434+
2435+
/**
2436+
* Add a 'query_string' statement to query
2437+
*
2438+
* @throws Exception
2439+
*/
2440+
public function searchQueryString(mixed $query, mixed $columns = null, $options = []): self
2441+
{
2442+
return $this->buildQueryStringWheres($columns, $query, 'and', false, $options);
2443+
}
2444+
2445+
/**
2446+
* @throws Exception
2447+
*/
2448+
public function orSearchQueryString(mixed $query, mixed $columns = null, $options = []): self
2449+
{
2450+
return $this->buildQueryStringWheres($columns, $query, 'or', false, $options);
2451+
}
2452+
2453+
/**
2454+
* @throws Exception
2455+
*/
2456+
public function searchNotQueryString(mixed $query, mixed $columns = null, $options = []): self
2457+
{
2458+
return $this->buildQueryStringWheres($columns, $query, 'and', true, $options);
2459+
}
2460+
2461+
/**
2462+
* @throws Exception
2463+
*/
2464+
public function orSearchNotQueryString(mixed $query, mixed $columns = null, $options = []): self
2465+
{
2466+
return $this->buildQueryStringWheres($columns, $query, 'or', true, $options);
2467+
}
2468+
2469+
/**
2470+
* @throws Exception
2471+
*/
2472+
protected function buildQueryStringWheres($columns, $value, $boolean, $not, $options): self
2473+
{
2474+
$type = 'QueryString';
2475+
[$columns, $options] = $this->extractSearch($columns, $options, 'querystring');
2476+
$options = $this->setOptions($options, 'querystring')->toArray();
2477+
$this->wheres[] = compact('columns', 'value', 'type', 'boolean', 'not', 'options');
2478+
2479+
return $this;
2480+
}
2481+
24142482
// ----------------------------------------------------------------------
24152483
// Internal Operations
24162484
// ----------------------------------------------------------------------

src/Query/Grammar.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ public function compileInsert($query, array $values): array
9292
unset($doc['id'], $doc['_id']);
9393
}
9494

95+
if (! empty($doc['_op_type'])) {
96+
$options['op_type'] = $doc['_op_type'];
97+
unset($doc['_op_type']);
98+
} elseif ($optType = $query->getOption('op_type')) {
99+
$options['op_type'] = $optType;
100+
}
101+
95102
// Add the document index operation
96103
$index = DslFactory::indexOperation(
97104
index: $query->getFrom(),
@@ -717,6 +724,15 @@ protected function compileWhereSearch(Builder $builder, array $where): array
717724

718725
}
719726

727+
private function compileWhereQueryString(Builder $builder, array $where)
728+
{
729+
$fields = $where['columns'];
730+
$query = $where['value'];
731+
$options = $where['options'] ?? [];
732+
733+
return DslFactory::queryString($query, $fields, $options);
734+
}
735+
720736
/**
721737
* Compile a child clause
722738
*/

src/Query/ManagesOptions.php

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PDPhilip\OpenSearch\Query\Options\PhraseOptions;
1414
use PDPhilip\OpenSearch\Query\Options\PhrasePrefixOptions;
1515
use PDPhilip\OpenSearch\Query\Options\PrefixOptions;
16+
use PDPhilip\OpenSearch\Query\Options\QueryStringOptions;
1617
use PDPhilip\OpenSearch\Query\Options\RegexOptions;
1718
use PDPhilip\OpenSearch\Query\Options\SearchOptions;
1819
use PDPhilip\OpenSearch\Query\Options\TermOptions;
@@ -74,25 +75,16 @@ public function extractOptionsWithNot($type, $column, $value, $boolean, $not, $o
7475
return [$column, $value, $not, $boolean, $options];
7576
}
7677

77-
public function extractSearch($columns = null, $options = []): array
78+
public function extractSearch($columns = null, $options = [], $as = 'search'): array
7879
{
7980
if ($options) {
8081
return [$columns, $options];
8182
}
8283
if (is_callable($columns) && ! is_string($columns)) {
83-
$options = $columns;
84-
$columns = null;
85-
86-
return [$columns, $options];
84+
return [null, $columns];
8785
}
88-
if (is_array($columns)) {
89-
$isOptions = $this->validatePossibleOptions($columns, 'search');
90-
if ($isOptions) {
91-
$options = $columns;
92-
$columns = null;
93-
94-
return [$columns, $options];
95-
}
86+
if (is_array($columns) && $this->validatePossibleOptions($columns, $as)) {
87+
return [null, $columns];
9688
}
9789

9890
return [$columns, $options];
@@ -219,6 +211,7 @@ protected function getOptionClass($type)
219211
'prefix' => PrefixOptions::class,
220212
'regex' => RegexOptions::class,
221213
'nested' => NestedOptions::class,
214+
'querystring' => QueryStringOptions::class,
222215
default => null
223216
};
224217
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace PDPhilip\OpenSearch\Query\Options;
4+
5+
/**
6+
* QueryStringOptions for Query String Queries.
7+
*
8+
*
9+
* @method $this type(string $value) //Options: best_fields, most_fields, cross_fields, phrase, phrase_prefix, bool_prefix
10+
* @method $this allowLeadingWildcard(bool $value)
11+
* @method $this analyzeWildcard(bool $value)
12+
* @method $this analyzer(string $analyzer)
13+
* @method $this autoGenerateSynonymsPhraseQuery(bool $value)
14+
* @method $this boost(float $value)
15+
* @method $this defaultOperator(string $value) OR|AND
16+
* @method $this fuzziness(string|int $value)
17+
* @method $this fuzzyMaxExpansions(int $value)
18+
* @method $this fuzzyPrefixLength(int $value)
19+
* @method $this fuzzyTranspositions(bool $value)
20+
* @method $this fuzzyRewrite(int $value)
21+
* @method $this lenient(bool $value)
22+
* @method $this maxDeterminizedStates(int $value)
23+
* @method $this minimumShouldMatch(string $value)
24+
* @method $this quoteAnalyzer(string $value)
25+
* @method $this phraseSlop(int $value)
26+
* @method $this quoteFieldSuffix(string $value)
27+
* @method $this rewrite(string $value)
28+
* @method $this timeZone(string $value)
29+
*/
30+
class QueryStringOptions extends QueryOptions
31+
{
32+
public function allowedOptions(): array
33+
{
34+
return [
35+
'type',
36+
'allow_leading_wildcard',
37+
'analyze_wildcard',
38+
'analyzer',
39+
'auto_generate_synonyms_phrase_query',
40+
'boost',
41+
'default_operator',
42+
'fuzziness',
43+
'fuzzy_max_expansions',
44+
'fuzzy_prefix_length',
45+
'fuzzy_transpositions',
46+
'fuzzy_rewrite',
47+
'lenient',
48+
'max_determinized_states',
49+
'minimum_should_match',
50+
'quote_analyzer',
51+
'phrase_slop',
52+
'quote_field_suffix',
53+
'rewrite',
54+
'time_zone',
55+
];
56+
}
57+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace PDPhilip\OpenSearch\Query\Options;
4+
5+
/**
6+
* SimpleQueryStringOptions for Simple Query String queries.
7+
*
8+
* Mirrors OpenSearch simple_query_string parameters:
9+
* - Safer parsing (ignores invalid syntax)
10+
* - Supports flags, boosting, default operator, wildcard analysis, etc.
11+
*
12+
* @method $this flags(string $value) // e.g. "ALL", "AND|OR|NOT|PHRASE|PREFIX|PRECEDENCE|ESCAPE|WHITESPACE|FUZZY|NEAR|SLOP"
13+
* @method $this defaultOperator(string $value) // "OR" | "AND"
14+
* @method $this analyzeWildcard(bool $value)
15+
* @method $this analyzer(string $analyzer)
16+
* @method $this autoGenerateSynonymsPhraseQuery(bool $value)
17+
* @method $this boost(float $value)
18+
* @method $this fuzzyMaxExpansions(int $value)
19+
* @method $this fuzzyPrefixLength(int $value)
20+
* @method $this fuzzyTranspositions(bool $value)
21+
* @method $this lenient(bool $value)
22+
* @method $this minimumShouldMatch(string $value)
23+
* @method $this quoteFieldSuffix(string $value)
24+
*/
25+
class SimpleQueryStringOptions extends QueryOptions
26+
{
27+
public function allowedOptions(): array
28+
{
29+
return [
30+
'flags',
31+
'default_operator',
32+
'analyze_wildcard',
33+
'analyzer',
34+
'auto_generate_synonyms_phrase_query',
35+
'boost',
36+
'fuzzy_max_expansions',
37+
'fuzzy_prefix_length',
38+
'fuzzy_transpositions',
39+
'lenient',
40+
'minimum_should_match',
41+
'quote_field_suffix',
42+
];
43+
}
44+
}

0 commit comments

Comments
 (0)