Narrow component search to each project's latest scan (Q2) - #218
Merged
Conversation
Component search (both the palette and the full results page) joined scan_components through every scan a project had ever run instead of just its current one, so search cost grew with retained scan history (up to 30x) rather than catalog size. Both now resolve through services.scan_resolution.latest_succeeded_scan_select, the same helper vulnerability/licence search already used, closing that asymmetry. Contract change: a component that only ever appeared in an older, superseded scan no longer surfaces in search. Pinned with real captured CycloneDX scan-history fixtures (two disjoint SBOMs across two scans of one project) plus M3's miniature EXPLAIN-plan test, whose growth assertion now asserts flatness instead. Also updates the inventory page's now-dead "search every scan" escape hatch (search reads the same latest-scan scope inventory already did) and the EN/KO user docs for both surfaces.
Q2 added a scan-id resolution query as statement 0 ahead of components search's COUNT/page/facet queries, shifting every later index by one. The EXPLAIN-capture tests still pointed at index=1 expecting the page SELECT, which now lands on COUNT instead — CI's fresh database exposed the mismatch (local runs happened to pass by coincidence). Bumped to index=2 in both the PR-gate miniature test and the nightly heavy baseline helper.
The EXPLAIN-based physical row count (total_actual_rows on the scan_components relation) was fragile against the CI integration suite's large shared database: at that scale Postgres can choose a plan for the marker's highly-selective ILIKE where scan_components never surfaces as a named EXPLAIN node, even though the join still correctly touches it (the real search call logged the correct total=1 both times in the failing run, so the search itself was never wrong, only the plan introspection). Reasserts the same property against the search RESULT instead, which is also what the plan's own regression contract names and is insensitive to which access path the planner picks.
Neither component search surface had a test for a team with no succeeded scan anywhere, so the empty-scan-id short-circuit each _components/ _search_components takes before building the main join was unexercised. Coverage gate flagged both lines on the PR.
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.
Summary
/v1/searchpalette and/v1/search/results?kind=components) joinedscan_componentsthrough every scan a project had ever run instead of just its current one, so search cost grew with retained scan history (up to 30x under the scan-series retention policy) rather than catalog size. Both surfaces now resolve throughservices.scan_resolution.latest_succeeded_scan_select, the same helper vulnerability/licence search already used.search_service.py) to also coversearch_results_service.py's component search, matching the precedent set by Q1: the results-page surface had the identical defect, and M3's own miniature regression test (test_search_query_plan_contracts.py) measures the results-page query, so leaving it unfixed would have made that test's "flat" assertion false./search, which now reads the identical latest-scan-only scope inventory already used, so the link would always land on another empty result.docs-site/docs/user-guide/search.md,inventory.md+ KO mirrors);SearchPage.tsx's on-screen scope indicator flips the Components tab from "all scans" to "current scan".Test plan
tests/fixtures/sbom_ingest/realistic.cdx.json+centos7-rpm-no-os.cdx.json) via the shippedpersist_sbom_components, asserting an old-scan-only component drops out and a latest-scan component stays found (test_search_api.py,test_search_results_api.py).test_search_query_plan_contracts.py::test_scan_components_rows_stay_flat_across_scan_history), per the note M3 left for this exact landing.test_search_explain_load_baseline.py) updated to compare the shipped latest-only query against a rebuilt legacy all-history variant (was skip-by-default locally, no load-test seed).ruff check,mypy .(773 files), backendtests/unit+tests/integrationgreen locally (10 pre-existing failures reproduce in isolation and are this laptop's disk-usage guard tripping at 95%+, unrelated to these files).typecheck,vitest run(2491 passed),lint(0 errors),i18n:checkgreen.node tools/ko-style/lint.mjs --all --fail-on S2: 0 findings.node tools/em-dash/lint.mjs --base origin/main: clean.