fix(search): relevance ranking, per-version de-dupe, product-scoped version filter#1227
Open
aidenhongnet wants to merge 8 commits into
Open
fix(search): relevance ranking, per-version de-dupe, product-scoped version filter#1227aidenhongnet wants to merge 8 commits into
aidenhongnet wants to merge 8 commits into
Conversation
The search page re-sorted every Algolia hit alphabetically by product name before paginating, so the top-ranked hit for a query could land pages deep while low-scoring hits from alphabetically-early products always filled page one. Product grouping is now an opt-in view (?group=1) that re-sorts the already-fetched hits client-side, with no extra Algolia request; the default is Algolia's relevance order, with a product/version badge on each hit so the context the grouped headings used to provide is still visible. Also fixed, each of which suppressed or distorted results: - Typo tolerance was disabled on both the search page and the modal, so any misspelling returned zero results. Now uses the index default. - distinct was missing on the search page helper though the modal already set it, letting one document appear as many near-duplicate hits. - advancedSyntax was enabled on the page only, so the page and modal answered the same query differently, and a query containing a CLI flag such as "Get-ADUser -Filter" silently excluded every doc containing "Filter". Removed, for parity with the modal. - The Algolia result listener was registered during render, adding a listener per re-render. Each stale listener fired with a stale resultsPerPage and sliced pages wrong. Now registered once in an effect, with cleanup. - The 300ms search debounce never cleared its timeout, so every keystroke fired a full 1000-hit query. - Version extraction treated the first path segment as a version for single-version products, yielding badges like "1Secure admin". - The robots meta used property= instead of name=, so crawlers ignored the noindex on the search results page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DocSearch groups hits by hierarchy.lvl0 and caps each group at
maxResultsPerGroup (default 5). The Algolia crawler sets lvl0 to
"Product > Version", so the preview bucketed results per product: a
later hit from an already-seen product was hoisted above higher-ranked
hits from other products, and anything past the fifth hit in a product
was dropped without trace. For "install agent" that meant 7 groups and
18 of 20 hits shown, in an order Algolia never returned.
Collapsing lvl0 to a single value leaves one group, so hits render in
the order Algolia ranked them, and maxResultsPerGroup now caps the whole
list rather than each product.
This is the crawler's `lvl0: {defaultValue: ...}` applied client-side.
The real fix is to stop putting the product in lvl0 in the crawler
config, after which this can be deleted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… preview Rework both search surfaces so results are ordered by Algolia's ranking score, and drop the machinery that made that expensive. Search page: - Replace the fetch-1000-hits-and-paginate-client-side design with Algolia server-side pagination (hitsPerPage = page size, setPage(n)). This cuts the per-search payload from ~5.4 MB to ~170 KB and deletes allItemsRef, the client-side slicing, and the re-slice effect. - Remove the opt-in "Group by product" toggle: the default is now Algolia's relevance order, and product filtering is still available via the product facet dropdowns. Deleting it is what lets pagination go server-side. - A restored or stale ?page=N that overshoots the result set recovers to the first page instead of rendering an empty, control-less dead end. Preview (Ctrl+K modal): - DocSearch buckets hits by hierarchy.lvl0 (which the crawler sets to "Product > Version") and, within that, by hierarchy.lvl1, then caps each group. That reordered hits away from relevance and dropped a product's hits past the cap. ungroup() collapses lvl0 to one section and makes lvl1 unique per hit so neither pass reorders; the title is fed back through _snippetResult (Algolia-escaped, highlighting preserved) and the product is shown per-hit. maxResultsPerGroup is raised so the single group isn't capped at five. This is a client-side shim around the crawler's lvl0; fixing lvl0 in the crawler config lets the whole shim be deleted. Also delete dead code the above surfaced: the config transformItems (never survived themeConfig serialization) and its now-unreachable branch, an unused clsx import, the unused SearchPage styles.module.css, and disjunctiveFacets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Algolia caps pagination at ~1000 hits (paginationLimitedTo) and nbHits is an estimate, so "13,477 documents found" implied more was reachable than the pages allow. When fewer results are reachable than match, show "the top N of M matches — refine your search" instead; otherwise show the exact count. This restores guidance that the server-side pagination change had dropped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…o products Same article no longer appears once per product version: both surfaces now collapse each fetched page to the newest version present (config order, not digit parsing — passwordreset 3.3 outranks legacy 3.23), keyed by the hit's URL minus fragment/version segment, with trailing-slash and standalone-KB shapes normalized. De-dupe turns off whenever a real version filter is active. The Versions filter now renders only while a specific product is selected and lists only that product's versions (shared getVersionsForProducts in the new src/theme/searchUtils.js replaces the two diverged copies). makeSearch and the URL writer enforce the same rule, so stale versions from bookmarked URLs or sessionStorage can never filter invisibly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The accessanalyzer-2601 pinned KB source publishes under its directory basename instead of the product id — as the standalone route segment and as the copied landing page's slug — so the KB landing still rendered one row per version. articleKey now normalizes pinned basenames (built from the config's kbSource overrides) to the product id, collapsing all five live shapes of the landing to the newest version's copy. Found by adversarial review against the live index; verified on the dev server: /search?q=Access Analyzer Knowledge Base renders one row (2601). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The full search page re-sorted every Algolia hit alphabetically by product before paginating, so the top-ranked result for a query could land pages deep while low-scoring hits from alphabetically-early products always filled page one. The Ctrl+K preview had the same problem from a different cause: DocSearch buckets hits by
hierarchy.lvl0, which the crawler sets to"Product > Version", so the preview grouped by product and dropped results past a per-group cap. Both surfaces hid Algolia's ranking.Two further problems on the same surfaces (second round of commits): the same article appeared once per product version — the index legitimately stores one record set per
(product, version)copy of a page, and both surfaces rendered them all — and the Versions filter rendered even with no product selected; on/searchit listed the union of all 27 products' versions, which is meaningless without a product.What changed
Search results page — now server-side, relevance-ordered.
hitsPerPage= page size,setPage(n)). Per-search payload dropped from ~4.7 MB to ~170 KB.?page=Nrecovers to the first page instead of rendering an empty, control-less dead end.Preview (Ctrl+K modal) — now relevance-ordered.
ungroup()collapses DocSearch's two grouping passes (hierarchy.lvl0andhierarchy.lvl1) so hits render in Algolia's order; the real title is fed back through_snippetResult(Algolia-escaped, highlighting preserved) and the product is shown per hit.lvl0. The root-cause fix is to setlvl0to a constant in the Algolia crawler config, after which the shim can be deleted — noted in-code.Version de-dupe — one row per article, newest version wins (both surfaces).
#fragmentminus its version path segment, with trailing-slash variants normalized, standalone/docs/kb/...pages rewritten to their versioned-copy shape, and version-pinned KB source basenames (kbSourceoverrides, e.g.accessanalyzer-2601) normalized to the product id.products.js(versions[0]is the uniqueisLatestfor all 27 products) — digit parsing is provably wrong here (passwordreset's latest3.3predates legacy hidden3.23numerically). Versions missing from config rank oldest.src/theme/searchUtils.jsholds the de-dupe and version-list logic; it replaces two diverged local copies ofgetVersionsForProducts.Version filter — only meaningful with a product selected (both surfaces).
versionsURL param, so stale bookmarked URLs or leftover sessionStorage can never filter invisibly while the panel is hidden.attributesToRetrieve(DocSearch replaces its defaults wholesale, so they are re-listed alongsideproduct_name/product_version— pinned with a re-verify note for future DocSearch bumps).Adjacent fixes on the same surfaces.
distinct: trueto the page helper (deduped near-duplicate hits; matches the modal).advancedSyntax(a query likeGet-ADUser -Filtersilently excluded docs containing "Filter").useEffectwith cleanup (was re-registering every render with stale closures); added a real debounce; fixed a<meta property="robots">→name="robots"typo.transformItems(never serialized), an unusedclsximport, an unused CSS module, anddisjunctiveFacets.Verification (de-dupe/filter commits)
All nine planned dev-server flows pass: modal and
/searchde-dupe, product-scoped version lists, version selection restoring per-version rows, staleversions=URL scrubbing with de-dupe intact, modal→page filter handoff, and identitymanager (currentwins), passwordreset (3.3beats legacy3.23), and KB standalone/versioned merge spot-checks. A standalone logic suite exercises the de-dupe key against the realproducts.js. An adversarial multi-agent review of the diff confirmed one finding (the pinned-KB landing slug divergence), which is fixed in the second commit.Known follow-ups (not in this PR)
/docs/kb/{general,recoveryad,tags}/have emptyproduct_namein the index and badge as "Documentation" — a crawler/index gap.ungroupshim depends on DocSearch internals; a future@docsearch/reactmajor upgrade could silently revert the preview to product order (documented in-code with a re-check note). The crawlerlvl0fix removes this dependency entirely.docs/kb/recoveryadstandalone set, which backs two products and has no unambiguous merge target.nbHits/banner counts still count all versions (fixing that needs global knowledge of the result set; de-dupe is per fetched page by design).🤖 Generated with Claude Code