Skip to content

Commit

Permalink
PHPORM-277 Add Builder::vectorSearch()
Browse files Browse the repository at this point in the history
  • Loading branch information
GromNaN committed Jan 9, 2025
1 parent d6d8004 commit e853f22
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 4 deletions.
23 changes: 23 additions & 0 deletions src/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use MongoDB\BSON\Document;
use MongoDB\Builder\Type\QueryInterface;
use MongoDB\Builder\Type\SearchOperatorInterface;
use MongoDB\Driver\CursorInterface;
use MongoDB\Driver\Exception\WriteException;
Expand Down Expand Up @@ -101,6 +102,28 @@ public function search(
return $this->model->hydrate($results->all());
}

/**
* Performs a full-text search of the field or fields in an Atlas collection.
* NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments.
*
* @see https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/
*
* @return Collection<int, TModel>
*/
public function vectorSearch(
string $index,
array|string $path,
array $queryVector,
int $limit,
bool $exact = false,
QueryInterface|array $filter = [],
int|null $numCandidates = null,
): Collection {
$results = $this->toBase()->vectorSearch($index, $path, $queryVector, $limit, $exact, $filter, $numCandidates);

return $this->model->hydrate($results->all());
}

/** @inheritdoc */
public function update(array $values, array $options = [])
{
Expand Down
35 changes: 35 additions & 0 deletions src/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use MongoDB\BSON\UTCDateTime;
use MongoDB\Builder\Search;
use MongoDB\Builder\Stage\FluentFactoryTrait;
use MongoDB\Builder\Type\QueryInterface;
use MongoDB\Builder\Type\SearchOperatorInterface;
use MongoDB\Driver\Cursor;
use Override;
Expand Down Expand Up @@ -1532,6 +1533,40 @@ public function search(
return $this->aggregate()->search(...$args)->get();
}

/**
* Performs a full-text search of the field or fields in an Atlas collection.
* NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments.
*
* @see https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/
*
* @return Collection<object|array>
*/
public function vectorSearch(
string $index,
array|string $path,
array $queryVector,
int $limit,
bool $exact = false,
QueryInterface|array|null $filter = null,
int|null $numCandidates = null,
): Collection {
// Forward named arguments to the vectorSearch stage, skip null values
$args = array_filter([
'index' => $index,
'limit' => $limit,
'path' => $path,
'queryVector' => $queryVector,
'exact' => $exact,
'filter' => $filter,
'numCandidates' => $numCandidates,
], fn ($arg) => $arg !== null);

return $this->aggregate()
->vectorSearch(...$args)
->addFields(vectorSearchScore: ['$meta' => 'vectorSearchScore'])
->get();
}

/**
* Performs an autocomplete search of the field using an Atlas Search index.
* NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments.
Expand Down
70 changes: 66 additions & 4 deletions tests/AtlasSearchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,31 @@
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection as LaravelCollection;
use Illuminate\Support\Facades\Schema;
use MongoDB\Builder\Query;
use MongoDB\Builder\Search;
use MongoDB\Collection as MongoDBCollection;
use MongoDB\Driver\Exception\ServerException;
use MongoDB\Laravel\Schema\Builder;
use MongoDB\Laravel\Tests\Models\Book;

use function array_map;
use function assert;
use function mt_getrandmax;
use function rand;
use function range;
use function srand;
use function usleep;
use function usort;

class AtlasSearchTest extends TestCase
{
private array $vectors;

public function setUp(): void
{
parent::setUp();

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

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

$collection->createSearchIndex([
'fields' => [
['type' => 'vector', 'numDimensions' => 16, 'path' => 'vector16', 'similarity' => 'cosine'],
['type' => 'vector', 'numDimensions' => 4, 'path' => 'vector4', 'similarity' => 'cosine'],
['type' => 'vector', 'numDimensions' => 32, 'path' => 'vector32', 'similarity' => 'euclidean'],
['type' => 'filter', 'path' => 'title'],
],
], ['name' => 'vector', 'type' => 'vectorSearch']);
} catch (ServerException $e) {
Expand Down Expand Up @@ -131,7 +140,7 @@ public function testGetIndexes()
],
[
'name' => 'vector',
'columns' => ['vector16', 'vector32'],
'columns' => ['vector4', 'vector32', 'title'],
'type' => 'vectorSearch',
'primary' => false,
'unique' => false,
Expand Down Expand Up @@ -199,4 +208,57 @@ public function testDatabaseBuilderAutocomplete()
'Modern Operating Systems',
], $results->all());
}

public function testDatabaseBuilderVectorSearch()
{
$results = $this->getConnection('mongodb')->table('books')
->vectorSearch(
index: 'vector',
path: 'vector4',
queryVector: $this->vectors[7], // This is an exact match of the vector
limit: 4,
exact: true,
);

self::assertInstanceOf(LaravelCollection::class, $results);
self::assertCount(4, $results);
self::assertSame('The Art of Computer Programming', $results->first()['title']);
self::assertSame(1.0, $results->first()['vectorSearchScore']);
}

public function testEloquentBuilderVectorSearch()
{
$results = Book::vectorSearch(
index: 'vector',
path: 'vector4',
queryVector: $this->vectors[7],
limit: 5,
numCandidates: 15,
// excludes the exact match
filter: Query::query(
title: Query::ne('The Art of Computer Programming'),
),
);

self::assertInstanceOf(EloquentCollection::class, $results);
self::assertCount(5, $results);
self::assertInstanceOf(Book::class, $results->first());
self::assertNotSame('The Art of Computer Programming', $results->first()->title);
self::assertSame('The Mythical Man-Month: Essays on Software Engineering', $results->first()->title);
self::assertThat(
$results->first()->vectorSearchScore,
self::logicalAnd(self::isType('float'), self::greaterThan(0.9), self::lessThan(1.0)),
);
}

/** Generate random vectors */
private function addVector(array $items): array
{
srand(1);
foreach ($items as &$item) {
$this->vectors[] = $item['vector4'] = array_map(fn () => rand() / mt_getrandmax(), range(0, 3));
}

return $items;
}
}

0 comments on commit e853f22

Please sign in to comment.