Skip to content

feat(search): surface Zoekt non-exhaustive results in UI and logs (#504)#1098

Open
h30s wants to merge 2 commits intosourcebot-dev:mainfrom
h30s:feat/search-zoekt-limit-ux
Open

feat(search): surface Zoekt non-exhaustive results in UI and logs (#504)#1098
h30s wants to merge 2 commits intosourcebot-dev:mainfrom
h30s:feat/search-zoekt-limit-ux

Conversation

@h30s
Copy link
Copy Markdown
Contributor

@h30s h30s commented Apr 8, 2026

  • Alert on search page when results are non-exhaustive, with copy from Zoekt stats (skipped shards, flush reason, match budget, skipped files).
  • Log non-exhaustive completions from Zoekt search/stream.
  • Persist search stats in streamed-search cache; add unit tests for limit messaging.

Closes #504

Screenshot 2026-04-08 143545

Summary by CodeRabbit

  • New Features

    • Search results now show an informational alert banner when a query may be incomplete (e.g., due to match limits, skipped data, or streaming/early flush), displayed above results with a concise summary and optional detail.
  • Tests

    • Added tests covering various search-limit and early-stop scenarios to ensure accurate user-facing explanations.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

Walkthrough

Adds user-facing explanations when searches are incomplete by propagating search stats through the streaming hook, computing limit explanations, logging non-exhaustive Zoekt searches, and conditionally rendering an alert in the search results UI.

Changes

Cohort / File(s) Summary
Limit explanation utility & tests
packages/web/src/features/search/searchLimitExplanation.ts, packages/web/src/features/search/searchLimitExplanation.test.ts
New exported getSearchLimitExplanation(stats, maxMatchDisplayCount) producing summary/detail messages for non-exhaustive scenarios; comprehensive unit tests for various stats conditions.
Search results UI
packages/web/src/app/(app)/search/components/searchResultsPage.tsx
Passes maxMatchDisplayCount into PanelGroup; memoizes and conditionally renders an amber Alert with explanation when search is incomplete. PanelGroup props updated to accept maxMatchDisplayCount.
Streaming hook / stats propagation
packages/web/src/app/(app)/search/useStreamedSearch.ts
Cache and hook state extended with optional stats?: SearchStats; cache reads/writes and streaming finalization now persist stats into cached entries and state.
Search feature exports
packages/web/src/features/search/index.ts
Re-exports getSearchLimitExplanation from the search feature barrel.
Zoekt searcher logging
packages/web/src/features/search/zoektSearcher.ts
Computes isSearchExhaustive once and logs info when searches are non-exhaustive, including totalMatchCount, actualMatchCount, flushReason, shardsSkipped, and filesSkipped.
Changelog
CHANGELOG.md
Notes added describing the new informational alert for potentially incomplete queries and non-exhaustive Zoekt logging.

Sequence Diagram(s)

sequenceDiagram
  participant UI as SearchResultsPage (UI)
  participant Hook as useStreamedSearch (hook/cache)
  participant Zoekt as zoektSearcher (backend)

  UI->>Hook: initiate search request
  Hook->>Zoekt: stream search request
  Zoekt-->>Hook: incremental stream responses (hits / partial stats)
  Hook->>Hook: update cached entry with partial results
  Zoekt-->>Hook: final response with accumulated stats
  Hook->>Hook: persist stats into cache/state
  Hook-->>UI: deliver final results + stats
  UI->>UI: compute getSearchLimitExplanation(stats, maxMatchDisplayCount)
  alt explanation present and not streaming
    UI->>UI: render amber Alert with summary/detail above results
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • brendan-kellam
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically describes the main change: surfacing Zoekt non-exhaustive results in UI and logs, which aligns with the PR's core objective of implementing UX and logging for non-exhaustive search results.
Linked Issues check ✅ Passed All coding requirements from issue #504 are met: displaying UI alerts for non-exhaustive results with specific reasons (skipped shards, flush reason, match limits), logging non-exhaustive completions, and persisting search stats. Tests are included for the limit messaging logic.
Out of Scope Changes check ✅ Passed All changes directly support the core objective of communicating non-exhaustive search results. No extraneous refactoring, unrelated feature additions, or scope creep detected in the modified files.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/web/src/app/(app)/search/useStreamedSearch.ts (1)

110-119: ⚠️ Potential issue | 🟡 Minor

Missing stats reset when starting a new search.

When initiating a new search (not from cache), the state is reset but stats is not explicitly set to undefined. This could potentially leave stale stats from a previous search visible until the new search completes.

🛠️ Proposed fix
             setState({
                 isStreaming: true,
                 isExhaustive: false,
                 error: null,
                 files: [],
                 repoInfo: {},
                 timeToSearchCompletionMs: 0,
                 timeToFirstSearchResultMs: 0,
                 numMatches: 0,
+                stats: undefined,
             });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/`(app)/search/useStreamedSearch.ts around lines 110 -
119, When starting a new search in useStreamedSearch you reset many state fields
but forgot to clear previous stats, which can leave stale stats visible; update
the setState call inside the new-search branch to also set stats: undefined (or
null) so the previous SearchStats value is cleared, referencing the setState
call in useStreamedSearch and the stats property on the search state object.
🧹 Nitpick comments (2)
packages/web/src/features/search/searchLimitExplanation.ts (1)

59-62: Fallback message may be unreachable or indicate a logic gap.

This fallback returns a generic "more matches may exist" message when none of the preceding conditions match. However, if the search is non-exhaustive (which is the precondition for calling this function based on searchResultsPage.tsx), at least one of the earlier conditions should logically be true:

  • totalMatchCount > actualMatchCount implies totalMatchCount > maxMatchDisplayCount (given how limits are configured)
  • Or shardsSkipped > 0 / filesSkipped > 0 / a non-default flushReason

Consider whether this fallback is reachable. If it is, it may indicate an unexpected edge case worth investigating. If it's purely defensive, adding a comment would help future maintainers understand its purpose.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/search/searchLimitExplanation.ts` around lines 59 -
62, The fallback message in searchLimitExplanation appears defensive/unreachable
given the preconditions; update the function searchLimitExplanation to either
(a) add a concise comment above the fallback return explaining that this branch
is purely defensive (describe the expected conditions such as
totalMatchCount/actualMatchCount, shardsSkipped/filesSkipped, or non-default
flushReason) so future maintainers understand why it exists, or (b) if you
prefer stronger guarantees, replace the fallback with an explicit assertion or
error/log when reached (e.g., throw or process a diagnostic) so unexpected edge
cases are surfaced; ensure the change references the same return object shape
(summary/detail) to keep callers unchanged.
packages/web/src/features/search/searchLimitExplanation.test.ts (1)

73-83: Consider adding test coverage for FLUSH_REASON_MAX_SIZE.

The test suite covers FLUSH_REASON_TIMER_EXPIRED but not FLUSH_REASON_MAX_SIZE. Adding a test would ensure both flush reason branches behave correctly.

💡 Suggested test case
+test('flushReason max size when no higher-priority signal', () => {
+    const out = getSearchLimitExplanation(
+        stats({
+            flushReason: 'FLUSH_REASON_MAX_SIZE',
+            totalMatchCount: 10,
+            actualMatchCount: 10,
+        }),
+        100,
+    );
+    expect(out.summary).toContain('size limit');
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/search/searchLimitExplanation.test.ts` around lines
73 - 83, Add a new unit test for the FLUSH_REASON_MAX_SIZE branch by duplicating
the existing timer test pattern: call getSearchLimitExplanation with stats({
flushReason: 'FLUSH_REASON_MAX_SIZE', totalMatchCount: 10, actualMatchCount: 10
}) and the same limit (100), then assert the returned out.summary reflects the
max-size flush (e.g., expect(out.summary).toContain('max size') or another
project-specific phrase used for that branch). Ensure the test name references
"flushReason max size" so getSearchLimitExplanation and the
FLUSH_REASON_MAX_SIZE branch are covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/web/src/app/`(app)/search/useStreamedSearch.ts:
- Around line 110-119: When starting a new search in useStreamedSearch you reset
many state fields but forgot to clear previous stats, which can leave stale
stats visible; update the setState call inside the new-search branch to also set
stats: undefined (or null) so the previous SearchStats value is cleared,
referencing the setState call in useStreamedSearch and the stats property on the
search state object.

---

Nitpick comments:
In `@packages/web/src/features/search/searchLimitExplanation.test.ts`:
- Around line 73-83: Add a new unit test for the FLUSH_REASON_MAX_SIZE branch by
duplicating the existing timer test pattern: call getSearchLimitExplanation with
stats({ flushReason: 'FLUSH_REASON_MAX_SIZE', totalMatchCount: 10,
actualMatchCount: 10 }) and the same limit (100), then assert the returned
out.summary reflects the max-size flush (e.g.,
expect(out.summary).toContain('max size') or another project-specific phrase
used for that branch). Ensure the test name references "flushReason max size" so
getSearchLimitExplanation and the FLUSH_REASON_MAX_SIZE branch are covered.

In `@packages/web/src/features/search/searchLimitExplanation.ts`:
- Around line 59-62: The fallback message in searchLimitExplanation appears
defensive/unreachable given the preconditions; update the function
searchLimitExplanation to either (a) add a concise comment above the fallback
return explaining that this branch is purely defensive (describe the expected
conditions such as totalMatchCount/actualMatchCount, shardsSkipped/filesSkipped,
or non-default flushReason) so future maintainers understand why it exists, or
(b) if you prefer stronger guarantees, replace the fallback with an explicit
assertion or error/log when reached (e.g., throw or process a diagnostic) so
unexpected edge cases are surfaced; ensure the change references the same return
object shape (summary/detail) to keep callers unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2831eea1-3e4f-497b-b06e-ce7a55043400

📥 Commits

Reviewing files that changed from the base of the PR and between 937d04b and 5e1ea7f.

📒 Files selected for processing (6)
  • packages/web/src/app/(app)/search/components/searchResultsPage.tsx
  • packages/web/src/app/(app)/search/useStreamedSearch.ts
  • packages/web/src/features/search/index.ts
  • packages/web/src/features/search/searchLimitExplanation.test.ts
  • packages/web/src/features/search/searchLimitExplanation.ts
  • packages/web/src/features/search/zoektSearcher.ts

@h30s
Copy link
Copy Markdown
Contributor Author

h30s commented Apr 8, 2026

@brendan-kellam PTA, all green 💚

@brendan-kellam
Copy link
Copy Markdown
Contributor

could you include screenshots in your description?

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.

[FR] Communicate when a zoekt search limit is hit

2 participants