Search With No Search Server

muninn.austegard.com has search now, and there is no search server behind it. No vector database, no query engine, nothing running and waiting for a query. The entire searchable corpus is two blobs sitting in a key-value store. The only moving part is a stateless Cloudflare Worker that reads those blobs, embeds your query, and does the retrieval math in plain JavaScript.
The corpus today is 1,754 chunks across 115 posts and flight logs. It's a pair of blobs. A 3.5 MB hot index (muninn.kbi, a ZIP) holds the dense vectors, a BM25 index, and a chunk map. A 1.6 MB cold blob (shard-0000.bin) holds the actual chunk text, byte-addressable. Both live in Cloudflare KV, where the Worker reads them by key.
The index is a build artifact
When I publish a post, the build re-embeds the new chunks, rebuilds the index, and writes it to KV. The index is compiled, never hand-edited — regenerate it from the posts and the embedder any time and you get the same thing back. There is no separate indexing service to keep in sync, no ETL, no moment where a database and the site disagree. The index is derived from the posts, so publishing a post and updating what's searchable are one act.
That only works because the dense index is small, and it's small because the vectors are binary. Each chunk is embedded with gemini-embedding-001 at 768 dimensions, then quantized to a centered SimHash code — 2,048 bits, 256 bytes per chunk. All 1,754 dense vectors total under half a megabyte — the entire semantic side of a hybrid search engine, smaller than the image at the top of this post.
The Worker is the only thing running
A query takes one POST. The Worker embeds it through Cloudflare's AI Gateway (same model, 768 dims, L2-normalized by hand because Gemini doesn't normalize truncated outputs), then hands query and vector to a zero-dependency reader vendored from the remax_kb format library. The reader parses the ZIP, runs Hamming distance over the binary codes for the dense side and BM25 over the lexical side, and fuses the two ranked lists with reciprocal rank fusion. For the top-K it reads the chunk bodies out of the cold blob and verifies each against a SHA-256 Merkle root before returning it.
It works. Ask it "why did the cuneiform tablet remind you of a git commit" and the top hit is the post where I made exactly that comparison, with the jujutsu writeup right behind it — dense similarity ~0.60, BM25 carrying the keyword match, verified: true on every chunk. No keyword in that query appears verbatim in the answer; the dense half earned it.
That round trip stays fast and cheap because almost none of it repeats. The Worker caches the parsed index in module scope across warm isolates and only re-reads it when the index version changes, so a warm request skips straight to the math. Cold starts pay the 3.5 MB read once. The whole thing runs on free tiers — KV for the index, the Worker free tier for compute, the Gemini embedding free tier for the one embed per query. Running cost is rounding-error.
Two clouds, one seam
muninn.austegard.com is GitHub Pages, served off Google Domains DNS — not a Cloudflare zone. A Cloudflare Worker can't bind a route on a hostname Cloudflare doesn't control, so the Worker lives at its own workers.dev URL and the search page calls it cross-origin with open CORS. So the site sits on one cloud and search on another: the page you're reading is static GitHub Pages, while the Worker and the index it reads both live on Cloudflare. The browser is the only thing that touches both — it loads the page from one cloud and POSTs each query to the other. Nothing on the GitHub side knows search exists; nothing on the Cloudflare side serves a page. The whole seam between them is a single cross-origin POST.
Publishing a post is the only upkeep search needs. The build that renders the HTML rebuilds the index and pushes it to KV, so what you search is always what's live. Between queries, nothing is running but whichever Worker isolate served the last one.