Skip to content

Commit 2fde08d

Browse files
committed
Support OpenSearch Scout search options
1 parent 7f8d729 commit 2fde08d

2 files changed

Lines changed: 228 additions & 3 deletions

File tree

src/Factories/SearchRequestFactory.php

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use DirectoryTree\OpenSearchAdapter\Search\SearchRequest as OpenSearchRequest;
66
use DirectoryTree\OpenSearchScoutDriver\SearchRequest;
7+
use Illuminate\Contracts\Support\Arrayable;
78
use Laravel\Scout\Builder;
89
use stdClass;
910

@@ -19,23 +20,78 @@ class SearchRequestFactory implements SearchRequestFactoryInterface
1920
*/
2021
public function makeFromBuilder(Builder $builder, array $options = []): SearchRequest
2122
{
23+
if ($compiled = $this->compileBuilder($builder, $options)) {
24+
return $this->makeFromCompiled($builder, $compiled);
25+
}
26+
2227
$request = new OpenSearchRequest($this->makeQuery($builder));
2328

2429
if ($sort = $this->makeSort($builder)) {
2530
$request->sort($sort);
2631
}
2732

28-
if ($from = $this->makeFrom($options)) {
33+
if (! is_null($from = $this->makeFrom($options))) {
2934
$request->from($from);
3035
}
3136

32-
if ($size = $this->makeSize($builder, $options)) {
37+
if (! is_null($size = $this->makeSize($builder, $options))) {
3338
$request->size($size);
3439
}
3540

41+
$this->applyOptions($request, $builder->options);
42+
43+
return new SearchRequest($this->makeIndex($builder), $request);
44+
}
45+
46+
/**
47+
* Make an OpenSearch request from a compiled builder payload.
48+
*
49+
* @param array{query?: array<string, mixed>|null, sort?: array<int|string, mixed>|null, from?: int|null, size?: int|string|null, aggs?: array<string, mixed>|null, aggregations?: array<string, mixed>|null} $compiled
50+
*/
51+
protected function makeFromCompiled(Builder $builder, array $compiled): SearchRequest
52+
{
53+
$request = new OpenSearchRequest($compiled['query'] ?? []);
54+
55+
if (! empty($compiled['sort'])) {
56+
$request->sort($compiled['sort']);
57+
}
58+
59+
if (! empty($compiled['from'])) {
60+
$request->from((int) $compiled['from']);
61+
}
62+
63+
if (isset($compiled['size']) && is_numeric($compiled['size'])) {
64+
$request->size((int) $compiled['size']);
65+
}
66+
67+
if (! empty($compiled['aggs'])) {
68+
$request->aggregations($compiled['aggs']);
69+
}
70+
71+
if (! empty($compiled['aggregations'])) {
72+
$request->aggregations($compiled['aggregations']);
73+
}
74+
75+
$this->applyOptions($request, $builder->options);
76+
3677
return new SearchRequest($this->makeIndex($builder), $request);
3778
}
3879

80+
/**
81+
* Compile builders that expose their own OpenSearch payload.
82+
*
83+
* @param array<string, mixed> $options
84+
* @return array<string, mixed>|null
85+
*/
86+
protected function compileBuilder(Builder $builder, array $options): ?array
87+
{
88+
if (! $builder instanceof Arrayable) {
89+
return null;
90+
}
91+
92+
return $builder->toArray($options);
93+
}
94+
3995
/**
4096
* Get the OpenSearch index name for the builder.
4197
*/
@@ -139,6 +195,72 @@ protected function makeFrom(array $options): ?int
139195
*/
140196
protected function makeSize(Builder $builder, array $options): ?int
141197
{
142-
return $options['perPage'] ?? $builder->limit;
198+
$size = $options['perPage'] ?? $builder->limit;
199+
200+
return is_numeric($size) ? (int) $size : null;
201+
}
202+
203+
/**
204+
* Apply Scout builder options to the OpenSearch search request.
205+
*
206+
* @param array<string, mixed> $options
207+
*/
208+
protected function applyOptions(OpenSearchRequest $request, array $options): void
209+
{
210+
if (isset($options['highlight'])) {
211+
$request->highlight($options['highlight']);
212+
}
213+
214+
if (isset($options['rescore'])) {
215+
$request->rescore($options['rescore']);
216+
}
217+
218+
if (isset($options['suggest'])) {
219+
$request->suggest($options['suggest']);
220+
}
221+
222+
if (isset($options['collapse'])) {
223+
$request->collapse($options['collapse']);
224+
}
225+
226+
if (isset($options['aggregations'])) {
227+
$request->aggregations($options['aggregations']);
228+
}
229+
230+
if (isset($options['post_filter'])) {
231+
$request->postFilter($options['post_filter']);
232+
}
233+
234+
if (isset($options['indices_boost'])) {
235+
$request->indicesBoost($options['indices_boost']);
236+
}
237+
238+
if (isset($options['min_score'])) {
239+
$request->minScore($options['min_score']);
240+
}
241+
242+
if (isset($options['script_fields'])) {
243+
$request->scriptFields($options['script_fields']);
244+
}
245+
246+
if (isset($options['search_type'])) {
247+
$request->searchType($options['search_type']);
248+
}
249+
250+
if (isset($options['preference'])) {
251+
$request->preference($options['preference']);
252+
}
253+
254+
if (array_key_exists('_source', $options)) {
255+
$request->source($options['_source']);
256+
}
257+
258+
if (array_key_exists('track_total_hits', $options)) {
259+
$request->trackTotalHits($options['track_total_hits']);
260+
}
261+
262+
if (array_key_exists('track_scores', $options)) {
263+
$request->trackScores($options['track_scores']);
264+
}
143265
}
144266
}

tests/Unit/Factories/SearchRequestFactoryTest.php

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

33
use DirectoryTree\OpenSearchScoutDriver\Factories\SearchRequestFactory;
44
use DirectoryTree\OpenSearchScoutDriver\Tests\Fixtures\Client;
5+
use Illuminate\Contracts\Support\Arrayable;
56
use Laravel\Scout\Builder;
67

78
it('creates search requests with empty query strings', function () {
@@ -79,6 +80,15 @@
7980
expect($request->toArray()['body']['size'])->toBe(10);
8081
});
8182

83+
it('creates search requests with zero limits', function () {
84+
$builder = new Builder(new Client, 'john');
85+
$builder->take(0);
86+
87+
$request = (new SearchRequestFactory)->makeFromBuilder($builder);
88+
89+
expect($request->toArray()['body']['size'])->toBe(0);
90+
});
91+
8292
it('creates search requests with pagination', function () {
8393
$request = (new SearchRequestFactory)->makeFromBuilder(new Builder(new Client, 'john'), [
8494
'page' => 3,
@@ -88,3 +98,96 @@
8898
expect($request->toArray()['body']['from'])->toBe(60)
8999
->and($request->toArray()['body']['size'])->toBe(30);
90100
});
101+
102+
it('creates search requests with opensearch body options', function () {
103+
$builder = (new Builder(new Client, 'john'))->options([
104+
'highlight' => ['fields' => ['name' => new stdClass]],
105+
'rescore' => ['window_size' => 50],
106+
'suggest' => ['name_suggest' => ['text' => 'john']],
107+
'_source' => false,
108+
'collapse' => ['field' => 'email'],
109+
'aggregations' => ['emails' => ['terms' => ['field' => 'email']]],
110+
'post_filter' => ['term' => ['active' => true]],
111+
'track_total_hits' => true,
112+
'indices_boost' => [['clients' => 1.5]],
113+
'track_scores' => false,
114+
'min_score' => 1.25,
115+
'script_fields' => ['score_name' => ['script' => ['source' => 'doc["name"].value']]],
116+
]);
117+
118+
$request = (new SearchRequestFactory)->makeFromBuilder($builder)->toArray();
119+
120+
expect($request['body']['highlight'])->toEqual(['fields' => ['name' => new stdClass]])
121+
->and($request['body']['rescore'])->toBe(['window_size' => 50])
122+
->and($request['body']['suggest'])->toBe(['name_suggest' => ['text' => 'john']])
123+
->and($request['body']['_source'])->toBeFalse()
124+
->and($request['body']['collapse'])->toBe(['field' => 'email'])
125+
->and($request['body']['aggregations'])->toBe(['emails' => ['terms' => ['field' => 'email']]])
126+
->and($request['body']['post_filter'])->toBe(['term' => ['active' => true]])
127+
->and($request['body']['track_total_hits'])->toBeTrue()
128+
->and($request['body']['indices_boost'])->toBe([['clients' => 1.5]])
129+
->and($request['body']['track_scores'])->toBeFalse()
130+
->and($request['body']['min_score'])->toBe(1.25)
131+
->and($request['body']['script_fields'])->toBe(['score_name' => ['script' => ['source' => 'doc["name"].value']]]);
132+
});
133+
134+
it('creates search requests with opensearch top-level options', function () {
135+
$builder = (new Builder(new Client, 'john'))->options([
136+
'search_type' => 'dfs_query_then_fetch',
137+
'preference' => '_local',
138+
]);
139+
140+
$request = (new SearchRequestFactory)->makeFromBuilder($builder)->toArray();
141+
142+
expect($request['search_type'])->toBe('dfs_query_then_fetch')
143+
->and($request['preference'])->toBe('_local');
144+
});
145+
146+
it('creates search requests from compiled arrayable builders', function () {
147+
$builder = new class(new Client, null) extends Builder implements Arrayable
148+
{
149+
/**
150+
* Compile the query into its array form.
151+
*
152+
* @param array<string, mixed> $options
153+
* @return array<string, mixed>
154+
*/
155+
public function toArray(array $options = []): array
156+
{
157+
return [
158+
'query' => [],
159+
'sort' => [
160+
['foo' => ['order' => 'desc']],
161+
],
162+
'from' => ($options['page'] - 1) * $options['perPage'],
163+
'size' => $options['perPage'],
164+
'aggs' => [
165+
'emails' => ['terms' => ['field' => 'email']],
166+
],
167+
];
168+
}
169+
};
170+
171+
$builder->options([
172+
'track_total_hits' => 5_000_000,
173+
]);
174+
175+
$request = (new SearchRequestFactory)->makeFromBuilder($builder, [
176+
'page' => 2,
177+
'perPage' => 0,
178+
])->toArray();
179+
180+
expect($request)->toEqual([
181+
'body' => [
182+
'sort' => [
183+
['foo' => ['order' => 'desc']],
184+
],
185+
'size' => 0,
186+
'aggregations' => [
187+
'emails' => ['terms' => ['field' => 'email']],
188+
],
189+
'track_total_hits' => 5_000_000,
190+
],
191+
'index' => 'clients',
192+
]);
193+
});

0 commit comments

Comments
 (0)