Skip to content

Commit 99f3b77

Browse files
authored
PHPLIB-1394: Support "type" option for createSearchIndex(es) (#1375)
Synced with mongodb/specifications@8be1189 * Update psalm baseline
1 parent 131e002 commit 99f3b77

9 files changed

+216
-35
lines changed

psalm-baseline.xml

-5
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,6 @@
5757
<code><![CDATA[$mergedDriver['platform']]]></code>
5858
</MixedAssignment>
5959
</file>
60-
<file src="src/Collection.php">
61-
<MixedArgumentTypeCoercion>
62-
<code><![CDATA[$options]]></code>
63-
</MixedArgumentTypeCoercion>
64-
</file>
6560
<file src="src/Command/ListCollections.php">
6661
<MixedAssignment>
6762
<code><![CDATA[$cmd[$option]]]></code>

src/Collection.php

+11-11
Original file line numberDiff line numberDiff line change
@@ -371,22 +371,22 @@ public function createIndexes(array $indexes, array $options = [])
371371
*
372372
* @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/
373373
* @see https://mongodb.com/docs/manual/reference/method/db.collection.createSearchIndex/
374-
* @param array|object $definition Atlas Search index mapping definition
375-
* @param array{name?: string, comment?: mixed} $options Command options
374+
* @param array|object $definition Atlas Search index mapping definition
375+
* @param array{comment?: mixed, name?: string, type?: string} $options Index and command options
376376
* @return string The name of the created search index
377377
* @throws UnsupportedException if options are not supported by the selected server
378378
* @throws InvalidArgumentException for parameter/option parsing errors
379379
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
380380
*/
381381
public function createSearchIndex($definition, array $options = []): string
382382
{
383-
$index = ['definition' => $definition];
384-
if (isset($options['name'])) {
385-
$index['name'] = $options['name'];
386-
unset($options['name']);
387-
}
383+
$indexOptionKeys = ['name' => 1, 'type' => 1];
384+
/** @psalm-var array{name?: string, type?: string} */
385+
$indexOptions = array_intersect_key($options, $indexOptionKeys);
386+
/** @psalm-var array{comment?: mixed} */
387+
$operationOptions = array_diff_key($options, $indexOptionKeys);
388388

389-
$names = $this->createSearchIndexes([$index], $options);
389+
$names = $this->createSearchIndexes([['definition' => $definition] + $indexOptions], $operationOptions);
390390

391391
return current($names);
392392
}
@@ -400,16 +400,16 @@ public function createSearchIndex($definition, array $options = []): string
400400
* For example:
401401
*
402402
* $indexes = [
403-
* // Create a search index with the default name, on
403+
* // Create a search index with the default name on a single field
404404
* ['definition' => ['mappings' => ['dynamic' => false, 'fields' => ['title' => ['type' => 'string']]]]],
405405
* // Create a named search index on all fields
406406
* ['name' => 'search_all', 'definition' => ['mappings' => ['dynamic' => true]]],
407407
* ];
408408
*
409409
* @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/
410410
* @see https://mongodb.com/docs/manual/reference/method/db.collection.createSearchIndex/
411-
* @param list<array{name?: string, definition: array|object}> $indexes List of search index specifications
412-
* @param array{comment?: string} $options Command options
411+
* @param list<array{definition: array|object, name?: string, type?: string}> $indexes List of search index specifications
412+
* @param array{comment?: mixed} $options Command options
413413
* @return string[] The names of the created search indexes
414414
* @throws UnsupportedException if options are not supported by the selected server
415415
* @throws InvalidArgumentException for parameter/option parsing errors

src/Model/SearchIndexInput.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class SearchIndexInput implements Serializable
3939
private array $index;
4040

4141
/**
42-
* @param array{name?: string, definition: array|object} $index Search index specification
42+
* @param array{definition: array|object, name?: string, type?: string} $index Search index specification
4343
* @throws InvalidArgumentException
4444
*/
4545
public function __construct(array $index)
@@ -57,6 +57,10 @@ public function __construct(array $index)
5757
throw InvalidArgumentException::invalidType('"name" option', $index['name'], 'string');
5858
}
5959

60+
if (isset($index['type']) && ! is_string($index['type'])) {
61+
throw InvalidArgumentException::invalidType('"type" option', $index['type'], 'string');
62+
}
63+
6064
$this->index = $index;
6165
}
6266

tests/Model/SearchIndexInputTest.php

