Skip to content

Commit 7890518

Browse files
authored
PHPORM-274 List search indexes in Schema::getIndexes() introspection method (mongodb#3233)
1 parent a257a9f commit 7890518

File tree

3 files changed

+217
-17
lines changed

3 files changed

+217
-17
lines changed

src/Schema/Builder.php

+44-3
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
namespace MongoDB\Laravel\Schema;
66

77
use Closure;
8+
use MongoDB\Collection;
9+
use MongoDB\Driver\Exception\ServerException;
810
use MongoDB\Model\CollectionInfo;
911
use MongoDB\Model\IndexInfo;
1012

13+
use function array_column;
1114
use function array_fill_keys;
1215
use function array_filter;
1316
use function array_keys;
1417
use function array_map;
18+
use function array_merge;
1519
use function assert;
1620
use function count;
1721
use function current;
@@ -225,9 +229,11 @@ public function getColumns($table)
225229

226230
public function getIndexes($table)
227231
{
228-
$indexes = $this->connection->getMongoDB()->selectCollection($table)->listIndexes();
229-
232+
$collection = $this->connection->getMongoDB()->selectCollection($table);
233+
assert($collection instanceof Collection);
230234
$indexList = [];
235+
236+
$indexes = $collection->listIndexes();
231237
foreach ($indexes as $index) {
232238
assert($index instanceof IndexInfo);
233239
$indexList[] = [
@@ -238,12 +244,35 @@ public function getIndexes($table)
238244
$index->isText() => 'text',
239245
$index->is2dSphere() => '2dsphere',
240246
$index->isTtl() => 'ttl',
241-
default => 'default',
247+
default => null,
242248
},
243249
'unique' => $index->isUnique(),
244250
];
245251
}
246252

253+
try {
254+
$indexes = $collection->listSearchIndexes(['typeMap' => ['root' => 'array', 'array' => 'array', 'document' => 'array']]);
255+
foreach ($indexes as $index) {
256+
$indexList[] = [
257+
'name' => $index['name'],
258+
'columns' => match ($index['type']) {
259+
'search' => array_merge(
260+
$index['latestDefinition']['mappings']['dynamic'] ? ['dynamic'] : [],
261+
array_keys($index['latestDefinition']['mappings']['fields'] ?? []),
262+
),
263+
'vectorSearch' => array_column($index['latestDefinition']['fields'], 'path'),
264+
},
265+
'type' => $index['type'],
266+
'primary' => false,
267+
'unique' => false,
268+
];
269+
}
270+
} catch (ServerException $exception) {
271+
if (! self::isAtlasSearchNotSupportedException($exception)) {
272+
throw $exception;
273+
}
274+
}
275+
247276
return $indexList;
248277
}
249278

@@ -290,4 +319,16 @@ protected function getAllCollections()
290319

291320
return $collections;
292321
}
322+
323+
/** @internal */
324+
public static function isAtlasSearchNotSupportedException(ServerException $e): bool
325+
{
326+
return in_array($e->getCode(), [
327+
59, // MongoDB 4 to 6, 7-community: no such command: 'createSearchIndexes'
328+
40324, // MongoDB 4 to 6: Unrecognized pipeline stage name: '$listSearchIndexes'
329+
115, // MongoDB 7-ent: Search index commands are only supported with Atlas.
330+
6047401, // MongoDB 7: $listSearchIndexes stage is only allowed on MongoDB Atlas
331+
31082, // MongoDB 8: Using Atlas Search Database Commands and the $listSearchIndexes aggregation stage requires additional configuration.
332+
], true);
333+
}
293334
}

