Vector Search
Maravilla’s database has native vector search. Store embeddings alongside your documents, declare a vector index, and query by semantic similarity — optionally combined with regular metadata filters in a single call.
Bring your own embeddings (from OpenAI, Mistral, Cohere, a local model — anything). Maravilla handles the indexing, storage, and search.
import { getPlatform } from '@maravilla-labs/platform';
const platform = getPlatform();
// 1. Declare a vector index (one-time setup)
await platform.DB.createVectorIndex('products', {
field: 'embedding',
dimensions: 1536,
metric: 'cosine',
});
// 2. Insert documents with embeddings — vectors sync automatically
await platform.DB.insertOne('products', {
name: 'Wireless Headphones',
category: 'electronics',
embedding: [0.12, -0.45, /* ...1536 numbers */],
});
// 3. Search: metadata filter + vector similarity in one call
const hits = await platform.DB.find('products',
{ category: 'electronics', inStock: true },
{
vector: {
field: 'embedding',
value: queryEmbedding,
k: 10,
},
},
);
console.log(hits[0]._score); // 0–1 similarity score
console.log(hits[0]._distance); // raw distance
Every hit is a regular document with _score and _distance added. _score is normalized to [0, 1] — higher means more similar — so you can apply consistent thresholds across metrics.
Creating a Vector Index
await platform.DB.createVectorIndex('products', {
field: 'embedding', // JSON path to the vector field inside each doc
dimensions: 1536, // must match your embedding model's output
metric: 'cosine', // 'cosine' | 'l2' | 'hamming' — default 'cosine'
storage: 'float32', // 'float32' | 'int8' | 'bit' — default 'float32'
matryoshka: false, // allow queries with shorter vectors
multiVector: false, // each document holds an array of vectors
});
Creating the same index twice is a no-op. Creating an index with the same field but different configuration errors — drop the old one first with dropVectorIndex() if you want to change shape.
Declarative config — recommended
Declare vector indexes in maravilla.config.ts so they provision automatically when you run maravilla dev or deploy:
// maravilla.config.ts
import { defineConfig } from '@maravilla-labs/platform/config';
export default defineConfig({
database: {
vectorIndexes: [
{
collection: 'products',
field: 'embedding',
dimensions: 1536,
metric: 'cosine',
storage: 'int8',
},
],
},
});
Inserting Documents
Just set the field you declared. Maravilla syncs the vector into the index transparently, in the same transaction as the document write — either both commit, or neither does.
await platform.DB.insertOne('products', {
name: 'Wireless Headphones',
category: 'electronics',
price: 199,
embedding: [0.12, -0.45, /* ... */],
});
A few things to know:
- Dimension mismatches are rejected. If your index is 1536-dim and you pass a 768-dim array, insert fails with a clear error.
- Missing vector fields are tolerated. A document that doesn’t carry the declared field just skips the vector side; the document still gets stored, and it won’t appear in vector search results.
- Updates sync automatically.
updateOne()that changes the vector field rewrites the vector row in the index. - Deletes cascade.
deleteOne()removes the document and its vector rows together.
Searching
Hybrid search — metadata filter + vector
Pass a vector clause inside the existing find() options. The metadata filter is applied alongside the vector ranking so you can narrow to a segment of your collection before (or during) the similarity search.
const hits = await platform.DB.find('products',
// metadata filter — same operators you'd use without vectors
{ category: 'electronics', inStock: true, price: { $lte: 500 } },
{
limit: 10,
vector: {
field: 'embedding',
value: queryEmbedding,
k: 10,
metric: 'cosine', // optional — defaults to the index's metric
minScore: 0.7, // optional — drop low-similarity results
},
},
);
Pure vector search — findSimilar()
When there’s no metadata filter, findSimilar() is a slightly cleaner shape:
const similar = await platform.DB.findSimilar('products', {
field: 'embedding',
value: queryEmbedding,
k: 10,
filter: { category: 'electronics' }, // optional
minScore: 0.7, // optional
});
Query options
| Option | Type | Description |
|---|---|---|
field | string | Must match a registered vector index field on the collection. |
value | number[] or number[][] | The query vector. Use a flat array for single-vector queries; use an array of arrays for late-interaction (see Multi-vector). |
k | number | Top-k result count. Must be greater than 0. |
metric | string | Per-query override: 'cosine', 'l2', or 'hamming'. Defaults to the index’s configured metric. |
minScore | number | Drop results below this normalized _score. Applied after scoring. |
queryMode | string | 'single' (default) or 'late-interaction' for ColBERT-style queries. |
aggregation | string | 'max-sim' (default) or 'sum' — how multi-vector distances combine per document. |
Storage Options
Float32 — default
Full precision. 4 bytes per dimension. Highest accuracy. Use this unless you have a reason not to.
Int8 quantization — 4× smaller, ~same quality
await platform.DB.createVectorIndex('products', {
field: 'embedding',
dimensions: 1536,
metric: 'cosine',
storage: 'int8',
});
You still insert and query with regular float arrays — Maravilla quantizes on write and compares correctly. Typical accuracy loss is under 2% for normalized embeddings.
Bit quantization — 32× smaller
For large-scale candidate retrieval where you can tolerate significant precision loss:
await platform.DB.createVectorIndex('articles', {
field: 'embedding_bits',
dimensions: 1536,
metric: 'hamming', // required for bit storage
storage: 'bit',
});
Common pattern: bit index for fast candidate retrieval, then rerank top candidates with a float32 model.
Matryoshka Embeddings
Matryoshka-trained embeddings (OpenAI text-embedding-3-*, Mistral, Nomic) let you truncate a vector to a shorter length without retraining. Maravilla supports this as an index-level opt-in:
await platform.DB.createVectorIndex('docs', {
field: 'embedding',
dimensions: 1536,
matryoshka: true,
});
// Query with any length <= 1536 — Maravilla slices the stored vectors to match
const shorter = queryEmbedding.slice(0, 256);
const hits = await platform.DB.findSimilar('docs', {
field: 'embedding',
value: shorter,
k: 10,
});
Without matryoshka: true, query-length and index-length must match exactly. Use matryoshka when you want to trade a bit of accuracy for much smaller query vectors on the hot path.
Multi-Vector (ColBERT-style)
For late-interaction retrieval — where each document stores an array of vectors (one per chunk, sentence, or token) and queries are compared against every stored vector:
// Declare a multi-vector index
await platform.DB.createVectorIndex('passages', {
field: 'tokenEmbeddings',
dimensions: 128,
metric: 'cosine',
multiVector: true,
});
// Each document holds an array of vectors
await platform.DB.insertOne('passages', {
title: 'Introduction to Widgets',
tokenEmbeddings: [
[0.1, 0.2, /* ... */], // chunk 1
[0.3, 0.1, /* ... */], // chunk 2
[0.0, 0.4, /* ... */], // chunk 3
// ...
],
});
// Single-vector query — finds docs with any chunk close to the query
const hits = await platform.DB.find('passages', {}, {
vector: {
field: 'tokenEmbeddings',
value: queryVector,
k: 10,
aggregation: 'max-sim', // default — rank docs by their closest chunk
},
});
// Late-interaction (true ColBERT) — compare each query token to every stored chunk
const hits = await platform.DB.find('passages', {}, {
vector: {
field: 'tokenEmbeddings',
value: queryTokenEmbeddings, // number[][] — one vector per query token
k: 10,
queryMode: 'late-interaction',
aggregation: 'sum', // sum the per-token max-sim scores
},
});
Managing Indexes
// List every vector index on a collection
const indexes = await platform.DB.listVectorIndexes('products');
// Drop a vector index (does not touch the documents — just the vector data)
await platform.DB.dropVectorIndex('products', 'embedding');
// `listIndexes()` returns both document and vector indexes in one unified list
const all = await platform.DB.listIndexes('products');
// → [{ kind: 'document', ... }, { kind: 'vector', ... }]
Working with the CLI
# Create a vector index
maravilla platform vector create-index products embedding \
--dimensions 1536 --metric cosine --storage int8
# Search from the terminal — handy for debugging
maravilla platform vector search products embedding \
--vector '[0.1, 0.2, 0.3, ...]' --k 10
# List vector indexes
maravilla platform vector list products
# Drop an index
maravilla platform vector drop-index products embedding
Metric Selection Guide
| Metric | When to use | Storage required |
|---|---|---|
cosine | Text embeddings (OpenAI, Mistral, Cohere, sentence-transformers). The default. | float32 or int8 |
l2 | Image or audio embeddings where magnitude carries meaning. | float32 or int8 |
hamming | Bit-quantized indexes for fast candidate retrieval. | bit (required) |
If your embedding model is documented as working with “dot product” or “inner product” — normalize your vectors to unit length and use cosine. The ranking is equivalent.
Limits
| Parameter | Value |
|---|---|
| Max dimensions per index | 4,096 |
| Supported metrics | cosine, l2, hamming |
| Supported storage precisions | float32, int8, bit |
Best Practices
- Declare indexes in
maravilla.config.ts— keeps your vector schema versioned in git and provisions automatically on deploy. - Match
dimensionsto your embedding model exactly — there’s no silent truncation or padding. - Use
int8storage for text embeddings — 4× smaller, accuracy loss is usually negligible. - Normalize vectors for cosine similarity — if your model doesn’t already produce unit vectors, normalize on the client side before insert.
- Combine metadata filters with vector search — narrows the result set and usually improves relevance for domain-specific queries.
- Set
minScore— vector search always returnskresults even when none are genuinely similar. A threshold stops spurious matches from reaching your UI.
Next Steps
- Database API Reference — the full document-query API that hybrid search builds on
- Platform Services Overview — how all services fit together
- CLI Reference —
maravilla platform vectorcommands