Concepts

Four signals. One ranking.

Every search runs four retrievers in parallel and fuses their results with Reciprocal Rank Fusion. Each retriever covers a different failure mode of the others.

Dense embeddings

A local embedding model (BGE-M3 by default) turns each document and your query into a 1024-dim vector. We then ask sqlite-vec for the K nearest neighbours. This finds semantically related content even when the words differ — "sneaker" matches "running shoe".

BM25 (lexical)

SQLite's built-in FTS5 engine returns rows by BM25 score. This finds exact keywords, SKU codes, product names, prefixes — anything where the user's words appear in the document.

Sparse embeddings

BGE-M3 also returns learned sparse token weights in the same forward pass as the dense vector. These act like SPLADE-style expansions and capture synonyms / paraphrases that pure BM25 would miss.

Knowledge graph

Tags, categories, brands, and entities you put in metadata are extracted into a graph automatically. We then traverse it with a recursive CTE to find documents related through structure, not text. Useful for "customers who bought A also bought B".

Reciprocal Rank Fusion

For each retriever, we rank the results. We then compute RRF_score(d) = Σ weight_r / (k + rank_r(d)) and take the top ~50. Those 50 are sent to the rerank stage.

Reranking

A cross-encoder (BGE-reranker-v2-m3) reads the query and each of the top 50 candidates, and assigns a final relevance score. This is the only slow step, but it operates on 50 docs not 100K, so it stays under ~300ms.

Tuning the weights

Every account has four weights (one per signal) that default to dense: 0.4, bm25: 0.3, sparse: 0.2, kg: 0.1. Adjust them via the search playground or the weights field on every search request.