Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0172a2a
Add vector attribute support for PostgreSQL with pgvector extension
cursoragent Aug 15, 2025
5d5d118
Checkpoint before follow-up message
cursoragent Aug 15, 2025
2130be7
Add getSupportForVectors method to database adapters for vector support
cursoragent Aug 15, 2025
f13bb3b
Add vector support configuration and improve type validation
cursoragent Aug 15, 2025
3c6f328
Fix vector type default validation to prevent unnecessary recursion
cursoragent Aug 15, 2025
c8b33cc
Checkpoint before follow-up message
cursoragent Aug 15, 2025
04c05f6
Checkpoint before follow-up message
cursoragent Aug 15, 2025
98aacd5
Refactor vector validation to use 'size' instead of 'dimensions'
cursoragent Aug 15, 2025
755e6e8
Add vector type validation checks in Database class
cursoragent Aug 15, 2025
aa4798a
Add comprehensive vector query tests for PostgreSQL adapter
cursoragent Aug 15, 2025
bd9adb2
Merge remote-tracking branch 'origin/main' into feat/postgresql-vecto…
abnegate Sep 2, 2025
07bdc65
Ensure extension on createCollection
abnegate Sep 2, 2025
4ffcad9
Validate dimensions
abnegate Sep 2, 2025
8fc8ebf
Add HNSW index support
abnegate Sep 2, 2025
9af414f
Fix vector query
abnegate Sep 2, 2025
e2ec1cd
Update tests
abnegate Sep 2, 2025
dd2443e
Add ext for tests
abnegate Sep 2, 2025
4cbef8c
Fix queries
abnegate Sep 2, 2025
ad060b8
Fix query value mapping
abnegate Sep 2, 2025
a6561b3
Add byte counting
abnegate Sep 2, 2025
e8c8dde
Validate single attribute for indexes
abnegate Sep 2, 2025
f1d7daa
Add more tests
abnegate Sep 2, 2025
5752bd0
Fix decode
abnegate Sep 2, 2025
574e129
Fix lint
abnegate Sep 2, 2025
a33609c
Merge branch 'main' into feat/postgresql-vector-support
abnegate Sep 2, 2025
a7df34f
Fix test
abnegate Sep 2, 2025
89a33de
Fix tests
abnegate Sep 2, 2025
0a8fc09
Merge branch 'feat/postgresql-vector-support' of github.com:utopia-ph…
abnegate Sep 2, 2025
089c706
Fix tests
abnegate Sep 2, 2025
a402cb0
Fix tests
abnegate Sep 2, 2025
768fffb
Use const for max dims
abnegate Sep 3, 2025
952ddb7
Improve index validation
abnegate Sep 3, 2025
793be26
Fix tests
abnegate Sep 4, 2025
2aa62af
Merge branch 'main' into feat/postgresql-vector-support
abnegate Sep 4, 2025
aedec64
Update src/Database/Validator/Index.php
abnegate Sep 4, 2025
8912d5e
Validate default
abnegate Sep 4, 2025
3a4477e
Format
abnegate Sep 4, 2025
c2e3bea
Merge branch 'main' into feat/postgresql-vector-support
abnegate Sep 4, 2025
4310bfd
Merge remote-tracking branch 'origin/main' into feat/postgresql-vecto…
abnegate Oct 17, 2025
2a53c11
Check spatial attribute type in validator
abnegate Oct 17, 2025
2fc601a
Cleanup
abnegate Oct 17, 2025
2faf75a
Use filters instead of manual encode/decode
abnegate Oct 17, 2025
e8318df
Add more tests
abnegate Oct 17, 2025
08c1407
Update tests/e2e/Adapter/Scopes/VectorTests.php
abnegate Oct 17, 2025
afbf531
Fix tests
abnegate Oct 17, 2025
a05d4af
Merge branch 'feat/postgresql-vector-support' of github.com:utopia-ph…
abnegate Oct 17, 2025
b28102c
Fix test
abnegate Oct 17, 2025
50c040f
Fix validator
abnegate Oct 17, 2025
e16689d
Reject multiple vector queries
abnegate Oct 17, 2025
966b637
Stricter validation
abnegate Oct 17, 2025
fc907a3
Fix tests
abnegate Oct 17, 2025
ffe9b9a
Fix permission test
abnegate Oct 17, 2025
f02aa64
Simplify extension install
abnegate Oct 17, 2025
ed69345
Remove dead code
abnegate Oct 17, 2025
363cc8d
Simplify encode
abnegate Oct 17, 2025
b5a3c95
Simplify check
abnegate Oct 17, 2025
f06ef55
Update src/Database/Validator/Query/Filter.php
abnegate Oct 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Database/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,13 @@ abstract public function getSupportForGetConnectionId(): bool;
*/
abstract public function getSupportForUpserts(): bool;

