diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 3b7cc671c..f83429905 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -53,4 +53,8 @@ <rule ref="PSR1.Classes.ClassDeclaration.MultipleClasses"> <exclude-pattern>tests/Ticket/*.php</exclude-pattern> </rule> + + <rule ref="SlevomatCodingStandard.Commenting.DocCommentSpacing.IncorrectAnnotationsGroup"> + <exclude-pattern>src/Schema/Blueprint.php</exclude-pattern> + </rule> </ruleset> diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index f107bd7e5..b77a7799e 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -303,6 +303,42 @@ public function sparse_and_unique($columns = null, $options = []) return $this; } + /** + * Create an Atlas Search Index. + * + * @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-search-index-definition-create + * + * @phpstan-param array{ + * analyzer?: string, + * analyzers?: list<array>, + * searchAnalyzer?: string, + * mappings: array{dynamic: true} | array{dynamic?: bool, fields: array<string, array>}, + * storedSource?: bool|array, + * synonyms?: list<array>, + * ... + * } $definition + */ + public function searchIndex(array $definition, string $name = 'default'): static + { + $this->collection->createSearchIndex($definition, ['name' => $name, 'type' => 'search']); + + return $this; + } + + /** + * Create an Atlas Vector Search Index. + * + * @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-vector-search-index-definition-create + * + * @phpstan-param array{fields: array<string, array{type: string, ...}>} $definition + */ + public function vectorSearchIndex(array $definition, string $name = 'default'): static + { + $this->collection->createSearchIndex($definition, ['name' => $name, 'type' => 'vectorSearch']); + + return $this; + } + /** * Allow fluent columns. * diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index a4e8149f3..fe806f0e5 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -253,6 +253,11 @@ public function getIndexes($table) try { $indexes = $collection->listSearchIndexes(['typeMap' => ['root' => 'array', 'array' => 'array', 'document' => 'array']]); foreach ($indexes as $index) { + // Status 'DOES_NOT_EXIST' means the index has been dropped but is still in the process of being removed + if ($index['status'] === 'DOES_NOT_EXIST') { + continue; + } + $indexList[] = [ 'name' => $index['name'], 'columns' => match ($index['type']) { diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index ec1ae47dd..e23fa3d25 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -8,8 +8,11 @@ use Illuminate\Support\Facades\Schema; use MongoDB\BSON\Binary; use MongoDB\BSON\UTCDateTime; +use MongoDB\Collection; +use MongoDB\Database; use MongoDB\Laravel\Schema\Blueprint; +use function assert; use function collect; use function count; @@ -17,8 +20,10 @@ class SchemaTest extends TestCase { public function tearDown(): void { - Schema::drop('newcollection'); - Schema::drop('newcollection_two'); + $database = $this->getConnection('mongodb')->getMongoDB(); + assert($database instanceof Database); + $database->dropCollection('newcollection'); + $database->dropCollection('newcollection_two'); } public function testCreate(): void @@ -474,6 +479,7 @@ public function testGetColumns() $this->assertSame([], $columns); } + /** @see AtlasSearchTest::testGetIndexes() */ public function testGetIndexes() { Schema::create('newcollection', function (Blueprint $collection) { @@ -523,9 +529,54 @@ public function testGetIndexes() $this->assertSame([], $indexes); } + public function testSearchIndex(): void + { + $this->skipIfSearchIndexManagementIsNotSupported(); + + Schema::create('newcollection', function (Blueprint $collection) { + $collection->searchIndex([ + 'mappings' => [ + 'dynamic' => false, + 'fields' => [ + 'foo' => ['type' => 'string', 'analyzer' => 'lucene.whitespace'], + ], + ], + ]); + }); + + $index = $this->getSearchIndex('newcollection', 'default'); + self::assertNotNull($index); + + self::assertSame('default', $index['name']); + self::assertSame('search', $index['type']); + self::assertFalse($index['latestDefinition']['mappings']['dynamic']); + self::assertSame('lucene.whitespace', $index['latestDefinition']['mappings']['fields']['foo']['analyzer']); + } + + public function testVectorSearchIndex() + { + $this->skipIfSearchIndexManagementIsNotSupported(); + + Schema::create('newcollection', function (Blueprint $collection) { + $collection->vectorSearchIndex([ + 'fields' => [ + ['type' => 'vector', 'path' => 'foo', 'numDimensions' => 128, 'similarity' => 'euclidean', 'quantization' => 'none'], + ], + ], 'vector'); + }); + + $index = $this->getSearchIndex('newcollection', 'vector'); + self::assertNotNull($index); + + self::assertSame('vector', $index['name']); + self::assertSame('vectorSearch', $index['type']); + self::assertSame('vector', $index['latestDefinition']['fields'][0]['type']); + } + protected function getIndex(string $collection, string $name) { $collection = DB::getCollection($collection); + assert($collection instanceof Collection); foreach ($collection->listIndexes() as $index) { if (isset($index['key'][$name])) { @@ -535,4 +586,16 @@ protected function getIndex(string $collection, string $name) return false; } + + protected function getSearchIndex(string $collection, string $name): ?array + { + $collection = DB::getCollection($collection); + assert($collection instanceof Collection); + + foreach ($collection->listSearchIndexes(['name' => $name, 'typeMap' => ['root' => 'array', 'array' => 'array', 'document' => 'array']]) as $index) { + return $index; + } + + return null; + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 5f5bbecdc..d924777ce 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,7 +5,9 @@ namespace MongoDB\Laravel\Tests; use Illuminate\Foundation\Application; +use MongoDB\Driver\Exception\ServerException; use MongoDB\Laravel\MongoDBServiceProvider; +use MongoDB\Laravel\Schema\Builder; use MongoDB\Laravel\Tests\Models\User; use MongoDB\Laravel\Validation\ValidationServiceProvider; use Orchestra\Testbench\TestCase as OrchestraTestCase; @@ -64,4 +66,17 @@ protected function getEnvironmentSetUp($app): void $app['config']->set('queue.failed.database', 'mongodb2'); $app['config']->set('queue.failed.driver', 'mongodb'); } + + public function skipIfSearchIndexManagementIsNotSupported(): void + { + try { + $this->getConnection('mongodb')->getCollection('test')->listSearchIndexes(['name' => 'just_for_testing']); + } catch (ServerException $e) { + if (Builder::isAtlasSearchNotSupportedException($e)) { + self::markTestSkipped('Search index management is not supported on this server'); + } + + throw $e; + } + } }