Skip to content

Semantic Search: Native Vector Similarity in a Multi-Dialect ORM

Most ORMs stop short of vector similarity. The moment you need it, you drop to raw SQL, hand-writing distance expressions and working around dialect quirks outside your type-safe query API.

UQL 0.3 adds native semantic search to the regular query API, including automatic index migration for HNSW and IVFFlat indexes.

You write
const results = await querier.findMany(Article, {
$select: { id: true, title: true },
$sort: { embedding: { $vector: queryEmbedding, $distance: 'cosine' } },
$limit: 10,
});

UQL generates the right SQL for your database:

PostgreSQL
SELECT "id", "title" FROM "Article"
ORDER BY "embedding" <=> $1::vector
LIMIT 10
MariaDB
SELECT `id`, `title` FROM `Article`
ORDER BY VEC_DISTANCE_COSINE(`embedding`, ?)
LIMIT 10
SQLite
SELECT `id`, `title` FROM `Article`
ORDER BY vec_distance_cosine(`embedding`, ?)
LIMIT 10

The same query works on every dialect, with no raw SQL or dialect checks in your code.

For MongoDB, UQL translates the same query into an Atlas $vectorSearch pipeline. Note that the Atlas search index itself must be created in Atlas; see the reference for details.

MongoDB Atlas
[
{ "$vectorSearch": { "index": "embedding_index", "path": "embedding", "queryVector": ["..."], "numCandidates": 100, "limit": 10 } }
]

Define your vector field and index; UQL handles schema generation, extension creation, and index building:

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({ type: 'vector', dimensions: 1536 })
embedding?: number[];
}

For Postgres, UQL automatically emits CREATE EXTENSION IF NOT EXISTS vector. MariaDB has vector support built in; SQLite requires loading the sqlite-vec extension.

Project the computed distance into your results with $project, without computing the distance twice:

import type { WithDistance } from 'uql-orm';
const results = (await querier.findMany(Article, {
$select: { id: true, title: true },
$sort: { embedding: { $vector: queryVec, $distance: 'cosine', $project: 'distance' } },
$limit: 10,
})) as WithDistance<Article, 'distance'>[];
results[0].distance;

Annotate the result with the exported WithDistance<Article, 'distance'> helper to type the projected distance field.

Metric Postgres MariaDB SQLite MongoDB Atlas
cosine <=> ✅ (index-defined)
l2 <-> ✅ (index-defined)
inner <#> ✅ (index-defined)
l1 <+>
hamming <~>
Type Storage Use Case
'vector' 32-bit float Standard embeddings (OpenAI, etc.)
'halfvec' 16-bit float 50% storage savings, near-identical accuracy
'sparsevec' Sparse SPLADE, BM25-style sparse retrieval

halfvec and sparsevec are Postgres-only. MariaDB and SQLite transparently map them to their native VECTOR type, so your entities work everywhere.

Type Postgres MariaDB MongoDB Atlas
HNSW ✅ with m, efConstruction
IVFFlat ✅ with lists
Native VECTOR INDEX $vectorSearch

Vector similarity search is fundamentally sorting by distance. UQL reuses the existing $sort API, which composes naturally with $where, $select, $limit, and regular sort fields:

const results = await querier.findMany(Article, {
$where: { category: 'science' },
$sort: { embedding: { $vector: queryVec, $distance: 'cosine' }, title: 'asc' },
$limit: 10,
});
Terminal window
npm i uql-orm

If you run into issues or missing features, open an issue on GitHub.