#75: Index titles in FTS5 so title matching is word-based and bm25-scored#81
#75: Index titles in FTS5 so title matching is word-based and bm25-scored#81dmwyatt wants to merge 3 commits into
Conversation
Create a titles_fts external-content FTS5 index over documents(title), mirroring notes_fts, and backfill it from existing rows so databases synced before this migration work without a re-sync. upsert_documents rebuilds the index after writing titles so it stays current on sync. Registers migration v014, bumps the migration total and the schema version assertions, adds the index to the hand-written test schema and the fixture FTS rebuilds, and adds migration tests for a fresh database and for backfill from pre-existing rows. Claude-Session: https://claude.ai/code/session_01TmhUwR8kAqCjZFvf3vFJXQ
Replace the titles arm's `title LIKE %query%` with a titles_fts MATCH
scored by bm25, routed through sanitize_fts_query like the other
sources. Title matching becomes word-based (implicit AND, any order,
word-boundary tokens): a meeting titled "Budget review" now matches the
query "review budget", and the query no longer matches as a substring
inside a longer word ("art" stops matching "Quarterly").
Remove the binary title_hit ranking tier, which forced any title hit
above every scored content match and flooded ranking when a common word
matched many recurring titles. Documents are now ordered by their best
(lowest) bm25 across all sources, then recency. title_hit was only ever
a ranking alias inside this query (grep and search re-derive the title
card from the evidence layer), so it is deleted, not surfaced.
Adds db-layer regression tests for reordered words, non-adjacent words,
and the substring-inside-a-word case, and updates the former
title-ranks-first test to the pooled bm25 semantics.
Claude-Session: https://claude.ai/code/session_01TmhUwR8kAqCjZFvf3vFJXQ
Update the grep section so the "matches every word, in any order" claim covers titles, note the word-boundary (non-substring) semantics, and replace the "title matches come first" ranking description with the pooled bm25 ordering. Document the titles_fts index in the schema reference and SPEC. Claude-Session: https://claude.ai/code/session_01TmhUwR8kAqCjZFvf3vFJXQ
|
Claude finished @dmwyatt's task in 3m 18s —— View job Reviewing PR #75 fix/75-titles-fts
Review notesI read the diff against
No bugs or code-quality issues met the confidence bar for this review. The change is a clean, well-mirrored extension of the existing FTS5 pattern with solid test coverage (reordered words, non-adjacent words, substring-inside-word rejection, migration backfill). Code review: no issues found. |
|
Claude finished @dmwyatt's task in 56s —— View job Security Review
Security review: no issues found in this PR. Notes from the review (informational, not blocking):
|
Search quality benchmark: candidate vs baselineBenchmarked against the frozen 872-doc snapshot with the v2 golden set (93 queries, k=10). Baseline is origin/main Aggregate deltas (candidate minus baseline)
Per-query wins / losses / ties
The single fts win is an exact-term query that went from no relevant result in the top 10 to a relevant result at rank 1 (recall 0.00 → 0.50). Hybrid and rerank-jina are metric-identical to baseline on every query. Stratum-level aggregates (baseline → candidate)fts
hybrid and rerank-jina: all strata unchanged (exact-term 100.0% hit both sides; mixed and paraphrase identical to baseline in every metric). VerdictNet gain: the fts titles arm improves on exact-term queries with zero regressions in any mode, the end-to-end hybrid and rerank pipelines are quality-neutral, and fts latency drops from 6.9 ms to 0.9 ms avg (bm25 MATCH replacing the LIKE scan). |
Benchmark gate: passed
Before/after results are in the comment below (93-query golden set, fts/hybrid/rerank modes, frozen snapshot). Zero per-query regressions in any mode; hybrid and rerank metrics are per-query identical to baseline, and fts gains one exact-term query (first relevant hit from outside the top 10 to rank 1) with no losses.
What changed
The titles tier of
search_meetingsmatchedtitle LIKE '%query%': the whole query as one contiguous substring, contributing an unscored binarytitle_hit. The finalORDER BY m.title_hit DESC, ...then put any title hit above every scored content match. Both properties were the bug: whole-query-substring semantics miss reordered or non-adjacent words, and the unscored binary tier floods ranking when a common word matches many recurring titles.This indexes titles in FTS5 (
titles_fts, external-content overdocuments(title), mirroringnotes_fts) and replaces the LIKE arm with atitles_fts MATCHscored by bm25, routed through the samesanitize_fts_queryas the other sources. Thetitle_hittier is removed; documents are ordered by their best (lowest) bm25 across all sources, then recency.title_hitwas only ever a ranking alias inside that one SQL query; it never leftsearch_meetings. Grep and search re-derive the "title match" card independently from the evidence layer (token-basedtitle_matches), so no user-visible indicator depends on it. It is deleted, not surfaced.Semantics change (intended)
Title matching is now word-based (implicit AND, any order, word-boundary tokens), consistent with notes, transcripts, and panels.
Reordered and non-adjacent words now match:
Substring-inside-a-word no longer matches:
Grep counts and result ordering shift accordingly. This also feeds hybrid search, where the FTS list seeds
keyword_idsandkeyword_total.Ranking change and rationale
The binary title tier is gone from
ORDER BY. bm25 values are not calibrated across FTS tables, but the query already mixes notes/transcripts/panels bm25 viaMIN(score); titles joining that same pool is the consistent choice. The post-rerank title boost inquery/adjust.rsis untouched: it operates on candidate titles regardless of how candidates were retrieved.Migration and backfill
New migration
v014_titles_fts.sqlcreatestitles_ftsand backfills it from existingdocumentsrows (INSERT INTO titles_fts(titles_fts) VALUES('rebuild')), so databases synced before this migration work without a re-sync.upsert_documentsrebuilds the index after writing titles so it stays current on future syncs. Verified end-to-end against a copy of a real 875-document database: the migration applied (with the automatic pre-migration backup),PRAGMA user_versionadvanced to 14, all 875 titles were backfilled, and a reordered two-word query that the old substring semantics would miss returned the expected meeting.Tests
titles_ftssearchable on a fresh database, and backfill from a row inserted before v014 is applied.test_search_meetings_title_match_ranks_first(which pinned the removed title-first tier) to assert the pooled bm25 semantics: all three "kubernetes" documents are returned, order decided by bm25 rather than a title tier.titles_ftsto the hand-written integration test schema and the fixture FTS rebuilds.Scope boundaries kept:
grans show <title-substring>selection and the--meetingfilter keep their substring semantics (they are selectors, not search).Closes #75
https://claude.ai/code/session_01TmhUwR8kAqCjZFvf3vFJXQ