From 71f93bb5c1017a33a7469f11d528e99671eb114c Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Fri, 17 Jul 2026 15:53:10 +0100 Subject: [PATCH 1/2] Strip routed framework/version tokens from the search query text When a query names a framework/version, we route on it (framework filter in transformItems, version facet rewrite) but previously left the token in the query text sent to Algolia. That platform word skewed textual relevance - e.g. "barcode capture flutter" lifted /sdks/flutter/agent-skills/ (which lists frameworks) above /sdks/flutter/barcode-capture/get-started/. Strip the detected framework tokens (and the version marker when it routes) from the query text only. Routing is unchanged - it's still derived from the original query (latestQueryRef and versionTagInQuery both read the original); only the text Algolia scores against differs, so the product's main guide surfaces instead of a framework-listing page. Keep the original query if the strip would empty it (e.g. a lone "ios"). Verified before/after on the framework/version query set: get-started and product intro pages now rank first where framework-listing / release-notes pages did before. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/theme/SearchBar/index.js | 49 +++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/src/theme/SearchBar/index.js b/src/theme/SearchBar/index.js index 085788e7..6c80f0b2 100644 --- a/src/theme/SearchBar/index.js +++ b/src/theme/SearchBar/index.js @@ -112,6 +112,21 @@ function rewriteVersionTag(facetFilters, targetTag) { }; return swap(facetFilters); } +// Remove the framework tokens (and, when it actually routes, the version marker) +// from the query TEXT sent to Algolia. Routing is unaffected - it's driven by the +// original query (framework filter in transformItems, version facet rewrite) - +// this only stops the platform word from skewing textual relevance (e.g. lifting +// framework-listing pages above the product's get-started guide). +function stripRoutedTokens(query, stripVersion) { + let q = ` ${query || ""} `; + for (const { re } of QUERY_FRAMEWORK_TOKENS) { + q = q.replace(new RegExp(re.source, "gi"), " "); + } + if (stripVersion) { + q = q.replace(/\b(?:version|ver|v|sdk)\s*\.?\s*\d+\b/gi, " "); + } + return q.replace(/\s+/g, " ").trim(); +} function Hit({ hit, children }) { // Mouse clicks navigate through this Link directly and never reach the // modal's navigator (which only handles keyboard selection), so capture @@ -397,19 +412,35 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { const query = first && ((first.params && first.params.query) || first.query); // Record the query so transformItems can route by a typed framework. + // (Original query - routing is always derived from this.) latestQueryRef.current = query || ""; - // A version typed in the query overrides the page's version: swap the - // contextual docusaurus_tag filter to the newest tag for that major. + // A version typed in the query overrides the page's version (swap the + // docusaurus_tag facet). The framework/version tokens are also stripped + // from the query TEXT so the platform word doesn't skew relevance. Both + // are computed from the ORIGINAL query, so routing is unchanged; only + // the text Algolia scores against differs. Keep the original if the + // strip would empty the query. const targetTag = versionTagInQuery(query, versionTagByMajor); + const strippedRaw = stripRoutedTokens(query || "", !!targetTag); + const strippedQuery = + strippedRaw && strippedRaw !== (query || "").trim() + ? strippedRaw + : null; const effectiveRequests = - targetTag && Array.isArray(requests) + (targetTag || strippedQuery) && Array.isArray(requests) ? requests.map((r) => { - const ff = r.params ? r.params.facetFilters : r.facetFilters; - if (!ff) return r; - const rewritten = rewriteVersionTag(ff, targetTag); - return r.params - ? { ...r, params: { ...r.params, facetFilters: rewritten } } - : { ...r, facetFilters: rewritten }; + const p = r.params || r; + const params = { ...p }; + if (strippedQuery && typeof params.query === "string") { + params.query = strippedQuery; + } + if (targetTag && params.facetFilters) { + params.facetFilters = rewriteVersionTag( + params.facetFilters, + targetTag + ); + } + return r.params ? { ...r, params } : params; }) : requests; const resultPromise = originalSearch(effectiveRequests); From 973fc16b8732cac51b027e92221ba38e31ce6b56 Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Wed, 22 Jul 2026 12:30:28 +0100 Subject: [PATCH 2/2] Retry exact "Class.Member" API queries with the member stripped on zero results Exact enum-member queries like "rectangularviewfinderstyle.legacy" return zero because the member has no page of its own - it's documented on the parent symbol's page. Add stripTrailingMember() and, in the search client wrapper, retry once with the trailing ".member" removed when the full query yields zero hits, adopting the retry only when it actually finds results. Fires only on zero-result single-token dotted queries, so normal searches are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/theme/SearchBar/index.js | 58 ++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/theme/SearchBar/index.js b/src/theme/SearchBar/index.js index 6c80f0b2..150baa34 100644 --- a/src/theme/SearchBar/index.js +++ b/src/theme/SearchBar/index.js @@ -127,6 +127,21 @@ function stripRoutedTokens(query, stripVersion) { } return q.replace(/\s+/g, " ").trim(); } +// An exact single-token "Class.Member" API query (e.g. +// "rectangularviewfinderstyle.legacy", "scanintention.smart") has no page of +// its own - enum members/constants are documented on their parent symbol's +// page. Strip the trailing ".member" so a zero-result exact query can retry +// against the parent. Only single whitespace-free tokens with a dot qualify, so +// natural-language queries and product paths are left untouched. Returns null +// when there's nothing safe to strip. +function stripTrailingMember(query) { + const q = (query || "").trim(); + if (!q || /\s/.test(q)) return null; + const m = q.match(/^(.+)\.[A-Za-z0-9_]+$/); + if (!m) return null; + const base = m[1]; + return base.length >= 3 && base !== q ? base : null; +} function Hit({ hit, children }) { // Mouse clicks navigate through this Link directly and never reach the // modal's navigator (which only handles keyboard selection), so capture @@ -426,13 +441,16 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { strippedRaw && strippedRaw !== (query || "").trim() ? strippedRaw : null; - const effectiveRequests = - (targetTag || strippedQuery) && Array.isArray(requests) + // Build a request array with an optional query-text override (routed + // framework/version tokens stripped, or the enum-member fallback below) + // and the version-facet swap. Reused for the primary search and retry. + const buildRequests = (queryOverride) => + (queryOverride != null || targetTag) && Array.isArray(requests) ? requests.map((r) => { const p = r.params || r; const params = { ...p }; - if (strippedQuery && typeof params.query === "string") { - params.query = strippedQuery; + if (queryOverride != null && typeof params.query === "string") { + params.query = queryOverride; } if (targetTag && params.facetFilters) { params.facetFilters = rewriteVersionTag( @@ -443,17 +461,31 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { return r.params ? { ...r, params } : params; }) : requests; - const resultPromise = originalSearch(effectiveRequests); + const nbHitsOf = (response) => + response && response.results && response.results[0] + ? response.results[0].nbHits + : undefined; + // Enum-member fallback: when an exact "Class.Member" query returns zero + // (the member has no page of its own), retry once with the trailing + // ".member" stripped so the parent symbol's page is found. Fires only on + // zero results and only adopts the retry when it actually finds hits, so + // normal queries are untouched. + const resultPromise = originalSearch(buildRequests(strippedQuery)).then( + (response) => { + if (nbHitsOf(response) !== 0) return response; + const base = strippedQuery || query || ""; + const memberStripped = stripTrailingMember(base); + if (!memberStripped || memberStripped === base) return response; + return originalSearch(buildRequests(memberStripped)).then((retry) => + nbHitsOf(retry) > 0 ? retry : response + ); + } + ); if (query) { resultPromise - .then((response) => { - const nbHits = - response && - response.results && - response.results[0] && - response.results[0].nbHits; - captureSearchDebounced(query, nbHits); - }) + .then((response) => + captureSearchDebounced(query, nbHitsOf(response)) + ) .catch(() => {}); } return resultPromise;