From 6996cb97d5e926e6a8451d14578f388a2baae113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 18 Dec 2024 13:28:14 +0100 Subject: [PATCH 01/10] Add schema helpers to create search and vector indexes --- src/Schema/Blueprint.php | 36 ++++++++++++++++++++++++++ tests/SchemaTest.php | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index f107bd7e5..fcd89df81 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -16,6 +16,14 @@ use function is_string; use function key; +/** + * @phpstan-type SearchIndexField array{type: 'boolean'|'date'|'dateFacet'|'objectId'|'stringFacet'|'uuid'} | array{type: 'autocomplete', analyzer?: string, maxGrams?: int, minGrams?: int, tokenization?: 'edgeGram'|'rightEdgeGram'|'nGram', foldDiacritics?: bool} | array{type: 'document'|'embeddedDocuments', dynamic?:bool, fields: array} | array{type: 'geo', indexShapes?: bool} | array{type: 'number'|'numberFacet', representation?: 'int64'|'double', indexIntegers?: bool, indexDoubles?: bool} | array{type: 'token', normalizer?: 'lowercase'|'none'} | array{type: 'string', analyzer?: string, searchAnalyzer?: string, indexOptions?: 'docs'|'freqs'|'positions'|'offsets', store?: bool, ignoreAbove?: int, multi?: array, norms?: 'include'|'omit'} + * @phpstan-type SearchIndexAnalyser array{name: string, charFilters?: list>, tokenizer: array{type: string}, tokenFilters?: list>} + * @phpstan-type SearchIndexStoredSource bool | array{includes: array} | array{excludes: array} + * @phpstan-type SearchIndexDefinition array{analyser?: string, analyzers?: SearchIndexAnalyser[], searchAnalyzer?: string, mappings: array{dynamic: true} | array{dynamic?: bool, fields: array}, storedSource?: SearchIndexStoredSource} + * @phpstan-type VectorSearchIndexField array{type: 'vector', path: string, numDimensions: int, similarity: 'euclidean'|'cosine'|'dotProduct', quantization?: 'none'|'scalar'|'binary'} + * @phpstan-type VectorSearchIndexDefinition array{fields: array} + */ class Blueprint extends SchemaBlueprint { /** @@ -303,6 +311,34 @@ public function sparse_and_unique($columns = null, $options = []) return $this; } + /** + * Create an Atlas Search Index. + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/ + * + * @phpstan-param SearchIndexDefinition $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/atlas/atlas-vector-search/ + * + * @phpstan-param VectorSearchIndexDefinition $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/tests/SchemaTest.php b/tests/SchemaTest.php index ec1ae47dd..608b2ae1d 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -8,8 +8,10 @@ use Illuminate\Support\Facades\Schema; use MongoDB\BSON\Binary; use MongoDB\BSON\UTCDateTime; +use MongoDB\Collection; use MongoDB\Laravel\Schema\Blueprint; +use function assert; use function collect; use function count; @@ -523,9 +525,51 @@ public function testGetIndexes() $this->assertSame([], $indexes); } + /** @todo requires SearchIndex support */ + public function testSearchIndex(): void + { + Schema::create('newcollection', function (Blueprint $collection) { + $collection->searchIndex([ + 'mappings' => [ + 'dynamic' => false, + 'fields' => [ + 'foo' => ['type' => 'string', 'analyzer' => 'lucene.whitespace'], + ], + ], + ]); + }); + + $index = $this->getSearchIndex('newcollection', 'default'); + self::assertNotFalse($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() + { + 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::assertNotFalse($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 +579,16 @@ protected function getIndex(string $collection, string $name) return false; } + + protected function getSearchIndex(string $collection, string $name) + { + $collection = DB::getCollection($collection); + assert($collection instanceof Collection); + + foreach ($collection->listSearchIndexes(['name' => $name]) as $index) { + return $index; + } + + return false; + } } From 631141ee028460ad2032a248eaccb72024393751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Dec 2024 14:20:06 +0100 Subject: [PATCH 02/10] multiline type definition --- src/Schema/Blueprint.php | 87 +++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 9 deletions(-) diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index fcd89df81..4adaa9f32 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -17,12 +17,81 @@ use function key; /** - * @phpstan-type SearchIndexField array{type: 'boolean'|'date'|'dateFacet'|'objectId'|'stringFacet'|'uuid'} | array{type: 'autocomplete', analyzer?: string, maxGrams?: int, minGrams?: int, tokenization?: 'edgeGram'|'rightEdgeGram'|'nGram', foldDiacritics?: bool} | array{type: 'document'|'embeddedDocuments', dynamic?:bool, fields: array} | array{type: 'geo', indexShapes?: bool} | array{type: 'number'|'numberFacet', representation?: 'int64'|'double', indexIntegers?: bool, indexDoubles?: bool} | array{type: 'token', normalizer?: 'lowercase'|'none'} | array{type: 'string', analyzer?: string, searchAnalyzer?: string, indexOptions?: 'docs'|'freqs'|'positions'|'offsets', store?: bool, ignoreAbove?: int, multi?: array, norms?: 'include'|'omit'} - * @phpstan-type SearchIndexAnalyser array{name: string, charFilters?: list>, tokenizer: array{type: string}, tokenFilters?: list>} - * @phpstan-type SearchIndexStoredSource bool | array{includes: array} | array{excludes: array} - * @phpstan-type SearchIndexDefinition array{analyser?: string, analyzers?: SearchIndexAnalyser[], searchAnalyzer?: string, mappings: array{dynamic: true} | array{dynamic?: bool, fields: array}, storedSource?: SearchIndexStoredSource} - * @phpstan-type VectorSearchIndexField array{type: 'vector', path: string, numDimensions: int, similarity: 'euclidean'|'cosine'|'dotProduct', quantization?: 'none'|'scalar'|'binary'} - * @phpstan-type VectorSearchIndexDefinition array{fields: array} + * @phpstan-type TypeSearchIndexField array{ + * type: 'boolean'|'date'|'dateFacet'|'objectId'|'stringFacet'|'uuid', + * } | array{ + * type: 'autocomplete', + * analyzer?: string, + * maxGrams?: int, + * minGrams?: int, + * tokenization?: 'edgeGram'|'rightEdgeGram'|'nGram', + * foldDiacritics?: bool, + * } | array{ + * type: 'document'|'embeddedDocuments', + * dynamic?:bool, + * fields: array>, + * } | array{ + * type: 'geo', + * indexShapes?: bool, + * } | array{ + * type: 'number'|'numberFacet', + * representation?: 'int64'|'double', + * indexIntegers?: bool, + * indexDoubles?: bool, + * } | array{ + * type: 'token', + * normalizer?: 'lowercase'|'none', + * } | array{ + * type: 'string', + * analyzer?: string, + * searchAnalyzer?: string, + * indexOptions?: 'docs'|'freqs'|'positions'|'offsets', + * store?: bool, + * ignoreAbove?: int, + * multi?: array>, + * norms?: 'include'|'omit', + * } + * @phpstan-type TypeSearchIndexCharFilter array{ + * type: 'icuNormalize'|'persian', + * } | array{ + * type: 'htmlStrip', + * ignoredTags?: string[], + * } | array{ + * type: 'mapping', + * mappings?: array, + * } + * @phpstan-type TypeSearchIndexTokenFilter array{type: string, ...} + * @phpstan-type TypeSearchIndexAnalyzer array{ + * name: string, + * charFilters?: TypeSearchIndexCharFilter, + * tokenizer: array{type: string}, + * tokenFilters?: TypeSearchIndexTokenFilter, + * } + * @phpstan-type TypeSearchIndexStoredSource bool | array{ + * includes: array, + * } | array{ + * excludes: array, + * } + * @phpstan-type TypeSearchIndexDefinition array{ + * analyser?: string, + * analyzers?: TypeSearchIndexAnalyzer[], + * searchAnalyzer?: string, + * mappings: array{dynamic: true} | array{dynamic?: bool, fields: array}, + * storedSource?: TypeSearchIndexStoredSource, + * } + * @phpstan-type TypeVectorSearchIndexField array{ + * type: 'vector', + * path: string, + * numDimensions: int, + * similarity: 'euclidean'|'cosine'|'dotProduct', + * quantization?: 'none'|'scalar'|'binary', + * } | array{ + * type: 'filter', + * path: string, + * } + * @phpstan-type TypeVectorSearchIndexDefinition array{ + * fields: array, + * } */ class Blueprint extends SchemaBlueprint { @@ -314,9 +383,9 @@ public function sparse_and_unique($columns = null, $options = []) /** * Create an Atlas Search Index. * - * @see https://www.mongodb.com/docs/atlas/atlas-search/ + * @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/ * - * @phpstan-param SearchIndexDefinition $definition + * @phpstan-param TypeSearchIndexDefinition $definition */ public function searchIndex(array $definition, string $name = 'default'): static { @@ -330,7 +399,7 @@ public function searchIndex(array $definition, string $name = 'default'): static * * @see https://www.mongodb.com/docs/atlas/atlas-vector-search/ * - * @phpstan-param VectorSearchIndexDefinition $definition + * @phpstan-param TypeVectorSearchIndexDefinition $definition */ public function vectorSearchIndex(array $definition, string $name = 'default'): static { From 54bf40d4f218d1c0ad52f0c4e317534a3bf00c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Dec 2024 17:34:11 +0100 Subject: [PATCH 03/10] Skip tests that require Atlas Search --- src/Schema/Blueprint.php | 2 +- tests/SchemaTest.php | 5 ++++- tests/TestCase.php | 20 ++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index 4adaa9f32..961c9e1cb 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -87,7 +87,7 @@ * quantization?: 'none'|'scalar'|'binary', * } | array{ * type: 'filter', - * path: string, + * path: string, * } * @phpstan-type TypeVectorSearchIndexDefinition array{ * fields: array, diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 608b2ae1d..67d6dde01 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -525,9 +525,10 @@ public function testGetIndexes() $this->assertSame([], $indexes); } - /** @todo requires SearchIndex support */ public function testSearchIndex(): void { + $this->skipIfSearchIndexManagementIsNotSupported(); + Schema::create('newcollection', function (Blueprint $collection) { $collection->searchIndex([ 'mappings' => [ @@ -550,6 +551,8 @@ public function testSearchIndex(): void public function testVectorSearchIndex() { + $this->skipIfSearchIndexManagementIsNotSupported(); + Schema::create('newcollection', function (Blueprint $collection) { $collection->vectorSearchIndex([ 'fields' => [ diff --git a/tests/TestCase.php b/tests/TestCase.php index 5f5bbecdc..146c661cd 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,6 +5,7 @@ namespace MongoDB\Laravel\Tests; use Illuminate\Foundation\Application; +use MongoDB\Driver\Exception\ServerException; use MongoDB\Laravel\MongoDBServiceProvider; use MongoDB\Laravel\Tests\Models\User; use MongoDB\Laravel\Validation\ValidationServiceProvider; @@ -64,4 +65,23 @@ 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) { + switch ($e->getCode()) { + // MongoDB 6: Unrecognized pipeline stage name: '$listSearchIndexes' + case 40324: + // MongoDB 7: PlanExecutor error during aggregation :: caused by :: Search index commands are only supported with Atlas. + case 115: + // MongoDB 8: Using Atlas Search Database Commands and the $listSearchIndexes aggregation stage requires additional configuration. + case 31082: + self::markTestSkipped('Search index management is not supported on this server'); + } + + throw $e; + } + } } From fc785003409c6ece9683214081329836767cc802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Dec 2024 18:03:38 +0100 Subject: [PATCH 04/10] Add links to custom types --- phpcs.xml.dist | 4 ++++ src/Schema/Blueprint.php | 12 ++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) 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 @@ tests/Ticket/*.php + + + src/Schema/Blueprint.php + diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index 961c9e1cb..65f53784a 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -17,6 +17,7 @@ use function key; /** + * @link https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/#std-label-fts-field-mappings * @phpstan-type TypeSearchIndexField array{ * type: 'boolean'|'date'|'dateFacet'|'objectId'|'stringFacet'|'uuid', * } | array{ @@ -51,6 +52,7 @@ * multi?: array>, * norms?: 'include'|'omit', * } + * @link https://www.mongodb.com/docs/atlas/atlas-search/analyzers/character-filters/ * @phpstan-type TypeSearchIndexCharFilter array{ * type: 'icuNormalize'|'persian', * } | array{ @@ -60,18 +62,22 @@ * type: 'mapping', * mappings?: array, * } + * @link https://www.mongodb.com/docs/atlas/atlas-search/analyzers/token-filters/ * @phpstan-type TypeSearchIndexTokenFilter array{type: string, ...} + * @link https://www.mongodb.com/docs/atlas/atlas-search/analyzers/custom/ * @phpstan-type TypeSearchIndexAnalyzer array{ * name: string, * charFilters?: TypeSearchIndexCharFilter, * tokenizer: array{type: string}, * tokenFilters?: TypeSearchIndexTokenFilter, * } + * @link https://www.mongodb.com/docs/atlas/atlas-search/stored-source-definition/#std-label-fts-stored-source-definition * @phpstan-type TypeSearchIndexStoredSource bool | array{ * includes: array, * } | array{ * excludes: array, * } + * @link https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-search-index-definition-create * @phpstan-type TypeSearchIndexDefinition array{ * analyser?: string, * analyzers?: TypeSearchIndexAnalyzer[], @@ -79,6 +85,7 @@ * mappings: array{dynamic: true} | array{dynamic?: bool, fields: array}, * storedSource?: TypeSearchIndexStoredSource, * } + * @link https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-type/#atlas-vector-search-index-fields * @phpstan-type TypeVectorSearchIndexField array{ * type: 'vector', * path: string, @@ -89,6 +96,7 @@ * type: 'filter', * path: string, * } + * @link https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-type/#atlas-vector-search-index-fields * @phpstan-type TypeVectorSearchIndexDefinition array{ * fields: array, * } @@ -383,7 +391,7 @@ public function sparse_and_unique($columns = null, $options = []) /** * Create an Atlas Search Index. * - * @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/ + * @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-search-index-definition-create * * @phpstan-param TypeSearchIndexDefinition $definition */ @@ -397,7 +405,7 @@ public function searchIndex(array $definition, string $name = 'default'): static /** * Create an Atlas Vector Search Index. * - * @see https://www.mongodb.com/docs/atlas/atlas-vector-search/ + * @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-vector-search-index-definition-create * * @phpstan-param TypeVectorSearchIndexDefinition $definition */ From 073ecdfe811fcc30a1b10672b768c16b805a4778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Dec 2024 21:19:45 +0100 Subject: [PATCH 05/10] Fix return type of getSearchIndex --- tests/SchemaTest.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 67d6dde01..75bcf74c5 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use MongoDB\BSON\Binary; +use MongoDB\BSON\Document; use MongoDB\BSON\UTCDateTime; use MongoDB\Collection; use MongoDB\Laravel\Schema\Blueprint; @@ -541,7 +542,7 @@ public function testSearchIndex(): void }); $index = $this->getSearchIndex('newcollection', 'default'); - self::assertNotFalse($index); + self::assertNotNull($index); self::assertSame('default', $index['name']); self::assertSame('search', $index['type']); @@ -562,7 +563,7 @@ public function testVectorSearchIndex() }); $index = $this->getSearchIndex('newcollection', 'vector'); - self::assertNotFalse($index); + self::assertNotNull($index); self::assertSame('vector', $index['name']); self::assertSame('vectorSearch', $index['type']); @@ -583,15 +584,15 @@ protected function getIndex(string $collection, string $name) return false; } - protected function getSearchIndex(string $collection, string $name) + protected function getSearchIndex(string $collection, string $name): ?Document { $collection = DB::getCollection($collection); assert($collection instanceof Collection); - foreach ($collection->listSearchIndexes(['name' => $name]) as $index) { + foreach ($collection->listSearchIndexes(['name' => $name, 'typeMap' => ['root' => 'bson']]) as $index) { return $index; } - return false; + return null; } } From 9912317f0f3630e1f186c3f4e2e853db2868fc71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Dec 2024 21:40:52 +0100 Subject: [PATCH 06/10] Improve types --- src/Schema/Blueprint.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index 65f53784a..0a0c99356 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -49,7 +49,7 @@ * indexOptions?: 'docs'|'freqs'|'positions'|'offsets', * store?: bool, * ignoreAbove?: int, - * multi?: array>, + * multi?: array>, * norms?: 'include'|'omit', * } * @link https://www.mongodb.com/docs/atlas/atlas-search/analyzers/character-filters/ @@ -77,13 +77,20 @@ * } | array{ * excludes: array, * } + * @link https://www.mongodb.com/docs/atlas/atlas-search/synonyms/#std-label-synonyms-ref + * @phpstan-type TypeSearchIndexSynonyms array{ + * analyzer: string, + * name: string, + * source?: array{collection: string}, + * } * @link https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-search-index-definition-create * @phpstan-type TypeSearchIndexDefinition array{ - * analyser?: string, + * analyzer?: string, * analyzers?: TypeSearchIndexAnalyzer[], * searchAnalyzer?: string, * mappings: array{dynamic: true} | array{dynamic?: bool, fields: array}, * storedSource?: TypeSearchIndexStoredSource, + * synonyms?: TypeSearchIndexSynonyms[], * } * @link https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-type/#atlas-vector-search-index-fields * @phpstan-type TypeVectorSearchIndexField array{ From 80cea3bc784fcb2b7bea6d4fff342ab9a1a0bec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Dec 2024 22:10:14 +0100 Subject: [PATCH 07/10] Add missing code in atlas search detection --- tests/TestCase.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index 146c661cd..6686c2a5d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -76,6 +76,8 @@ public function skipIfSearchIndexManagementIsNotSupported(): void case 40324: // MongoDB 7: PlanExecutor error during aggregation :: caused by :: Search index commands are only supported with Atlas. case 115: + // MongoDB 7: $listSearchIndexes stage is only allowed on MongoDB Atlas + case 6047401: // MongoDB 8: Using Atlas Search Database Commands and the $listSearchIndexes aggregation stage requires additional configuration. case 31082: self::markTestSkipped('Search index management is not supported on this server'); From e7c202d29c79a1841f4ac3dcdd573ff6b3096df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 31 Dec 2024 13:01:51 +0100 Subject: [PATCH 08/10] Multiple mappings allowed per field name --- src/Schema/Blueprint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index 0a0c99356..ce8e08514 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -88,7 +88,7 @@ * analyzer?: string, * analyzers?: TypeSearchIndexAnalyzer[], * searchAnalyzer?: string, - * mappings: array{dynamic: true} | array{dynamic?: bool, fields: array}, + * mappings: array{dynamic: true} | array{dynamic?: bool, fields: array}, * storedSource?: TypeSearchIndexStoredSource, * synonyms?: TypeSearchIndexSynonyms[], * } From b38f88fb01dd2578aee4de4ce05b75589baa4410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 2 Jan 2025 18:00:42 +0100 Subject: [PATCH 09/10] Rebased on AtlasSearchTest --- src/Schema/Builder.php | 5 +++++ tests/SchemaTest.php | 13 ++++++++----- tests/TestCase.php | 13 +++---------- 3 files changed, 16 insertions(+), 15 deletions(-) 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 75bcf74c5..e23fa3d25 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -7,9 +7,9 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use MongoDB\BSON\Binary; -use MongoDB\BSON\Document; use MongoDB\BSON\UTCDateTime; use MongoDB\Collection; +use MongoDB\Database; use MongoDB\Laravel\Schema\Blueprint; use function assert; @@ -20,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 @@ -477,6 +479,7 @@ public function testGetColumns() $this->assertSame([], $columns); } + /** @see AtlasSearchTest::testGetIndexes() */ public function testGetIndexes() { Schema::create('newcollection', function (Blueprint $collection) { @@ -584,12 +587,12 @@ protected function getIndex(string $collection, string $name) return false; } - protected function getSearchIndex(string $collection, string $name): ?Document + protected function getSearchIndex(string $collection, string $name): ?array { $collection = DB::getCollection($collection); assert($collection instanceof Collection); - foreach ($collection->listSearchIndexes(['name' => $name, 'typeMap' => ['root' => 'bson']]) as $index) { + foreach ($collection->listSearchIndexes(['name' => $name, 'typeMap' => ['root' => 'array', 'array' => 'array', 'document' => 'array']]) as $index) { return $index; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 6686c2a5d..d924777ce 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,6 +7,7 @@ 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; @@ -71,16 +72,8 @@ public function skipIfSearchIndexManagementIsNotSupported(): void try { $this->getConnection('mongodb')->getCollection('test')->listSearchIndexes(['name' => 'just_for_testing']); } catch (ServerException $e) { - switch ($e->getCode()) { - // MongoDB 6: Unrecognized pipeline stage name: '$listSearchIndexes' - case 40324: - // MongoDB 7: PlanExecutor error during aggregation :: caused by :: Search index commands are only supported with Atlas. - case 115: - // MongoDB 7: $listSearchIndexes stage is only allowed on MongoDB Atlas - case 6047401: - // MongoDB 8: Using Atlas Search Database Commands and the $listSearchIndexes aggregation stage requires additional configuration. - case 31082: - self::markTestSkipped('Search index management is not supported on this server'); + if (Builder::isAtlasSearchNotSupportedException($e)) { + self::markTestSkipped('Search index management is not supported on this server'); } throw $e; From 7cf24fd65a172d426c79fb052465459f062f7472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 2 Jan 2025 18:11:27 +0100 Subject: [PATCH 10/10] Simplify search index definition type --- src/Schema/Blueprint.php | 104 ++++----------------------------------- 1 file changed, 10 insertions(+), 94 deletions(-) diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index ce8e08514..b77a7799e 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -16,98 +16,6 @@ use function is_string; use function key; -/** - * @link https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/#std-label-fts-field-mappings - * @phpstan-type TypeSearchIndexField array{ - * type: 'boolean'|'date'|'dateFacet'|'objectId'|'stringFacet'|'uuid', - * } | array{ - * type: 'autocomplete', - * analyzer?: string, - * maxGrams?: int, - * minGrams?: int, - * tokenization?: 'edgeGram'|'rightEdgeGram'|'nGram', - * foldDiacritics?: bool, - * } | array{ - * type: 'document'|'embeddedDocuments', - * dynamic?:bool, - * fields: array>, - * } | array{ - * type: 'geo', - * indexShapes?: bool, - * } | array{ - * type: 'number'|'numberFacet', - * representation?: 'int64'|'double', - * indexIntegers?: bool, - * indexDoubles?: bool, - * } | array{ - * type: 'token', - * normalizer?: 'lowercase'|'none', - * } | array{ - * type: 'string', - * analyzer?: string, - * searchAnalyzer?: string, - * indexOptions?: 'docs'|'freqs'|'positions'|'offsets', - * store?: bool, - * ignoreAbove?: int, - * multi?: array>, - * norms?: 'include'|'omit', - * } - * @link https://www.mongodb.com/docs/atlas/atlas-search/analyzers/character-filters/ - * @phpstan-type TypeSearchIndexCharFilter array{ - * type: 'icuNormalize'|'persian', - * } | array{ - * type: 'htmlStrip', - * ignoredTags?: string[], - * } | array{ - * type: 'mapping', - * mappings?: array, - * } - * @link https://www.mongodb.com/docs/atlas/atlas-search/analyzers/token-filters/ - * @phpstan-type TypeSearchIndexTokenFilter array{type: string, ...} - * @link https://www.mongodb.com/docs/atlas/atlas-search/analyzers/custom/ - * @phpstan-type TypeSearchIndexAnalyzer array{ - * name: string, - * charFilters?: TypeSearchIndexCharFilter, - * tokenizer: array{type: string}, - * tokenFilters?: TypeSearchIndexTokenFilter, - * } - * @link https://www.mongodb.com/docs/atlas/atlas-search/stored-source-definition/#std-label-fts-stored-source-definition - * @phpstan-type TypeSearchIndexStoredSource bool | array{ - * includes: array, - * } | array{ - * excludes: array, - * } - * @link https://www.mongodb.com/docs/atlas/atlas-search/synonyms/#std-label-synonyms-ref - * @phpstan-type TypeSearchIndexSynonyms array{ - * analyzer: string, - * name: string, - * source?: array{collection: string}, - * } - * @link https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-search-index-definition-create - * @phpstan-type TypeSearchIndexDefinition array{ - * analyzer?: string, - * analyzers?: TypeSearchIndexAnalyzer[], - * searchAnalyzer?: string, - * mappings: array{dynamic: true} | array{dynamic?: bool, fields: array}, - * storedSource?: TypeSearchIndexStoredSource, - * synonyms?: TypeSearchIndexSynonyms[], - * } - * @link https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-type/#atlas-vector-search-index-fields - * @phpstan-type TypeVectorSearchIndexField array{ - * type: 'vector', - * path: string, - * numDimensions: int, - * similarity: 'euclidean'|'cosine'|'dotProduct', - * quantization?: 'none'|'scalar'|'binary', - * } | array{ - * type: 'filter', - * path: string, - * } - * @link https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-type/#atlas-vector-search-index-fields - * @phpstan-type TypeVectorSearchIndexDefinition array{ - * fields: array, - * } - */ class Blueprint extends SchemaBlueprint { /** @@ -400,7 +308,15 @@ public function sparse_and_unique($columns = null, $options = []) * * @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-search-index-definition-create * - * @phpstan-param TypeSearchIndexDefinition $definition + * @phpstan-param array{ + * analyzer?: string, + * analyzers?: list, + * searchAnalyzer?: string, + * mappings: array{dynamic: true} | array{dynamic?: bool, fields: array}, + * storedSource?: bool|array, + * synonyms?: list, + * ... + * } $definition */ public function searchIndex(array $definition, string $name = 'default'): static { @@ -414,7 +330,7 @@ public function searchIndex(array $definition, string $name = 'default'): static * * @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-vector-search-index-definition-create * - * @phpstan-param TypeVectorSearchIndexDefinition $definition + * @phpstan-param array{fields: array} $definition */ public function vectorSearchIndex(array $definition, string $name = 'default'): static {