Vector Search (pgvector)
Rank rows by similarity to a query vector, for semantic search, recommendations, or RAG retrieval, without dropping to raw SQL. Turbine queries pgvector columns through the same findMany you already use, with orderBy for nearest-neighbor ranking and where for distance thresholds.
You need the pgvector extension installed and a vector column on your table. Everything else is ordinary Turbine.
KNN, order by distance#
Pass a distance object on a vector column in orderBy to sort rows by their distance to a query vector. Add limit to take the closest k rows.
const similar = await db.items.findMany({
orderBy: { embedding: { distance: { to: queryVector, metric: 'cosine' } } },
limit: 5,
});
// queryVector is a number[]; the 5 nearest rows, nearest-firstto is a number[]. The default sort direction is 'asc', smallest distance first, i.e. nearest-first. To invert it, pass direction: 'desc':
const farthest = await db.items.findMany({
orderBy: { embedding: { distance: { to: queryVector, metric: 'cosine', direction: 'desc' } } },
limit: 5,
});This compiles to ORDER BY "embedding" <=> $1::vector ASC with the vector bound as a parameter, never interpolated.
Distance filter, keep rows within a threshold#
Put the same distance object in where with a comparator to keep only rows inside (or outside) a distance threshold.
const close = await db.items.findMany({
where: { embedding: { distance: { to: queryVector, metric: 'l2', lt: 0.3 } } },
});
// Generates: WHERE "embedding" <-> $1::vector < $2Distance filters accept lt, lte, gt, and gte. Both the vector and the threshold are bound as parameters ($1::vector < $2). A distance filter with no comparator throws ValidationError.
Combine a filter with KNN ordering to retrieve the closest matches above a quality bar:
const results = await db.items.findMany({
where: { embedding: { distance: { to: queryVector, metric: 'cosine', lt: 0.4 } } },
orderBy: { embedding: { distance: { to: queryVector, metric: 'cosine' } } },
limit: 10,
});Metrics#
metric selects the pgvector distance operator:
metric | Operator | Distance |
|---|---|---|
'l2' | <-> | Euclidean (L2) distance |
'cosine' | <=> | Cosine distance |
'ip' | <#> | Negative inner product |
An unknown metric throws ValidationError. A distance op on a non-vector column also throws ValidationError.
Note: pg has no built-in parser for the
vectortype, so a fetchedvectorcolumn comes back as a string literal like'[1,2,3]'unless you register a parser (for example via pgvector's own client helpers). Querying by distance works regardless, this only affects how the column value reads when you select it.
The exported types#
The vector surface is fully typed and every type is exported from the package root, so you can build a query object in a helper and hand it to findMany without losing type safety.
import type {
VectorMetric,
VectorFilter,
VectorDistanceFilter,
VectorOrderBy,
VectorOrderByDistance,
} from 'turbine-orm';| Type | Shape | Where it goes |
|---|---|---|
VectorMetric | 'l2' | 'cosine' | 'ip' | The metric field of both specs below. A fixed allow-list, so a user-supplied string can never become a SQL operator. |
VectorDistanceFilter | { to: number[]; metric: VectorMetric; lt?, lte?, gt?, gte?: number } | The distance value inside a where. |
VectorFilter | { distance: VectorDistanceFilter } | The per-column value in where, i.e. where: { embedding: VectorFilter }. |
VectorOrderByDistance | { to: number[]; metric: VectorMetric; direction?: 'asc' | 'desc' } | The distance value inside an orderBy. direction defaults to 'asc', which is nearest-first. |
VectorOrderBy | { distance: VectorOrderByDistance } | The per-column value in orderBy, i.e. orderBy: { embedding: VectorOrderBy }. |
A typical use is a shared retrieval helper that takes the query embedding once and builds both halves:
import type { VectorFilter, VectorMetric, VectorOrderBy } from 'turbine-orm';
function knn(to: number[], metric: VectorMetric = 'cosine', maxDistance?: number) {
const order: VectorOrderBy = { distance: { to, metric } };
const filter: VectorFilter | undefined =
maxDistance === undefined ? undefined : { distance: { to, metric, lt: maxDistance } };
return { order, filter };
}
const { order, filter } = knn(queryVector, 'cosine', 0.4);
const results = await db.items.findMany({
where: filter ? { embedding: filter } : undefined,
orderBy: { embedding: order },
limit: 10,
});Validation and safety#
Every value in a vector query is bound as a parameter, so hostile input can't escape the query:
- The query vector is always bound as
$n::vector. - The distance threshold is always bound as
$n. - A non-number element in
to(includingNaNorInfinity) throwsValidationErrorbefore any query runs.
The same query, three ways#
The task: the 10 nearest rows by cosine distance, filtered to a quality threshold, fully parameterized.
Turbine, distance is a first-class where / orderBy operator on vector columns:
const results = await db.items.findMany({
where: { embedding: { distance: { to: queryVector, metric: 'cosine', lt: 0.4 } } },
orderBy: { embedding: { distance: { to: queryVector, metric: 'cosine' } } },
limit: 10,
});Prisma, pgvector columns are Unsupported("vector") in the schema, so querying by distance means $queryRaw with the vector serialized to a string and cast by hand:
const embedding = `[${queryVector.join(',')}]`;
const results = await prisma.$queryRaw<
{ id: number; title: string; distance: number }[]
>`
SELECT id, title, embedding <=> ${embedding}::vector AS distance
FROM items
WHERE embedding <=> ${embedding}::vector < 0.4
ORDER BY distance
LIMIT 10
`;Drizzle, a native vector column type plus SQL expression helpers; the distance expression is built with cosineDistance and threaded through where / orderBy:
import { asc, cosineDistance, lt } from 'drizzle-orm';
const distance = cosineDistance(items.embedding, queryVector);
const results = await db
.select({ id: items.id, title: items.title, distance })
.from(items)
.where(lt(distance, 0.4))
.orderBy(asc(distance))
.limit(10);All three run the same SQL. The difference is where the vector semantics live: in Turbine they're part of the typed query object; in Prisma they're raw SQL you maintain; in Drizzle they're SQL expression helpers you compose yourself.
See also#
- API Reference,
findMany,where,orderBy, and the full operator table. - Realtime,
LISTEN/NOTIFYpub/sub. - Relations, combine vector ranking with nested
withloads.