Pagefind vs ZBSearch
Head-to-head for an EN+PL static garden/docs site — researched and
empirically tested 2026-09-13: Pagefind v1.5.2 indexing the real
garden.h build (121 pages, 9228 words) in Chromium; ZBSearch v4.0.0
(built from source) indexing the same corpus via contentIndex.json,
including a Snowball-Polish-stemmer integration. See
Search solutions for static sites for the wider field,
zbsearch (OSS) / pagefind (OSS) for the captures.
Polish verification (the crux)
- Pagefind:
--force-language plworks —programowanie/
programowania/programowanall converge to the same 7 hits
(Snowball Polish shipped inside). Withlang="en"(garden today)
programowaniastill matched the right note but via fewer paths —
setlang="pl"or per-page langs for real stemming. - ZBSearch: no Polish stemmer shipped, but the custom-stemmer path
took ~15 minutes end-to-end: compile the official
algorithms/polish.sblwith the Snowball 3.1.1 compiler
(./snowball algorithms/polish.sbl -js), wrap the generated class as
(word) => new PolishStemmer().stem(word), pass as
components.tokenizer.stemmer. Result: same convergence
(programowanie/-nia/-n→ identical 3-hit sets) on the same corpus. - Both engines therefore reach EN+PL stemming; Pagefind out of the box,
ZBSearch with one generated file.
Feature matrix
| Pagefind | ZBSearch | |
|---|---|---|
| Architecture | post-build CLI → chunked static WASM index + JS API | embedded JS library (browser/Node/edge) |
| Runtime deps | none (WASM + JS files on your CDN) | the library itself (~2 KB core) |
| Index build | indexes rendered HTML (any SSG, zero config) | you write the indexing code from structured docs |
| Bandwidth | chunked: measured 207 KB total for 1 query incl. pagefind.js + WASM + 1 index chunk + fragments | whole index in memory (fine at 50 notes, linear growth) |
| Multilingual | lang attr → per-language indexes, auto UI language; Snowball stemming per lang (PL ✅ CS ✅, 30+) | language: multilingual zero-config (Intl.Segmenter, v4); 31 stemmer+stopword packs; per-field language skip lists |
| Polish | shipped (Snowball PL) | not shipped — compile polish.sbl → custom stemmer (verified) |
| Diacritics | folded per language rules | folded pre-stemming (ą→a etc., charcode table 192–383); significant-diacritics languages excluded |
| Full-text ranking | TF-based, filters, sorts, per-section results, exact-quote, term boosts | BM25, typo tolerance, exact match, fields boosting, facets, filters |
| Hybrid / vector | ❌ none (keyword only) | ✅ mode: hybrid + hybridWeights; @zbsearch/plugin-embeddings = offline TF.js embeddings at insert/search time (no API) |
| Autocomplete | via UI | built-in autocomplete |
| UI | drop-in <pagefind-search> web component, 15+ UI translations | searchbox-core/-react/-vue packages, Docusaurus/VitePress/Starlight plugins |
| Self-hosting | any static host (it is static files) | any JS runtime; edge runtimes packaged |
| License | MIT | MIT |
Strengths
Pagefind — operations-free: run the CLI after quartz build, done.
Scales to 10k+ pages with bounded bandwidth (chunked loading verified:
only the query’s chunk + 3 result fragments were fetched). Multilingual
is genuinely zero-config — set lang attributes. Best fit: static
garden/blog/docs where search must “just work” and stay cheap.
ZBSearch — programmable search platform: structured docs (facets,
boosting, geosearch), BM25, and uniquely hybrid keyword+vector in the
browser/server with offline embeddings — a mini-Meilisearch with no
server. Zero-config multilingual tokenization (Intl.Segmenter +
diacritics folding) handles mixed EN/PL content without language
tagging. Best fit: app-embedded search, faceted docs portal, or
EN+PL+semantic in one client-side package.
Weaknesses
Pagefind: no semantic/vector path at all; ranking is TF-only (no
BM25); index is HTML-derived (can’t index structured data or DB rows);
language determined per-page by lang attribute — a mixed-language page
gets one language’s rules.
ZBSearch: you own the indexing pipeline (build, host, update the
index yourself — no pagefind-style post-build CLI); whole index loads
into memory (bandwidth grows linearly — fine ≤ a few hundred docs);
Polish stemming is DIY (compile + wrap, as verified); no hosted UI
component as polished as Pagefind’s drop-in.
Verdict for garden.h
- Today (48 notes, EN UI, some PL content): stock Quartz FlexSearch is
still adequate; if we upgrade, Pagefind is the lower-friction
choice —--force-language plproven on our own build, chunked
payloads, drop-in UI, zero JS infra. - If we later want semantic search over the public garden
(find-by-meaning without qmd’s local models), ZBSearch hybrid +
offline embeddings is the only client-side option — and its
multilingual tokenizer already folds Polish diacritics correctly. - Both can coexist: Pagefind for site search, ZBSearch embedded in a
tool (its ESM core is 2 KB).
Cross-refs: Search solutions for static sites,
Polish stemming and lemmatization, Quartz search, QMD,
zbsearch (OSS), pagefind (OSS).