Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Pagination
Browse files Browse the repository at this point in the history
GromNaN committed Nov 15, 2024

Verified

This commit was signed with the committer’s verified signature.
GromNaN Jérôme Tamarelle
1 parent f8a4ace commit 12f3323
Showing 4 changed files with 120 additions and 21 deletions.
29 changes: 18 additions & 11 deletions src/Scout/ScoutEngine.php
Original file line number Diff line number Diff line change
@@ -25,7 +25,6 @@
use TypeError;

use function array_column;
use function array_filter;
use function array_flip;
use function array_map;
use function array_merge;
@@ -146,10 +145,7 @@ public function delete($models): void
*/
public function search(Builder $builder)
{
return $this->performSearch($builder, array_filter([
'filters' => $this->filters($builder),
'limit' => $builder->limit,
]));
return $this->performSearch($builder);
}

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

return $this->performSearch($builder, array_filter([
'filters' => $this->filters($builder),
'limit' => (int) $perPage,
'offset' => ($page - 1) * $perPage,
]));
$builder = clone $builder;
$builder->take($perPage);

return $this->performSearch($builder, $perPage * ($page - 1));
}

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

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

if ($builder->orders) {
$pipeline[0]['$search']['sort'] = array_merge(...array_map(fn ($order) => [$order['column'] => $order['direction'] === 'asc' ? 1 : -1], $builder->orders));
}

if ($builder->limit) {
$pipeline[] = ['$limit' => $builder->limit];
}

if ($offset) {
$pipeline[] = ['$skip' => $offset];
}

$options = [
'allowDiskUse' => true,
'typeMap' => ['root' => 'object', 'document' => 'array', 'array' => 'array'],
7 changes: 6 additions & 1 deletion tests/Models/SearchableModel.php
Original file line number Diff line number Diff line change
@@ -29,6 +29,11 @@ public function indexableAs(): string

public function getScoutKey(): string
{
return 'key_' . $this->id;
return $this->getAttribute($this->getScoutKeyName()) ?: 'key_' . $this->getKey();
}

public function getScoutKeyName(): string
{
return 'scout_key';
}
}
103 changes: 95 additions & 8 deletions tests/Scout/ScoutEngineTest.php
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@
use DateTimeImmutable;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Laravel\Scout\Builder;
use Laravel\Scout\Jobs\RemoveFromSearch;
use Laravel\Scout\Tests\Unit\AlgoliaEngineTest;
use Mockery as m;
use MongoDB\BSON\Document;
use MongoDB\BSON\Regex;
@@ -20,9 +22,14 @@
use MongoDB\Model\BSONDocument;
use PHPUnit\Framework\Attributes\DataProvider;

use function serialize;
use function unserialize;

/** Unit tests that do not require an Atlas Search cluster */
class ScoutEngineTest extends TestCase
{
private const EXPECTED_SEARCH_OPTIONS = ['allowDiskUse' => true, 'typeMap' => ['root' => 'object', 'document' => 'array', 'array' => 'array']];

/** @param callable(): Builder $builder */
#[DataProvider('provideSearchPipelines')]
public function testSearch(Closure $builder, array $expectedPipeline): void
@@ -36,8 +43,7 @@ public function testSearch(Closure $builder, array $expectedPipeline): void
$cursor = m::mock(CursorInterface::class);
$cursor->shouldReceive('toArray')->once()->with()->andReturn($data);

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

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

public function testPaginate()
{
$perPage = 5;
$page = 3;

$database = m::mock(Database::class);
$collection = m::mock(Collection::class);
$cursor = m::mock(CursorInterface::class);
$database->shouldReceive('selectCollection')
->with('table_searchable')
->andReturn($collection);
$collection->shouldReceive('aggregate')
->once()
->withArgs(function (...$args) {
self::assertSame([
[
'$search' => [
'index' => 'scout',
'text' => [
'query' => 'mustang',
'path' => [
'wildcard' => '*',
],
'fuzzy' => [
'maxEdits' => 2,
],
],
'count' => [
'type' => 'lowerBound',
],
'sort' => [
'name' => -1,
],
],
],
[
'$addFields' => [
'search_meta' => '$$SEARCH_META',
],
],
[
'$limit' => 5,
],
[
'$skip' => 10,
],
], $args[0]);

$this->assertSame(self::EXPECTED_SEARCH_OPTIONS, $args[1]);

return true;
})
->andReturn($cursor);
$cursor->shouldReceive('toArray')
->once()
->with()
->andReturn([['_id' => 'key_1'], ['_id' => 'key_2']]);

$engine = new ScoutEngine($database, softDelete: false, prefix: '');
$builder = new Builder(new SearchableModel(), 'mustang');
$builder->orderBy('name', 'desc');
$engine->paginate($builder, $perPage, $page);
}

#[DataProvider('provideResultsForMapIds')]
@@ -254,18 +320,17 @@ public function testUpdateWithSoftDelete(): void
[
'updateOne' => [
['_id' => 'key_1'],
['$setOnInsert' => ['_id' => 'key_1'], '$set' => ['id' => 1, 'date' => new UTCDateTime($date)]],
['$setOnInsert' => ['_id' => 'key_1'], '$set' => ['id' => 1]],
['upsert' => true],
],
],
]);

$model = new SearchableModel(['id' => 1]);
$model->delete();

$engine = new ScoutEngine($database, softDelete: false, prefix: '');
$engine->update(EloquentCollection::make([
(new SearchableModel([
'id' => 1,
])),
]));
$engine->update(EloquentCollection::make([$model]));
}

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

/** @see AlgoliaEngineTest::test_delete_with_removeable_scout_collection_using_custom_search_key */
public function testDeleteWithRemoveableScoutCollection(): void
{
$job = new RemoveFromSearch(EloquentCollection::make([
new SearchableModel(['id' => 5, 'scout_key' => 'key_5']),
]));

$job = unserialize(serialize($job));

$database = m::mock(Database::class);
$collection = m::mock(Collection::class);
$database->shouldReceive('selectCollection')
->with('table_indexable')
->andReturn($collection);
$collection->shouldReceive('deleteMany')
->once()
->with(['_id' => ['$in' => ['key_5']]]);

$engine = new ScoutEngine($database, softDelete: false, prefix: 'ignored_prefix_');
$engine->delete($job->models);
}

public function testDeleteAll(): void
{
$collectionNames = ['scout-prefix-table1', 'scout-prefix-table2'];
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
use function Orchestra\Testbench\artisan;
use function sleep;

class SearchableTest extends TestCase
class ScoutIntegrationTest extends TestCase
{
use SearchableTests {
defineScoutDatabaseMigrations as baseDefineScoutDatabaseMigrations;

0 comments on commit 12f3323

Please sign in to comment.