Skip to content

Commit 45734c6

Browse files
committed
Implement where/whereIn/whereNotIn
1 parent 3a23e91 commit 45734c6

File tree

2 files changed

+144
-53
lines changed

2 files changed

+144
-53
lines changed

src/Scout/ScoutEngine.php

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
use Laravel\Scout\Builder;
1313
use Laravel\Scout\Engines\Engine;
1414
use Laravel\Scout\Searchable;
15+
use LogicException;
1516
use MongoDB\BSON\Regex;
1617
use MongoDB\BSON\Serializable;
1718
use MongoDB\BSON\UTCDateTime;
1819
use MongoDB\Collection as MongoDBCollection;
1920
use MongoDB\Database;
2021
use MongoDB\Driver\Cursor;
22+
use MongoDB\Driver\CursorInterface;
2123
use MongoDB\Driver\Exception\ServerException;
2224
use MongoDB\Exception\Exception;
2325
use MongoDB\Exception\RuntimeException;
@@ -181,39 +183,43 @@ protected function performSearch(Builder $builder, ?int $offset = null): array
181183
$builder->query,
182184
$offset,
183185
);
186+
assert($result instanceof CursorInterface || is_array($result), new LogicException(sprintf('The search builder closure must return an array or a MongoDB cursor, %s returned', get_debug_type($result))));
184187

185-
return $result instanceof Cursor ? $result->toArray() : $result;
188+
return $result instanceof CursorInterface ? $result->toArray() : $result;
189+
}
190+
191+
$compound = [];
192+
193+
// Query String
194+
195+
foreach ($builder->wheres as $field => $value) {
196+
$compound['filter']['equals'][] = ['path' => $field, 'value' => $value];
197+
}
198+
199+
foreach ($builder->whereIns as $field => $value) {
200+
$compound['filter']['in'][] = ['path' => $field, 'value' => $value];
201+
}
202+
203+
foreach ($builder->whereNotIns as $field => $value) {
204+
$compound['mustNot']['in'][] = ['path' => $field, 'value' => $value];
186205
}
187206

188207
$pipeline = [
189208
[
190209
'$search' => [
191210
'index' => self::INDEX_NAME,
192-
'text' => [
193-
'query' => $builder->query,
194-
'path' => ['wildcard' => '*'],
195-
'fuzzy' => [
196-
'maxEdits' => 2,
197-
],
198-
],
199-
// Get an estimate of the total count
200-
'count' => [
201-
'type' => 'lowerBound',
202-
],
211+
'compound' => $compound,
212+
'count' => ['type' => 'lowerBound'],
213+
'sort' => array_merge(...array_map(fn ($order) => [$order['column'] => $order['direction'] === 'asc' ? 1 : -1], $builder->orders)),
203214
],
204215
],
205-
// Add metadata to the results
206216
[
207217
'$addFields' => [
208218
'search_meta' => '$$SEARCH_META',
209219
],
210220
],
211221
];
212222

