Skip to content
searcharchitecturecost

BM25 Over SQLite vs a Vector Database

The category assumes an embedding service, an ANN index and a GPU. We measured entry search at recall@5 = 1.00 at 1.3 ms p95 with BM25 over SQLite, and kept the embedder optional.

MonkeyLLM Team4 min read

Entry search in MonkeyLLM runs on BM25 over SQLite, and it reaches recall@5 = 1.00 at 1.3 ms p95 on our benchmark corpus. Before you object that lexical search died years ago, look at what the job actually is. The job is smaller than the category assumes, and that changes the economics.

What the category assumes

The default retrieval stack ships with assumptions baked in: an embedding model (a hosted API, or a GPU you keep warm), a vector database with an ANN index, a pipeline that re-embeds documents on every edit, and a migration story for the day you change embedding models and every stored vector goes stale. All of that is operational surface, and all of it is paid before the first query returns.

None of it is wrong. It is just rarely questioned, because "semantic search" sounds like a requirement rather than a design choice.

What entry search actually has to do

In a knowledge forest (a linked graph of documents, each carrying a curated passport: title, summary, tags), search is not the answer machine. It is the door. The agent searches once to find a starting node, then walks: it reads labels, follows links, and opens only what it needs. The walk resolves the question; search only has to land close.

That reframing shrinks the requirement. The door does not need to understand a paraphrase of the whole question. It needs to match a name, and matching names is what lexical search does best.

What we measured

Chart showing BM25-only entry search at recall@5 = 1.00 with 1.3 ms p95 latency
Chart showing BM25-only entry search at recall@5 = 1.00 with 1.3 ms p95 latency

BM25 over SQLite's full-text index: recall@5 = 1.00 at 1.3 ms p95. No embeddings, no vector database, no GPU. The detail that matters most: weighting curated fields (title, aliases) closed the gap to hybrid search entirely. Unweighted BM25 over raw bodies did not get there; BM25 over a curated passport did.

SQLite logo

SQLite earns a word here. It is a single file inside the deployment. There is no search service to run, back up or monitor, and the same database already stores the forest itself. For a self-hosted system that promises your data never leaves your infra, deleting an entire stateful service from the architecture diagram is not a small win.

When vectors do earn their keep

This is not an argument that embeddings are useless. They earn their keep when queries are paraphrase-heavy and share no vocabulary with the documents, when the corpus is cross-lingual, or when you only have raw chunks with no curated metadata and no write-time budget to create any. In those regimes, lexical matching has nothing to grip.

Which is why the embedder is optional and supported, not removed. Point a model binding at an embedding endpoint and entry search becomes hybrid. What we removed is the assumption, not the capability.

The trade we actually made

The recall number is not free; it is prepaid. Curation happens at write time: ingest runs at 1.71 s per document, with 100% of summaries passing a sixty-token contract, and that is where the titles, aliases and summaries that BM25 grips come from. Spend intelligence on the environment so you can spend less on the model. The long version of that argument is the Forest Principle.

Reproduce it

The corpus and harness are committed, and the entry-search table regenerates with one command. Reproduce it yourself, read how the pieces fit in the primitives docs, or start at monkeyllm.com.

Want the long version?

The paper carries the full architecture, the benchmark tables and the findings that failed their criteria.