+8
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ public function testConstructorShouldRequireNameToBeString($name): void
3232
new SearchIndexInput(['definition' => ['mapping' => ['dynamic' => true]], 'name' => $name]);
3333
}
3434

35+
/** @dataProvider provideInvalidStringValues */
36+
public function testConstructorShouldRequireTypeToBeString($type): void
37+
{
38+
$this->expectException(InvalidArgumentException::class);
39+
$this->expectExceptionMessage('Expected "type" option to have type "string"');
40+
new SearchIndexInput(['definition' => ['mapping' => ['dynamic' => true]], 'type' => $type]);
41+
}
42+
3543
public function testBsonSerialization(): void
3644
{
3745
$expected = (object) [

tests/Operation/CreateSearchIndexesTest.php

+8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ public function testConstructorIndexNameMustBeAString($name): void
3030
new CreateSearchIndexes($this->getDatabaseName(), $this->getCollectionName(), [['name' => $name, 'definition' => ['mappings' => ['dynamic' => true]]]], []);
3131
}
3232

33+
/** @dataProvider provideInvalidStringValues */
34+
public function testConstructorIndexTypeMustBeAString($type): void
35+
{
36+
$this->expectException(InvalidArgumentException::class);
37+
$this->expectExceptionMessage('Expected "type" option to have type "string"');
38+
new CreateSearchIndexes($this->getDatabaseName(), $this->getCollectionName(), [['type' => $type, 'definition' => ['mappings' => ['dynamic' => true]]]], []);
39+
}
40+
3341
public function testConstructorIndexDefinitionMustBeDefined(): void
3442
{
3543
$this->expectException(InvalidArgumentException::class);

tests/UnifiedSpecTests/Operation.php

+13-7
Original file line numberDiff line numberDiff line change
@@ -537,19 +537,25 @@ private function executeForCollection(Collection $collection)
537537
);
538538

539539
case 'createSearchIndex':
540-
$options = [];
541-
if (isset($args['model']->name)) {
542-
assertIsString($args['model']->name);
543-
$options['name'] = $args['model']->name;
544-
}
545-
540+
assertArrayHasKey('model', $args);
541+
assertIsObject($args['model']);
542+
assertObjectHasAttribute('definition', $args['model']);
546543
assertInstanceOf(stdClass::class, $args['model']->definition);
547544

548-
return $collection->createSearchIndex($args['model']->definition, $options);
545+
/* Note: tests specify options within "model". A top-level
546+
* "options" key (CreateSearchIndexOptions) is not used. */
547+
$definition = $args['model']->definition;
548+
$options = array_diff_key((array) $args['model'], ['definition' => 1]);
549+
550+
return $collection->createSearchIndex($definition, $options);
549551

550552
case 'createSearchIndexes':
553+
assertArrayHasKey('models', $args);
554+
assertIsArray($args['models']);
555+
551556
$indexes = array_map(function ($index) {
552557
$index = (array) $index;
558+
assertArrayHasKey('definition', $index);
553559
assertInstanceOf(stdClass::class, $index['definition']);
554560

555561
return $index;

tests/UnifiedSpecTests/index-management/createSearchIndex.json

+79-5
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,17 @@
2828
],
2929
"runOnRequirements": [
3030
{
31-
"minServerVersion": "7.0.0",
31+
"minServerVersion": "7.0.5",
32+
"maxServerVersion": "7.0.99",
33+
"topologies": [
34+
"replicaset",
35+
"load-balanced",
36+
"sharded"
37+
],
38+
"serverless": "forbid"
39+
},
40+
{
41+
"minServerVersion": "7.2.0",
3242
"topologies": [
3343
"replicaset",
3444
"load-balanced",
@@ -50,7 +60,8 @@
5060
"mappings": {
5161
"dynamic": true
5262
}
53-
}
63+
},
64+
"type": "search"
5465
}
5566
},
5667
"expectError": {
@@ -73,7 +84,8 @@
7384
"mappings": {
7485
"dynamic": true
7586
}
76-
}
87+
},
88+
"type": "search"
7789
}
7890
],
7991
"$db": "database0"
@@ -97,7 +109,8 @@
97109
"dynamic": true
98110
}
99111
},
100-
"name": "test index"
112+
"name": "test index",
113+
"type": "search"
101114
}
102115
},
103116
"expectError": {
@@ -121,7 +134,68 @@
121134
"dynamic": true
122135
}
123136
},
124-
"name": "test index"
137+
"name": "test index",
138+
"type": "search"
139+
}
140+
],
141+
"$db": "database0"
142+
}
143+
}
144+
}
145+
]
146+
}
147+
]
148+
},
149+
{
150+
"description": "create a vector search index",
151+
"operations": [
152+
{
153+
"name": "createSearchIndex",
154+
"object": "collection0",
155+
"arguments": {
156+
"model": {
157+
"definition": {
158+
"fields": [
159+
{
160+
"type": "vector",
161+
"path": "plot_embedding",
162+
"numDimensions": 1536,
163+
"similarity": "euclidean"
164+
}
165+
]
166+
},
167+
"name": "test index",
168+
"type": "vectorSearch"
169+
}
170+
},
171+
"expectError": {
172+
"isError": true,
173+
"errorContains": "Atlas"
174+
}
175+
}
176+
],
177+
"expectEvents": [
178+
{
179+
"client": "client0",
180+
"events": [
181+
{
182+
"commandStartedEvent": {
183+
"command": {
184+
"createSearchIndexes": "collection0",
185+
"indexes": [
186+
{
187+
"definition": {
188+
"fields": [
189+
{
190+
"type": "vector",
191+
"path": "plot_embedding",
192+
"numDimensions": 1536,
193+
"similarity": "euclidean"
194+
}
195+
]
196+
},
197+
"name": "test index",
198+
"type": "vectorSearch"
125199
}
126200
],
127201
"$db": "database0"

tests/UnifiedSpecTests/index-management/createSearchIndexes.json

+81-5
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,17 @@
2828
],
2929
"runOnRequirements": [
3030
{
31-
"minServerVersion": "7.0.0",
31+
"minServerVersion": "7.0.5",
32+
"maxServerVersion": "7.0.99",
33+
"topologies": [
34+
"replicaset",
35+
"load-balanced",
36+
"sharded"
37+
],
38+
"serverless": "forbid"
39+
},
40+
{
41+
"minServerVersion": "7.2.0",
3242
"topologies": [
3343
"replicaset",
3444
"load-balanced",
@@ -83,7 +93,8 @@
8393
"mappings": {
8494
"dynamic": true
8595
}
86-
}
96+
},
97+
"type": "search"
8798
}
8899
]
89100
},
@@ -107,7 +118,8 @@
107118
"mappings": {
108119
"dynamic": true
109120
}
110-
}
121+
},
122+
"type": "search"
111123
}
112124
],
113125
"$db": "database0"
@@ -132,7 +144,8 @@
132144
"dynamic": true
133145
}
134146
},
135-
"name": "test index"
147+
"name": "test index",
148+
"type": "search"
136149
}
137150
]
138151
},
@@ -157,7 +170,70 @@
157170
"dynamic": true
158171
}
159172
},
160-
"name": "test index"
173+
"name": "test index",
174+
"type": "search"
175+
}
176+
],
177+
"$db": "database0"
178+
}
179+
}
180+
}
181+
]
182+
}
183+
]
184+
},
185+
{
186+
"description": "create a vector search index",
187+
"operations": [
188+
{
189+
"name": "createSearchIndexes",
190+
"object": "collection0",
191+
"arguments": {
192+
"models": [
193+
{
194+
"definition": {
195+
"fields": [
196+
{
197+
"type": "vector",
198+
"path": "plot_embedding",
199+
"numDimensions": 1536,
200+
"similarity": "euclidean"
201+
}
202+
]
203+
},
204+
"name": "test index",
205+
"type": "vectorSearch"
206+
}
207+
]
208+
},
209+
"expectError": {
210+
"isError": true,
211+
"errorContains": "Atlas"
212+
}
213+
}
214+
],
215+
"expectEvents": [
216+
{
217+
"client": "client0",
218+
"events": [
219+
{
220+
"commandStartedEvent": {
221+
"command": {
222+
"createSearchIndexes": "collection0",
223+
"indexes": [
224+
{
225+
"definition": {
226+
"fields": [
227+
{
228+
"type": "vector",
229+
"path": "plot_embedding",
230+
"numDimensions": 1536,
231+
"similarity": "euclidean"
232+
}
233+
]
234+
},
235+
"name": "test index",
236+
"type": "vectorSearch"
161237
}
162238
],
163239
"$db": "database0"

0 commit comments

Comments
 (0)