Skip to content

feat(search): replace the lunr search index with Pagefind - #2085

Closed
haranrk wants to merge 1 commit into
google:mainfrom
haranrk:pagefind-search
Closed

feat(search): replace the lunr search index with Pagefind#2085
haranrk wants to merge 1 commit into
google:mainfrom
haranrk:pagefind-search

Conversation

@haranrk

@haranrk haranrk commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces MkDocs' built-in lunr.js search with Pagefind.

The built-in search plugin builds one lunr index and the browser downloads all of it before the first query. On this site that's 3.5 MB (843 KB gzipped), fetched on every page load whether or not the reader opens search. Pagefind shards the index at build time and ships a WASM query engine, so the browser fetches only the shards a query touches.

Measured impact

Built the previous commit as a baseline, served both locally, and read the server access log — Pagefind fetches its index from a Web Worker, and those requests don't appear in page-level devtools events.

before (lunr) after (Pagefind)
on page load 855 KB gzip 46 KB gzip
on first search 0 378 KB gzip
total, if the reader searches 855 KB 425 KB

Readers who never search pay 95% less. Readers who do pay about half.

One honest caveat: unlike lunr, Pagefind's cost isn't one-time — it fetches more fragments as you scroll results, so a session with many queries eventually crosses over.

How it works

  • hooks/pagefind_index.py runs the indexer in on_post_build: +2.2s on a ~31s build.
  • Pagefind ships as a PyPI wheel with a prebuilt binary, so it installs from requirements.txt with no Node toolchain in CI. A manylinux wheel exists for ubuntu-latest.
  • Index scope is set by data-pagefind-body on the article, via a container block override mirroring the theme's own. Once that attribute exists anywhere, Pagefind indexes only pages carrying it — which keeps the 3,130 generated Dokka/Javadoc/TypeDoc files out. Those were never in the lunr index either, so search scope is unchanged: 232 pages in, 232 indexed.
  • UI is Pagefind's Component UI (<pagefind-modal>), which supersedes the older Default UI as of Pagefind 1.5.0. Keeps Cmd/Ctrl+K and adds WAI-ARIA compliance.
  • Theming maps --pf-* onto Material's --md-* on body, not :root — Material declares its light palette at :root but its dark palette on [data-md-color-scheme] on <body>, so mapping at :root would stay light after the palette toggle. Custom properties are exempt from Pagefind's all: initial host reset, so dark mode needs no JavaScript.
  • The components live in the header, outside every region Material's instant navigation swaps, so they stay mounted across client-side navigation.

PAGEFIND_SKIP=1 skips indexing, e.g. for link-checking builds.

Index quality fixes

Pagefind concatenates adjacent inline elements without a separator, so siblings authored without whitespace fuse into one token. It does not fix this for free — three constructs here hit it, removed via --exclude-selectors:

selector produced
.headerlink permalink anchors fused into headings on 230 of 232 pages
.tabbed-labels <label>Python</label><label>Java</label>PythonJava
.language-support-tag ADKPythonTypeScriptGoJava

Pages with fused language tokens: 109/232 → 5/232, and all five remaining are genuine identifiers (RxJava, pdfArtifactJava is a real variable in a Java sample). Tab contents stay indexed, so per-language content remains searchable — verified Kotlin agent → 25 results, Java streaming → 34.

--include-characters ".-@#+" keeps dotted identifiers intact; without it google.adk.agents degrades to three bare words (133 results with it).

Verification

Driven in headless Chrome against the built site:

  • trigger renders, Ctrl+K opens, queries return results, and permalink anchors no longer appear in indexed headings
  • zero requests for search_index.json or the lunr worker
  • no JS errors, no same-origin HTTP failures
  • dark mode: dialogBg resolves to rgb(30, 33, 41) with light text after the palette toggle
  • instant navigation: same component node persists, no duplicates, search still works. This needed care — instant nav is inert on localhost because Material gates interception on sitemap.xml, whose URLs are absolute to adk.dev. I rebuilt with a local site_url to reproduce production conditions, and confirmed production adk.dev does use it.
  • mkdocs build --strict passes; mkdocs serve works (search functions in dev too)
  • both failure paths tested: PAGEFIND_SKIP=1, and a clear error if the dependency is missing

