Build1 publisher3 min readPublished Updated
796 pages of semantic search with no vector database, and what it cost to skip one
A static site distilled its embedding model into an 8,900-word lookup table and ranks 796 documents in single-digit milliseconds in the browser. The trade-offs are published too.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- A site with 796 indexed pages runs semantic search entirely in the browser, with no server, no vector database, no index service, and no model running at query time.
- The author describes the default 2026 approach as: embed the corpus with an API, push the vectors into a hosted index, embed the query at request time, pay per call, and add a service that can be down.
- The site has a house rule that no page may send a reader's input to a third party, and it is a static build on Cloudflare's static assets, so there is no search server to add and a query box calling an embedding API would violate the rule.
- Shipping a sentence transformer to the browser as ONNX or WASM is tens of megabytes with a warm-up delay, and puts a model in the critical path of a text box.
- The Model2Vec idea used here runs the transformer once at build time over a fixed vocabulary and keeps only the resulting per-word output vectors, so embedding at runtime becomes a dictionary lookup and an average with no model.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A static site with 796 indexed pages now runs meaning-based search entirely in the reader's browser, with no server, no vector database, no index service, and no model executing when you type [1]. That is worth attention because it is a working counterexample to the architecture most teams reach for without arguing about it: embed the corpus through an API, push the vectors into a hosted index, embed the query at request time, pay per call, and add a service that can be down [2].
The constraint came first, which is usually how this goes. According to the author's write-up on dev.to, the site has a house rule that no page may send a reader's input to a third party, and it is a static build on Cloudflare's static assets, so there was no search server to add and an embedding API call from the query box was forbidden by policy [3]. The other browser-side option, shipping a sentence transformer as ONNX or WASM, is tens of megabytes plus a warm-up delay and puts a model in the critical path of a text box [4].
The method is Model2Vec: run the transformer once at build time over a fixed vocabulary and keep only the per-word output vectors, which turns runtime embedding into a dictionary lookup and an average [5]. The table here was distilled from Xenova/all-MiniLM-L6-v2 over the corpus's own vocabulary, one contextless 384-dimensional vector per word, 8,900 words at present [6]. Each document vector is the L2-normalized mean of its words' vectors, quantized to int8 and base64 encoded [7]. The query is embedded by exactly the same rule in the same code, which is what keeps queries and documents in the same space [8].
What ships is three static JSON files from the site's own origin, gzip and brotli by the host, plus one inline module: 401 lines of vanilla JavaScript using only atob, Math, Map, Set and typed arrays, with no DOM, no Buffer and no imports [9][10]. Ranking is cosine against 796 unit vectors, which is 796 dot products of length 384 [11], roughly 305,000 multiply-accumulate operations per query [12]. The author reports that unpacking every document vector takes 12 ms in Node on his machine and a full three-channel query takes 4 to 10 ms [13].
The second-order property is the one operators should copy. Because the word table is the expensive artifact and the document vectors are cheap, a build-start hook re-embeds the live corpus from the table that already exists, with no model and no network; the model is only needed to mint vectors for genuinely new words [14].
That is also where the bill arrives. A static word table has no context, so word order is gone, "dog bites man" and "man bites dog" embed identically, and negation is invisible [15]. Coverage decays as the corpus grows: 4,689 distinct words in the live corpus currently have no vector, and 459 documents dropped at least one word when their mean was taken [16]. The index reports this about itself in its own header, with vocabCovers set to false [17]. Those 459 documents are about 58 percent of the 796 [18], and the uncovered word count is now more than half the size of the covered table [19].
Two things to watch. First, whether retrieval quality visibly degrades before someone reruns the distillation, since the header flags the condition but nothing forces the fix [17][14]. Second, how the approach behaves on a less distinctive corpus; this one is described as dense, cross-referenced and deliberately weird, which flatters averaged word vectors [20]. The numbers are self-reported and the search is live at artwaste.land/ask [21].