Skip to content

feat: FTS5 search index with background rebuild and 30-day default wi…#120

Open
clean6378-max-it wants to merge 4 commits into
masterfrom
feat/search-fts-index
Open

feat: FTS5 search index with background rebuild and 30-day default wi…#120
clean6378-max-it wants to merge 4 commits into
masterfrom
feat/search-fts-index

Conversation

@clean6378-max-it

@clean6378-max-it clean6378-max-it commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Closes #112

Summary

This PR adds the FTS search index for Monday item first PR

utils/search_index.py builds a SQLite FTS5 index next to the PR #111 summary cache (~/.claude-code-chat-browser/). Rebuilds are keyed off project manifest mtime/size plus exclusion-rules fingerprint. Index files use the cppa pattern: search_index.<uuid>.sqlite and search_index.active.

/api/search checks the index first and falls back to live-scan when the index is missing, stale, or the FTS query fails. Default search window is 30 days; all_history=1 and since_days=N opt into wider ranges. Background rebuild starts from create_app (skipped when TESTING=True).

Summary by CodeRabbit

  • New Features
    • Search now uses an index-first approach with a live-scan fallback.
    • Added time-window filtering for results, with an option to include older history.
    • Snippets and matching now consider both message text and tool output.
  • Bug Fixes
    • Search and app startup are more resilient when indexing isn’t ready; failures are logged and fallback behavior is preserved.
  • Tests
    • Expanded integration and index-focused test coverage for windowing, ordering, and fallback scenarios.
  • Documentation
    • Refined benchmark gating/thresholds and updated baseline/gate notes for search benchmarks.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b5cbe4ae-b9a2-4f05-b676-8bf2577af05c

📥 Commits

Reviewing files that changed from the base of the PR and between 6339016 and ac694f1.

📒 Files selected for processing (4)
  • api/search.py
  • tests/test_search.py
  • tests/test_search_index.py
  • utils/search_index.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/test_search_index.py
  • api/search.py
  • tests/test_search.py
  • utils/search_index.py

📝 Walkthrough

Walkthrough

Adds a local FTS5 search index, switches /api/search to index-first lookup with live-scan fallback and time-window support, starts background indexing on app startup, expands search coverage, and reconfigures the search benchmark gate.

Changes

Search Index Feature

Layer / File(s) Summary
Search index core module
utils/search_index.py
Implements the SQLite FTS5 index, fingerprinting, build/query flow, background refresh, usability checks, and helper types/functions.
/api/search index-first flow
api/search.py, tests/test_api_integration.py, tests/test_search.py
Reworks /api/search to resolve search windows, query the index first, and fall back to live scanning; updates integration tests and adds search-index coverage.
App startup wiring
app.py
Starts the background index worker during app creation when not testing, selecting a projects directory and logging startup failures.
Search tests
tests/test_search_index.py
Adds dedicated search-index tests for indexing, windowing, fallback, query failure handling, environment bypass, and background behavior.

Benchmark Gate Update

Layer / File(s) Summary
Relaxed search benchmark gating
scripts/check_benchmark_regression.py, benchmarks/baselines.json, scripts/reduce_baselines.py, benchmarks/README.md
Updates the gate rule, baseline note text, timestamp, stored baseline value, and regression script threshold handling for test_search_full_corpus.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant api_search
  participant utils_search_index
  participant SessionCache

  Client->>api_search: GET /api/search?q=...&all_history/since_days
  api_search->>utils_search_index: resolve_search_since_ms()
  api_search->>utils_search_index: _search_via_index()
  alt index available
    utils_search_index-->>api_search: indexed hits
  else index unavailable
    api_search->>SessionCache: get_cached_session()
    SessionCache-->>api_search: session data
    api_search->>api_search: live scan + snippets
  end
  api_search-->>Client: SearchHitDict list
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the FTS5 search index and background rebuild changes.
Linked Issues check ✅ Passed The changes implement the FTS5 index, background rebuilds, 30-day default window, fallback, tests, and benchmark gate requested by #112.
Out of Scope Changes check ✅ Passed The touched files all support search indexing, windowing, tests, or benchmark gating, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/search-fts-index

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
scripts/check_benchmark_regression.py (1)

174-175: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Regression summary message uses the wrong threshold for relaxed benchmarks.

limit is correctly computed per-benchmark (relaxed SEARCH_BENCHMARK_THRESHOLD for RELAXED_THRESHOLD_TESTS), but the final summary at Line 175 always reports the generic threshold, even when a failure was actually gated against the relaxed 2.0x limit. This mislabels the reported percentage in CI logs (e.g., a search regression at >200% would be reported as "exceeded 20%").

🩹 Proposed fix
     if failures:
