Semantic search for your static site. No backend, ever.
1 Point it at your docs
Drag a docs folder here
Markdown, MDX, HTML and plain text.
A .zip works too.
2 Shape the index
First build downloads a 31.3 MB embedding table once, then works offline.
3 Build
4 Your pack
5 Try it before you ship it
This box runs the exact search.js and binaries in your download.
1 · You choose a folder
Markdown, MDX, HTML and text files are read in the tab. HTML is stripped of navigation,
sidebars and footers, and the heading id attributes are kept so a result can
deep-link straight to the right section rather than the top of the page.
2 · The model turns out to be a lookup table
Most embedding models are neural networks that need a WASM runtime and a matrix multiply per
sentence. This one is not. Its whole computation graph is
Gather → DequantizeLinear → ReduceMean: an embedding is literally the average of
one row per .
Searchpack reads the 30,522 × 1,024 table straight out of the model file and does the average
itself, which is why there is no inference engine here at all.
3 · Your text is chunked and embedded
Sections are split at sentence boundaries with a little overlap, and each chunk is embedded
with its heading trail prepended — so an orphaned paragraph still carries the context
("Billing › Refunds") that the paragraph itself assumes you already have.
4 · The table is cut down to what you actually use
A docs site touches a few thousand of the model's 30,522 tokens, so only those
rows
ship. The full list of token strings still ships, because tokenizing against a
trimmed vocabulary silently re-segments unknown words instead of ignoring them — it would make
a search for "carpet" retrieve every page mentioning cars.
5 · Two indexes, because one is not enough
Each chunk gets a 256-bit
and an
int8 vector, plus BM25 term counts. Semantic search finds the page about backoff when you ask
how to stop something retrying forever; BM25 nails flag names and error strings, where
averaged embeddings are weakest. The default fuses both.
6 · The pack verifies itself
Before you download anything, the emitted search.js is loaded and run against the
emitted binaries. It re-embeds sample chunks and checks they reproduce the packed vectors,
measures how much the fast binary pass costs against exhaustive scoring, and throws awkward
queries at it. A failing pack is still given to you — clearly labelled — so you have something
to diagnose.
Protected
Every file you select is read, parsed, chunked and embedded inside this tab. No file, no
chunk, no text and no embedding is ever uploaded.
The index, the vocabulary, the shim and the zip are all built on this device.
The pack you ship contains no beacon, no callback and no network code beyond fetching its
own two static files — so your readers' queries stay on your readers' machines. That is the
part a hosted search service can never offer you.
No cookies, no fingerprinting, no third-party fonts.
Settings are kept in localStorage; document content never is.
Not protected
The one-time model download is an anonymous GET to
huggingface.co, so Hugging Face and its CDN see your IP address and the fact
that you fetched this model. It carries none of your content. Both URLs appear in the event
log before they are requested, and after the first fetch the tool never contacts them again.
The cached model survives offline — but on iOS Safari, storage for a site you have not
visited in seven days is cleared, so the download can return. Installing Searchpack to your
Home Screen exempts it.
GitHub Pages logs the initial page load, as any host would.
The pack is derived from your documents and contains chunk text for snippets. If your docs
are private, treat search.zip exactly as you treat the folder it came from.
Trust surface
The static site bundle served by GitHub Pages, and the TLS chain to it.
huggingface.co and its CDN, for the one-time model fetch only. The model is
pinned to an immutable revision, so the bytes cannot change under you.
A Cloudflare Web Analytics beacon records anonymous page views — no cookies, no
fingerprinting, no cross-site tracking; your files and data are never sent to it.
Feedback you choose to send is sent to feedback.benrichardson.dev. Nothing is
sent unless you open the feedback form and press Send; your files and data never are.
Honest limits on quality
These are static embeddings: a token-to-vector lookup with weighted pooling and no
attention. That is what makes a dependency-free shim possible instead of shipping an 11 MB
WASM runtime to every one of your readers — but it also means word order is not modelled.
The published retrieval score for this model is about 35 on MTEB Retrieval, against roughly
43 for a small transformer like all-MiniLM-L6-v2 and 52 for bge-small. That gap is real and
you will feel it.
Concretely: on the bundled 18-page sample, five conversational questions phrased to avoid the
wording of the page that answers them put the right page in the top three
twice. Expect it to shine when a query shares vocabulary with the answer, and
to struggle when it does not. Hybrid mode exists because BM25 rescues precisely the queries —
identifiers, flags, error codes — that pooled embeddings handle worst.
Judge it against what your site has today, which is usually keyword-only search or none at
all, rather than against a hosted neural service. If you need the last few points of quality
more than you need a free, zero-backend engine that leaks nothing about your readers, this is
the wrong tool — and that is a perfectly reasonable conclusion to reach.
Searchpack turns a folder of documentation into a self-contained semantic
search engine you drop into your own static site: a binary index, a subsetted embedding table
and a small dependency-free search.js, zipped. No account, no vector database, no
per-query cost, and nothing hosted.
It exists because the alternatives are all the wrong shape for a few hundred pages of
markdown on free hosting — an application-gated service, a monthly bill, or a serverless
function that turns every keystroke into a line item.