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.
Entity Setup
Section titled “Entity Setup”Define a vector field with type: 'vector' and dimensions. Optionally, add a vector index for efficient approximate nearest-neighbor (ANN) search.
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[];}MariaDB
Section titled “MariaDB”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.
SQLite, libSQL and Turso
Section titled “SQLite, libSQL and Turso”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:
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.
Query by Similarity
Section titled “Query by Similarity”Use $sort on a vector field with $vector and an optional $distance metric:
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::vectorLIMIT 10SELECT `id`, `title` FROM `Article`ORDER BY VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?))LIMIT 10SELECT `id`, `title` FROM `Article`ORDER BY vec_distance_cosine(`embedding`, ?)LIMIT 10SELECT `id`, `title` FROM `Article`ORDER BY vector_distance_cos(`embedding`, ?)LIMIT 10For MongoDB, UQL translates into a $vectorSearch aggregation pipeline:
[ { "$vectorSearch": { "index": "embedding_index", "path": "embedding", "queryVector": [/* queryEmbedding */], "numCandidates": 100, "limit": 10 } }]Combined with Filtering
Section titled “Combined with Filtering”Vector search composes naturally with $where and regular $sort fields:
const results = await pool.findMany(Article, { $where: { category: 'science' }, $sort: { embedding: { $vector: queryVec, $distance: 'cosine' }, title: 'asc' }, $limit: 10,});SELECT * FROM "Article"WHERE "category" = $1ORDER BY "embedding" <=> $2::vector, "title" ASCLIMIT 10SELECT * FROM `Article`WHERE `category` = ?ORDER BY VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?)), `title` ASCLIMIT 10SELECT * FROM `Article`WHERE `category` = ?ORDER BY vec_distance_cosine(`embedding`, ?), `title` ASCLIMIT 10For MongoDB, $where is merged into the $vectorSearch.filter for optimal pre-filtering, and secondary sorts become a separate $sort stage:
[ { "$vectorSearch": { "index": "embedding_index", "path": "embedding", "queryVector": [/* queryVec */], "numCandidates": 100, "limit": 10, "filter": { "category": "science" } } }, { "$sort": { "title": 1 } }]Distance Metrics
Section titled “Distance Metrics”| 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.
Distance Projection
Section titled “Distance Projection”Project the computed distance as a named field in the result with $project:
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 10SELECT `id`, `title`, VEC_DISTANCE_COSINE(`embedding`, VEC_FromText(?)) AS `distance` FROM `Article`ORDER BY `distance`LIMIT 10SELECT `id`, `title`, vec_distance_cosine(`embedding`, ?) AS `distance` FROM `Article`ORDER BY `distance`LIMIT 10For MongoDB, $project adds a $meta: 'vectorSearchScore' projection:
[ { "$vectorSearch": { "index": "embedding_index", "path": "embedding", "queryVector": ["..."], "numCandidates": 100, "limit": 10 } }, { "$project": { "id": true, "title": true, "distance": { "$meta": "vectorSearchScore" } } }]Vector Types
Section titled “Vector Types”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-002embedding?: number[];
@Field({ type: 'halfvec', dimensions: 1536 }) // Same model, half storageembedding?: number[];
@Field({ type: 'sparsevec', dimensions: 30000 }) // SPLADE sparsesparseEmbedding?: number[];Vector Indexes
Section titled “Vector Indexes”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.
@Index(['embedding'], { type: 'hnsw', distance: 'cosine', m: 16, efConstruction: 64 })@Index(['embedding'], { type: 'ivfflat', distance: 'l2', lists: 100 })@Index(['embedding'], { type: 'vector', distance: 'cosine' })@Index(['embedding'], { type: 'vector', distance: 'cosine', m: 8 })@Index(['embedding'], { type: 'vectorSearch', name: 'my_search_index' })Next Steps
Section titled “Next Steps”- AI & RAG: End-to-end walkthrough: ingestion, thresholds, fullstack usage.
- Indexes: Declaring
hnsw/ivfflatvector indexes and their metric. - Full-Text Search: Keyword search, and how to combine it with vectors.
- Querier API: The full query API.