perf(developer-hub): cap and rank docs search results (4.6x faster) - #3931
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
5 Skipped Deployments
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e742c274ef
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "generate:docs", | ||
| "generate:changelog", | ||
| "generate:price-feeds" | ||
| ], |
There was a problem hiding this comment.
Keep
.source in cached build outputs
Removing this override makes the task inherit the root outputs, which include .next/** but not .source/**. On a clean worker where build is restored from Turbo's cache, .source/server is therefore not restored even though src/lib/source.ts imports it; the subsequent test:types task can fail without running Next/Fumadocs to regenerate it. Retain .source/** in this package's effective build outputs.
AGENTS.md reference: apps/developer-hub/AGENTS.md:L19-L21
Useful? React with 👍 / 👎.
| const locale = params.get("locale"); | ||
|
|
||
| const highlighter = createContentHighlighter(query); | ||
| const exact: SortedResult[] = directFeedMatches(query).map((index) => ({ |
There was a problem hiding this comment.
Apply tag filtering to direct feed matches
When callers provide a tag filter, the direct-match pass ignores it while only the Orama search receives the filter below. For example, ?query=BTC&tag=price-feed-core can still prepend up to six Pro results, whereas the previous createSearchAPI handler restricted every result to the requested tag. Filter these candidates using their existing index.tag before combining them with the server results.
Useful? React with 👍 / 👎.
| if (result.url.startsWith(CORE_FEED_PATH)) return "core"; | ||
| if (result.url.startsWith(PRO_FEED_PATH)) return "pro"; | ||
| return "docs"; | ||
| } | ||
|
|
There was a problem hiding this comment.
🟡 Two key documentation pages are counted as price-feed results, shrinking their slot share
The two feed-listing documentation pages are classified as price-feed entries (familyOf at apps/developer-hub/src/app/api/search/route.ts:209-213) because each page's address is a prefix of the corresponding feed links, so they draw from the small per-family feed quota instead of the larger documentation quota.
Impact: For some searches, these important documentation pages — or real feed rows — can be squeezed out of the results.
Prefix collision between doc page URLs and feed URLs
CORE_FEED_PATH is /price-feeds/core/price-feeds/price-feed-ids and PRO_FEED_PATH is /price-feeds/pro/price-feed-ids (route.ts:16-17). Feed rows use ${CORE_FEED_PATH}?search=SYMBOL / ${PRO_FEED_PATH}?search=SYMBOL (route.ts:45, route.ts:56). But the real docs pages content/docs/price-feeds/core/price-feeds/price-feed-ids.mdx and content/docs/price-feeds/pro/price-feed-ids.mdx produce Orama results whose url is exactly CORE_FEED_PATH/PRO_FEED_PATH (and heading/text sub-results like ${CORE_FEED_PATH}#...). result.url.startsWith(CORE_FEED_PATH) returns true for all of these, so familyOf buckets them as core/pro. They then consume the 6-slot feed quotas rather than the 12-slot docs quota in applyQuotas (route.ts:220-237), so a query that surfaces these docs plus 6+ real feeds can drop either the docs page or actual feeds. Matching against ${CORE_FEED_PATH}? avoids the collision.
| if (result.url.startsWith(CORE_FEED_PATH)) return "core"; | |
| if (result.url.startsWith(PRO_FEED_PATH)) return "pro"; | |
| return "docs"; | |
| } | |
| function familyOf(result: SortedResult): Family { | |
| if (result.url.startsWith(`${CORE_FEED_PATH}?`)) return "core"; | |
| if (result.url.startsWith(`${PRO_FEED_PATH}?`)) return "pro"; | |
| return "docs"; | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
| "outputs": [ | ||
| ".next/**", | ||
| "!.next/cache/**", | ||
| ".source/**" | ||
| ], |
There was a problem hiding this comment.
🟡 Build cache stops saving generated docs content, which can break type and lint checks
The generated docs content directory is dropped from what the build step saves to its cache (removing the outputs override at apps/developer-hub/turbo.json:11-15), even though the inherited default outputs do not list that directory, so a cache-restored build is missing files the type-check and lint steps read.
Impact: On a build cache hit, the generated content is absent, causing intermittent type-check/lint failures in CI or dev.
Inherited outputs do not include .source/**
The previous developer-hub build task overrode outputs with [".next/**", "!.next/cache/**", ".source/**"]. Removing the override makes the task inherit the root build outputs ["lib/**", "dist/**", ".next/**", "!.next/cache/**"] (turbo.json:68), which do NOT contain .source/**. In Turborepo, extends replaces the outputs array rather than merging it. .source/ is gitignored (apps/developer-hub/.gitignore) and generated by next build via fumadocs-mdx/next (next.config.js). src/lib/source.ts imports ../../.source/server, so test:types (tsc) and test:lint:eslint, which both dependsOn build, need .source/ present. When build is a cache hit (restored, not executed) without .source/** in outputs, .source/ is not restored and those checks fail to resolve ../../.source/server. The PR's justification that the override "restated what the task already inherits" is inaccurate.
Prompt for agents
The developer-hub build task in apps/developer-hub/turbo.json previously overrode outputs with [".next/**", "!.next/cache/**", ".source/**"]. This PR removed that override so the task now inherits the root build outputs ["lib/**", "dist/**", ".next/**", "!.next/cache/**"] (see turbo.json:68). Turborepo replaces (does not merge) the outputs array on extends, so the inherited outputs do NOT include .source/**. The .source/ directory is gitignored and generated by next build via fumadocs-mdx/next, and src/lib/source.ts imports ../../.source/server, which tsc (test:types) and eslint (test:lint) require. Without .source/** in the build task outputs, a cache-restored build omits .source/, breaking type-check/lint on cache hits. Restore .source/** to the build task's outputs (e.g. re-add an outputs override that includes .next/**, !.next/cache/**, and .source/**), while still dropping only the truly redundant duplicate-key concern the PR describes.
Was this helpful? React with 👍 or 👎 to provide feedback.
| ...(tag === null ? {} : { tag: tag.split(",") }), | ||
| ...(locale === null ? {} : { locale }), | ||
| }); |
There was a problem hiding this comment.
🔍 tag param passed as array to server.search
server.search(query, { tag: tag.split(",") }) (apps/developer-hub/src/app/api/search/route.ts:257) passes an array where the previous createSearchAPI flow passed the raw tag string. This relies on fumadocs-core 16.4.7's advanced search accepting string[] for tag. The PR claims tsc is clean, which suggests the type accepts arrays, but behavior of multi-tag filtering (AND vs OR, single-element arrays) should be confirmed against the installed version since node_modules was not available to verify.
Was this helpful? React with 👍 or 👎 to provide feedback.
e742c27 to
8457d62
Compare
The search route returned every match with no cap: "e" produced 16,802
results / 8.1 MB and pasting a feed symbol took 4.66s against production.
fumadocs groups Orama hits by page_id with no limit on group count, and
every one of the ~9.4k feeds is its own group, so any common token
("usd", "price", "e") matched thousands of them. Note that Orama's
`limit` does not bound group count, so the cap has to be applied in the
route handler.
- Cap responses at 24 results with per-family quotas (12 docs / 6 Core /
6 Pro, unused slots backfilled). Without quotas "USD" pushed the first
documentation hit to rank 624 and "solana" surfaced no feeds at all.
- Drop `structuredData.contents` from feed entries. fumadocs already
indexes `title` and `description` as their own documents, so those
entries duplicated text at 2-3x the document count. Symbol, name,
description and feed ID all remain searchable.
- Deduplicate Core feeds by symbol. hermes-beta republishes 99.4% of the
mainnet symbol list (2,873 of 2,890) into rows with identical titles
and identical URLs, i.e. literal duplicate entries in the dialog.
Mainnet wins ties; the 17 beta-only symbols stay searchable.
- Add a deterministic exact-match pass over feed symbols and names.
BM25 over the remaining documents does not favour the feed a user
typed: searching "Crypto.BTC/USD" returned 24 results without that
feed among them.
- Apply the biome fixes CI misses (`biome ci --changed` reports
"Checked 0 files" on this repo's PR runs).
Measured over 6 interleaved A/B rounds of local production builds
(15-query set, 9 iterations each): 542.9ms -> 117.5ms median total
latency (4.62x) and 38.57 MB -> 0.12 MB total payload. Worst pairing of
fastest-baseline against slowest-optimized round is 3.23x. Loopback
makes payload nearly free, so this understates the production gain.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8457d62 to
ec0063c
Compare
Stacked on #3917 (base is that PR's branch, so this diff shows only the search work). Retarget to
mainonce #3917 lands.Problem
The docs search route returned every match with no cap. Measured against production:
BTCUSDeEquity.US.RIVN/USD.ONThat last row is a user pasting a feed symbol. The dialog debounces at 100ms, so typing it fires several of those in a row.
Cause:
searchAdvancedcalls Orama withgroupBy: { properties: ["page_id"], maxResult: 8 }and no limit on group count. Every one of the ~9.4k feeds is its own group, so any common token (usd,price,e) matches thousands of them.Worth knowing for anyone else touching this:
search: { limit: N }does nothing here. Orama'slimitbounds hits, not groups — I verified the output is byte-identical with and without it. The cap has to be applied in the route handler.Changes
USDthe first documentation hit sat at rank 624, and a naive top-24 slice madesolanareturn no feeds at all.structuredData.contentsfrom feeds. fumadocs indexestitleanddescriptionas their own documents, so those entries duplicated text already covered, at 2-3x the document count. Index build 660ms → 260ms. Symbol, name, description and feed ID all stay searchable.Crypto.BTC/USDreturned 24 results without that feed among them, because BM25 favours common tokens across thousands of near-identical symbols. Feeds are now matched directly by symbol/name tokens before Orama fills the rest.outputsoverride on the build task. It restated what the task already inherits, and merging it intomainproduced a duplicateoutputskey in the same object (git merge-treereproduces it — auto-merges clean, invalid result).//:test:lintlogsChecked 0 fileson PR runs, sobiome ci --changednever inspected these files.Results
6 interleaved A/B rounds against local production builds, 15-query set, 9 iterations per query. Interleaved so machine drift cannot bias one side.
4.62x median. Per-round: 4.75x, 4.27x, 3.23x, 4.44x, 4.86x, 5.32x. Pairing the fastest baseline round against the slowest optimized round still gives 3.23x. Worst single query went 102.9ms → 21.6ms.
Loopback makes an 8 MB response nearly free, so this understates the production gain — over the wire the payload reduction is the dominant term.
Verification
BTC+ETH+USDeach returning both families, docs still reachable on prose queries, no duplicate feed rows.pnpm turbo run build --filter @pythnetwork/developer-hubpasses on this branch's base.tscclean,biome checkclean on all changed files.Not addressed here
Two items from the review of #3917 are still open and belong with that PR: an upstream outage at build time silently ships an empty feed index (the "previous snapshot" fallback cannot work on Vercel, since
src/generated/is gitignored andcache: false), and there is no refresh path — no cron or ISR — so new feeds only appear on the next deploy. Lazer added 64 feeds in the 5 days after #3917 was opened.🤖 Generated with Claude Code