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.

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

OptionTypeDescription
fieldstringMust match a registered vector index field on the collection.
valuenumber[] or number[][]The query vector. Use a flat array for single-vector queries; use an array of arrays for late-interaction (see Multi-vector).
knumberTop-k result count. Must be greater than 0.
metricstringPer-query override: 'cosine', 'l2', or 'hamming'. Defaults to the index’s configured metric.
minScorenumberDrop results below this normalized _score. Applied after scoring.
queryModestring'single' (default) or 'late-interaction' for ColBERT-style queries.
aggregationstring'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

MetricWhen to useStorage required
cosineText embeddings (OpenAI, Mistral, Cohere, sentence-transformers). The default.float32 or int8
l2Image or audio embeddings where magnitude carries meaning.float32 or int8
hammingBit-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

ParameterValue
Max dimensions per index4,096
Supported metricscosine, l2, hamming
Supported storage precisionsfloat32, int8, bit

Best Practices

  1. Declare indexes in maravilla.config.ts — keeps your vector schema versioned in git and provisions automatically on deploy.
  2. Match dimensions to your embedding model exactly — there’s no silent truncation or padding.
  3. Use int8 storage for text embeddings — 4× smaller, accuracy loss is usually negligible.
  4. Normalize vectors for cosine similarity — if your model doesn’t already produce unit vectors, normalize on the client side before insert.
  5. Combine metadata filters with vector search — narrows the result set and usually improves relevance for domain-specific queries.
  6. Set minScore — vector search always returns k results even when none are genuinely similar. A threshold stops spurious matches from reaching your UI.

Next Steps