The search on this site, taken apart.

Press ⌘K anywhere on this site and a query runs against an inverted index built in your browser. This page is that engine with the lid off — the same code, the same corpus, live.

01 Analysis

Text has to become terms

Searching raw words fails on the first plural. Every string that enters the index and every query typed into the palette goes through the same four stages, because if they diverge the query can no longer match the document it came from.

Tokenising on /\w+/ would split Next.js into two words and destroy C++ and C# entirely. On a developer's site those are not edge cases, they are the queries — so internal separators and trailing + and # stay part of the token.

Folding lowercases and strips diacritics, so résumé and resume are one term. It runs per token, never on the whole document: Unicode normalisation changes string length, and the character offsets that highlight search snippets would land on the wrong letters.

Stopwords are dropped — with one deliberate omission. Standard lists remove single letters and very short words, which on a site carrying a C programming series would make the single most important query on the corpus return nothing. c, go and r are kept.

Stemming is Porter's 1980 algorithm, so deployment, deployed and deploying collapse toward one index term. It is 60 lines of regular expressions that fail silently when edited wrongly, which is why the repository carries a test suite pinning its output word by word — including one case that pins a known wart in the algorithm rather than quietly patching a published one.

02 Data structure

The inverted index

Scanning every document on the site for every keystroke is the obvious implementation and the wrong one. The index inverts it: term → the documents containing it, with the position of every occurrence. Look up a word, get its answer set — no scan.

Positions are what let the ranker tell a phrase from a coincidence. Two query terms occurring three words apart and two occurring four hundred words apart are the same match to a bag-of-words model; the smallest window covering all of them is found by a k-pointer sweep over the position lists, which is linear in the number of occurrences rather than quadratic.

The vocabulary is kept sorted, so prefix expansion — what makes results appear while you are still typing — is a binary search for the range rather than a scan of every term on every keystroke.

03 Ranking

BM25F, with the knobs exposed

Retrieval is the easy half. Ordering is the half people notice. Move the sliders and watch the ranking change — these are the two parameters every BM25 implementation has and almost no interface ever shows.

    Re-ranked in , in this browser, over the whole corpus.

    The F in BM25F is fields. A term in a project's name is not worth the same as the same term in paragraph nine, so title, subtitle, tags and body are weighted separately and each is length-normalised against its own average — titles are short by nature, and normalising them like prose would punish a four-word name for being four words long.

    Two multipliers sit on top of the sum. Coordination favours a document matching three of three query terms over one matching a single rare term very well, because BM25 sums and would otherwise prefer the second. Proximity rewards terms found close together. Both appear in the breakdown under every result.

    04 The corpus

    Why rare words carry the signal

    Every ranking decision above rests on one property of natural language: word frequency falls off a cliff. Plotted with both axes logarithmic, the corpus of this site lands close to a straight line — Zipf's law, measured here rather than cited.

    A handful of terms appear everywhere and separate nothing; the long tail appears once or twice and identifies a document almost by itself. That is exactly what inverse document frequency encodes, and it is why a search for "stripe" is decisive while a search for "code" is not.

    05 Judgement

    What was deliberately not built

    The interesting decisions in a small system are the ones that keep it small.

    No vector embeddings

    A sentence-transformer is 20+ MB of weights to make 60 KB of text searchable — the wrong instrument for the corpus, paid for by every visitor on a phone. BM25 with query expansion is the honest choice at this scale, and it remains the baseline dense retrieval is measured against in the literature.

    No search library

    The off-the-shelf options are 20–200 KB and would still need the field weighting, passage splitting and typo handling wired by hand. The engine here is 12 KB gzipped with no dependencies and no build step — it ships as it is written, comments included — and every behaviour in it is one I can be asked about.

    No prebuilt index

    The index could be generated at build time and shipped as JSON. It would be larger than the text it indexes — postings plus positions — so shipping it costs the visitor more, not less. Building it live costs the milliseconds reported at the top of this page, and is what lets the page show real numbers instead of a diagram.

    No search box on the page

    A permanent input would need a place in a layout that does not have one, and would be ignored by most visitors. A keyboard shortcut with a labelled trigger in the navigation costs no layout, and tells the people most likely to use it exactly how.

    Read the source on GitHub (opens in a new tab) Back to portfolio