Skip to content

Commit 3a23e91

Browse files
committed
Pagination
1 parent f8a4ace commit 3a23e91

File tree

4 files changed

+121
-22
lines changed

4 files changed

+121
-22
lines changed

src/Scout/ScoutEngine.php

+19-12
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
use TypeError;
2626

2727
use function array_column;
28-
use function array_filter;
2928
use function array_flip;
3029
use function array_map;
3130
use function array_merge;
@@ -146,10 +145,7 @@ public function delete($models): void
146145
*/
147146
public function search(Builder $builder)
148147
{
149-
return $this->performSearch($builder, array_filter([
150-
'filters' => $this->filters($builder),
151-
'limit' => $builder->limit,
152-
]));
148+
return $this->performSearch($builder);
153149
}
154150

155151
/**
@@ -165,17 +161,16 @@ public function paginate(Builder $builder, $perPage, $page)
165161
assert(is_int($perPage), new TypeError(sprintf('Argument #2 ($perPage) must be of type int, %s given', get_debug_type($perPage))));
166162
assert(is_int($page), new TypeError(sprintf('Argument #3 ($page) must be of type int, %s given', get_debug_type($page))));
167163

168-
return $this->performSearch($builder, array_filter([
169-
'filters' => $this->filters($builder),
170-
'limit' => (int) $perPage,
171-
'offset' => ($page - 1) * $perPage,
172-
]));
164+
$builder = clone $builder;
165+
$builder->take($perPage);
166+
167+
return $this->performSearch($builder, $perPage * ($page - 1));
173168
}
174169

175170
/**
176171
* Perform the given search on the engine.
177172
*/
178-
protected function performSearch(Builder $builder, array $searchParams = []): array
173+
protected function performSearch(Builder $builder, ?int $offset = null): array
179174
{
180175
$collection = $this->getSearchableCollection($builder->model);
181176

@@ -184,7 +179,7 @@ protected function performSearch(Builder $builder, array $searchParams = []): ar
184179
$builder->callback,
185180
$collection,
186181
$builder->query,
187-
$searchParams,
182+
$offset,
188183
);
189184

190185
return $result instanceof Cursor ? $result->toArray() : $result;
@@ -215,6 +210,18 @@ protected function performSearch(Builder $builder, array $searchParams = []): ar
215210
],
216211
];
217212

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+
217+
if ($builder->limit) {
218+
$pipeline[] = ['$limit' => $builder->limit];
219+
}
220+
221+
if ($offset) {
222+
$pipeline[] = ['$skip' => $offset];
223+
}
224+
218225
$options = [
219226
'allowDiskUse' => true,
220227
'typeMap' => ['root' => 'object', 'document' => 'array', 'array' => 'array'],

tests/Models/SearchableModel.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ public function indexableAs(): string
2929

3030
public function getScoutKey(): string
3131
{
32-
return 'key_' . $this->id;
32+
return $this->getAttribute($this->getScoutKeyName()) ?: 'key_' . $this->getKey();
33+
}
34+
35+
public function getScoutKeyName(): string
36+
{
37+
return 'scout_key';
3338
}
3439
}

tests/Scout/ScoutEngineTest.php

+95-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use DateTimeImmutable;
88
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
99
use Laravel\Scout\Builder;
10+
use Laravel\Scout\Jobs\RemoveFromSearch;
11+
use Laravel\Scout\Tests\Unit\AlgoliaEngineTest;
1012
use Mockery as m;
1113
use MongoDB\BSON\Document;
1214
use MongoDB\BSON\Regex;
@@ -20,9 +22,14 @@
2022
use MongoDB\Model\BSONDocument;
2123
use PHPUnit\Framework\Attributes\DataProvider;
2224