/**
* Is vector type supported?
*
* @return bool
*/
abstract public function getSupportForVectors(): bool;

/**
* Is Cache Fallback supported?
*
Expand Down
5 changes: 5 additions & 0 deletions src/Database/Adapter/Pool.php
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,11 @@ public function getSupportForUpserts(): bool
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function getSupportForVectors(): bool
{
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function getSupportForCacheSkipOnFailure(): bool
{
return $this->delegate(__FUNCTION__, \func_get_args());
Expand Down
46 changes: 46 additions & 0 deletions src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,11 @@ public function analyzeCollection(string $collection): bool
*/
public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false): bool
{
// Ensure pgvector extension is installed for vector types
if ($type === Database::VAR_VECTOR) {
$this->ensurePgVectorExtension();
}

Comment thread
abnegate marked this conversation as resolved.
$name = $this->filter($collection);
$id = $this->filter($id);
$type = $this->getSQLType($type, $size, $signed, $array);
Expand Down Expand Up @@ -1798,6 +1803,18 @@ protected function getSQLCondition(Query $query, array &$binds): string
$binds[":{$placeholder}_0"] = $this->getFulltextValue($query->getValue());
return "NOT (to_tsvector(regexp_replace({$attribute}, '[^\w]+',' ','g')) @@ websearch_to_tsquery(:{$placeholder}_0))";

case Query::TYPE_VECTOR_DOT:
$binds[":{$placeholder}_0"] = '[' . implode(',', $query->getValues()) . ']';
return "({$attribute} <#> :{$placeholder}_0)";

case Query::TYPE_VECTOR_COSINE:
$binds[":{$placeholder}_0"] = '[' . implode(',', $query->getValues()) . ']';
return "({$attribute} <=> :{$placeholder}_0)";

case Query::TYPE_VECTOR_EUCLIDEAN:
$binds[":{$placeholder}_0"] = '[' . implode(',', $query->getValues()) . ']';
return "({$attribute} <-> :{$placeholder}_0)";

case Query::TYPE_BETWEEN:
$binds[":{$placeholder}_0"] = $query->getValues()[0];
$binds[":{$placeholder}_1"] = $query->getValues()[1];
Expand Down Expand Up @@ -1924,6 +1941,9 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
case Database::VAR_DATETIME:
return 'TIMESTAMP(3)';

case Database::VAR_VECTOR:
return "vector({$size})";

default:
throw new DatabaseException('Unknown Type: ' . $type);
}
Expand All @@ -1943,6 +1963,22 @@ protected function getSQLSchema(): string
return "\"{$this->getDatabase()}\".";
}

/**
* Ensure pgvector extension is installed
*
* @return void
* @throws DatabaseException
*/
private function ensurePgVectorExtension(): void
{
try {
$stmt = $this->getPDO()->prepare("CREATE EXTENSION IF NOT EXISTS vector");
$this->execute($stmt);
} catch (PDOException $e) {
throw new DatabaseException('Failed to install pgvector extension: ' . $e->getMessage(), $e->getCode(), $e);
}
}

/**
* Get PDO Type
*
Expand Down Expand Up @@ -2049,6 +2085,16 @@ public function getSupportForUpserts(): bool
return true;
}

/**
* Is vector type supported?
*
* @return bool
*/
public function getSupportForVectors(): bool
{
return true;
}

/**
* @return string
*/
Expand Down
14 changes: 14 additions & 0 deletions src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -1426,6 +1426,16 @@ public function getSupportForBatchCreateAttributes(): bool
return true;
}

/**
* Is vector type supported?
*
* @return bool
*/
public function getSupportForVectors(): bool
{
return false;
}

