Field note · March 17, 2026

Site search without a server, in about 80 lines

A JSON index, term scoring, and why we did not reach for a search library.

1 min read ·Visualization ·engineering

Press / anywhere on this site and you get search across every lesson, article, dataset and tool. There is no server. There is no search service.

The build emits a search-index.json — title, url, kind, summary, and the first 1,600 characters of body text per document. About 110 documents, a few hundred kilobytes, fetched once when you first open the search box and never again.

Scoring is deliberately crude:

js
for (const term of terms) {
  let s = 0;
  if (title.includes(term))   s += title.startsWith(term) ? 14 : 9;
  if (summary.includes(term)) s += 4;
  if (body.includes(term))    s += 1;
  if (s === 0) { matchedAll = false; break; }   // every term must appear
  score += s;
}

Title hits beat summary hits beat body hits, and every term must appear somewhere. That is it. No stemming, no fuzzy matching, no inverted index, no BM25.

For a few hundred documents this is completely adequate and returns in a couple of milliseconds. A proper library — Lunr, FlexSearch, MiniSearch — would give better ranking and cost a dependency plus a bundle we would need to keep current.

The rule I would generalise: at small n, the naive algorithm is usually fine, and the interesting work is elsewhere. In this case "elsewhere" was arrow-key navigation, highlighting matched terms in the results, and remembering to make Escape close the thing. Those took longer than the scoring and matter more to whether anyone uses it.

If the corpus ever gets to a few thousand documents I will happily swap in a real index. It is not there, and building for the scale you do not have is how a static site acquires a build pipeline nobody understands.