├── .gitignore ├── src ├── doctrine │ ├── L1Distance.php │ ├── L2Distance.php │ ├── CosineDistance.php │ ├── HammingDistance.php │ ├── JaccardDistance.php │ ├── MaxInnerProduct.php │ ├── BitType.php │ ├── VectorType.php │ ├── DistanceNode.php │ ├── HalfVectorType.php │ ├── SparseVectorType.php │ └── PgvectorSetup.php ├── laravel │ ├── Distance.php │ ├── migrations │ │ └── 2022_08_03_000000_create_vector_extension.php │ ├── PgvectorServiceProvider.php │ ├── Vector.php │ ├── SparseVector.php │ ├── HalfVector.php │ ├── HasNeighbors.php │ └── Schema.php ├── Vector.php ├── HalfVector.php └── SparseVector.php ├── examples ├── citus │ ├── composer.json │ └── example.php ├── cohere │ ├── composer.json │ └── example.php ├── hybrid │ ├── composer.json │ └── example.php ├── openai │ ├── composer.json │ └── example.php ├── pgsql │ ├── composer.json │ └── example.php ├── sparse │ ├── composer.json │ └── example.php ├── loading │ ├── composer.json │ └── example.php ├── rdkit │ ├── composer.json │ └── example.php └── disco │ ├── composer.json │ └── example.php ├── CHANGELOG.md ├── .github └── workflows │ └── build.yml ├── LICENSE.txt ├── composer.json ├── tests ├── VectorTest.php ├── HalfVectorTest.php ├── models │ └── DoctrineItem.php ├── SparseVectorTest.php ├── PgSqlTest.php ├── DoctrineTest.php └── LaravelTest.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /src/doctrine/L1Distance.php: -------------------------------------------------------------------------------- 1 | '; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/doctrine/L2Distance.php: -------------------------------------------------------------------------------- 1 | '; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/doctrine/CosineDistance.php: -------------------------------------------------------------------------------- 1 | '; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/doctrine/HammingDistance.php: -------------------------------------------------------------------------------- 1 | '; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/doctrine/JaccardDistance.php: -------------------------------------------------------------------------------- 1 | '; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/doctrine/MaxInnerProduct.php: -------------------------------------------------------------------------------- 1 | '; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/laravel/Distance.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__.'/migrations'); 23 | 24 | $this->publishes([ 25 | __DIR__.'/migrations' => database_path('migrations') 26 | ], 'pgvector-migrations'); 27 | 28 | Schema::register(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.2 (2025-02-15) 2 | 3 | - Added support for Doctrine 4 | 5 | ## 0.2.1 (2025-02-01) 6 | 7 | - Added support for `SplFixedArray` 8 | 9 | ## 0.2.0 (2024-06-25) 10 | 11 | - Added support for `halfvec` and `sparsevec` types 12 | - Added `L1`, `Hamming`, and `Jaccard` distances 13 | - Changed `Distance` to enum 14 | - Dropped support for PHP < 8.1 15 | - Dropped support for Laravel < 10 16 | 17 | ## 0.1.4 (2023-11-14) 18 | 19 | - Moved package to `pgvector` namespace 20 | 21 | ## 0.1.3 (2023-10-03) 22 | 23 | - Added `HasNeighbors` trait for Laravel 24 | 25 | ## 0.1.2 (2023-04-11) 26 | 27 | - Fixed cast for Laraval 9 28 | 29 | ## 0.1.1 (2023-03-11) 30 | 31 | - Added `Vector` class 32 | - Added cast for Laravel 33 | 34 | ## 0.1.0 (2022-08-05) 35 | 36 | - First release 37 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | include: 9 | - php: 8.4 10 | - php: "8.1" 11 | composer: --prefer-lowest 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: shivammathur/setup-php@v2 16 | with: 17 | php-version: ${{ matrix.php }} 18 | - run: composer update ${{ matrix.composer }} 19 | - uses: ankane/setup-postgres@v1 20 | with: 21 | database: pgvector_php_test 22 | dev-files: true 23 | - run: | 24 | cd /tmp 25 | git clone --branch v0.8.1 https://github.com/pgvector/pgvector.git 26 | cd pgvector 27 | make 28 | sudo make install 29 | - run: composer test 30 | -------------------------------------------------------------------------------- /src/doctrine/BitType.php: -------------------------------------------------------------------------------- 1 | $1 LIMIT 5', [$embedding]); 20 | while ($row = pg_fetch_array($result)) { 21 | echo $row['id'] . ': ' . new Vector($row['embedding']) . "\n"; 22 | } 23 | pg_free_result($result); 24 | 25 | pg_query($db, 'CREATE INDEX ON items USING hnsw (embedding vector_l2_ops)'); 26 | 27 | pg_close($db); 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022-2025 Andrew Kane 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/rdkit/example.php: -------------------------------------------------------------------------------- 1 | morganFingerprint(); 8 | } 9 | 10 | $db = pg_connect('postgres://localhost/pgvector_example'); 11 | 12 | pg_query($db, 'CREATE EXTENSION IF NOT EXISTS vector'); 13 | pg_query($db, 'DROP TABLE IF EXISTS molecules'); 14 | pg_query($db, 'CREATE TABLE molecules (id text PRIMARY KEY, fingerprint bit(2048))'); 15 | 16 | $molecules = ['Cc1ccccc1', 'Cc1ncccc1', 'c1ccccn1']; 17 | foreach ($molecules as $molecule) { 18 | $fingerprint = generateFingerprint($molecule); 19 | pg_query_params($db, 'INSERT INTO molecules (id, fingerprint) VALUES ($1, $2)', [$molecule, $fingerprint]); 20 | } 21 | 22 | $queryMolecule = 'c1ccco1'; 23 | $queryFingerprint = generateFingerprint($queryMolecule); 24 | $result = pg_query_params($db, 'SELECT id, fingerprint <%> $1 AS distance FROM molecules ORDER BY distance LIMIT 5', [$queryFingerprint]); 25 | while ($row = pg_fetch_array($result)) { 26 | echo $row['id'] . ': ' . $row['distance'] . "\n"; 27 | } 28 | 29 | pg_free_result($result); 30 | pg_close($db); 31 | -------------------------------------------------------------------------------- /src/doctrine/VectorType.php: -------------------------------------------------------------------------------- 1 | match(TokenType::T_IDENTIFIER); 21 | $parser->match(TokenType::T_OPEN_PARENTHESIS); 22 | $this->left = $parser->ArithmeticPrimary(); 23 | $parser->match(TokenType::T_COMMA); 24 | $this->right = $parser->ArithmeticPrimary(); 25 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); 26 | } 27 | 28 | public function getSql(SqlWalker $sqlWalker): string 29 | { 30 | return sprintf( 31 | '(%s %s %s)', 32 | $sqlWalker->walkArithmeticPrimary($this->left), 33 | $this->getOp(), 34 | $sqlWalker->walkArithmeticPrimary($this->right) 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/doctrine/HalfVectorType.php: -------------------------------------------------------------------------------- 1 | = 8.1" 24 | }, 25 | "require-dev": { 26 | "doctrine/dbal": "^4", 27 | "doctrine/orm": "^3", 28 | "phpunit/phpunit": "^10", 29 | "illuminate/database": ">= 10", 30 | "laravel/serializable-closure": "^1.3", 31 | "symfony/cache": "^6" 32 | }, 33 | "extra": { 34 | "laravel": { 35 | "providers": [ 36 | "Pgvector\\Laravel\\PgvectorServiceProvider" 37 | ] 38 | } 39 | }, 40 | "scripts": { 41 | "test": "phpunit tests" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Vector.php: -------------------------------------------------------------------------------- 1 | toArray(); 24 | } 25 | 26 | if (!is_array($value)) { 27 | throw new \InvalidArgumentException("Expected array"); 28 | } 29 | 30 | if (!array_is_list($value)) { 31 | throw new \InvalidArgumentException("Expected array to be a list"); 32 | } 33 | } 34 | 35 | $this->value = $value; 36 | } 37 | 38 | public function __toString(): string 39 | { 40 | return json_encode($this->value, JSON_THROW_ON_ERROR, 1); 41 | } 42 | 43 | public function toArray(): array 44 | { 45 | return $this->value; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/HalfVector.php: -------------------------------------------------------------------------------- 1 | toArray(); 24 | } 25 | 26 | if (!is_array($value)) { 27 | throw new \InvalidArgumentException("Expected array"); 28 | } 29 | 30 | if (!array_is_list($value)) { 31 | throw new \InvalidArgumentException("Expected array to be a list"); 32 | } 33 | } 34 | 35 | $this->value = $value; 36 | } 37 | 38 | public function __toString(): string 39 | { 40 | return json_encode($this->value, JSON_THROW_ON_ERROR, 1); 41 | } 42 | 43 | public function toArray(): array 44 | { 45 | return $this->value; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/loading/example.php: -------------------------------------------------------------------------------- 1 | new Vector($e), $embeddings); 32 | pg_copy_from($db, 'items (embedding)', $rows); 33 | echo "Success!\n"; 34 | 35 | // create any indexes *after* loading initial data (skipping for this example) 36 | $createIndex = false; 37 | if ($createIndex) { 38 | echo "Creating index\n"; 39 | pg_query($db, "SET maintenance_work_mem = '8GB'"); 40 | pg_query($db, 'SET max_parallel_maintenance_workers = 7'); 41 | pg_query($db, 'CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops)'); 42 | } 43 | 44 | // update planner statistics for good measure 45 | pg_query($db, 'ANALYZE items'); 46 | 47 | pg_close($db); 48 | -------------------------------------------------------------------------------- /src/laravel/Vector.php: -------------------------------------------------------------------------------- 1 | $input, 19 | 'model' => 'text-embedding-3-small' 20 | ]; 21 | $opts = [ 22 | 'http' => [ 23 | 'method' => 'POST', 24 | 'header' => "Authorization: Bearer $apiKey\r\nContent-Type: application/json\r\n", 25 | 'content' => json_encode($data) 26 | ] 27 | ]; 28 | $context = stream_context_create($opts); 29 | $response = file_get_contents($url, false, $context); 30 | return array_map(fn ($v) => $v['embedding'], json_decode($response, true)['data']); 31 | } 32 | 33 | $input = [ 34 | 'The dog is barking', 35 | 'The cat is purring', 36 | 'The bear is growling' 37 | ]; 38 | $embeddings = embed($input); 39 | foreach ($input as $i => $content) { 40 | pg_query_params($db, 'INSERT INTO documents (content, embedding) VALUES ($1, $2)', [$content, new Vector($embeddings[$i])]); 41 | } 42 | 43 | $query = 'forest'; 44 | $queryEmbedding = embed([$query])[0]; 45 | $result = pg_query_params($db, 'SELECT * FROM documents ORDER BY embedding <=> $1 LIMIT 5', [new Vector($queryEmbedding)]); 46 | while ($row = pg_fetch_array($result)) { 47 | echo $row['content'] . "\n"; 48 | } 49 | 50 | pg_free_result($result); 51 | pg_close($db); 52 | -------------------------------------------------------------------------------- /examples/disco/example.php: -------------------------------------------------------------------------------- 1 | fit($data); 18 | 19 | foreach ($recommender->userIds() as $userId) { 20 | pg_query_params($db, 'INSERT INTO users (id, factors) VALUES ($1, $2)', [$userId, new Vector($recommender->userFactors($userId))]); 21 | } 22 | 23 | foreach ($recommender->itemIds() as $itemId) { 24 | $name = mb_convert_encoding($itemId, 'UTF-8', 'ISO-8859-1'); // fix encoding 25 | pg_query_params($db, 'INSERT INTO movies (name, factors) VALUES ($1, $2)', [$name, new Vector($recommender->itemFactors($itemId))]); 26 | } 27 | 28 | $movie = 'Star Wars (1977)'; 29 | echo "Item-based recommendations for $movie\n"; 30 | $result = pg_query_params($db, 'SELECT name FROM movies WHERE name != $1 ORDER BY factors <=> (SELECT factors FROM movies WHERE name = $1) LIMIT 5', [$movie]); 31 | while ($row = pg_fetch_array($result)) { 32 | echo $row['name'] . "\n"; 33 | } 34 | 35 | $userId = 123; 36 | echo "\nUser-based recommendations for user $userId\n"; 37 | $result = pg_query_params($db, 'SELECT name FROM movies ORDER BY factors <#> (SELECT factors FROM users WHERE id = $1) LIMIT 5', [$userId]); 38 | while ($row = pg_fetch_array($result)) { 39 | echo $row['name'] . "\n"; 40 | } 41 | 42 | pg_free_result($result); 43 | pg_close($db); 44 | -------------------------------------------------------------------------------- /tests/VectorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('[1,2,3]', (string) $embedding); 13 | } 14 | 15 | public function testToArray() 16 | { 17 | $embedding = new Vector([1, 2, 3]); 18 | $this->assertEquals([1, 2, 3], $embedding->toArray()); 19 | } 20 | 21 | public function testInvalidInteger() 22 | { 23 | $this->expectException(InvalidArgumentException::class); 24 | $this->expectExceptionMessage('Expected array'); 25 | 26 | new Vector(1); 27 | } 28 | 29 | public function testInvalidArray() 30 | { 31 | $this->expectException(InvalidArgumentException::class); 32 | $this->expectExceptionMessage('Expected array to be a list'); 33 | 34 | new Vector(['a' => 1]); 35 | } 36 | 37 | public function testInvalidString() 38 | { 39 | $this->expectException(InvalidArgumentException::class); 40 | $this->expectExceptionMessage('Invalid text representation'); 41 | 42 | new Vector('{"a": 1}'); 43 | } 44 | 45 | public function testInvalidJson() 46 | { 47 | $this->expectException(InvalidArgumentException::class); 48 | $this->expectExceptionMessage('Invalid text representation'); 49 | 50 | new Vector("tru"); 51 | } 52 | 53 | public function testEmptyArray() 54 | { 55 | $embedding = new Vector([]); 56 | $this->assertEquals('[]', (string) $embedding); 57 | } 58 | 59 | public function testSplFixedArray() 60 | { 61 | $embedding = new Vector(SplFixedArray::fromArray([1, 2, 3])); 62 | $this->assertEquals('[1,2,3]', (string) $embedding); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/HalfVectorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('[1,2,3]', (string) $embedding); 13 | } 14 | 15 | public function testToArray() 16 | { 17 | $embedding = new HalfVector([1, 2, 3]); 18 | $this->assertEquals([1, 2, 3], $embedding->toArray()); 19 | } 20 | 21 | public function testInvalidInteger() 22 | { 23 | $this->expectException(InvalidArgumentException::class); 24 | $this->expectExceptionMessage('Expected array'); 25 | 26 | new HalfVector(1); 27 | } 28 | 29 | public function testInvalidArray() 30 | { 31 | $this->expectException(InvalidArgumentException::class); 32 | $this->expectExceptionMessage('Expected array to be a list'); 33 | 34 | new HalfVector(['a' => 1]); 35 | } 36 | 37 | public function testInvalidString() 38 | { 39 | $this->expectException(InvalidArgumentException::class); 40 | $this->expectExceptionMessage('Invalid text representation'); 41 | 42 | new HalfVector('{"a": 1}'); 43 | } 44 | 45 | public function testInvalidJson() 46 | { 47 | $this->expectException(InvalidArgumentException::class); 48 | $this->expectExceptionMessage('Invalid text representation'); 49 | 50 | new HalfVector("tru"); 51 | } 52 | 53 | public function testEmptyArray() 54 | { 55 | $embedding = new HalfVector([]); 56 | $this->assertEquals('[]', (string) $embedding); 57 | } 58 | 59 | public function testSplFixedArray() 60 | { 61 | $embedding = new HalfVector(SplFixedArray::fromArray([1, 2, 3])); 62 | $this->assertEquals('[1,2,3]', (string) $embedding); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/cohere/example.php: -------------------------------------------------------------------------------- 1 | $texts, 18 | 'model' => 'embed-v4.0', 19 | 'input_type' => $inputType, 20 | 'embedding_types' => ['ubinary'] 21 | ]; 22 | $opts = [ 23 | 'http' => [ 24 | 'method' => 'POST', 25 | 'header' => "Authorization: Bearer $apiKey\r\nContent-Type: application/json\r\n", 26 | 'content' => json_encode($data) 27 | ] 28 | ]; 29 | $context = stream_context_create($opts); 30 | $response = file_get_contents($url, false, $context); 31 | return array_map(fn ($e) => implode(array_map(fn ($v) => str_pad(decbin($v), 8, '0', STR_PAD_LEFT), $e)), json_decode($response, true)['embeddings']['ubinary']); 32 | } 33 | 34 | $input = [ 35 | 'The dog is barking', 36 | 'The cat is purring', 37 | 'The bear is growling' 38 | ]; 39 | $embeddings = embed($input, 'search_document'); 40 | foreach ($input as $i => $content) { 41 | pg_query_params($db, 'INSERT INTO documents (content, embedding) VALUES ($1, $2)', [$content, $embeddings[$i]]); 42 | } 43 | 44 | $query = 'forest'; 45 | $queryEmbedding = embed([$query], 'search_query')[0]; 46 | $result = pg_query_params($db, 'SELECT * FROM documents ORDER BY embedding <~> $1 LIMIT 5', [$queryEmbedding]); 47 | while ($row = pg_fetch_array($result)) { 48 | echo $row['content'] . "\n"; 49 | } 50 | 51 | pg_free_result($result); 52 | pg_close($db); 53 | -------------------------------------------------------------------------------- /src/doctrine/PgvectorSetup.php: -------------------------------------------------------------------------------- 1 | getConnection()->getDatabasePlatform()); 16 | self::addFunctions($entityManager->getConfiguration()); 17 | } 18 | 19 | private static function addTypes(): void 20 | { 21 | Type::addType('vector', 'Pgvector\Doctrine\VectorType'); 22 | Type::addType('halfvec', 'Pgvector\Doctrine\HalfVectorType'); 23 | Type::addType('bit', 'Pgvector\Doctrine\BitType'); 24 | Type::addType('sparsevec', 'Pgvector\Doctrine\SparseVectorType'); 25 | } 26 | 27 | private static function registerTypeMapping(AbstractPlatform $platform): void 28 | { 29 | $platform->registerDoctrineTypeMapping('vector', 'vector'); 30 | $platform->registerDoctrineTypeMapping('halfvec', 'halfvec'); 31 | $platform->registerDoctrineTypeMapping('bit', 'bit'); 32 | $platform->registerDoctrineTypeMapping('sparsevec', 'sparsevec'); 33 | } 34 | 35 | private static function addFunctions(Configuration $config): void 36 | { 37 | $config->addCustomNumericFunction('l2_distance', 'Pgvector\Doctrine\L2Distance'); 38 | $config->addCustomNumericFunction('max_inner_product', 'Pgvector\Doctrine\MaxInnerProduct'); 39 | $config->addCustomNumericFunction('cosine_distance', 'Pgvector\Doctrine\CosineDistance'); 40 | $config->addCustomNumericFunction('l1_distance', 'Pgvector\Doctrine\L1Distance'); 41 | $config->addCustomNumericFunction('hamming_distance', 'Pgvector\Doctrine\HammingDistance'); 42 | $config->addCustomNumericFunction('jaccard_distance', 'Pgvector\Doctrine\JaccardDistance'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/models/DoctrineItem.php: -------------------------------------------------------------------------------- 1 | id; 32 | } 33 | 34 | public function setId(?int $id): void 35 | { 36 | $this->id = $id; 37 | } 38 | 39 | public function getEmbedding(): ?Vector 40 | { 41 | return $this->embedding; 42 | } 43 | 44 | public function setEmbedding(?Vector $embedding): void 45 | { 46 | $this->embedding = $embedding; 47 | } 48 | 49 | public function getHalfEmbedding(): ?HalfVector 50 | { 51 | return $this->halfEmbedding; 52 | } 53 | 54 | public function setHalfEmbedding(?HalfVector $embedding): void 55 | { 56 | $this->halfEmbedding = $embedding; 57 | } 58 | 59 | public function getBinaryEmbedding(): ?string 60 | { 61 | return $this->binaryEmbedding; 62 | } 63 | 64 | public function setBinaryEmbedding(?string $embedding): void 65 | { 66 | $this->binaryEmbedding = $embedding; 67 | } 68 | 69 | public function getSparseEmbedding(): ?SparseVector 70 | { 71 | return $this->sparseEmbedding; 72 | } 73 | 74 | public function setSparseEmbedding(?SparseVector $embedding): void 75 | { 76 | $this->sparseEmbedding = $embedding; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/laravel/HasNeighbors.php: -------------------------------------------------------------------------------- 1 | '; 15 | break; 16 | case Distance::InnerProduct: 17 | $op = '<#>'; 18 | break; 19 | case Distance::Cosine: 20 | $op = '<=>'; 21 | break; 22 | case Distance::L1: 23 | $op = '<+>'; 24 | break; 25 | case Distance::Hamming: 26 | $op = '<~>'; 27 | break; 28 | case Distance::Jaccard: 29 | $op = '<%>'; 30 | break; 31 | default: 32 | throw new \InvalidArgumentException("Invalid distance"); 33 | } 34 | $wrapped = $query->getGrammar()->wrap($column); 35 | $order = "$wrapped $op ?"; 36 | $neighborDistance = $distance == Distance::InnerProduct ? "($order) * -1" : $order; 37 | $vector = is_array($value) ? new Vector($value) : $value; 38 | 39 | // ideally preserve existing select, but does not appear to be a way to get columns 40 | $query->select() 41 | ->selectRaw("$neighborDistance AS neighbor_distance", [$vector]) 42 | ->withCasts(['neighbor_distance' => 'double']) 43 | ->whereNotNull($column) 44 | ->orderByRaw($order, [$vector]); 45 | } 46 | 47 | public function nearestNeighbors(string $column, Distance $distance): Builder 48 | { 49 | $id = $this->getKey(); 50 | if (!array_key_exists($column, $this->attributes)) { 51 | throw new MissingAttributeException($this, $column); 52 | } 53 | $value = $this->getAttributeValue($column); 54 | return static::whereKeyNot($id)->nearestNeighbors($column, $value, $distance); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/sparse/example.php: -------------------------------------------------------------------------------- 1 | $inputs 25 | ]; 26 | $opts = [ 27 | 'http' => [ 28 | 'method' => 'POST', 29 | 'header' => "Content-Type: application/json\r\n", 30 | 'content' => json_encode($data) 31 | ] 32 | ]; 33 | $context = stream_context_create($opts); 34 | $response = file_get_contents($url, false, $context); 35 | $embeddings = []; 36 | foreach (json_decode($response, true) as $item) { 37 | $embedding = []; 38 | foreach ($item as $e) { 39 | $embedding[$e['index']] = $e['value']; 40 | } 41 | $embeddings[] = $embedding; 42 | } 43 | return $embeddings; 44 | } 45 | 46 | $input = [ 47 | 'The dog is barking', 48 | 'The cat is purring', 49 | 'The bear is growling' 50 | ]; 51 | $embeddings = embed($input); 52 | foreach ($input as $i => $content) { 53 | pg_query_params($db, 'INSERT INTO documents (content, embedding) VALUES ($1, $2)', [$content, new SparseVector($embeddings[$i], 30522)]); 54 | } 55 | 56 | $query = 'forest'; 57 | $queryEmbedding = embed([$query])[0]; 58 | $result = pg_query_params($db, 'SELECT content FROM documents ORDER BY embedding <#> $1 LIMIT 5', [new SparseVector($queryEmbedding, 30522)]); 59 | while ($row = pg_fetch_array($result)) { 60 | echo $row['content'] . "\n"; 61 | } 62 | 63 | pg_free_result($result); 64 | pg_close($db); 65 | -------------------------------------------------------------------------------- /tests/SparseVectorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(6, $embedding->dimensions()); 13 | $this->assertEquals([0, 2, 4], $embedding->indices()); 14 | $this->assertEquals([1, 2, 3], $embedding->values()); 15 | } 16 | 17 | public function testFromDenseSplFixedArray() 18 | { 19 | $embedding = new SparseVector(SplFixedArray::fromArray([1, 0, 2, 0, 3, 0])); 20 | $this->assertEquals('{1:1,3:2,5:3}/6', (string) $embedding); 21 | } 22 | 23 | public function testFromMap() 24 | { 25 | $map = [2 => 2, 4 => 3, 0 => 1, 3 => 0]; 26 | $embedding = new SparseVector($map, 6); 27 | $this->assertEquals([1, 0, 2, 0, 3, 0], $embedding->toArray()); 28 | $this->assertEquals([0, 2, 4], $embedding->indices()); 29 | $this->assertEquals([2, 4, 0, 3], array_keys($map)); 30 | } 31 | 32 | public function testFromString() 33 | { 34 | $embedding = new SparseVector('{1:1,3:2,5:3}/6'); 35 | $this->assertEquals(6, $embedding->dimensions()); 36 | $this->assertEquals([0, 2, 4], $embedding->indices()); 37 | $this->assertEquals([1, 2, 3], $embedding->values()); 38 | } 39 | 40 | public function testFromStringDimensions() 41 | { 42 | $this->expectException(InvalidArgumentException::class); 43 | $this->expectExceptionMessage('Extra argument'); 44 | 45 | new SparseVector('{1:1,3:2,5:3}/6', 6); 46 | } 47 | 48 | public function testInvalidInteger() 49 | { 50 | $this->expectException(InvalidArgumentException::class); 51 | $this->expectExceptionMessage('Expected array'); 52 | 53 | new SparseVector(1); 54 | } 55 | 56 | public function testToString() 57 | { 58 | $embedding = new SparseVector([1, 0, 2, 0, 3, 0]); 59 | $this->assertEquals('{1:1,3:2,5:3}/6', (string) $embedding); 60 | } 61 | 62 | public function testToArray() 63 | { 64 | $embedding = new SparseVector([1, 0, 2, 0, 3, 0]); 65 | $this->assertEquals([1, 0, 2, 0, 3, 0], $embedding->toArray()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/laravel/Schema.php: -------------------------------------------------------------------------------- 1 | get('dimensions')) { 15 | return 'vector(' . intval($column->get('dimensions')) . ')'; 16 | } else { 17 | return 'vector'; 18 | } 19 | }); 20 | 21 | PostgresGrammar::macro('typeHalfvec', function (ColumnDefinition $column): string { 22 | if ($column->get('dimensions')) { 23 | return 'halfvec(' . intval($column->get('dimensions')) . ')'; 24 | } else { 25 | return 'halfvec'; 26 | } 27 | }); 28 | 29 | PostgresGrammar::macro('typeBit', function (ColumnDefinition $column): string { 30 | if ($column->get('length')) { 31 | return 'bit(' . intval($column->get('length')) . ')'; 32 | } else { 33 | return 'bit'; 34 | } 35 | }); 36 | 37 | PostgresGrammar::macro('typeSparsevec', function (ColumnDefinition $column): string { 38 | if ($column->get('dimensions')) { 39 | return 'sparsevec(' . intval($column->get('dimensions')) . ')'; 40 | } else { 41 | return 'sparsevec'; 42 | } 43 | }); 44 | 45 | Blueprint::macro('vector', function (string $column, mixed $dimensions = null): ColumnDefinition { 46 | return $this->addColumn('vector', $column, compact('dimensions')); 47 | }); 48 | 49 | Blueprint::macro('halfvec', function (string $column, mixed $dimensions = null): ColumnDefinition { 50 | return $this->addColumn('halfvec', $column, compact('dimensions')); 51 | }); 52 | 53 | Blueprint::macro('bit', function (string $column, mixed $length = null): ColumnDefinition { 54 | return $this->addColumn('bit', $column, compact('length')); 55 | }); 56 | 57 | Blueprint::macro('sparsevec', function (string $column, mixed $dimensions = null): ColumnDefinition { 58 | return $this->addColumn('sparsevec', $column, compact('dimensions')); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/citus/example.php: -------------------------------------------------------------------------------- 1 | $e) { 50 | $row = [new Vector($e), $categories[$i]]; 51 | $line = join("\t", array_map(fn ($v) => pg_escape_string($db, $v), $row)) . "\n"; 52 | pg_put_line($db, $line); 53 | } 54 | pg_put_line($db, "\\.\n"); 55 | pg_end_copy($db); 56 | 57 | echo "Creating index in parallel\n"; 58 | pg_query($db, 'CREATE INDEX ON items USING hnsw (embedding vector_l2_ops)'); 59 | 60 | echo "Running distributed queries\n"; 61 | for ($i = 0; $i < 10; $i++) { 62 | $result = pg_query_params($db, 'SELECT id FROM items ORDER BY embedding <-> $1 LIMIT 10', [new Vector($embeddings[rand(0, $rows - 1)])]); 63 | $ids = []; 64 | while ($row = pg_fetch_array($result)) { 65 | $ids[] = $row['id']; 66 | } 67 | echo join(', ', $ids) . "\n"; 68 | pg_free_result($result); 69 | } 70 | 71 | pg_close($db); 72 | -------------------------------------------------------------------------------- /examples/hybrid/example.php: -------------------------------------------------------------------------------- 1 | $taskType . ': ' . $v, $input); 19 | 20 | $url = 'http://localhost:11434/api/embed'; 21 | $data = [ 22 | 'input' => $input, 23 | 'model' => 'nomic-embed-text' 24 | ]; 25 | $opts = [ 26 | 'http' => [ 27 | 'method' => 'POST', 28 | 'header' => "Content-Type: application/json\r\n", 29 | 'content' => json_encode($data) 30 | ] 31 | ]; 32 | $context = stream_context_create($opts); 33 | $response = file_get_contents($url, false, $context); 34 | return json_decode($response, true)['embeddings']; 35 | } 36 | 37 | $input = [ 38 | 'The dog is barking', 39 | 'The cat is purring', 40 | 'The bear is growling' 41 | ]; 42 | $embeddings = embed($input, 'search_document'); 43 | 44 | foreach ($input as $i => $content) { 45 | pg_query_params($db, 'INSERT INTO documents (content, embedding) VALUES ($1, $2)', [$content, new Vector($embeddings[$i])]); 46 | } 47 | 48 | $sql = << $2) AS rank 51 | FROM documents 52 | ORDER BY embedding <=> $2 53 | LIMIT 20 54 | ), 55 | keyword_search AS ( 56 | SELECT id, RANK () OVER (ORDER BY ts_rank_cd(to_tsvector('english', content), query) DESC) 57 | FROM documents, plainto_tsquery('english', $1) query 58 | WHERE to_tsvector('english', content) @@ query 59 | ORDER BY ts_rank_cd(to_tsvector('english', content), query) DESC 60 | LIMIT 20 61 | ) 62 | SELECT 63 | COALESCE(semantic_search.id, keyword_search.id) AS id, 64 | COALESCE(1.0 / ($3 + semantic_search.rank), 0.0) + 65 | COALESCE(1.0 / ($3 + keyword_search.rank), 0.0) AS score 66 | FROM semantic_search 67 | FULL OUTER JOIN keyword_search ON semantic_search.id = keyword_search.id 68 | ORDER BY score DESC 69 | LIMIT 5 70 | SQL; 71 | $query = 'growling bear'; 72 | $queryEmbedding = embed([$query], 'search_query')[0]; 73 | $k = 60; 74 | $result = pg_query_params($db, $sql, [$query, new Vector($queryEmbedding), $k]); 75 | while ($row = pg_fetch_array($result)) { 76 | echo 'document: ' . $row['id'] . ', RRF score: ' . $row['score'] . "\n"; 77 | } 78 | 79 | pg_free_result($result); 80 | pg_close($db); 81 | -------------------------------------------------------------------------------- /tests/PgSqlTest.php: -------------------------------------------------------------------------------- 1 | $1 LIMIT 5', [$embedding]); 40 | while ($row = pg_fetch_array($result)) { 41 | $ids[] = $row['id']; 42 | $embeddings[] = $row['embedding']; 43 | $halfEmbeddings[] = $row['half_embedding']; 44 | $binaryEmbeddings[] = $row['binary_embedding']; 45 | $sparseEmbeddings[] = $row['sparse_embedding']; 46 | } 47 | pg_free_result($result); 48 | 49 | $this->assertEquals([1, 3, 2], $ids); 50 | $this->assertEquals(['[1,1,1]', '[1,1,2]', '[2,2,2]'], $embeddings); 51 | $this->assertEquals([1, 1, 1], (new Vector($embeddings[0]))->toArray()); 52 | $this->assertEquals([1, 1, 1], (new HalfVector($halfEmbeddings[0]))->toArray()); 53 | $this->assertEquals('000', $binaryEmbeddings[0]); 54 | $this->assertEquals([1, 1, 1], (new SparseVector($sparseEmbeddings[0]))->toArray()); 55 | 56 | $rows = [$embedding1, $embedding2, $embedding3]; 57 | pg_copy_from($db, 'items (embedding)', $rows); 58 | 59 | pg_close($db); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/SparseVector.php: -------------------------------------------------------------------------------- 1 | 1) { 17 | throw new \InvalidArgumentException("Extra argument"); 18 | } 19 | 20 | $this->fromString($value); 21 | } else { 22 | if ($value instanceof \SplFixedArray) { 23 | $value = $value->toArray(); 24 | } 25 | 26 | if (!is_array($value)) { 27 | throw new \InvalidArgumentException("Expected array"); 28 | } 29 | 30 | if ($numArgs > 1) { 31 | $this->fromMap($value, $dimensions); 32 | } else { 33 | $this->fromDense($value); 34 | } 35 | } 36 | } 37 | 38 | private function fromDense(array $value): void 39 | { 40 | $this->dimensions = count($value); 41 | $this->indices = []; 42 | $this->values = []; 43 | 44 | foreach ($value as $i => $v) { 45 | if ($v != 0) { 46 | $this->indices[] = intval($i); 47 | $this->values[] = floatval($v); 48 | } 49 | } 50 | } 51 | 52 | private function fromMap(array $map, mixed $dimensions): void 53 | { 54 | $this->dimensions = intval($dimensions); 55 | $this->indices = []; 56 | $this->values = []; 57 | 58 | // okay to update in-place since parameter is not a reference 59 | ksort($map); 60 | 61 | foreach ($map as $i => $v) { 62 | if ($v != 0) { 63 | $this->indices[] = intval($i); 64 | $this->values[] = floatval($v); 65 | } 66 | } 67 | } 68 | 69 | private function fromString(string $value): void 70 | { 71 | $parts = explode('/', $value, 2); 72 | 73 | $this->dimensions = intval($parts[1]); 74 | $this->indices = []; 75 | $this->values = []; 76 | 77 | $elements = explode(',', substr($parts[0], 1, -1)); 78 | foreach ($elements as $e) { 79 | $ep = explode(':', $e, 2); 80 | $this->indices[] = intval($ep[0]) - 1; 81 | $this->values[] = floatval($ep[1]); 82 | } 83 | } 84 | 85 | public function __toString(): string 86 | { 87 | $elements = []; 88 | for ($i = 0; $i < count($this->indices); $i++) { 89 | $elements[] = ($this->indices[$i] + 1) . ':' . $this->values[$i]; 90 | } 91 | return '{' . implode(',', $elements) . '}/' . $this->dimensions; 92 | } 93 | 94 | public function dimensions(): int 95 | { 96 | return $this->dimensions; 97 | } 98 | 99 | public function indices(): array 100 | { 101 | return $this->indices; 102 | } 103 | 104 | public function values(): array 105 | { 106 | return $this->values; 107 | } 108 | 109 | public function toArray(): array 110 | { 111 | $result = array_fill(0, $this->dimensions, 0.0); 112 | for ($i = 0; $i < count($this->indices); $i++) { 113 | $result[$this->indices[$i]] = $this->values[$i]; 114 | } 115 | return $result; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pgvector-php 2 | 3 | [pgvector](https://github.com/pgvector/pgvector) support for PHP 4 | 5 | Supports [Laravel](https://github.com/laravel/laravel), [Doctrine](https://github.com/doctrine/orm), and [PgSql](https://www.php.net/manual/en/book.pgsql.php) 6 | 7 | [![Build Status](https://github.com/pgvector/pgvector-php/actions/workflows/build.yml/badge.svg)](https://github.com/pgvector/pgvector-php/actions) 8 | 9 | ## Getting Started 10 | 11 | Follow the instructions for your database library: 12 | 13 | - [Laravel](#laravel) 14 | - [Doctrine](#doctrine) 15 | - [PgSql](#pgsql) 16 | 17 | Or check out some examples: 18 | 19 | - [Embeddings](examples/openai/example.php) with OpenAI 20 | - [Binary embeddings](examples/cohere/example.php) with Cohere 21 | - [Hybrid search](examples/hybrid/example.php) with Ollama (Reciprocal Rank Fusion) 22 | - [Sparse search](examples/sparse/example.php) with Text Embeddings Inference 23 | - [Morgan fingerprints](examples/rdkit/example.php) with RDKit 24 | - [Recommendations](examples/disco/example.php) with Disco 25 | - [Horizontal scaling](examples/citus/example.php) with Citus 26 | - [Bulk loading](examples/loading/example.php) with `COPY` 27 | 28 | ### Laravel 29 | 30 | Install the package 31 | 32 | ```sh 33 | composer require pgvector/pgvector 34 | ``` 35 | 36 | Enable the extension 37 | 38 | ```sh 39 | php artisan vendor:publish --tag="pgvector-migrations" 40 | php artisan migrate 41 | ``` 42 | 43 | You can now use the `vector` type in future migrations 44 | 45 | ```php 46 | Schema::create('items', function (Blueprint $table) { 47 | $table->vector('embedding', 3); 48 | }); 49 | ``` 50 | 51 | Update your model 52 | 53 | ```php 54 | use Pgvector\Laravel\Vector; 55 | 56 | class Item extends Model 57 | { 58 | use HasNeighbors; 59 | 60 | protected $casts = ['embedding' => Vector::class]; 61 | } 62 | ``` 63 | 64 | Insert a vector 65 | 66 | ```php 67 | $item = new Item(); 68 | $item->embedding = [1, 2, 3]; 69 | $item->save(); 70 | ``` 71 | 72 | Get the nearest neighbors to a record 73 | 74 | ```php 75 | use Pgvector\Laravel\Distance; 76 | 77 | $neighbors = $item->nearestNeighbors('embedding', Distance::L2)->take(5)->get(); 78 | ``` 79 | 80 | Also supports `InnerProduct`, `Cosine`, `L1`, `Hamming`, and `Jaccard` distance 81 | 82 | Get the nearest neighbors to a vector 83 | 84 | ```php 85 | $neighbors = Item::query()->nearestNeighbors('embedding', [1, 2, 3], Distance::L2)->take(5)->get(); 86 | ``` 87 | 88 | Get the distances 89 | 90 | ```php 91 | $neighbors->pluck('neighbor_distance'); 92 | ``` 93 | 94 | Add an approximate index in a migration 95 | 96 | ```php 97 | public function up() 98 | { 99 | DB::statement('CREATE INDEX my_index ON items USING hnsw (embedding vector_l2_ops)'); 100 | // or 101 | DB::statement('CREATE INDEX my_index ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 100)'); 102 | } 103 | 104 | public function down() 105 | { 106 | DB::statement('DROP INDEX my_index'); 107 | } 108 | ``` 109 | 110 | Use `vector_ip_ops` for inner product and `vector_cosine_ops` for cosine distance 111 | 112 | ### Doctrine 113 | 114 | Install the package 115 | 116 | ```sh 117 | composer require pgvector/pgvector 118 | ``` 119 | 120 | Register the types and distance functions 121 | 122 | ```php 123 | use Pgvector\Doctrine\PgvectorSetup; 124 | 125 | PgvectorSetup::registerTypes($entityManager); 126 | ``` 127 | 128 | Enable the extension 129 | 130 | ```php 131 | $entityManager->getConnection()->executeStatement('CREATE EXTENSION IF NOT EXISTS vector'); 132 | ``` 133 | 134 | Update your model 135 | 136 | ```php 137 | use Pgvector\Vector; 138 | 139 | #[ORM\Entity] 140 | class Item 141 | { 142 | #[ORM\Column(type: 'vector', length: 3)] 143 | private Vector $embedding; 144 | 145 | public function setEmbedding(Vector $embedding): void 146 | { 147 | $this->embedding = $embedding; 148 | } 149 | } 150 | ``` 151 | 152 | Insert a vector 153 | 154 | ```php 155 | $item = new Item(); 156 | $item->setEmbedding(new Vector([1, 2, 3])); 157 | $entityManager->persist($item); 158 | $entityManager->flush(); 159 | ``` 160 | 161 | Get the nearest neighbors to a vector 162 | 163 | ```php 164 | $neighbors = $entityManager->createQuery('SELECT i FROM Item i ORDER BY l2_distance(i.embedding, ?1)') 165 | ->setParameter(1, new Vector([1, 2, 3])) 166 | ->setMaxResults(5) 167 | ->getResult(); 168 | ``` 169 | 170 | Also supports `max_inner_product`, `cosine_distance`, `l1_distance`, `hamming_distance`, and `jaccard_distance` 171 | 172 | ### PgSql 173 | 174 | Enable the extension 175 | 176 | ```php 177 | pg_query($db, 'CREATE EXTENSION IF NOT EXISTS vector'); 178 | ``` 179 | 180 | Create a table 181 | 182 | ```php 183 | pg_query($db, 'CREATE TABLE items (embedding vector(3))'); 184 | ``` 185 | 186 | Insert a vector 187 | 188 | ```php 189 | use Pgvector\Vector; 190 | 191 | $embedding = new Vector([1, 2, 3]); 192 | pg_query_params($db, 'INSERT INTO items (embedding) VALUES ($1)', [$embedding]); 193 | ``` 194 | 195 | Get the nearest neighbors to a vector 196 | 197 | ```php 198 | $embedding = new Vector([1, 2, 3]); 199 | $result = pg_query_params($db, 'SELECT * FROM items ORDER BY embedding <-> $1 LIMIT 5', [$embedding]); 200 | ``` 201 | 202 | Add an approximate index 203 | 204 | ```php 205 | pg_query($db, 'CREATE INDEX ON items USING hnsw (embedding vector_l2_ops)'); 206 | // or 207 | pg_query($db, 'CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 100)'); 208 | ``` 209 | 210 | See a [full example](examples/pgsql/example.php) 211 | 212 | ## Reference 213 | 214 | ### Vectors 215 | 216 | Create a vector from an array 217 | 218 | ```php 219 | $vec = new Vector([1, 2, 3]); 220 | ``` 221 | 222 | Get an array 223 | 224 | ```php 225 | $arr = $vec->toArray(); 226 | ``` 227 | 228 | ### Half Vectors 229 | 230 | Create a half vector from an array 231 | 232 | ```php 233 | $vec = new HalfVector([1, 2, 3]); 234 | ``` 235 | 236 | Get an array 237 | 238 | ```php 239 | $arr = $vec->toArray(); 240 | ``` 241 | 242 | ### Sparse Vectors 243 | 244 | Create a sparse vector from an indexed array 245 | 246 | ```php 247 | $vec = new SparseVector([1, 0, 2, 0, 3, 0]); 248 | ``` 249 | 250 | Or an associative array of non-zero elements 251 | 252 | ```php 253 | $elements = [0 => 1, 2 => 2, 4 => 3]; 254 | $vec = new SparseVector($elements, 6); 255 | ``` 256 | 257 | Note: Indices start at 0 258 | 259 | Get the number of dimensions 260 | 261 | ```php 262 | $dim = $vec->dimensions(); 263 | ``` 264 | 265 | Get the indices of non-zero elements 266 | 267 | ```php 268 | $indices = $vec->indices(); 269 | ``` 270 | 271 | Get the values of non-zero elements 272 | 273 | ```php 274 | $values = $vec->values(); 275 | ``` 276 | 277 | Get an array 278 | 279 | ```php 280 | $arr = $vec->toArray(); 281 | ``` 282 | 283 | ## History 284 | 285 | View the [changelog](https://github.com/pgvector/pgvector-php/blob/master/CHANGELOG.md) 286 | 287 | ## Contributing 288 | 289 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 290 | 291 | - [Report bugs](https://github.com/pgvector/pgvector-php/issues) 292 | - Fix bugs and [submit pull requests](https://github.com/pgvector/pgvector-php/pulls) 293 | - Write, clarify, or fix documentation 294 | - Suggest or add new features 295 | 296 | To get started with development: 297 | 298 | ```sh 299 | git clone https://github.com/pgvector/pgvector-php.git 300 | cd pgvector-php 301 | composer install 302 | createdb pgvector_php_test 303 | composer test 304 | ``` 305 | 306 | To run an example: 307 | 308 | ```sh 309 | cd examples/loading 310 | composer install 311 | createdb pgvector_example 312 | php example.php 313 | ``` 314 | -------------------------------------------------------------------------------- /tests/DoctrineTest.php: -------------------------------------------------------------------------------- 1 | 'pgsql', 32 | 'dbname' => 'pgvector_php_test' 33 | ], $config); 34 | 35 | $entityManager = new EntityManager($connection, $config); 36 | $entityManager->getConnection()->executeStatement('CREATE EXTENSION IF NOT EXISTS vector'); 37 | PgvectorSetup::registerTypes($entityManager); 38 | 39 | $schemaManager = $entityManager->getConnection()->createSchemaManager(); 40 | try { 41 | $schemaManager->dropTable('doctrine_items'); 42 | } catch (TableNotFoundException $e) { 43 | // do nothing 44 | } 45 | 46 | $schemaTool = new SchemaTool($entityManager); 47 | $schemaTool->createSchema([$entityManager->getClassMetadata('DoctrineItem')]); 48 | 49 | self::$em = $entityManager; 50 | } 51 | 52 | public function setUp(): void 53 | { 54 | self::$em->getConnection()->executeStatement('TRUNCATE doctrine_items RESTART IDENTITY'); 55 | self::$em->clear(); 56 | } 57 | 58 | public function testTypes() 59 | { 60 | $item = new DoctrineItem(); 61 | $item->setEmbedding(new Vector([1, 2, 3])); 62 | $item->setHalfEmbedding(new HalfVector([4, 5, 6])); 63 | $item->setBinaryEmbedding('101'); 64 | $item->setSparseEmbedding(new SparseVector([7, 8, 9])); 65 | self::$em->persist($item); 66 | self::$em->flush(); 67 | 68 | $itemRepository = self::$em->getRepository('DoctrineItem'); 69 | $item = $itemRepository->find(1); 70 | $this->assertEquals([1, 2, 3], $item->getEmbedding()->toArray()); 71 | $this->assertEquals([4, 5, 6], $item->getHalfEmbedding()->toArray()); 72 | $this->assertEquals('101', $item->getBinaryEmbedding()); 73 | $this->assertEquals([7, 8, 9], $item->getSparseEmbedding()->toArray()); 74 | } 75 | 76 | public function testVectorL2Distance() 77 | { 78 | $this->createItems(); 79 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY l2_distance(i.embedding, ?1)') 80 | ->setParameter(1, new Vector([1, 1, 1])) 81 | ->setMaxResults(5) 82 | ->getResult(); 83 | $this->assertEquals([1, 3, 2], array_map(fn ($v) => $v->getId(), $neighbors)); 84 | $this->assertEquals([[1, 1, 1], [1, 1, 2], [2, 2, 2]], array_map(fn ($v) => $v->getEmbedding()->toArray(), $neighbors)); 85 | } 86 | 87 | public function testVectorMaxInnerProduct() 88 | { 89 | $this->createItems(); 90 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY max_inner_product(i.embedding, ?1)') 91 | ->setParameter(1, new Vector([1, 1, 1])) 92 | ->setMaxResults(5) 93 | ->getResult(); 94 | $this->assertEquals([2, 3, 1], array_map(fn ($v) => $v->getId(), $neighbors)); 95 | } 96 | 97 | public function testVectorCosineDistance() 98 | { 99 | $this->createItems(); 100 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY cosine_distance(i.embedding, ?1)') 101 | ->setParameter(1, new Vector([1, 1, 1])) 102 | ->setMaxResults(5) 103 | ->getResult(); 104 | $this->assertEquals([1, 2, 3], array_map(fn ($v) => $v->getId(), $neighbors)); 105 | } 106 | 107 | public function testVectorL1Distance() 108 | { 109 | $this->createItems(); 110 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY l1_distance(i.embedding, ?1)') 111 | ->setParameter(1, new Vector([1, 1, 1])) 112 | ->setMaxResults(5) 113 | ->getResult(); 114 | $this->assertEquals([1, 3, 2], array_map(fn ($v) => $v->getId(), $neighbors)); 115 | } 116 | 117 | public function testHalfvecL2Distance() 118 | { 119 | $this->createItems('halfEmbedding'); 120 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY l2_distance(i.halfEmbedding, ?1)') 121 | ->setParameter(1, new HalfVector([1, 1, 1])) 122 | ->setMaxResults(5) 123 | ->getResult(); 124 | $this->assertEquals([1, 3, 2], array_map(fn ($v) => $v->getId(), $neighbors)); 125 | $this->assertEquals([[1, 1, 1], [1, 1, 2], [2, 2, 2]], array_map(fn ($v) => $v->getHalfEmbedding()->toArray(), $neighbors)); 126 | } 127 | 128 | public function testHalfvecMaxInnerProduct() 129 | { 130 | $this->createItems('halfEmbedding'); 131 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY max_inner_product(i.halfEmbedding, ?1)') 132 | ->setParameter(1, new HalfVector([1, 1, 1])) 133 | ->setMaxResults(5) 134 | ->getResult(); 135 | $this->assertEquals([2, 3, 1], array_map(fn ($v) => $v->getId(), $neighbors)); 136 | } 137 | 138 | public function testHalfvecCosineDistance() 139 | { 140 | $this->createItems('halfEmbedding'); 141 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY cosine_distance(i.halfEmbedding, ?1)') 142 | ->setParameter(1, new HalfVector([1, 1, 1])) 143 | ->setMaxResults(5) 144 | ->getResult(); 145 | $this->assertEquals([1, 2, 3], array_map(fn ($v) => $v->getId(), $neighbors)); 146 | } 147 | 148 | public function testHalfvecL1Distance() 149 | { 150 | $this->createItems('halfEmbedding'); 151 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY l1_distance(i.halfEmbedding, ?1)') 152 | ->setParameter(1, new HalfVector([1, 1, 1])) 153 | ->setMaxResults(5) 154 | ->getResult(); 155 | $this->assertEquals([1, 3, 2], array_map(fn ($v) => $v->getId(), $neighbors)); 156 | } 157 | 158 | public function testBitHammingDistance() 159 | { 160 | $this->createBitItems(); 161 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY hamming_distance(i.binaryEmbedding, ?1)') 162 | ->setParameter(1, '101') 163 | ->setMaxResults(5) 164 | ->getResult(); 165 | $this->assertEquals([2, 3, 1], array_map(fn ($v) => $v->getId(), $neighbors)); 166 | } 167 | 168 | public function testBitJaccardDistance() 169 | { 170 | $this->createBitItems(); 171 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY jaccard_distance(i.binaryEmbedding, ?1)') 172 | ->setParameter(1, '101') 173 | ->setMaxResults(5) 174 | ->getResult(); 175 | $this->assertEquals([2, 3, 1], array_map(fn ($v) => $v->getId(), $neighbors)); 176 | } 177 | 178 | public function testSparsevecL2Distance() 179 | { 180 | $this->createItems('sparseEmbedding'); 181 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY l2_distance(i.sparseEmbedding, ?1)') 182 | ->setParameter(1, new SparseVector([1, 1, 1])) 183 | ->setMaxResults(5) 184 | ->getResult(); 185 | $this->assertEquals([1, 3, 2], array_map(fn ($v) => $v->getId(), $neighbors)); 186 | $this->assertEquals([[1, 1, 1], [1, 1, 2], [2, 2, 2]], array_map(fn ($v) => $v->getSparseEmbedding()->toArray(), $neighbors)); 187 | } 188 | 189 | public function testSparsevecMaxInnerProduct() 190 | { 191 | $this->createItems('sparseEmbedding'); 192 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY max_inner_product(i.sparseEmbedding, ?1)') 193 | ->setParameter(1, new SparseVector([1, 1, 1])) 194 | ->setMaxResults(5) 195 | ->getResult(); 196 | $this->assertEquals([2, 3, 1], array_map(fn ($v) => $v->getId(), $neighbors)); 197 | } 198 | 199 | public function testSparsevecCosineDistance() 200 | { 201 | $this->createItems('sparseEmbedding'); 202 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY cosine_distance(i.sparseEmbedding, ?1)') 203 | ->setParameter(1, new SparseVector([1, 1, 1])) 204 | ->setMaxResults(5) 205 | ->getResult(); 206 | $this->assertEquals([1, 2, 3], array_map(fn ($v) => $v->getId(), $neighbors)); 207 | } 208 | 209 | public function testSparsevecL1Distance() 210 | { 211 | $this->createItems('sparseEmbedding'); 212 | $neighbors = self::$em->createQuery('SELECT i FROM DoctrineItem i ORDER BY l1_distance(i.sparseEmbedding, ?1)') 213 | ->setParameter(1, new SparseVector([1, 1, 1])) 214 | ->setMaxResults(5) 215 | ->getResult(); 216 | $this->assertEquals([1, 3, 2], array_map(fn ($v) => $v->getId(), $neighbors)); 217 | } 218 | 219 | private function createItems($attribute = 'embedding') 220 | { 221 | foreach ([[1, 1, 1], [2, 2, 2], [1, 1, 2]] as $v) { 222 | $item = new DoctrineItem(); 223 | if ($attribute == 'halfEmbedding') { 224 | $item->setHalfEmbedding(new HalfVector($v)); 225 | } else if ($attribute == 'sparseEmbedding') { 226 | $item->setSparseEmbedding(new SparseVector($v)); 227 | } else { 228 | $item->setEmbedding(new Vector($v)); 229 | } 230 | self::$em->persist($item); 231 | } 232 | self::$em->flush(); 233 | } 234 | 235 | private function createBitItems() 236 | { 237 | foreach (['000', '101', '111'] as $v) { 238 | $item = new DoctrineItem(); 239 | $item->setBinaryEmbedding($v); 240 | self::$em->persist($item); 241 | } 242 | self::$em->flush(); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /tests/LaravelTest.php: -------------------------------------------------------------------------------- 1 | addConnection([ 16 | 'driver' => 'pgsql', 17 | 'database' => 'pgvector_php_test', 18 | 'prefix' => 'laravel_' 19 | ]); 20 | $capsule->setAsGlobal(); 21 | $capsule->bootEloquent(); 22 | 23 | Pgvector\Laravel\Schema::register(); 24 | 25 | Capsule::statement('CREATE EXTENSION IF NOT EXISTS vector'); 26 | Capsule::schema()->dropIfExists('items'); 27 | Capsule::schema()->create('items', function ($table) { 28 | $table->increments('id'); 29 | $table->vector('embedding', 3)->nullable(); 30 | $table->halfvec('half_embedding', 3)->nullable(); 31 | $table->bit('binary_embedding', 3)->nullable(); 32 | $table->sparsevec('sparse_embedding', 3)->nullable(); 33 | }); 34 | 35 | class Item extends Model 36 | { 37 | use HasNeighbors; 38 | 39 | public $timestamps = false; 40 | protected $fillable = ['id', 'embedding', 'half_embedding', 'binary_embedding', 'sparse_embedding']; 41 | protected $casts = ['embedding' => Vector::class, 'half_embedding' => HalfVector::class, 'sparse_embedding' => SparseVector::class]; 42 | } 43 | 44 | final class LaravelTest extends TestCase 45 | { 46 | public function setUp(): void 47 | { 48 | Item::truncate(); 49 | } 50 | 51 | public function testVectorL2Distance() 52 | { 53 | $this->createItems(); 54 | $neighbors = Item::orderByRaw('embedding <-> ?', [new Vector([1, 1, 1])])->take(5)->get(); 55 | $this->assertEquals([1, 3, 2], $neighbors->pluck('id')->toArray()); 56 | $this->assertEquals([[1, 1, 1], [1, 1, 2], [2, 2, 2]], array_map(fn ($v) => $v->toArray(), $neighbors->pluck('embedding')->toArray())); 57 | } 58 | 59 | public function testVectorMaxInnerProduct() 60 | { 61 | $this->createItems(); 62 | $neighbors = Item::orderByRaw('embedding <#> ?', [new Vector([1, 1, 1])])->take(5)->get(); 63 | $this->assertEquals([2, 3, 1], $neighbors->pluck('id')->toArray()); 64 | } 65 | 66 | public function testVectorCosineDistance() 67 | { 68 | $this->createItems(); 69 | $neighbors = Item::orderByRaw('embedding <=> ?', [new Vector([1, 1, 1])])->take(5)->get(); 70 | $this->assertEquals([1, 2, 3], $neighbors->pluck('id')->toArray()); 71 | } 72 | 73 | public function testVectorL1Distance() 74 | { 75 | $this->createItems(); 76 | $neighbors = Item::orderByRaw('embedding <+> ?', [new Vector([1, 1, 1])])->take(5)->get(); 77 | $this->assertEquals([1, 3, 2], $neighbors->pluck('id')->toArray()); 78 | } 79 | 80 | public function testVectorScopeL2Distance() 81 | { 82 | $this->createItems(); 83 | $neighbors = Item::query()->nearestNeighbors('embedding', [1, 1, 1], Distance::L2)->take(5)->get(); 84 | $this->assertEquals([1, 3, 2], $neighbors->pluck('id')->toArray()); 85 | $this->assertEqualsWithDelta([0, 1, sqrt(3)], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 86 | } 87 | 88 | public function testVectorScopeMaxInnerProduct() 89 | { 90 | $this->createItems(); 91 | $neighbors = Item::query()->nearestNeighbors('embedding', [1, 1, 1], Distance::InnerProduct)->take(5)->get(); 92 | $this->assertEquals([2, 3, 1], $neighbors->pluck('id')->toArray()); 93 | $this->assertEqualsWithDelta([6, 4, 3], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 94 | } 95 | 96 | public function testVectorScopeCosineDistance() 97 | { 98 | $this->createItems(); 99 | $neighbors = Item::query()->nearestNeighbors('embedding', [1, 1, 1], Distance::Cosine)->take(5)->get(); 100 | $this->assertEquals([1, 2, 3], $neighbors->pluck('id')->toArray()); 101 | $this->assertEqualsWithDelta([0, 0, 0.05719], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 102 | } 103 | 104 | public function testVectorScopeL1Distance() 105 | { 106 | $this->createItems(); 107 | $neighbors = Item::query()->nearestNeighbors('embedding', [1, 1, 1], Distance::L1)->take(5)->get(); 108 | $this->assertEquals([1, 3, 2], $neighbors->pluck('id')->toArray()); 109 | $this->assertEqualsWithDelta([0, 1, 3], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 110 | } 111 | 112 | public function testVectorInstanceL2Distance() 113 | { 114 | $this->createItems(); 115 | $item = Item::find(1); 116 | $neighbors = $item->nearestNeighbors('embedding', Distance::L2)->take(5)->get(); 117 | $this->assertEquals([3, 2], $neighbors->pluck('id')->toArray()); 118 | $this->assertEqualsWithDelta([1, sqrt(3)], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 119 | } 120 | 121 | public function testVectorInstanceMaxInnerProduct() 122 | { 123 | $this->createItems(); 124 | $item = Item::find(1); 125 | $neighbors = $item->nearestNeighbors('embedding', Distance::InnerProduct)->take(5)->get(); 126 | $this->assertEquals([2, 3], $neighbors->pluck('id')->toArray()); 127 | $this->assertEqualsWithDelta([6, 4], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 128 | } 129 | 130 | public function testVectorInstanceCosineDistance() 131 | { 132 | $this->createItems(); 133 | $item = Item::find(1); 134 | $neighbors = $item->nearestNeighbors('embedding', Distance::Cosine)->take(5)->get(); 135 | $this->assertEquals([2, 3], $neighbors->pluck('id')->toArray()); 136 | $this->assertEqualsWithDelta([0, 0.05719], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 137 | } 138 | 139 | public function testVectorInstanceL1Distance() 140 | { 141 | $this->createItems(); 142 | $item = Item::find(1); 143 | $neighbors = $item->nearestNeighbors('embedding', Distance::L1)->take(5)->get(); 144 | $this->assertEquals([3, 2], $neighbors->pluck('id')->toArray()); 145 | $this->assertEqualsWithDelta([1, 3], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 146 | } 147 | 148 | public function testHalfvecL2Distance() 149 | { 150 | $this->createItems('half_embedding'); 151 | $neighbors = Item::orderByRaw('half_embedding <-> ?', [new HalfVector([1, 1, 1])])->take(5)->get(); 152 | $this->assertEquals([1, 3, 2], $neighbors->pluck('id')->toArray()); 153 | $this->assertEquals([[1, 1, 1], [1, 1, 2], [2, 2, 2]], array_map(fn ($v) => $v->toArray(), $neighbors->pluck('half_embedding')->toArray())); 154 | } 155 | 156 | public function testHalfvecScopeL2Distance() 157 | { 158 | $this->createItems('half_embedding'); 159 | $neighbors = Item::query()->nearestNeighbors('half_embedding', [1, 1, 1], Distance::L2)->take(5)->get(); 160 | $this->assertEquals([1, 3, 2], $neighbors->pluck('id')->toArray()); 161 | $this->assertEqualsWithDelta([0, 1, sqrt(3)], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 162 | } 163 | 164 | public function testHalfvecScopeMaxInnerProduct() 165 | { 166 | $this->createItems('half_embedding'); 167 | $neighbors = Item::query()->nearestNeighbors('half_embedding', [1, 1, 1], Distance::InnerProduct)->take(5)->get(); 168 | $this->assertEquals([2, 3, 1], $neighbors->pluck('id')->toArray()); 169 | $this->assertEqualsWithDelta([6, 4, 3], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 170 | } 171 | 172 | public function testHalfvecInstanceL2Distance() 173 | { 174 | $this->createItems('half_embedding'); 175 | $item = Item::find(1); 176 | $neighbors = $item->nearestNeighbors('half_embedding', Distance::L2)->take(5)->get(); 177 | $this->assertEquals([3, 2], $neighbors->pluck('id')->toArray()); 178 | $this->assertEqualsWithDelta([1, sqrt(3)], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 179 | } 180 | 181 | public function testHalfvecInstanceL1Distance() 182 | { 183 | $this->createItems('half_embedding'); 184 | $item = Item::find(1); 185 | $neighbors = $item->nearestNeighbors('half_embedding', Distance::L1)->take(5)->get(); 186 | $this->assertEquals([3, 2], $neighbors->pluck('id')->toArray()); 187 | $this->assertEqualsWithDelta([1, 3], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 188 | } 189 | 190 | public function testBitScopeHammingDistance() 191 | { 192 | $this->createBitItems(); 193 | $neighbors = Item::query()->nearestNeighbors('binary_embedding', '101', Distance::Hamming)->take(5)->get(); 194 | $this->assertEquals([2, 3, 1], $neighbors->pluck('id')->toArray()); 195 | $this->assertEqualsWithDelta([0, 1, 2], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 196 | } 197 | 198 | public function testBitScopeJaccardDistance() 199 | { 200 | $this->createBitItems(); 201 | $neighbors = Item::query()->nearestNeighbors('binary_embedding', '101', Distance::Jaccard)->take(5)->get(); 202 | $this->assertEquals([2, 3, 1], $neighbors->pluck('id')->toArray()); 203 | $this->assertEqualsWithDelta([0, 1 / 3, 1], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 204 | } 205 | 206 | public function testBitInstanceHammingDistance() 207 | { 208 | $this->createBitItems(); 209 | $item = Item::find(2); 210 | $neighbors = $item->nearestNeighbors('binary_embedding', Distance::Hamming)->take(5)->get(); 211 | $this->assertEquals([3, 1], $neighbors->pluck('id')->toArray()); 212 | $this->assertEqualsWithDelta([1, 2], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 213 | } 214 | 215 | public function testBitInstanceJaccardDistance() 216 | { 217 | $this->createBitItems(); 218 | $item = Item::find(2); 219 | $neighbors = $item->nearestNeighbors('binary_embedding', Distance::Jaccard)->take(5)->get(); 220 | $this->assertEquals([3, 1], $neighbors->pluck('id')->toArray()); 221 | $this->assertEqualsWithDelta([1 / 3, 1], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 222 | } 223 | 224 | public function testSparsevecL2Distance() 225 | { 226 | $this->createItems('sparse_embedding'); 227 | $neighbors = Item::orderByRaw('sparse_embedding <-> ?', [new SparseVector([1, 1, 1])])->take(5)->get(); 228 | $this->assertEquals([1, 3, 2], $neighbors->pluck('id')->toArray()); 229 | $this->assertEquals([[1, 1, 1], [1, 1, 2], [2, 2, 2]], array_map(fn ($v) => $v->toArray(), $neighbors->pluck('sparse_embedding')->toArray())); 230 | } 231 | 232 | public function testSparsevecScopeL2Distance() 233 | { 234 | $this->createItems('sparse_embedding'); 235 | $neighbors = Item::query()->nearestNeighbors('sparse_embedding', '{1:1,2:1,3:1}/3', Distance::L2)->take(5)->get(); 236 | $this->assertEquals([1, 3, 2], $neighbors->pluck('id')->toArray()); 237 | $this->assertEqualsWithDelta([0, 1, sqrt(3)], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 238 | } 239 | 240 | public function testSparsevecScopeMaxInnerProduct() 241 | { 242 | $this->createItems('sparse_embedding'); 243 | $neighbors = Item::query()->nearestNeighbors('sparse_embedding', '{1:1,2:1,3:1}/3', Distance::InnerProduct)->take(5)->get(); 244 | $this->assertEquals([2, 3, 1], $neighbors->pluck('id')->toArray()); 245 | $this->assertEqualsWithDelta([6, 4, 3], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 246 | } 247 | 248 | public function testSparsevecInstanceL2Distance() 249 | { 250 | $this->createItems('sparse_embedding'); 251 | $item = Item::find(1); 252 | $neighbors = $item->nearestNeighbors('sparse_embedding', Distance::L2)->take(5)->get(); 253 | $this->assertEquals([3, 2], $neighbors->pluck('id')->toArray()); 254 | $this->assertEqualsWithDelta([1, sqrt(3)], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 255 | } 256 | 257 | public function testSparsevecInstanceL1Distance() 258 | { 259 | $this->createItems('sparse_embedding'); 260 | $item = Item::find(1); 261 | $neighbors = $item->nearestNeighbors('sparse_embedding', Distance::L1)->take(5)->get(); 262 | $this->assertEquals([3, 2], $neighbors->pluck('id')->toArray()); 263 | $this->assertEqualsWithDelta([1, 3], $neighbors->pluck('neighbor_distance')->toArray(), 0.00001); 264 | } 265 | 266 | public function testDistances() 267 | { 268 | $this->createItems(); 269 | $distances = Item::selectRaw('embedding <-> ? AS distance', [new Vector([1, 1, 1])])->pluck('distance'); 270 | $this->assertEqualsWithDelta([0, sqrt(3), 1], $distances->toArray(), 0.00001); 271 | } 272 | 273 | public function testMissingAttribute() 274 | { 275 | $this->expectException(MissingAttributeException::class); 276 | $this->expectExceptionMessage('The attribute [factors] either does not exist or was not retrieved for model [Item].'); 277 | 278 | $this->createItems(); 279 | $item = Item::find(1); 280 | $item->nearestNeighbors('factors', Distance::L2); 281 | } 282 | 283 | public function testInvalidDistance() 284 | { 285 | $this->expectException(TypeError::class); 286 | 287 | Item::query()->nearestNeighbors('embedding', [1, 2, 3], 4); 288 | } 289 | 290 | public function testCast() 291 | { 292 | Item::create(['id' => 1, 'embedding' => [1, 2, 3]]); 293 | $item = Item::find(1); 294 | $this->assertEquals([1, 2, 3], $item->embedding->toArray()); 295 | } 296 | 297 | public function testCastNull() 298 | { 299 | Item::create(['id' => 1]); 300 | $item = Item::find(1); 301 | $this->assertNull($item->embedding); 302 | } 303 | 304 | private function createItems($attribute = 'embedding') 305 | { 306 | foreach ([[1, 1, 1], [2, 2, 2], [1, 1, 2]] as $i => $v) { 307 | Item::create(['id' => $i + 1, $attribute => $v]); 308 | } 309 | } 310 | 311 | private function createBitItems() 312 | { 313 | foreach (['000', '101', '111'] as $i => $v) { 314 | Item::create(['id' => $i + 1, 'binary_embedding' => $v]); 315 | } 316 | } 317 | } 318 | --------------------------------------------------------------------------------