diff --git a/src/Scout/ScoutEngine.php b/src/Scout/ScoutEngine.php index 422291dc1..1d71cbc45 100644 --- a/src/Scout/ScoutEngine.php +++ b/src/Scout/ScoutEngine.php @@ -68,7 +68,7 @@ final class ScoutEngine extends Engine ]; public function __construct( - private Database $mongodb, + private Database $database, private bool $softDelete, private string $prefix, ) { @@ -203,6 +203,7 @@ protected function performSearch(Builder $builder, ?int $offset = null): array ], ], ], + 'minimumShouldMatch' => 1, ]; // "filter" specifies conditions on exact values to match @@ -238,7 +239,7 @@ protected function performSearch(Builder $builder, ?int $offset = null): array ], [ '$addFields' => [ - 'search_meta' => '$$SEARCH_META', + '__count' => '$$SEARCH_META.count.lowerBound', ], ], ]; @@ -298,7 +299,7 @@ public function map(Builder $builder, $results, $model): Collection $result = $results[$objectIdPositions[$model->getScoutKey()]] ?? []; foreach ($result as $key => $value) { - if (substr($key, 0, 1) === '_') { + if ($key[0] === '_') { $model->withScoutMetadata($key, $value); } } @@ -321,7 +322,7 @@ public function getTotalCount($results): int return 0; } - return $results[0]->search_meta['count']['lowerBound']; + return $results[0]->__count; } /** @@ -393,9 +394,9 @@ public function createIndex($name, array $options = []): void assert(is_string($name), new TypeError(sprintf('Argument #1 ($name) must be of type string, %s given', get_debug_type($name)))); // Ensure the collection exists before creating the search index - $this->mongodb->createCollection($name); + $this->database->createCollection($name); - $collection = $this->mongodb->selectCollection($name); + $collection = $this->database->selectCollection($name); $collection->createSearchIndex( self::DEFAULT_DEFINITION, ['name' => self::INDEX_NAME], @@ -420,7 +421,7 @@ public function deleteIndex($name): void { assert(is_string($name), new TypeError(sprintf('Argument #1 ($name) must be of type string, %s given', get_debug_type($name)))); - $this->mongodb->selectCollection($name)->drop(); + $this->database->selectCollection($name)->drop(); } /** @@ -428,14 +429,14 @@ public function deleteIndex($name): void */ public function deleteAllIndexes() { - $collectionNames = $this->mongodb->listCollectionNames([ + $collectionNames = $this->database->listCollectionNames([ 'filter' => [ 'name' => new Regex('^' . preg_quote($this->prefix)), ], ]); foreach ($collectionNames as $collectionName) { - $this->mongodb->selectCollection($collectionName)->drop(); + $this->database->selectCollection($collectionName)->drop(); } } @@ -448,7 +449,7 @@ private function getSearchableCollection(Model|EloquentCollection $model): Mongo assert(in_array(Searchable::class, class_uses_recursive($model)), sprintf('Model "%s" must use "%s" trait', $model::class, Searchable::class)); - return $this->mongodb->selectCollection($model->searchableAs()); + return $this->database->selectCollection($model->searchableAs()); } /** Get the MongoDB collection used to index the provided model */ @@ -463,13 +464,13 @@ private function getIndexableCollection(Model|EloquentCollection $model): MongoD if ( $model->getConnection() instanceof Connection - && $model->getConnection()->getDatabaseName() === $this->mongodb->getDatabaseName() + && $model->getConnection()->getDatabaseName() === $this->database->getDatabaseName() && $model->getTable() === $model->indexableAs() ) { - throw new LogicException(sprintf('The MongoDB Scout collection "%s.%s" must use a different collection from the collection name of the model "%s". Set the "scout.prefix" configuration or use a distinct MongoDB database', $this->mongodb->getDatabaseName(), $model->indexableAs(), $model::class)); + throw new LogicException(sprintf('The MongoDB Scout collection "%s.%s" must use a different collection from the collection name of the model "%s". Set the "scout.prefix" configuration or use a distinct MongoDB database', $this->database->getDatabaseName(), $model->indexableAs(), $model::class)); } - return $this->mongodb->selectCollection($model->indexableAs()); + return $this->database->selectCollection($model->indexableAs()); } private static function serialize(mixed $value): mixed diff --git a/tests/ModelTest.php b/tests/ModelTest.php index c532eea55..ef71a5fe0 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -406,8 +406,9 @@ public function testSoftDelete(): void $this->assertEquals(2, Soft::count()); } + /** @param class-string $model */ #[DataProvider('provideId')] - public function testPrimaryKey(string $model, $id, $expected, bool $expectedFound): void + public function testPrimaryKey(string $model, mixed $id, mixed $expected, bool $expectedFound): void { $model::truncate(); $expectedType = get_debug_type($expected); diff --git a/tests/Scout/ScoutEngineTest.php b/tests/Scout/ScoutEngineTest.php index 92da48114..adf3f431e 100644 --- a/tests/Scout/ScoutEngineTest.php +++ b/tests/Scout/ScoutEngineTest.php @@ -9,6 +9,7 @@ use Illuminate\Support\Collection as IlluminateCollection; use Laravel\Scout\Builder; use Laravel\Scout\Jobs\RemoveFromSearch; +use LogicException; use Mockery as m; use MongoDB\BSON\Document; use MongoDB\BSON\Regex; @@ -17,13 +18,16 @@ use MongoDB\Database; use MongoDB\Driver\CursorInterface; use MongoDB\Laravel\Scout\ScoutEngine; +use MongoDB\Laravel\Tests\Models\SearchableInSameNamespace; use MongoDB\Laravel\Tests\Models\SearchableModel; use MongoDB\Laravel\Tests\TestCase; use MongoDB\Model\BSONDocument; use PHPUnit\Framework\Attributes\DataProvider; use function array_replace_recursive; +use function env; use function serialize; +use function sprintf; use function unserialize; /** Unit tests that do not require an Atlas Search cluster */ @@ -35,7 +39,7 @@ class ScoutEngineTest extends TestCase #[DataProvider('provideSearchPipelines')] public function testSearch(Closure $builder, array $expectedPipeline): void { - $data = [['_id' => 'key_1'], ['_id' => 'key_2']]; + $data = [['_id' => 'key_1', '__count' => 15], ['_id' => 'key_2', '__count' => 15]]; $database = m::mock(Database::class); $collection = m::mock(Collection::class); $database->shouldReceive('selectCollection') @@ -83,6 +87,7 @@ public function provideSearchPipelines(): iterable ], ], ], + 'minimumShouldMatch' => 1, ], 'count' => [ 'type' => 'lowerBound', @@ -91,7 +96,7 @@ public function provideSearchPipelines(): iterable ], [ '$addFields' => [ - 'search_meta' => '$$SEARCH_META', + '__count' => '$$SEARCH_META.count.lowerBound', ], ], ]; @@ -361,6 +366,7 @@ public function testPaginate() ], ], ], + 'minimumShouldMatch' => 1, ], 'count' => [ 'type' => 'lowerBound', @@ -372,7 +378,7 @@ public function testPaginate() ], [ '$addFields' => [ - 'search_meta' => '$$SEARCH_META', + '__count' => '$$SEARCH_META.count.lowerBound', ], ], [ @@ -391,7 +397,7 @@ public function testPaginate() $cursor->shouldReceive('toArray') ->once() ->with() - ->andReturn([['_id' => 'key_1'], ['_id' => 'key_2']]); + ->andReturn([['_id' => 'key_1', '__count' => 17], ['_id' => 'key_2', '__count' => 17]]); $engine = new ScoutEngine($database, softDelete: false, prefix: ''); $builder = new Builder(new SearchableModel(), 'mustang'); @@ -564,4 +570,16 @@ public function testDeleteAll(): void $engine = new ScoutEngine($database, softDelete: false, prefix: 'scout-prefix-'); $engine->deleteAllIndexes(); } + + public function testItCannotIndexInTheSameNamespace() + { + self::expectException(LogicException::class); + self::expectExceptionMessage(sprintf( + 'The MongoDB Scout collection "%s.searchable_in_same_namespaces" must use a different collection from the collection name of the model "%s". Set the "scout.prefix" configuration or use a distinct MongoDB database', + env('MONGODB_DATABASE', 'unittest'), + SearchableInSameNamespace::class, + ),); + + SearchableInSameNamespace::create(['name' => 'test']); + } } diff --git a/tests/Scout/ScoutIntegrationTest.php b/tests/Scout/ScoutIntegrationTest.php index 1b8acc53f..e4b70bf29 100644 --- a/tests/Scout/ScoutIntegrationTest.php +++ b/tests/Scout/ScoutIntegrationTest.php @@ -5,20 +5,16 @@ use Illuminate\Database\Eloquent\Factories\Sequence; use Illuminate\Support\Facades\DB; use Illuminate\Support\LazyCollection; -use LogicException; use MongoDB\Driver\Exception\ServerException; -use MongoDB\Laravel\Tests\Models\SearchableInSameNamespace; use MongoDB\Laravel\Tests\Models\SqlUser; use MongoDB\Laravel\Tests\TestCase; use Orchestra\Testbench\Factories\UserFactory; use PHPUnit\Framework\Attributes\Depends; use function count; -use function env; use function in_array; use function Orchestra\Testbench\artisan; use function range; -use function sprintf; use function usleep; class ScoutIntegrationTest extends TestCase @@ -40,7 +36,7 @@ public function testItCanCreateTheCollection() } catch (ServerException $exception) { if ( in_array($exception->getCode(), [ - 40324, // Old version: Unrecognized pipeline stage name: '$listSearchIndexes' + 40324, // Prior to MongoDB 6: Unrecognized pipeline stage name: '$listSearchIndexes' 31082, // Community Server: Using Atlas Search Database Commands and the $listSearchIndexes aggregation stage requires additional configuration. 115, // Enterprise Server: PlanExecutor error during aggregation :: caused by :: Search index commands are only supported with Atlas. ]) @@ -228,29 +224,20 @@ public function testItCanUsePaginatedSearchWithQueryCallback() return $query->whereNotNull('email_verified_at'); }; - $page1 = SqlUser::search('lar')->take(10)->query($queryCallback)->paginate(3, 'page', 1); - $page2 = SqlUser::search('lar')->take(10)->query($queryCallback)->paginate(3, 'page', 2); + $page1 = SqlUser::search('lar')->take(10)->query($queryCallback)->paginate(5, 'page', 1); + $page2 = SqlUser::search('lar')->take(10)->query($queryCallback)->paginate(5, 'page', 2); self::assertSame([ 42 => 'Dax Larkin', + 44 => 'Amos Larson Sr.', + 43 => 'Dana Larson Sr.', ], $page1->pluck('name', 'id')->all()); self::assertSame([ - 44 => 'Amos Larson Sr.', - 43 => 'Dana Larson Sr.', 41 => 'Gudrun Larkin', + 40 => 'Otis Larson MD', + 12 => 'Reta Larkin', + 1 => 'Laravel Framework', ], $page2->pluck('name', 'id')->all()); } - - public function testItCannotIndexInTheSameNamespace() - { - self::expectException(LogicException::class); - self::expectExceptionMessage(sprintf( - 'The MongoDB Scout collection "%s.searchable_in_same_namespaces" must use a different collection from the collection name of the model "%s". Set the "scout.prefix" configuration or use a distinct MongoDB database', - env('MONGODB_DATABASE', 'unittest'), - SearchableInSameNamespace::class, - ),); - - SearchableInSameNamespace::create(['name' => 'test']); - } }