Skip to content

Semantic Search

UQL supports vector similarity search natively, on PostgreSQL (pgvector), CockroachDB, MariaDB, SQLite (sqlite-vec), libSQL and Turso (built in, no extension), and MongoDB Atlas ($vectorSearch). The same $vector query works on all of them. This page is the operator reference; for an end-to-end walkthrough (ingestion, fullstack usage, RAG thresholds), see AI & RAG.

Define a vector field with type: 'vector' and dimensions. Optionally, add a vector index for efficient approximate nearest-neighbor (ANN) search.

You write
import { Entity, Id, Field, Index } from 'uql-orm';
@Entity()
@Index(['embedding'], { type: 'hnsw', distance: 'cosine', m: 16, efConstruction: 64 })
export class Article {
@Id({ type: Number }) id?: number;
@Field({ type: String }) title?: string;
@Field({ type: String }) category?: string;
@Field({ type: 'vector', dimensions: 1536 })
embedding?: number[];
}

Vector search is built in from 11.7, with VECTOR(n) columns holding a packed float32 blob rather than text. UQL converts in both directions for you (VEC_FromText on write and inside the distance call, VEC_ToText on read), so a vector field still reads back as '[1,0,0]' like everywhere else. Two MariaDB rules to know: dimensions is required on the field, and a column carrying a vector index is emitted NOT NULL, because MariaDB rejects the index otherwise.

libSQL and Turso have vector functions built in, so their entities need nothing extra. Plain SQLite has none, and gets them from sqlite-vec - pass its path to the pool, which loads it on the connection:

Loading sqlite-vec
import { getLoadablePath } from 'sqlite-vec';
import { Sqlite3QuerierPool } from 'uql-orm/sqlite';
const pool = new Sqlite3QuerierPool('app.db', { extensions: [getLoadablePath()] });

Vectors are stored as a JSON array of floats on all three, which every distance function below accepts directly.


Use $sort on a vector field with $vector and an optional $distance metric:

You write
import { pool } from './uql.config.js';
const results = await pool.findMany(Article, {
$select: { id: true, title: true },
$sort: { embedding: { $vector: queryEmbedding, $distance: 'cosine' } },
$limit: 10,
});
SELECT "id", "title" FROM "Article"
ORDER BY "embedding" <=> $1::vector
LIMIT 10

For MongoDB, UQL translates into a $vectorSearch aggregation pipeline:

Generated Pipeline (MongoDB Atlas)
[
{
"$vectorSearch": {
"index": "embedding_index",
"path": "embedding",
"queryVector": [/* queryEmbedding */],
"numCandidates": 100,
"limit": 10
}
}
]

Vector search composes naturally with $where and regular $sort fields:

You write
const results = await pool.findMany(Article, {
$where: { category: 'science' },
$sort: { embedding: { $vector: queryVec, $distance: 'cosine' }, title: 'asc' },
$limit: 10,
});
SELECT * FROM "Article"
WHERE "category" = $1
ORDER BY "embedding" <=> $2::vector, "title" ASC
LIMIT 10

For MongoDB, $where is merged into the $vectorSearch.filter for optimal pre-filtering, and secondary sorts become a separate $sort stage:

Generated Pipeline (MongoDB Atlas)
[
{
"$vectorSearch": {
"index": "embedding_index",
"path": "embedding",
"queryVector": [/* queryVec */],
"numCandidates": 100,
"limit": 10,
"filter": { "category": "science" }
}
},
{ "$sort": { "title": 1 } }
]

Metric Postgres Operator CockroachDB Operator MariaDB Function SQLite Function (sqlite-vec) libSQL Function Turso Function MongoDB Atlas Use Case
cosine <=> <=> VEC_DISTANCE_COSINE vec_distance_cosine vector_distance_cos vector_distance_cos ✅ (index-defined) Text embeddings (OpenAI, Cohere)
l2 <-> <-> VEC_DISTANCE_EUCLIDEAN vec_distance_L2 vector_distance_l2 vector_distance_l2 ✅ (index-defined) Image search, spatial data
inner <#> <#> vector_distance_dot ✅ (index-defined) Maximum inner product
l1 <+> vec_distance_L1 Manhattan distance