/**
* @param string $tableName
* @param string $columns
Expand Down Expand Up @@ -1505,6 +1515,10 @@ protected function getSQLOperator(string $method): string
case Query::TYPE_NOT_ENDS_WITH:
case Query::TYPE_NOT_CONTAINS:
return $this->getLikeOperator();
case Query::TYPE_VECTOR_DOT:
case Query::TYPE_VECTOR_COSINE:
case Query::TYPE_VECTOR_EUCLIDEAN:
throw new DatabaseException('Vector queries are only supported in PostgreSQL adapter');
default:
throw new DatabaseException('Unknown method: ' . $method);
}
Expand Down
75 changes: 72 additions & 3 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ class Database
public const VAR_DATETIME = 'datetime';
public const VAR_ID = 'id';
public const VAR_OBJECT_ID = 'objectId';
public const VAR_VECTOR = 'vector';

public const INT_MAX = 2147483647;
public const BIG_INT_MAX = PHP_INT_MAX;
public const DOUBLE_MAX = PHP_FLOAT_MAX;
public const VECTOR_MAX_SIZE = 16000; // pgvector limit

// Relationship Types
public const VAR_RELATIONSHIP = 'relationship';
Expand Down Expand Up @@ -1834,8 +1836,33 @@ private function validateAttribute(
case self::VAR_DATETIME:
case self::VAR_RELATIONSHIP:
break;
case self::VAR_VECTOR:
if (!$this->adapter->getSupportForVectors()) {
throw new DatabaseException('Vector type is only supported in PostgreSQL adapter');
Comment thread
abnegate marked this conversation as resolved.
Outdated
}
if ($array) {
throw new DatabaseException('Vector type cannot be an array');
}
if ($size <= 0) {
throw new DatabaseException('Vector size must be a positive integer');
}
if ($size > self::VECTOR_MAX_SIZE) {
throw new DatabaseException('Vector size cannot exceed ' . self::VECTOR_MAX_SIZE);
}
break;
Comment thread
abnegate marked this conversation as resolved.
default:
throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . self::VAR_STRING . ', ' . self::VAR_INTEGER . ', ' . self::VAR_FLOAT . ', ' . self::VAR_BOOLEAN . ', ' . self::VAR_DATETIME . ', ' . self::VAR_RELATIONSHIP);
$supportedTypes = [
self::VAR_STRING,
self::VAR_INTEGER,
self::VAR_FLOAT,
self::VAR_BOOLEAN,
self::VAR_DATETIME,
self::VAR_RELATIONSHIP
];
if ($this->adapter->getSupportForVectors()) {
$supportedTypes[] = self::VAR_VECTOR;
}
throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . implode(', ', $supportedTypes));
}

// Only execute when $default is given
Expand Down Expand Up @@ -1904,8 +1931,25 @@ protected function validateDefaultTypes(string $type, mixed $default): void
throw new DatabaseException('Default value ' . $default . ' does not match given type ' . $type);
}
break;
case self::VAR_VECTOR:
// When validating individual vector components (from recursion), they should be numeric
if ($defaultType !== 'double' && $defaultType !== 'integer') {
throw new DatabaseException('Vector components must be numeric values (float or integer)');
}
break;
Comment thread
abnegate marked this conversation as resolved.
default:
throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . self::VAR_STRING . ', ' . self::VAR_INTEGER . ', ' . self::VAR_FLOAT . ', ' . self::VAR_BOOLEAN . ', ' . self::VAR_DATETIME . ', ' . self::VAR_RELATIONSHIP);
$supportedTypes = [
self::VAR_STRING,
self::VAR_INTEGER,
self::VAR_FLOAT,
self::VAR_BOOLEAN,
self::VAR_DATETIME,
self::VAR_RELATIONSHIP
];
if ($this->adapter->getSupportForVectors()) {
$supportedTypes[] = self::VAR_VECTOR;
}
throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . implode(', ', $supportedTypes));
}
}

Expand Down Expand Up @@ -2146,8 +2190,33 @@ public function updateAttribute(string $collection, string $id, ?string $type =
throw new DatabaseException('Size must be empty');
}
break;
case self::VAR_VECTOR:
if (!$this->adapter->getSupportForVectors()) {
throw new DatabaseException('Vector type is only supported in PostgreSQL adapter');
}
if ($array) {
throw new DatabaseException('Vector type cannot be an array');
}
if ($size <= 0) {
throw new DatabaseException('Vector size must be a positive integer');
}
if ($size > self::VECTOR_MAX_SIZE) {
throw new DatabaseException('Vector size cannot exceed ' . self::VECTOR_MAX_SIZE);
}
break;
Comment thread
abnegate marked this conversation as resolved.
default:
throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . self::VAR_STRING . ', ' . self::VAR_INTEGER . ', ' . self::VAR_FLOAT . ', ' . self::VAR_BOOLEAN . ', ' . self::VAR_DATETIME . ', ' . self::VAR_RELATIONSHIP);
$supportedTypes = [
self::VAR_STRING,
self::VAR_INTEGER,
self::VAR_FLOAT,
self::VAR_BOOLEAN,
self::VAR_DATETIME,
self::VAR_RELATIONSHIP
];
if ($this->adapter->getSupportForVectors()) {
$supportedTypes[] = self::VAR_VECTOR;
}
throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . implode(', ', $supportedTypes));
}

/** Ensure required filters for the attribute are passed */
Expand Down
44 changes: 44 additions & 0 deletions src/Database/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ class Query
public const TYPE_ENDS_WITH = 'endsWith';
public const TYPE_NOT_ENDS_WITH = 'notEndsWith';

