Skip to content

perf(developer-hub): cap and rank docs search results (4.6x faster) - #3931

Merged
aditya520 merged 1 commit into
hydra/i-uhuwizmn/headfrom
perf/search-relevance
Jul 27, 2026
Merged

perf(developer-hub): cap and rank docs search results (4.6x faster)#3931
aditya520 merged 1 commit into
hydra/i-uhuwizmn/headfrom
perf/search-relevance

Conversation

@aditya520

@aditya520 aditya520 commented Jul 26, 2026

Copy link
Copy Markdown
Member

Stacked on #3917 (base is that PR's branch, so this diff shows only the search work). Retarget to main once #3917 lands.

Problem

The docs search route returned every match with no cap. Measured against production:

query time payload results
BTC 0.10s 67 KB 179
USD 0.50s 5.2 MB 15,769
e 0.55s 8.1 MB 16,802
Equity.US.RIVN/USD.ON 4.66s 6.2 MB 23,494

That 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: searchAdvanced calls Orama with groupBy: { 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's limit bounds 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

  • Cap at 24 results with per-family quotas (12 docs / 6 Core / 6 Pro, unused slots backfilled). Quotas are load-bearing, not cosmetic: for USD the first documentation hit sat at rank 624, and a naive top-24 slice made solana return no feeds at all.
  • Drop structuredData.contents from feeds. fumadocs indexes title and description as 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.
  • 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 — literal duplicate entries in the dialog. Mainnet wins ties; the 17 beta-only symbols stay searchable.
  • Add a deterministic exact-match pass. Capping exposed a latent relevance bug: searching the exact symbol Crypto.BTC/USD returned 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.
  • Drop the outputs override on the build task. It restated what the task already inherits, and merging it into main produced a duplicate outputs key in the same object (git merge-tree reproduces it — auto-merges clean, invalid result).
  • Apply the biome fixes CI misses. //:test:lint logs Checked 0 files on PR runs, so biome ci --changed never 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.

baseline this PR
median total latency 542.9ms 117.5ms
mean 539.4ms 122.0ms
total payload 38.57 MB 0.12 MB

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

  • 20/20 correctness checks: Core by symbol / bare ticker / 64-char feed ID, Pro by name / symbol / numeric Lazer ID / description word, BTC+ETH+USD each returning both families, docs still reachable on prose queries, no duplicate feed rows.
  • pnpm turbo run build --filter @pythnetwork/developer-hub passes on this branch's base.
  • tsc clean, biome check clean 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 and cache: 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


Open in Devin Review

@aditya520
aditya520 requested a review from a team as a code owner July 26, 2026 17:05
@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
component-library Ready Ready Preview, Comment Jul 26, 2026 5:43pm
developer-hub Ready Ready Preview, Comment Jul 26, 2026 5:43pm
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
api-reference Skipped Skipped Jul 26, 2026 5:43pm
entropy-explorer Skipped Skipped Jul 26, 2026 5:43pm
insights Skipped Skipped Jul 26, 2026 5:43pm
proposals Skipped Skipped Jul 26, 2026 5:43pm
staking Skipped Skipped Jul 26, 2026 5:43pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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"
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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) => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Open in Devin Review

Comment on lines +209 to +213
if (result.url.startsWith(CORE_FEED_PATH)) return "core";
if (result.url.startsWith(PRO_FEED_PATH)) return "pro";
return "docs";
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
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";
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread apps/developer-hub/turbo.json Outdated
Comment on lines -11 to -15
"outputs": [
".next/**",
"!.next/cache/**",
".source/**"
],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +257 to +259
...(tag === null ? {} : { tag: tag.split(",") }),
...(locale === null ? {} : { locale }),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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>
@aditya520
aditya520 force-pushed the perf/search-relevance branch from 8457d62 to ec0063c Compare July 26, 2026 17:41
@vercel
vercel Bot temporarily deployed to Preview – proposals July 26, 2026 17:41 Inactive
@vercel
vercel Bot temporarily deployed to Preview – entropy-explorer July 26, 2026 17:41 Inactive
@vercel
vercel Bot temporarily deployed to Preview – insights July 26, 2026 17:41 Inactive
@vercel
vercel Bot temporarily deployed to Preview – staking July 26, 2026 17:41 Inactive
@vercel
vercel Bot temporarily deployed to Preview – api-reference July 26, 2026 17:41 Inactive
@aditya520
aditya520 merged commit ec0063c into main Jul 27, 2026
13 checks passed
@aditya520
aditya520 deleted the perf/search-relevance branch July 27, 2026 13:14
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