Any metric marked ❌ throws at query build time on that dialect, naming the metric, rather than reaching the database as a call to a function it does not have.

l1 is not yet implemented on CockroachDB, and inner needs Turso’s Rust engine (vector_distance_dot) - no libSQL build has it.

If omitted, $distance defaults to 'cosine'. You can also set a default per-field:

@Field({ type: 'vector', dimensions: 1536, distance: 'l2' })
embedding?: number[];

Queries on this field will use l2 unless overridden with $distance at query time.


Project the computed distance as a named field in the result with $project:

You write
import type { WithDistance } from 'uql-orm';
const results = (await pool.findMany(Article, {
$select: { id: true, title: true },
$sort: { embedding: { $vector: queryVec, $distance: 'cosine', $project: 'distance' } },
$limit: 10,
})) as WithDistance<Article, 'distance'>[];
results.forEach((r) => console.log(r.title, r.distance));
SELECT "id", "title", "embedding" <=> $1::vector AS "distance" FROM "Article"
ORDER BY "distance"
LIMIT 10

For MongoDB, $project adds a $meta: 'vectorSearchScore' projection:

Generated Pipeline (MongoDB Atlas)
[
{ "$vectorSearch": { "index": "embedding_index", "path": "embedding", "queryVector": ["..."], "numCandidates": 100, "limit": 10 } },
{ "$project": { "id": true, "title": true, "distance": { "$meta": "vectorSearchScore" } } }
]

UQL supports three vector storage types; use the one that best fits your model and performance needs:

Type SQL (Postgres) SQL (CockroachDB) Storage Max Dimensions Use Case
'vector' VECTOR(n) VECTOR(n) 32-bit float 2,000 Standard embeddings (OpenAI, etc.)
'halfvec' HALFVEC(n) VECTOR(n) 16-bit float 4,000 50% storage savings, near-identical accuracy
'sparsevec' SPARSEVEC(n) VECTOR(n) Sparse 1,000,000 SPLADE, BM25-style sparse retrieval
@Field({ type: 'vector', dimensions: 1536 }) // OpenAI ada-002
embedding?: number[];
@Field({ type: 'halfvec', dimensions: 1536 }) // Same model, half storage
embedding?: number[];
@Field({ type: 'sparsevec', dimensions: 30000 }) // SPLADE sparse
sparseEmbedding?: number[];

Define vector indexes with @Index() for efficient approximate nearest-neighbor (ANN) search:

Index Type Postgres CockroachDB MariaDB SQLite family MongoDB Atlas Notes
hnsw USING hnsw with operator classes Best accuracy, higher memory
ivfflat USING ivfflat with lists param Faster build, large datasets
vector ✅ Native CREATE VECTOR INDEX ✅ Inline VECTOR INDEX CockroachDB’s and MariaDB’s own native vector index
vectorSearch ✅ Atlas vector search index MongoDB’s managed ANN index

MySQL is absent from the table on purpose: it has no vector index, so any of these types throws when migrations are generated rather than emitting DDL the server rejects.

Postgres HNSW
@Index(['embedding'], { type: 'hnsw', distance: 'cosine', m: 16, efConstruction: 64 })
Postgres IVFFlat
@Index(['embedding'], { type: 'ivfflat', distance: 'l2', lists: 100 })
CockroachDB
@Index(['embedding'], { type: 'vector', distance: 'cosine' })
MariaDB
@Index(['embedding'], { type: 'vector', distance: 'cosine', m: 8 })
MongoDB Atlas
@Index(['embedding'], { type: 'vectorSearch', name: 'my_search_index' })

  • AI & RAG: End-to-end walkthrough: ingestion, thresholds, fullstack usage.
  • Indexes: Declaring hnsw / ivfflat vector indexes and their metric.
  • Full-Text Search: Keyword search, and how to combine it with vectors.
  • Querier API: The full query API.