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.