25+
use function serialize;
26+
use function unserialize;
27+
2328
/** Unit tests that do not require an Atlas Search cluster */
2429
class ScoutEngineTest extends TestCase
2530
{
31+
private const EXPECTED_SEARCH_OPTIONS = ['allowDiskUse' => true, 'typeMap' => ['root' => 'object', 'document' => 'array', 'array' => 'array']];
32+
2633
/** @param callable(): Builder $builder */
2734
#[DataProvider('provideSearchPipelines')]
2835
public function testSearch(Closure $builder, array $expectedPipeline): void
@@ -36,8 +43,7 @@ public function testSearch(Closure $builder, array $expectedPipeline): void
3643
$cursor = m::mock(CursorInterface::class);
3744
$cursor->shouldReceive('toArray')->once()->with()->andReturn($data);
3845

39-
$options = ['allowDiskUse' => true, 'typeMap' => ['root' => 'object', 'document' => 'array', 'array' => 'array']];
40-
$collection->shouldReceive('aggregate')->once()->with($expectedPipeline, $options)->andReturn($cursor);
46+
$collection->shouldReceive('aggregate')->once()->with($expectedPipeline, self::EXPECTED_SEARCH_OPTIONS)->andReturn($cursor);
4147

4248
$engine = new ScoutEngine($database, softDelete: false, prefix: '');
4349
$result = $engine->search($builder());
@@ -157,6 +163,66 @@ function () {
157163

158164
public function testPaginate()
159165
{
166+
$perPage = 5;
167+
$page = 3;
168+
169+
$database = m::mock(Database::class);
170+
$collection = m::mock(Collection::class);
171+
$cursor = m::mock(CursorInterface::class);
172+
$database->shouldReceive('selectCollection')
173+
->with('table_searchable')
174+
->andReturn($collection);
175+
$collection->shouldReceive('aggregate')
176+
->once()
177+
->withArgs(function (...$args) {
178+
self::assertSame([
179+
[
180+
'$search' => [
181+
'index' => 'scout',
182+
'text' => [
183+
'query' => 'mustang',
184+
'path' => [
185+
'wildcard' => '*',
186+
],
187+
'fuzzy' => [
188+
'maxEdits' => 2,
189+
],
190+
],
191+
'count' => [
192+
'type' => 'lowerBound',
193+
],
194+
'sort' => [
195+
'name' => -1,
196+
],
197+
],
198+
],
199+
[
200+
'$addFields' => [
201+
'search_meta' => '$$SEARCH_META',
202+
],
203+
],
204+
[
205+
'$limit' => 5,
206+
],
207+
[
208+
'$skip' => 10,
209+
],
210+
], $args[0]);
211+
212+
$this->assertSame(self::EXPECTED_SEARCH_OPTIONS, $args[1]);
213+
214+
return true;
215+
})
216+
->andReturn($cursor);
217+
$cursor->shouldReceive('toArray')
218+
->once()
219+
->with()
220+
->andReturn([['_id' => 'key_1'], ['_id' => 'key_2']]);
221+
222+
$engine = new ScoutEngine($database, softDelete: false, prefix: '');
223+
$builder = new Builder(new SearchableModel(), 'mustang');
224+
$builder->orderBy('name', 'desc');
225+
$engine->paginate($builder, $perPage, $page);
160226
}
161227

162228
#[DataProvider('provideResultsForMapIds')]
@@ -254,18 +320,17 @@ public function testUpdateWithSoftDelete(): void
254320
[
255321
'updateOne' => [
256322
['_id' => 'key_1'],
257-
['$setOnInsert' => ['_id' => 'key_1'], '$set' => ['id' => 1, 'date' => new UTCDateTime($date)]],
323+
['$setOnInsert' => ['_id' => 'key_1'], '$set' => ['id' => 1]],
258324
['upsert' => true],
259325
],
260326
],
261327
]);
262328

329+
$model = new SearchableModel(['id' => 1]);
330+
$model->delete();
331+
263332
$engine = new ScoutEngine($database, softDelete: false, prefix: '');
264-
$engine->update(EloquentCollection::make([
265-
(new SearchableModel([
266-
'id' => 1,
267-
])),
268-
]));
333+
$engine->update(EloquentCollection::make([$model]));
269334
}
270335

271336
public function testDelete(): void
@@ -286,6 +351,28 @@ public function testDelete(): void
286351
]));
287352
}
288353

354+
/** @see AlgoliaEngineTest::test_delete_with_removeable_scout_collection_using_custom_search_key */
355+
public function testDeleteWithRemoveableScoutCollection(): void
356+
{
357+
$job = new RemoveFromSearch(EloquentCollection::make([
358+
new SearchableModel(['id' => 5, 'scout_key' => 'key_5']),
359+
]));
360+
361+
$job = unserialize(serialize($job));
362+
363+
$database = m::mock(Database::class);
364+
$collection = m::mock(Collection::class);
365+
$database->shouldReceive('selectCollection')
366+
->with('table_indexable')
367+
->andReturn($collection);
368+
$collection->shouldReceive('deleteMany')
369+
->once()
370+
->with(['_id' => ['$in' => ['key_5']]]);
371+
372+
$engine = new ScoutEngine($database, softDelete: false, prefix: 'ignored_prefix_');
373+
$engine->delete($job->models);
374+
}
375+
289376
public function testDeleteAll(): void
290377
{
291378
$collectionNames = ['scout-prefix-table1', 'scout-prefix-table2'];

tests/Scout/SearchableTest.php tests/Scout/ScoutIntegrationTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use function Orchestra\Testbench\artisan;
1010
use function sleep;
1111

12-
class SearchableTest extends TestCase
12+
class ScoutIntegrationTest extends TestCase
1313
{
1414
use SearchableTests {
1515
defineScoutDatabaseMigrations as baseDefineScoutDatabaseMigrations;

0 commit comments

Comments
 (0)