Vector Search
Vector search enables similarity retrieval using embedding vectors — useful for AI-powered search, recommendations, and RAG applications. Define vector fields and indexes in your .as schema, generate embeddings externally, then search with a simple API.
Defining a Vector Field
Use @db.search.vector on a field to mark it as a vector embedding:
@db.table 'documents'
export interface Document {
@meta.id
id: number
title: string
@db.search.vector 1536, "cosine"
embedding: db.vector
}The annotation takes up to three arguments:
| Argument | Type | Required | Description |
|---|---|---|---|
dimensions | number | Yes | Vector size — must match your embedding model output (e.g., 1536 for OpenAI text-embedding-3-small) |
similarity | string | No | Distance metric: "cosine" (default), "euclidean", or "dotProduct" |
indexName | string | No | Index name — defaults to the field name |
Dimensions
dimensions is any positive integer — the size of your embedding model's output. Anything else (0, -1, 1.5) is rejected at compile time. Editor completion offers the common sizes 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 16384; before 0.1.168 only these were accepted. Common choices: 384 (Sentence-Transformers MiniLM), 768 (BERT, Cohere embed-multilingual-v3.0), 1024 (Cohere embed-english-v3.0), 1536 (OpenAI text-embedding-3-small, text-embedding-ada-002), 3072 (OpenAI text-embedding-3-large).
The field type can be db.vector (a semantic alias for number[]) or number[] directly.
TIP
Use db.vector as the field type to make the intent clearer — it compiles to number[] but signals that this field holds an embedding vector.
Setting a Default Threshold
Use @db.search.vector.threshold to set a minimum similarity score. Results below this threshold are excluded from search results:
@db.search.vector 1536, "cosine"
@db.search.vector.threshold 0.7
embedding: db.vectorThe threshold range is 0 to 1, where 1 means exact match. This default can be overridden at query time via the $threshold control.
Pre-Filtering
Use @db.search.filter to mark fields as pre-filters for vector search. Pre-filters narrow the candidate set before similarity computation, improving both performance and relevance:
@db.table 'documents'
export interface Document {
@meta.id
id: number
title: string
@db.search.vector 1536, "cosine"
@db.search.vector.threshold 0.7
embedding: db.vector
@db.search.filter "embedding"
category: string
@db.search.filter "embedding"
status: string
}The indexName argument in @db.search.filter must match the name of a @db.search.vector index (the field name or explicit indexName). A field can be a pre-filter for multiple vector indexes, and an index can have several pre-filter fields.
On MongoDB (since 0.1.151), the top-level conditions of the query filter on pre-filter fields move into the $vectorSearch stage's filter, so the nearest neighbours are taken among the matching documents and a page comes back full. A condition moves when it compares a string, number, boolean, Date or ObjectId with $eq (or a bare value), $ne, $gt, $gte, $lt, $lte, $in or $nin (a non-empty list of such values). Every other condition — on other fields, against null, $regex, $exists, $or, relational predicates — filters the top-k results afterwards, so such a query can return fewer rows than $limit. $skip is part of the top-k: { $skip: 20, $limit: 10 } asks $vectorSearch for 30 results and returns the last 10.
Programmatic API
Checking Capabilities
const docs = db.getTable(Document);
docs.isVectorSearchable(); // true
docs.getSearchIndexes();
// [{ name: 'embedding', description: '...', type: 'vector' }]Basic Vector Search
Pass a pre-computed embedding vector to find similar records:
// Generate embedding externally (OpenAI, Cohere, etc.)
const queryVector = await generateEmbedding("search query text");
const results = await docs.vectorSearch(queryVector, {
filter: {},
controls: { $limit: 20 },
});Results are ordered by similarity (most similar first): the nearest rows are the result, so there is no unranked vector search, and controls.$rank: false — the unranked text search control — is rejected with INVALID_QUERY (since 0.1.175). controls.$select projects the rows exactly like findMany() — inclusion and exclusion forms, on every adapter (the SQL adapters returned every column before 0.1.143, so @db.writeOnly fields leaked through $vector queries over HTTP).
Vector Search with Count
For paginated results:
const result = await docs.vectorSearchWithCount(queryVector, {
filter: {},
controls: { $limit: 10 },
});
console.log(result.data); // similar documents
console.log(result.count); // total matches above thresholdOverriding Threshold at Query Time
Use the $threshold control to override the schema-level default:
const results = await docs.vectorSearch(queryVector, {
filter: {},
controls: { $threshold: 0.9, $limit: 10 }, // stricter than default
});Targeting a Specific Index
When a table has multiple vector fields, pass the index name as the first argument:
const results = await docs.vectorSearch("contentEmbedding", queryVector, {
filter: {},
controls: { $limit: 10 },
});Combining with Filters
Pre-filters
Fields marked with @db.search.filter narrow candidates before similarity computation. Pass them in the query filter:
const results = await docs.vectorSearch(queryVector, {
filter: { category: "tutorials" },
controls: { $limit: 10 },
});Post-filters
Additional filter conditions that are not declared as @db.search.filter (or that a pre-filter cannot express — see Pre-Filtering) apply after vector search as standard query filters, to the top-k results — they can shorten the page:
const results = await docs.vectorSearch(queryVector, {
filter: {
category: "tutorials", // pre-filter (declared with @db.search.filter)
title: { $regex: "^Intro" }, // post-filter (title is not a pre-filter field)
},
controls: { $limit: 10 },
});INFO
Pre-filtering is more efficient than post-filtering because it reduces the candidate set before computing distances. Mark frequently-filtered fields with @db.search.filter for best performance.
Multiple Vector Fields
Use indexName to distinguish multiple embedding fields on the same record:
@db.table 'documents'
export interface Document {
@meta.id
id: number
title: string
body: string
@db.search.vector 1536, "cosine", "content"
contentEmbedding: db.vector
@db.search.vector 384, "cosine", "title"
titleEmbedding: db.vector
@db.search.filter "content"
@db.search.filter "title"
category: string
}Target each index by name:
// Search by content similarity
const byContent = await docs.vectorSearch("content", contentVector);
// Search by title similarity
const byTitle = await docs.vectorSearch("title", titleVector);HTTP Access
Vector search over HTTP uses $search (the text query) combined with $vector (the vector field name). The controller converts the text to an embedding internally via computeEmbedding():
GET /documents/query?$search=machine+learning&$vector=embedding
GET /documents/query?$search=machine+learning&$vector=embedding&$threshold=0.8
GET /documents/query?$search=cooking&$vector=embedding&category=food&$limit=10
GET /documents/pages?$search=optimization&$vector=embedding&$page=1&$size=20| Parameter | Description |
|---|---|
$search | Text query (required — the controller converts it to an embedding) |
$vector | Vector field name to search on (e.g., embedding) |
$threshold | Minimum similarity score override (0–1); 0 disables threshold filtering |
When a table has only one vector field, $vector=embedding (the field name) is the minimal form. For multiple vector fields, $vector selects which one to search.
WARNING
Your controller must override computeEmbedding() to convert the $search text into a vector. Without this override, vector search via HTTP returns 501 Not Implemented. See HTTP — Advanced for setup details.
Similarity Metrics
| Metric | Description | Best for |
|---|---|---|
| Cosine | Angle between vectors (normalized, range 0–1) | Text embeddings, most common choice |
| Euclidean | Straight-line distance (lower = more similar) | Spatial data, image features |
| Dot product | Inner product (requires normalized vectors) | Recommendation systems, pre-normalized embeddings |
TIP
Cosine similarity is the default and works well for most text embedding models (OpenAI, Cohere, etc.). Only change the metric if your embedding model specifically recommends a different one.
Adapter Support
| Adapter | Column type | Index type | Notes |
|---|---|---|---|
| PostgreSQL | vector(N) via pgvector | HNSW index | Auto-provisions pgvector extension; distance operators <->, <#>, <=> |
| MongoDB | Embedded array | Atlas $vectorSearch | Requires Atlas M10+ cluster |
| MySQL | VECTOR(N) | Native (9.0+) | VEC_DISTANCE_* functions |
| SQLite | BLOB (vec0) when sqlite-vec loaded; JSON TEXT otherwise | vec0 virtual table | Requires the sqlite-vec extension; falls back to JSON storage without it |
SQLite vector search requires sqlite-vec
SQLite supports vector search only when the sqlite-vec extension is loaded. Construct the driver with new BetterSqlite3Driver(path, { vector: true }) and ensure the extension is installed.
Without sqlite-vec, vectors are stored as JSON text and vectorSearch() throws — schema sync, inserts, and reads still work, but similarity queries are unavailable.
Next Steps
- Text Search — full-text search with ranked results and field weighting
- Geo Search — distance-ranked search over coordinates (same API shape)
- HTTP — Advanced — vector search URL parameters
- Indexes & Constraints — other index types