AI & RAG
Semantic search inside your ORM
Section titled “Semantic search inside your ORM”Embeddings are a column type and similarity is a sort, so searching by meaning is an ordinary query rather than a separate search stack to run and keep in sync. The same query runs on every engine with vector support, and on every runtime UQL runs on, including the browser.
This page walks one RAG feature end to end. For the operators, distance metrics and per-dialect index tuning, see the semantic search reference.
End-to-end example
Section titled “End-to-end example”1. Define an entity with a vector field
Section titled “1. Define an entity with a vector field”A vector field is a @Field with dimensions. Index it so lookups are approximate-nearest-neighbor rather than a full scan; the tuning parameters are per-dialect, covered in vector indexes:
import { Entity, Id, Field, Index } from 'uql-orm';
@Entity()@Index((article) => [article.embedding], { type: 'hnsw', distance: 'cosine', m: 16, efConstruction: 64,})export class Article { @Id({ type: Number }) id?: number; @Field({ type: String }) title?: string | null; @Field({ type: String }) category?: string | null;
@Field({ type: 'vector', dimensions: 1536 }) embedding?: number[] | null;}hnsw is pgvector’s index (PostgreSQL, PGlite); CockroachDB, libSQL and Turso Cloud build it as their own vector index, and MariaDB declares type: 'vector'. Plain SQLite, the embedded Turso engine and MSSQL have none, so every query computes the distance exactly: leave the index off MSSQL, whose migrations refuse it.
2. Ingest content with embeddings
Section titled “2. Ingest content with embeddings”A single operation runs directly on the pool; pool.withQuerier() pins one connection for several.
import { pool } from './uql.config.js';import { Article } from './entities.js';
const embedding = await embed('What is UQL?'); // any embedding model
await pool.insertOne(Article, { title: 'What is UQL?', category: 'docs', embedding,});3. Query by meaning
Section titled “3. Query by meaning”import { pool } from './uql.config.js';import { Article } from './entities.js';import type { WithProjection } from 'uql-orm';
const queryEmbedding = await embed('TypeScript ORM with vector search'); // any embedding model
const results = (await pool.findMany(Article, { $where: { category: 'docs' }, $sort: { embedding: { $vector: queryEmbedding, $distance: 'cosine', $project: 'distance', }, }, $limit: 10,})) as WithProjection<Article, 'distance'>[];
for (const article of results) { console.log(article.title, article.distance);}$project adds the computed score to each row so your app can filter and rank on it, and WithProjection types that extra field.
Production tips
Section titled “Production tips”Threshold in the database, not in your app
Section titled “Threshold in the database, not in your app”For RAG, keep low-signal results out of your context window with $near, so the threshold runs where the rows are instead of over rows you already paid to transfer:
import type { WithProjection } from 'uql-orm';
const context = (await pool.findMany(Article, { $where: { category: 'docs', embedding: { $near: { $vector: queryEmbedding, $distance: 'cosine', $lt: 0.35 }, }, }, $sort: { embedding: { $vector: queryEmbedding, $distance: 'cosine', $project: 'score', }, }, $limit: 30,})) as WithProjection<Article, 'score'>[];With cosine distance, lower values are better matches, so $lt is the bound you want. Tune it from real logs and user feedback. $sort still ranks what survives, and $project returns the score so you can show or log it.
The category filter narrows the candidate set before ranking; combined with filtering shows how each database executes that.