This is a technology spike — nothing here is a committed HDC deployment.Learn more →

← Back to all entries

Search engine comparison

SPECIFICATION.md’s “Integrate search” section lists 5 in-browser search library candidates. Pagefind is already the main integrated engine for /search/ and the homepage’s free-text box (see EVALUATION.md). This page adds a working demo of each of the other four — Lunr.js, FlexSearch, MiniSearch, Orama — each on its own page, each searching the same 13 HDC entries with the full same filter set as the homepage (Category, Readiness tier, License, Intended users, Primary purpose, Cost model, Support level, Number-adopting range, Go/Reset), so this is a genuine like-for-like comparison of how each library performs as a search engine under identical filters, not just a bare text box. Each is tested against the same bar as Pagefind: searching “Sangya” must return Cohort Discovery Service, because that word only exists in contactTeamPerson, a field no page displays.

All 4 passed that test, filters and all. Try each one:

The structural difference from Pagefind, upfront

Pagefind indexes the built HTML — it crawls dist/ after astro build and reads whatever text and data-pagefind-* attributes are on the page. None of these four libraries do that. All four index JS objects you hand them — there’s no equivalent of Pagefind’s “crawl the site” step. That single difference shapes almost everything else about integrating them:

Lunr.js

Verdict: easy, if you accept its search-syntax quirks.

const idx = lunr(function () {
  this.ref('id');
  this.field('name');
  this.field('description');
  this.field('body');
  for (const doc of docs) this.add(doc);
});
idx.search('Sangya*'); // -> [{ ref: 'cohort-discovery-service', ... }]

FlexSearch

Verdict: most powerful, most configuration surface to learn.

const index = new Document({
  document: { id: 'id', index: ['name', 'description', 'body'] },
  tokenize: 'forward',
});
for (const doc of docs) index.add(doc);
index.search('Sangya', { merge: true }); // -> [{ id: 'cohort-discovery-service', field: ['body'] }]

MiniSearch

Verdict: easiest of the four. If picking one today, this is the one.

const mini = new MiniSearch({ fields: ['name', 'description', 'body'], idField: 'id' });
mini.addAll(docs);
mini.search('Sangya', { prefix: true, fuzzy: 0.2 }); // -> [{ id: 'cohort-discovery-service', score: ... }]

Orama

Verdict: easy once you accept it’s async-first and schema-typed; the most “modern TypeScript library” feel of the four.

const db = await create({ schema: { id: 'string', name: 'string', description: 'string', body: 'string' } });
for (const doc of docs) await insert(db, doc);
const found = await search(db, { term: 'Sangya', properties: ['name', 'description', 'body'] });
// found.hits -> [{ document: { id: 'cohort-discovery-service', ... }, score }]

Net read for the next SSG iteration

If the goal is “smallest amount of code to get free text + filters working over a small-to-medium entry set,” MiniSearch had the least friction. If the goal is “prepare for a much larger corpus with incremental updates and possibly a persistent index,” FlexSearch’s extra configuration surface and Orama’s async/schema-typed design are the two worth a longer look — both are built with that scale in mind in a way Lunr and MiniSearch aren’t. None of the four required anything Pagefind didn’t already prove necessary for this project (a corpus that includes every field, not just what’s displayed) — the difference is entirely in how each library wants that corpus handed to it.