Skip to content

Commit 4362be5

Browse files
committed
Pagination
1 parent 6b3524a commit 4362be5

File tree

4 files changed

+121
-20
lines changed

4 files changed

+121
-20
lines changed

src/Scout/ScoutEngine.php

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,7 @@ public function delete($models): void
146146
*/
147147
public function search(Builder $builder)
148148
{
149-
return $this->performSearch($builder, array_filter([
150-
'filters' => $this->filters($builder),
151-
'limit' => $builder->limit,
152-
]));
149+
return $this->performSearch($builder);
153150
}
154151

155152
/**
@@ -165,17 +162,16 @@ public function paginate(Builder $builder, $perPage, $page)
165162
assert(is_int($perPage), new TypeError(sprintf('Argument #2 ($perPage) must be of type int, %s given', get_debug_type($perPage))));
166163
assert(is_int($page), new TypeError(sprintf('Argument #3 ($page) must be of type int, %s given', get_debug_type($page))));
167164

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

175171
/**
176172
* Perform the given search on the engine.
177173
*/
178-
protected function performSearch(Builder $builder, array $searchParams = []): array
174+
protected function performSearch(Builder $builder, ?int $offset = null): array
179175
{
180176
$collection = $this->getSearchableCollection($builder->model);
181177

@@ -215,6 +211,18 @@ protected function performSearch(Builder $builder, array $searchParams = []): ar
215211
],
216212
];
217213

214+
if ($builder->orders) {
215+
$pipeline[0]['$search']['sort'] = array_merge(...array_map(fn ($order) => [$order['column'] => $order['direction'] === 'asc' ? 1 : -1], $builder->orders));
216+
}
217+
218+
if ($builder->limit) {
219+
$pipeline[] = ['$limit' => $builder->limit];
220+
}
221+
222+
if ($offset) {
223+
$pipeline[] = ['$skip' => $offset];
224+
}
225+
218226
$options = [
219227
'allowDiskUse' => true,
220228
'typeMap' => ['root' => 'object', 'document' => 'array', 'array' => 'array'],

tests/Models/SearchableModel.php

Lines changed: 6 additions & 1 deletion
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

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
use DateTimeImmutable;
88
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
99
use Laravel\Scout\Builder;
10+
use Laravel\Scout\Engines\MeilisearchEngine;
11+
use Laravel\Scout\Jobs\RemoveFromSearch;
12+
use Laravel\Scout\Tests\Unit\AlgoliaEngineTest;
1013
use Mockery as m;
1114
use MongoDB\BSON\Document;
1215
use MongoDB\BSON\Regex;
@@ -20,9 +23,14 @@
2023
use MongoDB\Model\BSONDocument;
2124
use PHPUnit\Framework\Attributes\DataProvider;
2225

26+
use function serialize;
27+
use function unserialize;
28+
2329
/** Unit tests that do not require an Atlas Search cluster */
2430
class ScoutEngineTest extends TestCase
2531
{
32+
private const EXPECTED_SEARCH_OPTIONS = ['allowDiskUse' => true, 'typeMap' => ['root' => 'object', 'document' => 'array', 'array' => 'array']];
33+
2634
/** @param callable(): Builder $builder */
2735
#[DataProvider('provideSearchPipelines')]
2836
public function testSearch(Closure $builder, array $expectedPipeline): void
@@ -36,8 +44,7 @@ public function testSearch(Closure $builder, array $expectedPipeline): void
3644
$cursor = m::mock(CursorInterface::class);
3745
$cursor->shouldReceive('toArray')->once()->with()->andReturn($data);
3846

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

4249
$engine = new ScoutEngine($database, softDelete: false, prefix: '');
4350
$result = $engine->search($builder());
@@ -157,6 +164,66 @@ function () {
157164

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

162229
#[DataProvider('provideResultsForMapIds')]
@@ -254,18 +321,17 @@ public function testUpdateWithSoftDelete(): void
254321
[
255322
'updateOne' => [
256323
['_id' => 'key_1'],
257-
['$setOnInsert' => ['_id' => 'key_1'], '$set' => ['id' => 1, 'date' => new UTCDateTime($date)]],
324+
['$setOnInsert' => ['_id' => 'key_1'], '$set' => ['id' => 1]],
258325
['upsert' => true],
259326
],
260327
],
261328
]);
262329

330+
$model = new SearchableModel(['id' => 1]);
331+
$model->delete();
332+
263333
$engine = new ScoutEngine($database, softDelete: false, prefix: '');
264-
$engine->update(EloquentCollection::make([
265-
(new SearchableModel([
266-
'id' => 1,
267-
])),
268-
]));
334+
$engine->update(EloquentCollection::make([$model]));
269335
}
270336

271337
public function testDelete(): void
@@ -286,6 +352,28 @@ public function testDelete(): void
286352
]));
287353
}
288354

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

tests/Scout/SearchableTest.php renamed to tests/Scout/ScoutIntegrationTest.php

Lines changed: 1 addition & 1 deletion
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)