Semantic search: native vector similarity in a multi-dialect ORM


Most ORMs stop short of vector similarity. Once you need it, you drop to raw SQL: hand-written distance expressions and dialect quirks, outside your type-safe query API.
UQL 0.3 adds semantic search to the regular query API, with automatic migration of HNSW and IVFFlat indexes.
What it looks like
Section titled “What it looks like”const queryVec = await embed('How do vector indexes work?');
const results = await pool.findMany(Article, { $select: { id: true, title: true }, $sort: { embedding: { $vector: queryVec, $distance: 'cosine' } }, $limit: 10,});UQL generates each database’s SQL:
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 10One query for every dialect, with no raw SQL or dialect checks in your code.
On MongoDB, the same query becomes an Atlas $vectorSearch pipeline. The search index itself is created in Atlas; the reference has the details.
[ { "$vectorSearch": { "index": "embedding_index", "path": "embedding", "queryVector": ["..."], "numCandidates": 100, "limit": 10 } }]Entity setup
Section titled “Entity setup”Declare the vector field and its index; UQL creates the column, the extension and the index:
import { f, Entity, Field } from 'uql-orm';
@Entity({ indexes: (article, index) => [ index([article.embedding], { type: 'hnsw', distance: 'cosine', m: 16, efConstruction: 64, }), ],})export class Article { @Field(f.number().autoIncrement()) id?: number; @Field(f.string().nullable()) title?: string | null; @Field(f.string().nullable()) category?: string | null;
@Field(f.vector(1536).nullable()) embedding?: number[] | null;}On Postgres, UQL emits CREATE EXTENSION IF NOT EXISTS vector. MariaDB has vectors built in; SQLite needs the sqlite-vec extension loaded.
What else shipped
Section titled “What else shipped”$project returns the computed distance as a named field, without computing it twice, typed with the WithDistance helper (now inferred). Alongside it came four distance metrics (cosine, l2, inner, l1), three vector types (vector, halfvec, sparsevec), and HNSW, IVFFlat and native vector indexes, each where the engine has it. The reference lists which engine has which.
Why $sort?
Section titled “Why $sort?”Similarity search is sorting by distance, so UQL reuses $sort, which combines with $where, $select, $limit and ordinary sort fields:
const results = await pool.findMany(Article, { $where: { category: 'science' }, $sort: { embedding: { $vector: queryVec, $distance: 'cosine' }, title: 'asc', }, $limit: 10,});Get started
Section titled “Get started”npm i uql-ormIf something is broken or missing, open an issue on GitHub.
UQL is a JSON-native TypeScript ORM for Node.js and Bun. Supports PostgreSQL, PGlite, MySQL, MariaDB, MSSQL, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.