Skip to main content
Version: 0.3.36

Vector Search

RaisinDB stores vector embeddings alongside your content and answers approximate nearest-neighbour queries from a built-in HNSW index. Semantic search, "more like this" and hybrid keyword-plus-meaning queries are all SQL.

How it works

  1. Embed: when a node is created or updated, a job sends its text to the tenant's configured embedding provider and stores the vectors.
  2. Index: vectors go into an HNSW (Hierarchical Navigable Small World) graph for logarithmic-time approximate search.
  3. Query: use the KNN and HYBRID_SEARCH table functions, or the <=> distance operator in an ordinary query, with filters as needed.

What gets embedded

  • Text is taken from the node's fields marked index: [Vector] in its node type, plus its name and path when the tenant configuration says so.
  • Long text is chunked (256 tokens with 64 overlap by default) and each chunk gets its own vector, keyed by node and chunk index.
  • Vectors are stored per tenant, repository, branch and workspace, and grouped into partitions by embedding model and kind (text or image).

Similarity queries

SELECT path, properties->>'title'::String AS title, vector_distance, chunk_index
FROM KNN('how do vector indexes work', 10, workspaces => 'default');
  • The first argument is the query. Plain text is embedded with the tenant's provider; EMBEDDING('...'), a literal vector, or VECTOR_OF('ws:/path') also work.
  • 10 is the number of rows.
  • workspaces is required: a name, a comma-separated list, a glob such as 'content-*', or 'ALL READABLE'.
  • vector_distance is the cosine distance; lower is closer.

Interpreting distance

Cosine distance is 1 - cosine similarity:

DistanceInterpretation
0.0identical
up to about 0.4close in meaning
0.4 to 0.6loosely related
above 0.6filtered out by default

The cutoff is configurable per tenant (ALTER EMBEDDING CONFIG SET DEFAULT_MAX_DISTANCE = '0.5') and per query (max_distance => 0.3).

Vector search plus filters

SELECT path, vector_distance
FROM KNN('sustainable packaging', 20, workspaces => 'default')
WHERE node_type = 'blog:Article'
AND properties->>'status'::String = 'published'
LIMIT 10;

The WHERE is applied after the search, so ask the index for more rows than you need when a filter is selective.

Vector plus full-text

HYBRID_SEARCH runs both legs and fuses them by rank (reciprocal rank fusion), so an exact product name and a paraphrase both count:

SELECT path, score, fulltext_rank, vector_rank
FROM HYBRID_SEARCH('how does authentication work', 10, workspaces => 'default');

Search within a subtree

The operator form is planned as an index scan when the distance is the ORDER BY key and there is a LIMIT. Structural predicates are pushed into it:

SELECT path, embedding <=> EMBEDDING('index maintenance') AS distance
FROM 'default'
WHERE PATH_STARTS_WITH(path, '/content/docs/')
ORDER BY distance
LIMIT 10;

One row per node, with chunk_index

The unit the index stores is a chunk, but results are fused per node by default, so a long handbook occupies one row. chunk_index and chunk_text name the passage that answered:

SELECT path, chunk_index, chunk_text, vector_distance
FROM KNN('rotating an API key', 5, workspaces => 'handbook');

Ask for granularity => 'chunk' to get one row per passage instead, which is what a RAG prompt usually wants. chunk_index is 0 for a document that was never chunked and NULL when a hybrid hit came from the lexical leg only.

Embedding providers

Providers are configured per tenant. The embedding configuration can point at any of these kinds:

KindNotes
openaitext-embedding-3-small (1536), text-embedding-3-large (3072)
azure_openai, groq, openrouter, customany OpenAI-compatible /embeddings endpoint
anthropicembeddings are served by Voyage AI
ollamalocal models such as nomic-embed-text (768) or bge-m3 (1024)

API keys are encrypted with AES-256-GCM before storage and are never returned by any read. See the AI Provider Configuration guide.

Isolation

Vectors and indexes are scoped by tenant, repository and branch, and searches are filtered by workspace and row-level security inside the graph walk:

  • no cross-tenant results
  • a branch searches its own content; creating a branch copies the parent's embeddings and index
  • a caller only ever sees hits they could read as nodes

Index settings

Two index properties are configurable, both on the tenant's embedding configuration:

SettingValuesNotes
distance_metriccosine (default), l2, inner_product, hammingthe metric the partition is built with
quantizationF32 (default), F16, Int8storage precision; F16 halves memory, Int8 quarters it

The HNSW graph parameters (connectivity and expansion factors) use the library defaults and are not exposed.

Performance

  • HNSW gives approximate search in logarithmic time; a query over a few hundred vectors answers in tens of milliseconds including embedding the query text.
  • Loaded indexes are kept in a memory cache with a 512 MB budget.
  • Changed indexes are written to disk about every 60 seconds and on shutdown.
  • SHOW VECTOR INDEX HEALTH, VERIFY VECTOR INDEX and REBUILD VECTOR INDEX inspect and repair the index for the current branch.

Next Steps