diff --git a/src/MongoDBServiceProvider.php b/src/MongoDBServiceProvider.php index b0c085b8e..dc9caf082 100644 --- a/src/MongoDBServiceProvider.php +++ b/src/MongoDBServiceProvider.php @@ -167,10 +167,11 @@ private function registerScoutEngine(): void $connectionName = $app->get('config')->get('scout.mongodb.connection', 'mongodb'); $connection = $app->get('db')->connection($connectionName); $softDelete = (bool) $app->get('config')->get('scout.soft_delete', false); + $indexDefinitions = $app->get('config')->get('scout.mongodb.index-definitions', []); assert($connection instanceof Connection, new InvalidArgumentException(sprintf('The connection "%s" is not a MongoDB connection.', $connectionName))); - return new ScoutEngine($connection->getMongoDB(), $softDelete); + return new ScoutEngine($connection->getMongoDB(), $softDelete, $indexDefinitions); }); return $engineManager; diff --git a/src/Scout/ScoutEngine.php b/src/Scout/ScoutEngine.php index e3c9c68c3..dc70a39e2 100644 --- a/src/Scout/ScoutEngine.php +++ b/src/Scout/ScoutEngine.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Collection; use Illuminate\Support\LazyCollection; +use InvalidArgumentException; use Laravel\Scout\Builder; use Laravel\Scout\Engines\Engine; use Laravel\Scout\Searchable; @@ -66,9 +67,11 @@ final class ScoutEngine extends Engine private const TYPEMAP = ['root' => 'object', 'document' => 'bson', 'array' => 'bson']; + /** @param array $indexDefinitions */ public function __construct( private Database $database, private bool $softDelete, + private array $indexDefinitions = [], ) { } @@ -435,14 +438,16 @@ 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)))); + $definition = $this->indexDefinitions[$name] ?? self::DEFAULT_DEFINITION; + if (! isset($definition['mappings'])) { + throw new InvalidArgumentException(sprintf('Invalid search index definition for collection "%s", the "mappings" key is required. Find documentation at https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#search-index-definition-syntax', $name)); + } + // Ensure the collection exists before creating the search index $this->database->createCollection($name); $collection = $this->database->selectCollection($name); - $collection->createSearchIndex( - self::DEFAULT_DEFINITION, - ['name' => self::INDEX_NAME], - ); + $collection->createSearchIndex($definition, ['name' => self::INDEX_NAME]); if ($options['wait'] ?? true) { $this->wait(function () use ($collection) { diff --git a/tests/Scout/ScoutEngineTest.php b/tests/Scout/ScoutEngineTest.php index a079ae530..f1244d060 100644 --- a/tests/Scout/ScoutEngineTest.php +++ b/tests/Scout/ScoutEngineTest.php @@ -2,6 +2,7 @@ namespace MongoDB\Laravel\Tests\Scout; +use ArrayIterator; use Closure; use DateTimeImmutable; use Illuminate\Database\Eloquent\Collection as EloquentCollection; @@ -9,7 +10,9 @@ use Illuminate\Support\LazyCollection; use Laravel\Scout\Builder; use Laravel\Scout\Jobs\RemoveFromSearch; +use LogicException; use Mockery as m; +use MongoDB\BSON\Document; use MongoDB\BSON\UTCDateTime; use MongoDB\Collection; use MongoDB\Database; @@ -31,6 +34,82 @@ class ScoutEngineTest extends TestCase { private const EXPECTED_TYPEMAP = ['root' => 'object', 'document' => 'bson', 'array' => 'bson']; + public function testCreateIndexInvalidDefinition(): void + { + $database = m::mock(Database::class); + $engine = new ScoutEngine($database, false, ['collection_invalid' => ['foo' => 'bar']]); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Invalid search index definition for collection "collection_invalid", the "mappings" key is required.'); + $engine->createIndex('collection_invalid'); + } + + public function testCreateIndex(): void + { + $collectionName = 'collection_custom'; + $expectedDefinition = [ + 'mappings' => [ + 'dynamic' => true, + ], + ]; + + $database = m::mock(Database::class); + $collection = m::mock(Collection::class); + $database->shouldReceive('createCollection') + ->once() + ->with($collectionName); + $database->shouldReceive('selectCollection') + ->with($collectionName) + ->andReturn($collection); + $collection->shouldReceive('createSearchIndex') + ->once() + ->with($expectedDefinition, ['name' => 'scout']); + $collection->shouldReceive('listSearchIndexes') + ->once() + ->with(['name' => 'scout', 'typeMap' => ['root' => 'bson']]) + ->andReturn(new ArrayIterator([Document::fromPHP(['name' => 'scout', 'status' => 'READY'])])); + + $engine = new ScoutEngine($database, false, []); + $engine->createIndex($collectionName); + } + + public function testCreateIndexCustomDefinition(): void + { + $collectionName = 'collection_custom'; + $expectedDefinition = [ + 'mappings' => [ + [ + 'analyzer' => 'lucene.standard', + 'fields' => [ + [ + 'name' => 'wildcard', + 'type' => 'string', + ], + ], + ], + ], + ]; + + $database = m::mock(Database::class); + $collection = m::mock(Collection::class); + $database->shouldReceive('createCollection') + ->once() + ->with($collectionName); + $database->shouldReceive('selectCollection') + ->with($collectionName) + ->andReturn($collection); + $collection->shouldReceive('createSearchIndex') + ->once() + ->with($expectedDefinition, ['name' => 'scout']); + $collection->shouldReceive('listSearchIndexes') + ->once() + ->with(['name' => 'scout', 'typeMap' => ['root' => 'bson']]) + ->andReturn(new ArrayIterator([Document::fromPHP(['name' => 'scout', 'status' => 'READY'])])); + + $engine = new ScoutEngine($database, false, [$collectionName => $expectedDefinition]); + $engine->createIndex($collectionName); + } + /** @param callable(): Builder $builder */ #[DataProvider('provideSearchPipelines')] public function testSearch(Closure $builder, array $expectedPipeline): void diff --git a/tests/Scout/ScoutIntegrationTest.php b/tests/Scout/ScoutIntegrationTest.php index ff4617352..b40a455ab 100644 --- a/tests/Scout/ScoutIntegrationTest.php +++ b/tests/Scout/ScoutIntegrationTest.php @@ -17,6 +17,7 @@ use function array_merge; use function count; use function env; +use function iterator_to_array; use function Orchestra\Testbench\artisan; use function range; use function sprintf; @@ -38,6 +39,9 @@ protected function getEnvironmentSetUp($app): void $app['config']->set('scout.driver', 'mongodb'); $app['config']->set('scout.prefix', 'prefix_'); + $app['config']->set('scout.mongodb.index-definitions', [ + 'prefix_scout_users' => ['mappings' => ['dynamic' => true, 'fields' => ['bool_field' => ['type' => 'boolean']]]], + ]); } public function setUp(): void @@ -103,8 +107,9 @@ public function testItCanCreateTheCollection() self::assertSame(44, $collection->countDocuments()); - $searchIndexes = $collection->listSearchIndexes(['name' => 'scout']); + $searchIndexes = $collection->listSearchIndexes(['name' => 'scout', 'typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']]); self::assertCount(1, $searchIndexes); + self::assertSame(['mappings' => ['dynamic' => true, 'fields' => ['bool_field' => ['type' => 'boolean']]]], iterator_to_array($searchIndexes)[0]['latestDefinition']); // Wait for all documents to be indexed asynchronously $i = 100;