Semantic Search
UQL provides first-class vector similarity search, enabling AI-powered semantic queries out of the box. Works across PostgreSQL (pgvector), MariaDB, SQLite (sqlite-vec), and MongoDB Atlas ($vectorSearch).
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() id?: number; @Field() title?: string; @Field() category?: string;
@Field({ type: 'vector', dimensions: 1536 }) embedding?: number[];}Query by Similarity
Section titled “Query by Similarity”Use $sort on a vector field with $vector and an optional $distance metric:
const results = await querier.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`, ?)LIMIT 10SELECT `id`, `title` FROM `Article`ORDER BY vec_distance_cosine(`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 querier.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`, ?), `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 | MariaDB Function | SQLite Function | MongoDB Atlas | Use Case |
|---|---|---|---|---|---|
cosine | <=> | VEC_DISTANCE_COSINE | vec_distance_cosine | ✅ (index-defined) | Text embeddings (OpenAI, Cohere) |
l2 | <-> | VEC_DISTANCE_EUCLIDEAN | vec_distance_L2 | ✅ (index-defined) | Image search, spatial data |
inner | <#> | — | — | ✅ (index-defined) | Maximum inner product |
l1 | <+> | — | — | — | Manhattan distance |
hamming | <~> | — | vec_distance_hamming | — | Binary embeddings |
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 querier.findMany(Article, { $select: { id: true, title: true }, $sort: { embedding: { $vector: queryVec, $distance: 'cosine', $project: 'similarity' } }, $limit: 10,}) as WithDistance<Article, 'similarity'>[];
results.forEach((r) => console.log(r.title, r.similarity));SELECT "id", "title", "embedding" <=> $1::vector AS "similarity" FROM "Article"ORDER BY "similarity"LIMIT 10SELECT `id`, `title`, VEC_DISTANCE_COSINE(`embedding`, ?) AS `similarity` FROM `Article`ORDER BY `similarity`LIMIT 10SELECT `id`, `title`, vec_distance_cosine(`embedding`, ?) AS `similarity` FROM `Article`ORDER BY `similarity`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, "similarity": { "$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) | Storage | Max Dimensions | Use Case |
|---|---|---|---|---|
'vector' | VECTOR(n) | 32-bit float | 2,000 | Standard embeddings (OpenAI, etc.) |
'halfvec' | HALFVEC(n) | 16-bit float | 4,000 | 50% storage savings, near-identical accuracy |
'sparsevec' | SPARSEVEC(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 | MariaDB | MongoDB Atlas | Notes |
|---|---|---|---|---|
hnsw | ✅ USING hnsw with operator classes | ❌ | ❌ | Best accuracy, higher memory |
ivfflat | ✅ USING ivfflat with lists param | ❌ | ❌ | Faster build, large datasets |
vector | — | ✅ Inline VECTOR INDEX | ❌ | MariaDB’s native vector index |
vectorSearch | — | — | ✅ Atlas vector search index | MongoDB’s managed ANN index |
@Index(['embedding'], { type: 'hnsw', distance: 'cosine', m: 16, efConstruction: 64 })@Index(['embedding'], { type: 'ivfflat', distance: 'l2', lists: 100 })@Index(['embedding'], { type: 'vector', distance: 'cosine', m: 8 })@Index(['embedding'], { type: 'vectorSearch', name: 'my_search_index' })