Speed up search by ranking from a title map - #3408
Conversation
|
Pull request environment is available at https://stoctodocspr3408.z22.web.core.windows.net. You can view the ephemeral environment status in Octopus Deploy. This environment will be automatically deprovisioned when the pull request is closed, or after 7 days of inactivity. |
Ranking needed a url and a title, and Pagefind keeps both in the per-page fragment — so ordering thirty results meant fetching thirty files before the panel could draw, and a page ranked past thirty could not be reached at all. The build now writes a map of result id to url and title beside the index, which is the join Pagefind's own result stub already carries. So the whole result set is ranked before anything is fetched, and fragments are fetched only for the rows being drawn: ten per batch, against thirty to thirty-five for every settled query before. On Slow 4G with the map served uncompressed, first results arrive in 7.2s against 8.9s; the map is 28 KB gzipped, so most of that 122 KB is transfer a CDN removes. The shallow-page search this replaces is gone with it — the second Pagefind query, the landing filter, LANDING_DEPTH and the attribute it needed on every page's content div. A page the query names now wins from anywhere in the list rather than from a shortlist of 227. Relevance holds on both traffic-weighted sets: real-searches 57% w-S@1 and 84% w-S@5, top-pages 90% and 98%, unchanged either side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
176d4d7 to
399b5a1
Compare
Two things found reviewing the branch before publishing it. The fallback for a missing or mismatched title map drew results in Pagefind's own order and never ranked them, while its comment claimed otherwise. That is the order the reorder exists to correct — a bare BM25 list puts a getting-started page above the section it belongs to. `draw` now orders each batch as it draws it when the list reached it unranked, which is what the engine did before the map existed, and `more()` carries the same flag so later batches match. Verified by blocking the map: three of four sample queries still land on the right page, and the fourth is one whose answer ranks past the batch. A map that matches the index only partly dropped the unmatched results quietly, because the fallback fires only when nothing joins at all. It now warns with the counts. Also removed a stale doc comment left on the Ranked type. Relevance unchanged: real-searches holds at 57% w-S@1 and 84% w-S@5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An unhashed name lets a cache serve one build's map against another build's index. Pagefind hashes its own chunks for that reason, and the map has to be versioned with them: read against the wrong index it joins against nothing. The build now reads the hash out of pagefind-entry.json and writes docs-titles.<hash>.json; the client reads the same file to build the same name. Two requests where there was one, both while the index is warming and neither on the path of a search. It also means the map can carry an immutable cache header, which an unhashed name could not. The build fails if the index has more than one language, because one map cannot carry two hashes and the client would look under a name that does not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
efad2a1 to
3cf4285
Compare
Front Door caches `.js` for a week as immutable and compressed, and gives anything it does not recognise `no-cache`. It also strips ETags, so revalidating is a full re-download — and the map loads on every page that opens the overlay. A `.json` name meant paying 29 KB per page view. The map is now `docs-titles.<hash>.js`, an `export default`, loaded with the same dynamic import the engine already uses for `pagefind.js`. It lands in the existing static-content rule with no infrastructure change, and the hash in the name is what makes a week of immutable caching safe. `pagefind-entry.json` stays on the default no-cache rule, which is correct: it changes every build and the client reads the hash out of it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
borland
left a comment
There was a problem hiding this comment.
While this is technically very amazing, and I can't fault the improved outcome, we're essentially writing code that should be private inside pagefind. The fact that we have to unzip the compressed pagefind fragments, understand their structure, and more makes this incredibly brittle. If pagefind vNext moves things even slightly, it's all going to blow up.
That's an awful lot of complexity and risk to be handing over to the web team; I'm not comfortable with it.
How about, leave pagefind alone, but instead of loading 30 rows in the search dialog, load 20. That will get us some of the improvement, and I'd guess people probably don't read beyond the first 20 anyway
The problem
🤖 AI Generated PR 🤖
Putting results in order needs each result's URL and title. Pagefind keeps both inside that result's own file, one file per result.
That tied two things together: how many results we could order was the same number as how many files we downloaded. We downloaded 30 files on every search to order 30 results, before a single row could be drawn.
The change
The build writes one map of result id → URL and title, beside the index.
Ordering reads the map, so it can order every match without downloading anything. Result files are then downloaded only for the rows actually on screen.
Why the map, and not just fewer downloads
Cutting downloads from 30 to 10 is the obvious fix. On its own it drops top-five accuracy from 84% to 71%, because only the results you download can be ordered.
The map breaks that link. Ordering reads the map, so every match is ordered however few files come down. This PR downloads 10 and holds 84%.
What does not change
Matching. Pagefind's word index is untouched, so the full body text of every page is searched exactly as before — a phrase from the middle of a page still finds it. The map holds only a URL and a title per page, and is read after matching to decide what order the matches go in.
Effects
The map is 29 KB gzipped, loaded once per page. The first screen draws 10 rows where it drew 30, so 6 of 155 measured queries need one scroll to reach their answer — none ranked any lower.