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 pl works — programowanie /
    programowania / programowan all converge to the same 7 hits
    (Snowball Polish shipped inside). With lang="en" (garden today)
    programowania still matched the right note but via fewer paths —
    set lang="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.sbl with 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

PagefindZBSearch
Architecturepost-build CLI → chunked static WASM index + JS APIembedded JS library (browser/Node/edge)
Runtime depsnone (WASM + JS files on your CDN)the library itself (~2 KB core)
Index buildindexes rendered HTML (any SSG, zero config)you write the indexing code from structured docs
Bandwidthchunked: measured 207 KB total for 1 query incl. pagefind.js + WASM + 1 index chunk + fragmentswhole index in memory (fine at 50 notes, linear growth)
Multilinguallang 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
Polishshipped (Snowball PL)not shipped — compile polish.sbl → custom stemmer (verified)
Diacriticsfolded per language rulesfolded pre-stemming (ą→a etc., charcode table 192–383); significant-diacritics languages excluded
Full-text rankingTF-based, filters, sorts, per-section results, exact-quote, term boostsBM25, 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)
Autocompletevia UIbuilt-in autocomplete
UIdrop-in <pagefind-search> web component, 15+ UI translationssearchbox-core/-react/-vue packages, Docusaurus/VitePress/Starlight plugins
Self-hostingany static host (it is static files)any JS runtime; edge runtimes packaged
LicenseMITMIT

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 pl proven 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).