tests/AtlasSearchTest.php

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
namespace MongoDB\Laravel\Tests;
4+
5+
use Illuminate\Support\Facades\Schema;
6+
use MongoDB\Collection as MongoDBCollection;
7+
use MongoDB\Driver\Exception\ServerException;
8+
use MongoDB\Laravel\Schema\Builder;
9+
use MongoDB\Laravel\Tests\Models\Book;
10+
11+
use function assert;
12+
use function usleep;
13+
use function usort;
14+
15+
class AtlasSearchTest extends TestCase
16+
{
17+
public function setUp(): void
18+
{
19+
parent::setUp();
20+
21+
Book::insert([
22+
['title' => 'Introduction to Algorithms'],
23+
['title' => 'Clean Code: A Handbook of Agile Software Craftsmanship'],
24+
['title' => 'Design Patterns: Elements of Reusable Object-Oriented Software'],
25+
['title' => 'The Pragmatic Programmer: Your Journey to Mastery'],
26+
['title' => 'Artificial Intelligence: A Modern Approach'],
27+
['title' => 'Structure and Interpretation of Computer Programs'],
28+
['title' => 'Code Complete: A Practical Handbook of Software Construction'],
29+
['title' => 'The Art of Computer Programming'],
30+
['title' => 'Computer Networks'],
31+
['title' => 'Operating System Concepts'],
32+
['title' => 'Database System Concepts'],
33+
['title' => 'Compilers: Principles, Techniques, and Tools'],
34+
['title' => 'Introduction to the Theory of Computation'],
35+
['title' => 'Modern Operating Systems'],
36+
['title' => 'Computer Organization and Design'],
37+
['title' => 'The Mythical Man-Month: Essays on Software Engineering'],
38+
['title' => 'Algorithms'],
39+
['title' => 'Understanding Machine Learning: From Theory to Algorithms'],
40+
['title' => 'Deep Learning'],
41+
['title' => 'Pattern Recognition and Machine Learning'],
42+
]);
43+
44+
$collection = $this->getConnection('mongodb')->getCollection('books');
45+
assert($collection instanceof MongoDBCollection);
46+
try {
47+
$collection->createSearchIndex([
48+
'mappings' => [
49+
'fields' => [
50+
'title' => [
51+
['type' => 'string', 'analyzer' => 'lucene.english'],
52+
['type' => 'autocomplete', 'analyzer' => 'lucene.english'],
53+
],
54+
],
55+
],
56+
]);
57+
58+
$collection->createSearchIndex([
59+
'mappings' => ['dynamic' => true],
60+
], ['name' => 'dynamic_search']);
61+
62+
$collection->createSearchIndex([
63+
'fields' => [
64+
['type' => 'vector', 'numDimensions' => 16, 'path' => 'vector16', 'similarity' => 'cosine'],
65+
['type' => 'vector', 'numDimensions' => 32, 'path' => 'vector32', 'similarity' => 'euclidean'],
66+
],
67+
], ['name' => 'vector', 'type' => 'vectorSearch']);
68+
} catch (ServerException $e) {
69+
if (Builder::isAtlasSearchNotSupportedException($e)) {
70+
self::markTestSkipped('Atlas Search not supported. ' . $e->getMessage());
71+
}
72+
73+
throw $e;
74+
}
75+
76+
// Wait for the index to be ready
77+
do {
78+
$ready = true;
79+
usleep(10_000);
80+
foreach ($collection->listSearchIndexes() as $index) {
81+
if ($index['status'] !== 'READY') {
82+
$ready = false;
83+
}
84+
}
85+
} while (! $ready);
86+
}
87+
88+
public function tearDown(): void
89+
{
90+
$this->getConnection('mongodb')->getCollection('books')->drop();
91+
92+
parent::tearDown();
93+
}
94+
95+
public function testGetIndexes()
96+
{
97+
$indexes = Schema::getIndexes('books');
98+
99+
self::assertIsArray($indexes);
100+
self::assertCount(4, $indexes);
101+
102+
// Order of indexes is not guaranteed
103+
usort($indexes, fn ($a, $b) => $a['name'] <=> $b['name']);
104+
105+
$expected = [
106+
[
107+
'name' => '_id_',
108+
'columns' => ['_id'],
109+
'primary' => true,
110+
'type' => null,
111+
'unique' => false,
112+
],
113+
[
114+
'name' => 'default',
115+
'columns' => ['title'],
116+
'type' => 'search',
117+
'primary' => false,
118+
'unique' => false,
119+
],
120+
[
121+
'name' => 'dynamic_search',
122+
'columns' => ['dynamic'],
123+
'type' => 'search',
124+
'primary' => false,
125+
'unique' => false,
126+
],
127+
[
128+
'name' => 'vector',
129+
'columns' => ['vector16', 'vector32'],
130+
'type' => 'vectorSearch',
131+
'primary' => false,
132+
'unique' => false,
133+
],
134+
];
135+
136+
self::assertSame($expected, $indexes);
137+
}
138+
}

tests/SchemaTest.php

+35-14
Original file line numberDiff line numberDiff line change
@@ -482,20 +482,41 @@ public function testGetIndexes()
482482
$collection->string('mykey3')->index();
483483
});
484484
$indexes = Schema::getIndexes('newcollection');
485-
$this->assertIsArray($indexes);
486-
$this->assertCount(4, $indexes);
487-
488-
$indexes = collect($indexes)->keyBy('name');
489-
490-
$indexes->each(function ($index) {
491-
$this->assertIsString($index['name']);
492-
$this->assertIsString($index['type']);
493-
$this->assertIsArray($index['columns']);
494-
$this->assertIsBool($index['unique']);
495-
$this->assertIsBool($index['primary']);
496-
});
497-
$this->assertTrue($indexes->get('_id_')['primary']);
498-
$this->assertTrue($indexes->get('unique_index_1')['unique']);
485+
self::assertIsArray($indexes);
486+
self::assertCount(4, $indexes);
487+
488+
$expected = [
489+
[
490+
'name' => '_id_',
491+
'columns' => ['_id'],
492+
'primary' => true,
493+
'type' => null,
494+
'unique' => false,
495+
],
496+
[
497+
'name' => 'mykey1_1',
498+
'columns' => ['mykey1'],
499+
'primary' => false,
500+
'type' => null,
501+
'unique' => false,
502+
],
503+
[
504+
'name' => 'unique_index_1',
505+
'columns' => ['unique_index'],
506+
'primary' => false,
507+
'type' => null,
508+
'unique' => true,
509+
],
510+
[
511+
'name' => 'mykey3_1',
512+
'columns' => ['mykey3'],
513+
'primary' => false,
514+
'type' => null,
515+
'unique' => false,
516+
],
517+
];
518+
519+
self::assertSame($expected, $indexes);
499520

500521
// Non-existent collection
501522
$indexes = Schema::getIndexes('missing');

0 commit comments

Comments
 (0)