Skip to content

Commit 6cb3838

Browse files
authored
PHPORM-273 Add schema helpers to create search and vector indexes (mongodb#3230)
1 parent 7890518 commit 6cb3838

File tree

5 files changed

+125
-2
lines changed

5 files changed

+125
-2
lines changed

phpcs.xml.dist

+4
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,8 @@
5353
<rule ref="PSR1.Classes.ClassDeclaration.MultipleClasses">
5454
<exclude-pattern>tests/Ticket/*.php</exclude-pattern>
5555
</rule>
56+
57+
<rule ref="SlevomatCodingStandard.Commenting.DocCommentSpacing.IncorrectAnnotationsGroup">
58+
<exclude-pattern>src/Schema/Blueprint.php</exclude-pattern>
59+
</rule>
5660
</ruleset>

src/Schema/Blueprint.php

+36
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,42 @@ public function sparse_and_unique($columns = null, $options = [])
303303
return $this;
304304
}
305305

306+
/**
307+
* Create an Atlas Search Index.
308+
*
309+
* @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-search-index-definition-create
310+
*
311+
* @phpstan-param array{
312+
* analyzer?: string,
313+
* analyzers?: list<array>,
314+
* searchAnalyzer?: string,
315+
* mappings: array{dynamic: true} | array{dynamic?: bool, fields: array<string, array>},
316+
* storedSource?: bool|array,
317+
* synonyms?: list<array>,
318+
* ...
319+
* } $definition
320+
*/
321+
public function searchIndex(array $definition, string $name = 'default'): static
322+
{
323+
$this->collection->createSearchIndex($definition, ['name' => $name, 'type' => 'search']);
324+
325+
return $this;
326+
}
327+
328+
/**
329+
* Create an Atlas Vector Search Index.
330+
*
331+
* @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#std-label-vector-search-index-definition-create
332+
*
333+
* @phpstan-param array{fields: array<string, array{type: string, ...}>} $definition
334+
*/
335+
public function vectorSearchIndex(array $definition, string $name = 'default'): static
336+
{
337+
$this->collection->createSearchIndex($definition, ['name' => $name, 'type' => 'vectorSearch']);
338+
339+
return $this;
340+
}
341+
306342
/**
307343
* Allow fluent columns.
308344
*

src/Schema/Builder.php

+5
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,11 @@ public function getIndexes($table)
253253
try {
254254
$indexes = $collection->listSearchIndexes(['typeMap' => ['root' => 'array', 'array' => 'array', 'document' => 'array']]);
255255
foreach ($indexes as $index) {
256+
// Status 'DOES_NOT_EXIST' means the index has been dropped but is still in the process of being removed
257+
if ($index['status'] === 'DOES_NOT_EXIST') {
258+
continue;
259+
}
260+
256261
$indexList[] = [
257262
'name' => $index['name'],
258263
'columns' => match ($index['type']) {

tests/SchemaTest.php

+65-2
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,22 @@
88
use Illuminate\Support\Facades\Schema;
99
use MongoDB\BSON\Binary;
1010
use MongoDB\BSON\UTCDateTime;
11+
use MongoDB\Collection;
12+
use MongoDB\Database;
1113
use MongoDB\Laravel\Schema\Blueprint;
1214

15+
use function assert;
1316
use function collect;
1417
use function count;
1518

1619
class SchemaTest extends TestCase
1720
{
1821
public function tearDown(): void
1922
{
20-
Schema::drop('newcollection');
21-
Schema::drop('newcollection_two');
23+
$database = $this->getConnection('mongodb')->getMongoDB();
24+
assert($database instanceof Database);
25+
$database->dropCollection('newcollection');
26+
$database->dropCollection('newcollection_two');
2227
}
2328

2429
public function testCreate(): void
@@ -474,6 +479,7 @@ public function testGetColumns()
474479
$this->assertSame([], $columns);
475480
}
476481

482+
/** @see AtlasSearchTest::testGetIndexes() */
477483
public function testGetIndexes()
478484
{
479485
Schema::create('newcollection', function (Blueprint $collection) {
@@ -523,9 +529,54 @@ public function testGetIndexes()
523529
$this->assertSame([], $indexes);
524530
}
525531

532+
public function testSearchIndex(): void
533+
{
534+
$this->skipIfSearchIndexManagementIsNotSupported();
535+
536+
Schema::create('newcollection', function (Blueprint $collection) {
537+
$collection->searchIndex([
538+
'mappings' => [
539+
'dynamic' => false,
540+
'fields' => [
541+
'foo' => ['type' => 'string', 'analyzer' => 'lucene.whitespace'],
542+
],
543+
],
544+
]);
545+
});
546+
547+
$index = $this->getSearchIndex('newcollection', 'default');
548+
self::assertNotNull($index);
549+
550+
self::assertSame('default', $index['name']);
551+
self::assertSame('search', $index['type']);
552+
self::assertFalse($index['latestDefinition']['mappings']['dynamic']);
553+
self::assertSame('lucene.whitespace', $index['latestDefinition']['mappings']['fields']['foo']['analyzer']);
554+
}
555+
556+
public function testVectorSearchIndex()
557+
{
558+
$this->skipIfSearchIndexManagementIsNotSupported();
559+
560+
Schema::create('newcollection', function (Blueprint $collection) {
561+
$collection->vectorSearchIndex([
562+
'fields' => [
563+
['type' => 'vector', 'path' => 'foo', 'numDimensions' => 128, 'similarity' => 'euclidean', 'quantization' => 'none'],
564+
],
565+
], 'vector');
566+
});
567+
568+
$index = $this->getSearchIndex('newcollection', 'vector');
569+
self::assertNotNull($index);
570+
571+
self::assertSame('vector', $index['name']);
572+
self::assertSame('vectorSearch', $index['type']);
573+
self::assertSame('vector', $index['latestDefinition']['fields'][0]['type']);
574+
}
575+
526576
protected function getIndex(string $collection, string $name)
527577
{
528578
$collection = DB::getCollection($collection);
579+
assert($collection instanceof Collection);
529580

530581
foreach ($collection->listIndexes() as $index) {
531582
if (isset($index['key'][$name])) {
@@ -535,4 +586,16 @@ protected function getIndex(string $collection, string $name)
535586

536587
return false;
537588
}
589+
590+
protected function getSearchIndex(string $collection, string $name): ?array
591+
{
592+
$collection = DB::getCollection($collection);
593+
assert($collection instanceof Collection);
594+
595+
foreach ($collection->listSearchIndexes(['name' => $name, 'typeMap' => ['root' => 'array', 'array' => 'array', 'document' => 'array']]) as $index) {
596+
return $index;
597+
}
598+
599+
return null;
600+
}
538601
}

tests/TestCase.php

+15
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
namespace MongoDB\Laravel\Tests;
66

77
use Illuminate\Foundation\Application;
8+
use MongoDB\Driver\Exception\ServerException;
89
use MongoDB\Laravel\MongoDBServiceProvider;
10+
use MongoDB\Laravel\Schema\Builder;
911
use MongoDB\Laravel\Tests\Models\User;
1012
use MongoDB\Laravel\Validation\ValidationServiceProvider;
1113
use Orchestra\Testbench\TestCase as OrchestraTestCase;
@@ -64,4 +66,17 @@ protected function getEnvironmentSetUp($app): void
6466
$app['config']->set('queue.failed.database', 'mongodb2');
6567
$app['config']->set('queue.failed.driver', 'mongodb');
6668
}
69+
70+
public function skipIfSearchIndexManagementIsNotSupported(): void
71+
{
72+
try {
73+
$this->getConnection('mongodb')->getCollection('test')->listSearchIndexes(['name' => 'just_for_testing']);
74+
} catch (ServerException $e) {
75+
if (Builder::isAtlasSearchNotSupportedException($e)) {
76+
self::markTestSkipped('Search index management is not supported on this server');
77+
}
78+
79+
throw $e;
80+
}
81+
}
6782
}

0 commit comments

Comments
 (0)