213-
if ($builder->orders) {
214-
$pipeline[0]['$search']['sort'] = array_merge(...array_map(fn ($order) => [$order['column'] => $order['direction'] === 'asc' ? 1 : -1], $builder->orders));
215-
}
216-
217223
if ($builder->limit) {
218224
$pipeline[] = ['$limit' => $builder->limit];
219225
}
@@ -239,12 +245,13 @@ protected function filters(Builder $builder): array
239245
{
240246
$filters = $builder->wheres;
241247

248+
// https://www.mongodb.com/docs/atlas/atlas-search/in/
242249
foreach ($builder->whereIns as $field => $values) {
243-
$filters[$field] = ['$in' => $values];
250+
$filters['in'][] = ['path' => $field, 'value' => $values];
244251
}
245252

246253
foreach ($builder->whereNotIns as $field => $values) {
247-
$filters[$field] = ['$nin' => $values];
254+
$filters['in'][] = ['path' => $field, 'value' => $values];
248255
}
249256

250257
return $filters;

tests/Scout/ScoutEngineTest.php

Lines changed: 118 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use MongoDB\Model\BSONDocument;
2323
use PHPUnit\Framework\Attributes\DataProvider;
2424

25+
use function array_merge_recursive;
2526
use function serialize;
2627
use function unserialize;
2728

@@ -52,37 +53,40 @@ public function testSearch(Closure $builder, array $expectedPipeline): void
5253

5354
public function provideSearchPipelines(): iterable
5455
{
55-
yield 'simple string' => [
56-
fn () => new Builder(new SearchableModel(), ''),
56+
$defaultPipeline = [
5757
[
58-
[
59-
'$search' => [
60-
'index' => 'scout',
61-
'text' => [
62-
'query' => '',
63-
'path' => [
64-
'wildcard' => '*',
65-
],
66-
'fuzzy' => [
67-
'maxEdits' => 2,
68-
],
58+
'$search' => [
59+
'index' => 'scout',
60+
'text' => [
61+
'query' => 'lar',
62+
'path' => [
63+
'wildcard' => '*',
6964
],
70-
'count' => [
71-
'type' => 'lowerBound',
65+
'fuzzy' => [
66+
'maxEdits' => 2,
7267
],
7368
],
74-
],
75-
[
76-
'$addFields' => [
77-
'search_meta' => '$$SEARCH_META',
69+
'count' => [
70+
'type' => 'lowerBound',
7871
],
7972
],
8073
],
74+
[
75+
'$addFields' => [
76+
'search_meta' => '$$SEARCH_META',
77+
],
78+
],
79+
];
80+
81+
yield 'simple string' => [
82+
fn
83+
() => new Builder(new SearchableModel(), 'lar'),
84+
$defaultPipeline,
8185
];
8286

8387
yield 'where conditions' => [
8488
function () {
85-
$builder = new Builder(new SearchableModel(), '');
89+
$builder = new Builder(new SearchableModel(), 'lar');
8690
$builder->where('foo', 'bar');
8791
$builder->where('key', 'value');
8892

@@ -93,20 +97,86 @@ function () {
9397

9498
yield 'where in conditions' => [
9599
function () {
96-
$builder = new Builder(new SearchableModel(), '');
100+
$builder = new Builder(new SearchableModel(), 'lar');
97101
$builder->where('foo', 'bar');
98102
$builder->where('bar', 'baz');
99103
$builder->whereIn('qux', [1, 2]);
100104
$builder->whereIn('quux', [1, 2]);
101105

102106
return $builder;
103107
},
104-
[],
108+
array_merge_recursive($defaultPipeline, [
109+
[
110+
'$search' => [
111+
'compound' => [
112+
'must' => [
113+
[
114+
'text' => [
115+
'query' => 'lar',
116+
'path' => [
117+
'wildcard' => '*',
118+
],
119+
'fuzzy' => [
120+
'maxEdits' => 2,
121+
],
122+
],
123+
],
124+
[
125+
'text' => [
126+
'query' => 'bar',
127+
'path' => [
128+
'wildcard' => '*',
129+
],
130+
'fuzzy' => [
131+
'maxEdits' => 2,
132+
],
133+
],
134+
],
135+
[
136+
'text' => [
137+
'query' => 'baz',
138+
'path' => [
139+
'wildcard' => '*',
140+
],
141+
'fuzzy' => [
142+
'maxEdits' => 2,
143+
],
144+
],
145+
],
146+
],
147+
'should' => [
148+
[
149+
'text' => [
150+
'query' => '1',
151+
'path' => [
152+
'wildcard' => '*',
153+
],
154+
'fuzzy' => [
155+
'maxEdits' => 2,
156+
],
157+
],
158+
],
159+
[
160+
'text' => [
161+
'query' => '2',
162+
'path' => [
163+
'wildcard' => '*',
164+
],
165+
'fuzzy' => [
166+
'maxEdits' => 2,
167+
],
168+
],
169+
],
170+
],
171+
],
172+
],
173+
],
174+
]),
105175
];
106176

107177
yield 'where not in conditions' => [
108178
function () {
109-
$builder = new Builder(new SearchableModel(), '');
179+
$builder = new Builder(new SearchableModel(), 'lar');
110180
$builder->where('foo', 'bar');
111181
$builder->where('bar', 'baz');
112182
$builder->whereIn('qux', [1, 2]);
@@ -115,49 +185,63 @@ function () {
115185

116186
return $builder;
117187
},
118-
[],
188+
$defaultPipeline,
119189
];
120190

121191
yield 'where in conditions without other conditions' => [
122192
function () {
123-
$builder = new Builder(new SearchableModel(), '');
193+
$builder = new Builder(new SearchableModel(), 'lar');
124194
$builder->whereIn('qux', [1, 2]);
125195
$builder->whereIn('quux', [1, 2]);
126196

127197
return $builder;
128198
},
129-
[],
199+
$defaultPipeline,
130200
];
131201

132202
yield 'where not in conditions without other conditions' => [
133203
function () {
134-
$builder = new Builder(new SearchableModel(), '');
204+
$builder = new Builder(new SearchableModel(), 'lar');
135205
$builder->whereIn('qux', [1, 2]);
136206
$builder->whereIn('quux', [1, 2]);
137207
$builder->whereNotIn('eaea', [3]);
138208

139209
return $builder;
140210
},
141-
[],
211+
$defaultPipeline,
142212
];
143213

144214
yield 'empty where in conditions' => [
145215
function () {
146-
$builder = new Builder(new SearchableModel(), '');
216+
$builder = new Builder(new SearchableModel(), 'lar');
147217
$builder->whereIn('qux', [1, 2]);
148218
$builder->whereIn('quux', [1, 2]);
149219
$builder->whereNotIn('eaea', [3]);
150220

151221
return $builder;
152222
},
153-
[],
223+
$defaultPipeline,
154224
];
155225

156226
yield 'exclude soft-deleted' => [
157-
function () {
158-
return new Builder(new SearchableModel(), '', softDelete: true);
159-
},
160-
[],
227+
fn
228+
() => new Builder(new SearchableModel(), '', softDelete: true),
229+
array_merge_recursive(
230+
$defaultPipeline,
231+
['$search' => ['compound' => ['filter' => [['equals' => ['path' => '__soft_deleted', 'value' => 1]]]]]],
232+
),
233+
];
234+
235+
yield 'with callback' => [
236+
fn () => new Builder(new SearchableModel(), 'query', callback: function (...$args) {
237+
$this->assertCount(3, $args);
238+
$this->assertInstanceOf(Collection::class, $args[0]);
239+
$this->assertSame('query', $args[1]);
240+
$this->assertNull($args[2]);
241+
242+
return $args[0]->aggregate(['pipeline'], self::EXPECTED_SEARCH_OPTIONS);
243+
}),
244+
['pipeline'],
161245
];
162246
}
163247

0 commit comments

Comments
 (0)