root@kaus.com:~/blog$ cat how-site-search-works.md
How this site's search works: trigrams, synonyms, and no server
2026-07-16 · Architecture Notes · 3 min read
The site is statically exported with no server, so search runs entirely client-side over a prebuilt index. Two tiers handle it: exact scoring on prefix and substring matches, plus a close-match tier using character trigrams and cosine similarity, both fed through a hand-curated synonym map.
The constraint
This site is statically exported and hosted on GitHub Pages, with no server, no API, and no database. That means search has to run entirely in the browser, over a prebuilt index that ships with the HTML. The index includes projects (with ingested README text), skills (deduplicated across the profile and every project), experience entries, publications, pages (home, projects, explore, writing, contact), and posts. Everything the search bar can find lives in that index, built once during the build step and frozen.
Two search tiers
Exact-match tier scores prefix hits highest (90 points for a name prefix, 75 for word-boundary prefix), then substrings (60), then tag or skill mentions (40), then body text (20). When the query expands through the synonym map, 'ml' becomes 'machine learning' and 'rl' becomes 'reinforcement learning', and those synonym hits score 35 in tags or 15 in the body. Entries are ranked by score, ties broken by category order (pages first, then projects, skills, experience, publications, writing) and alphabetically within each group.
The close-match tier runs on character trigrams (three-character substrings) and cosine similarity. Every document gets vectorized once at index build time: a character trigram becomes a map of three-character substrings to their counts. The query does the same, expands through the synonym map, then gets compared to every document's trigram vector using cosine similarity. The result only surfaces hits above 0.16 similarity, capped at six close matches so they stay a supplement to the exact tier, not a second results list. If the README text scores higher than the name/tags/summary fields, the close-match result shows 'found in README' instead of the project summary.
The performance fix
Character trigrams are expensive. The first iteration built them naively: every keystroke, the search ran, vectorized every query document including full README text, computed the magnitude (square root of sum of squares of trigram counts) for each document, then ran cosine similarity. Magnitudes are cheap individually but they add up when you have dozens of projects and hundreds of skills, and READMEs can be thousands of characters long.
The fix precomputes magnitudes once at index-build time and stores them alongside the trigram vectors. At query time, only the query's trigram vector gets built and its magnitude computed. The cosine similarity function takes both magnitudes as arguments and only iterates the query vector's entries, probing the document vector by key instead of walking it. That reduces per-keystroke cost from O(documents times document trigrams) to O(query trigrams), which is usually fewer than twenty. The index also warms itself asynchronously after the page mounts using requestIdleCallback, so the first keystroke doesn't pay the full index-build cost.
What it can and can't do
Character trigrams find things by shape, not meaning. They work well for typos and abbreviations, but they have limits. The synonym map only covers vocabulary someone thought to add. If you type 'neural networks' but someone wrote 'deep learning', they won't match unless the synonym map connects them, which it does. But if they wrote 'convolutional classifier' and you search 'cnn', it won't match, because 'cnn' doesn't appear anywhere and no synonym expands to it.
Trigrams also surface loosely related items. A query for 'agent' surfaces not just agent-based projects but anything with 'gent' in it: genetic algorithms, urgent requests to fix something, et cetera. That's why the similarity threshold exists: noisy hits below 0.16 are filtered out. Everything that surfaces in the close-match tier passed the threshold, but it's still a lexical measure, not semantic understanding. If you're looking for 'reinforcement learning' and the site has a project on game AI that doesn't use those words, trigrams won't find it.