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 (F2LLM-v2-0.6B 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
The embedder can also return 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.