-        print(f"\nREGRESSION: {len(failures)} benchmark(s) exceeded {threshold:.0%}")
+        print(f"\nREGRESSION: {len(failures)} benchmark(s) exceeded their threshold")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check_benchmark_regression.py` around lines 174 - 175, The regression
summary in check_benchmark_regression.py is reporting the generic threshold even
when a benchmark was checked against a relaxed per-test limit. Update the final
failures summary printed in the benchmark regression flow to use the actual
limit that applied to the failing benchmarks, using the existing benchmark
selection logic around RELAXED_THRESHOLD_TESTS and the per-benchmark limit
calculation instead of the global threshold. Ensure the message reflects the
same threshold used when deciding failures.
🧹 Nitpick comments (1)
utils/search_index.py (1)

87-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused search-index path constants. SEARCH_INDEX_POINTER_FILE and SEARCH_INDEX_FILE are never read; the paths are resolved through cache_dir() in the code paths that matter. The current “test override hook” comment is misleading because patching these names has no runtime effect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/search_index.py` around lines 87 - 88, The unused search-index path
constants should be removed from search_index.py because
SEARCH_INDEX_POINTER_FILE and SEARCH_INDEX_FILE are never referenced by the
runtime code paths. Update the module by deleting those constant definitions and
any misleading “test override hook” comment, and keep the actual path resolution
centralized in cache_dir()-based logic used by the search index helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/search.py`:
- Around line 50-59: The _parse_since_days helper is dropping oversized values,
which later causes resolve_search_since_ms to fall back to the default 30-day
window instead of honoring a larger request. Update _parse_since_days to clamp
inputs above _MAX_SEARCH_SINCE_DAYS to that maximum rather than returning None,
while still rejecting non-positive and invalid values. Use the existing
_parse_since_days and resolve_search_since_ms flow to keep the behavior
consistent.
- Around line 108-138: The indexed search path in _search_via_index is filtering
too late, so excluded hits can consume the capped query_index_hits() results and
return fewer than max_results. Move the _index_hit_excluded check into the
indexed fetch/backfill loop so query_index_hits() can keep pulling additional
hits until enough non-excluded SearchHitDict entries are collected, matching the
live scan behavior. Keep the change localized around query_index_hits(),
_index_hit_excluded(), and the results assembly in _search_via_index.

In `@utils/search_index.py`:
- Around line 219-232: The `_projects_fingerprint()` helper should ignore
sessions that disappear between `list_sessions()` and `os.stat()`, since the
uncaught `OSError` can break the `/api/search` index check and prevent fallback
search. Update `_projects_fingerprint()` to catch `OSError` around the
`sess["path"]` stat/relpath handling, skip that session when it vanishes, and
continue building the manifest normally.

---

Outside diff comments:
In `@scripts/check_benchmark_regression.py`:
- Around line 174-175: The regression summary in check_benchmark_regression.py
is reporting the generic threshold even when a benchmark was checked against a
relaxed per-test limit. Update the final failures summary printed in the
benchmark regression flow to use the actual limit that applied to the failing
benchmarks, using the existing benchmark selection logic around
RELAXED_THRESHOLD_TESTS and the per-benchmark limit calculation instead of the
global threshold. Ensure the message reflects the same threshold used when
deciding failures.

---

Nitpick comments:
In `@utils/search_index.py`:
- Around line 87-88: The unused search-index path constants should be removed
from search_index.py because SEARCH_INDEX_POINTER_FILE and SEARCH_INDEX_FILE are
never referenced by the runtime code paths. Update the module by deleting those
constant definitions and any misleading “test override hook” comment, and keep
the actual path resolution centralized in cache_dir()-based logic used by the
search index helpers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7ffadc8-b00a-4240-b3ed-f3ea45c2d8bb

📥 Commits

Reviewing files that changed from the base of the PR and between a05fd29 and 2bbdd7d.

📒 Files selected for processing (10)
  • api/search.py
  • app.py
  • benchmarks/README.md
  • benchmarks/baselines.json
  • scripts/check_benchmark_regression.py
  • scripts/reduce_baselines.py
  • tests/test_api_integration.py
  • tests/test_search.py
  • tests/test_search_index.py
  • utils/search_index.py

Comment thread api/search.py Outdated
Comment thread api/search.py Outdated
Comment thread utils/search_index.py
Clamp oversized since_days instead of dropping them back to the 30-day default.
Keep paging FTS hits when exclusion rules filter out the first batch so limit
still fills. Skip session files that vanish during the fingerprint walk, drop
dead index path constants, and print the real regression limit in the benchmark
gate. Refresh test_search_full_corpus baseline for the indexed search path.
Comment thread utils/search_index.py
Comment thread utils/search_index.py Outdated
…h_index.py Added ORDER BY m.timestamp_ms DESC so index hits are newest-first Replaced the 20× over-fetch with a fixed _FTS_BATCH_SIZE = 200; _search_via_index keeps paging until results are filled or FTS is exhausted Fixed offset: sql_rows_fetched is now rows actually scanned (not full batch size), so early break no longer skips unprocessed rows api/search.py Added _rank_search_hits() for timestamp-desc ordering Index and live-scan paths both rank before returning (live scan collects all matches, then sorts and slices — correct global newest-first on fallback) Tests test_results_ordered_newest_first — verifies DESC ordering test_phrase_filter_paginates_past_decoy_token_matches — 250 token-only decoys + real phrase at end; would fail with the old offset bug 30 tests passed (ruff + mypy clean). Suggested commit message: fix: FTS search ordering and pagination for phrase filtering Add ORDER BY timestamp_ms DESC, advance SQL offset by rows scanned, and page in fixed batches until enough hits or FTS exhausted. Rank live-scan results the same way for parity. Want me to commit and push to PR #120?  /humanizer commit message   fix: FTS search ordering and pagination
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.

claude-code-chat-browser: FTS5 search index with background rebuild and 30-day default window

2 participants