Note for reviewers

Screenshots of the modal in light and dark mode can be attached if useful. Licensing is unchanged: Pagefind is MIT, same as mkdocs-material and lunr.

@netlify

netlify Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploy Preview for adk-docs-preview ready!

Name Link
🔨 Latest commit df8821c
🔍 Latest deploy log https://app.netlify.com/projects/adk-docs-preview/deploys/6a762ad4c00b3600080a7e9d
😎 Deploy Preview https://deploy-preview-2085--adk-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@haranrk
haranrk force-pushed the pagefind-search branch 2 times, most recently from e24c46e to 6645a9c Compare August 6, 2026 23:11
MkDocs' built-in `search` plugin builds a single lunr.js index and the
browser downloads all of it before the first query. On this site that is
3.5 MB (843 KB gzipped) across 2,413 records, and it is fetched on every
page load whether or not the reader ever opens search.

Pagefind instead shards the index at build time and ships a WASM query
engine, so the browser fetches only the shards a query touches.

Measured against a build of the previous commit, serving both locally and
reading the server access log (worker-initiated fetches do not show up in
page-level devtools events):

                        before (lunr)   after (Pagefind)
  on page load            855 KB gzip        46 KB gzip
  on first search               0 KB       378 KB gzip
  total, if searching     855 KB gzip       425 KB gzip

Readers who never search pay 95% less. Readers who do pay about half.

Implementation
--------------
`hooks/pagefind_index.py` runs the indexer over the rendered HTML in
`on_post_build`, adding ~2.2s to a ~31s build. Pagefind is distributed as
a PyPI wheel with a prebuilt binary, so it installs from requirements.txt
and needs no Node toolchain in CI.

Index scope is controlled by `data-pagefind-body` on the article element,
added via a `container` block override that mirrors the theme's own. Once
that attribute exists anywhere, Pagefind indexes only pages carrying it,
which keeps the 3,130 generated Dokka/Javadoc/TypeDoc files out of the
index. Those were never in the lunr index either, so search scope is
unchanged: 232 pages in, 232 pages indexed.

The UI is Pagefind's Component UI (`<pagefind-modal>`), which as of
Pagefind 1.5.0 supersedes the older Default UI. It is themed entirely
through Material's palette: the `--pf-*` variables are mapped onto
`--md-*` on `body` rather than `:root`, because Material declares its
light palette at `:root` but its dark palette on `[data-md-color-scheme]`
on `<body>`, so mapping at `:root` would stay light after the palette
toggle. Custom properties are exempt from Pagefind's `all: initial` host
reset, so dark mode needs no JavaScript.

The components sit in the header, outside every region Material's instant
navigation swaps, so they stay mounted across client-side navigation.

Index quality
-------------
Pagefind concatenates adjacent inline elements without a separator, so
sibling elements authored without whitespace fuse into one token. Three
constructs here hit that, and `--exclude-selectors` removes them:

  .headerlink           "¶" permalink anchors, fused into headings on
                        230 of 232 pages
  .tabbed-labels        `<label>Python</label><label>Java</label>` from
                        pymdownx.tabbed, indexed as "PythonJava"
  .language-support-tag the "Supported in ADK" badge, which produced
                        "ADKPythonTypeScriptGoJava"

Pages with fused language tokens drop from 109/232 to 5/232, and all five
remaining are genuine identifiers ("RxJava", "pdfArtifactJava"). Tab
contents stay indexed, so per-language content remains searchable.

`--include-characters ".-@#+"` keeps dotted and prefixed identifiers
intact; without it "google.adk.agents" degrades to three bare words.

Set PAGEFIND_SKIP=1 to skip indexing, e.g. for link-checking builds.
@haranrk

haranrk commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #2096

@haranrk haranrk closed this Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant