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.