Skip to content

Commit 35f4699

Browse files
authored
PHPORM-277 Add Builder::vectorSearch() (#3242)
1 parent d6d8004 commit 35f4699

File tree

3 files changed

+128
-8
lines changed

3 files changed

+128
-8
lines changed

src/Eloquent/Builder.php

+23
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Database\Eloquent\Collection;
99
use Illuminate\Database\Eloquent\Model;
1010
use MongoDB\BSON\Document;
11+
use MongoDB\Builder\Type\QueryInterface;
1112
use MongoDB\Builder\Type\SearchOperatorInterface;
1213
use MongoDB\Driver\CursorInterface;
1314
use MongoDB\Driver\Exception\WriteException;
@@ -101,6 +102,28 @@ public function search(
101102
return $this->model->hydrate($results->all());
102103
}
103104

105+
/**
106+
* Performs a semantic search on data in your Atlas Vector Search index.
107+
* NOTE: $vectorSearch is only available for MongoDB Atlas clusters, and is not available for self-managed deployments.
108+
*
109+
* @see https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/
110+
*
111+
* @return Collection<int, TModel>
112+
*/
113+
public function vectorSearch(
114+
string $index,
115+
array|string $path,
116+
array $queryVector,
117+
int $limit,
118+
bool $exact = false,
119+
QueryInterface|array $filter = [],
120+
int|null $numCandidates = null,
121+
): Collection {
122+
$results = $this->toBase()->vectorSearch($index, $path, $queryVector, $limit, $exact, $filter, $numCandidates);
123+
124+
return $this->model->hydrate($results->all());
125+
}
126+
104127
/** @inheritdoc */
105128
public function update(array $values, array $options = [])
106129
{

src/Query/Builder.php

+35
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use MongoDB\BSON\UTCDateTime;
2626
use MongoDB\Builder\Search;
2727
use MongoDB\Builder\Stage\FluentFactoryTrait;
28+
use MongoDB\Builder\Type\QueryInterface;
2829
use MongoDB\Builder\Type\SearchOperatorInterface;
2930
use MongoDB\Driver\Cursor;
3031
use Override;
@@ -1532,6 +1533,40 @@ public function search(
15321533
return $this->aggregate()->search(...$args)->get();
15331534
}
15341535

1536+
/**
1537+
* Performs a semantic search on data in your Atlas Vector Search index.
1538+
* NOTE: $vectorSearch is only available for MongoDB Atlas clusters, and is not available for self-managed deployments.
1539+
*
1540+
* @see https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/
1541+
*
1542+
* @return Collection<object|array>
1543+
*/
1544+
public function vectorSearch(
1545+
string $index,
1546+
array|string $path,
1547+
array $queryVector,
1548+
int $limit,
1549+
bool $exact = false,
1550+
QueryInterface|array|null $filter = null,
1551+
int|null $numCandidates = null,
1552+
): Collection {
1553+
// Forward named arguments to the vectorSearch stage, skip null values
1554+
$args = array_filter([
1555+
'index' => $index,
1556+
'limit' => $limit,
1557+
'path' => $path,
1558+
'queryVector' => $queryVector,
1559+
'exact' => $exact,
1560+
'filter' => $filter,
1561+
'numCandidates' => $numCandidates,
1562+
], fn ($arg) => $arg !== null);
1563+
1564+
return $this->aggregate()
1565+
->vectorSearch(...$args)
1566+
->addFields(vectorSearchScore: ['$meta' => 'vectorSearchScore'])
1567+
->get();
1568+
}
1569+
15351570
/**
15361571
* Performs an autocomplete search of the field using an Atlas Search index.
15371572
* NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments.

tests/AtlasSearchTest.php

+70-8
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,31 @@
55
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
66
use Illuminate\Support\Collection as LaravelCollection;
77
use Illuminate\Support\Facades\Schema;
8+
use MongoDB\Builder\Query;
89
use MongoDB\Builder\Search;
910
use MongoDB\Collection as MongoDBCollection;
1011
use MongoDB\Driver\Exception\ServerException;
1112
use MongoDB\Laravel\Schema\Builder;
1213
use MongoDB\Laravel\Tests\Models\Book;
1314

15+
use function array_map;
1416
use function assert;
17+
use function mt_getrandmax;
18+
use function rand;
19+
use function range;
20+
use function srand;
1521
use function usleep;
1622
use function usort;
1723

1824
class AtlasSearchTest extends TestCase
1925
{
26+
private array $vectors;
27+
2028
public function setUp(): void
2129
{
2230
parent::setUp();
2331

24-
Book::insert([
32+
Book::insert($this->addVector([
2533
['title' => 'Introduction to Algorithms'],
2634
['title' => 'Clean Code: A Handbook of Agile Software Craftsmanship'],
2735
['title' => 'Design Patterns: Elements of Reusable Object-Oriented Software'],
@@ -42,7 +50,7 @@ public function setUp(): void
4250
['title' => 'Understanding Machine Learning: From Theory to Algorithms'],
4351
['title' => 'Deep Learning'],
4452
['title' => 'Pattern Recognition and Machine Learning'],
45-
]);
53+
]));
4654

4755
$collection = $this->getConnection('mongodb')->getCollection('books');
4856
assert($collection instanceof MongoDBCollection);
@@ -66,8 +74,9 @@ public function setUp(): void
6674

6775
$collection->createSearchIndex([
6876
'fields' => [
69-
['type' => 'vector', 'numDimensions' => 16, 'path' => 'vector16', 'similarity' => 'cosine'],
77+
['type' => 'vector', 'numDimensions' => 4, 'path' => 'vector4', 'similarity' => 'cosine'],
7078
['type' => 'vector', 'numDimensions' => 32, 'path' => 'vector32', 'similarity' => 'euclidean'],
79+
['type' => 'filter', 'path' => 'title'],
7180
],
7281
], ['name' => 'vector', 'type' => 'vectorSearch']);
7382
} catch (ServerException $e) {
@@ -131,7 +140,7 @@ public function testGetIndexes()
131140
],
132141
[
133142
'name' => 'vector',
134-
'columns' => ['vector16', 'vector32'],
143+
'columns' => ['vector4', 'vector32', 'title'],
135144
'type' => 'vectorSearch',
136145
'primary' => false,
137146
'unique' => false,
@@ -180,10 +189,10 @@ public function testEloquentBuilderAutocomplete()
180189
self::assertInstanceOf(LaravelCollection::class, $results);
181190
self::assertCount(3, $results);
182191
self::assertSame([
183-
'Operating System Concepts',
184192
'Database System Concepts',
185193
'Modern Operating Systems',
186-
], $results->all());
194+
'Operating System Concepts',
195+
], $results->sort()->values()->all());
187196
}
188197

189198
public function testDatabaseBuilderAutocomplete()
@@ -194,9 +203,62 @@ public function testDatabaseBuilderAutocomplete()
194203
self::assertInstanceOf(LaravelCollection::class, $results);
195204
self::assertCount(3, $results);
196205
self::assertSame([
197-
'Operating System Concepts',
198206
'Database System Concepts',
199207
'Modern Operating Systems',
200-
], $results->all());
208+
'Operating System Concepts',
209+
], $results->sort()->values()->all());
210+
}
211+
212+
public function testDatabaseBuilderVectorSearch()
213+
{
214+
$results = $this->getConnection('mongodb')->table('books')
215+
->vectorSearch(
216+
index: 'vector',
217+
path: 'vector4',
218+
queryVector: $this->vectors[7], // This is an exact match of the vector
219+
limit: 4,
220+
exact: true,
221+
);
222+
223+
self::assertInstanceOf(LaravelCollection::class, $results);
224+
self::assertCount(4, $results);
225+
self::assertSame('The Art of Computer Programming', $results->first()['title']);
226+
self::assertSame(1.0, $results->first()['vectorSearchScore']);
227+
}
228+
229+
public function testEloquentBuilderVectorSearch()
230+
{
231+
$results = Book::vectorSearch(
232+
index: 'vector',
233+
path: 'vector4',
234+
queryVector: $this->vectors[7],
235+
limit: 5,
236+
numCandidates: 15,
237+
// excludes the exact match
238+
filter: Query::query(
239+
title: Query::ne('The Art of Computer Programming'),
240+
),
241+
);
242+
243+
self::assertInstanceOf(EloquentCollection::class, $results);
244+
self::assertCount(5, $results);
245+
self::assertInstanceOf(Book::class, $results->first());
246+
self::assertNotSame('The Art of Computer Programming', $results->first()->title);
247+
self::assertSame('The Mythical Man-Month: Essays on Software Engineering', $results->first()->title);
248+
self::assertThat(
249+
$results->first()->vectorSearchScore,
250+
self::logicalAnd(self::isType('float'), self::greaterThan(0.9), self::lessThan(1.0)),
251+
);
252+
}
253+
254+
/** Generate random vectors using fixed seed to make tests deterministic */
255+
private function addVector(array $items): array
256+
{
257+
srand(1);
258+
foreach ($items as &$item) {
259+
$this->vectors[] = $item['vector4'] = array_map(fn () => rand() / mt_getrandmax(), range(0, 3));
260+
}
261+
262+
return $items;
201263
}
202264
}

0 commit comments

Comments
 (0)