// Vector query methods (PostgreSQL only)
public const TYPE_VECTOR_DOT = 'vectorDot';
public const TYPE_VECTOR_COSINE = 'vectorCosine';
public const TYPE_VECTOR_EUCLIDEAN = 'vectorEuclidean';

public const TYPE_SELECT = 'select';

// Order methods
Expand Down Expand Up @@ -64,6 +69,9 @@ class Query
self::TYPE_NOT_STARTS_WITH,
self::TYPE_ENDS_WITH,
self::TYPE_NOT_ENDS_WITH,
self::TYPE_VECTOR_DOT,
self::TYPE_VECTOR_COSINE,
self::TYPE_VECTOR_EUCLIDEAN,
self::TYPE_SELECT,
self::TYPE_ORDER_DESC,
self::TYPE_ORDER_ASC,
Expand Down Expand Up @@ -833,4 +841,40 @@ public function setOnArray(bool $bool): void
{
$this->onArray = $bool;
}

/**
* Helper method to create Query with vectorDot method
*
* @param string $attribute
* @param array<float> $vector
* @return Query
*/
public static function vectorDot(string $attribute, array $vector): self
{
return new self(self::TYPE_VECTOR_DOT, $attribute, $vector);
}

/**
* Helper method to create Query with vectorCosine method
*
* @param string $attribute
* @param array<float> $vector
* @return Query
*/
public static function vectorCosine(string $attribute, array $vector): self
{
return new self(self::TYPE_VECTOR_COSINE, $attribute, $vector);
}

/**
* Helper method to create Query with vectorEuclidean method
*
* @param string $attribute
* @param array<float> $vector
* @return Query
*/
public static function vectorEuclidean(string $attribute, array $vector): self
{
return new self(self::TYPE_VECTOR_EUCLIDEAN, $attribute, $vector);
}
}
52 changes: 52 additions & 0 deletions src/Database/Validator/Query/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,25 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s
case Database::VAR_RELATIONSHIP:
$validator = new Text(255, 0); // The query is always on uid
break;
case Database::VAR_VECTOR:
// For vector queries, validate that the value is an array of floats
if (!is_array($value)) {
$this->message = 'Vector query value must be an array';
return false;
}
foreach ($value as $component) {
if (!is_numeric($component)) {
$this->message = 'Vector query value must contain only numeric values';
return false;
}
}
// Check size match
$expectedSize = $attributeSchema['size'] ?? 0;
if (count($value) !== $expectedSize) {
$this->message = "Vector query value must have {$expectedSize} elements";
return false;
}
break;
default:
$this->message = 'Unknown Data type';
return false;
Expand Down Expand Up @@ -197,6 +216,18 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s
return false;
}

// Vector queries can only be used on vector attributes (not arrays)
if (in_array($method, [Query::TYPE_VECTOR_DOT, Query::TYPE_VECTOR_COSINE, Query::TYPE_VECTOR_EUCLIDEAN])) {
if ($attributeSchema['type'] !== Database::VAR_VECTOR) {
$this->message = 'Vector queries can only be used on vector attributes';
return false;
}
if ($array) {
$this->message = 'Vector queries cannot be used on array attributes';
return false;
}
}

return true;
}

Expand Down Expand Up @@ -273,6 +304,27 @@ public function isValid($value): bool
case Query::TYPE_IS_NOT_NULL:
return $this->isValidAttributeAndValues($attribute, $value->getValues(), $method);

case Query::TYPE_VECTOR_DOT:
case Query::TYPE_VECTOR_COSINE:
case Query::TYPE_VECTOR_EUCLIDEAN:
// Validate that the attribute is a vector type
if (!$this->isValidAttribute($attribute)) {
return false;
}

$attributeSchema = $this->schema[$attribute];
if ($attributeSchema['type'] !== Database::VAR_VECTOR) {
$this->message = 'Vector queries can only be used on vector attributes';
return false;
}

if (count($value->getValues()) != 1) {
$this->message = \ucfirst($method) . ' queries require exactly one vector value.';
return false;
}

return $this->isValidAttributeAndValues($attribute, $value->getValues(), $method);

case Query::TYPE_OR:
case Query::TYPE_AND:
$filters = Query::groupByType($value->getValues())['filters'];
Expand Down
5 changes: 5 additions & 0 deletions src/Database/Validator/Structure.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Utopia\Database\Document;
use Utopia\Database\Exception as DatabaseException;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Vector;
use Utopia\Validator;
use Utopia\Validator\Boolean;
use Utopia\Validator\FloatValidator;
Expand Down Expand Up @@ -350,6 +351,10 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys)
);
break;

case Database::VAR_VECTOR:
$validators[] = new Vector($attribute['size'] ?? 0);
break;

default:
$this->message = 'Unknown attribute type "'.$type.'"';
return false;
Expand Down
Loading
Loading