Skip to content

Make document titles searchable in hybrid retrieval#549

Closed
tobocop2 wants to merge 4 commits into
mainfrom
feat/title-aware-index
Closed

Make document titles searchable in hybrid retrieval#549
tobocop2 wants to merge 4 commits into
mainfrom
feat/title-aware-index

Conversation

@tobocop2

@tobocop2 tobocop2 commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Problem

The lexical index covers only chunk text, so BM25 can never match a document's title or filename; a query naming a document finds it only if the words happen to appear in the body.

Three adjacent defects in the same hybrid-search surface come along for the ride, all of which silently degrade retrieval. The FTS indexes were built without token positions, so a phrase query (raw user text reaches LanceDB as a phrase) errored instead of matching. Rank fusion weighed the vector and BM25 arms equally with no way to favor a strong dense embedder. And ensure_fts_index() runs table.optimize() on an existing index, which on a real-sized corpus can hit a LanceDB encoding bug and raise, leaving the index flag unset so every subsequent query silently fell back to vector-only with no error surfaced.

Solution

Every chunk carries a nullable title (extraction metadata when available, cleaned filename stem otherwise) with its own FTS index, fused as a third reciprocal-rank arm at a configurable weight when title_search is on (off by default until the eval harness proves it). Existing chunk-FTS call sites now pin their column explicitly, because once a second FTS index exists an unpinned query silently searches every indexed column. Old indexes keep working untouched: schemas evolve in place and the title arm contributes nothing until a reindex populates titles. Source rows also persist extraction title, authors, and creation date.

Both FTS indexes are built with token positions so phrase queries match instead of erroring. lexical_fusion_weight (default 1.0, so equal-weight fusion is byte-for-byte unchanged) scales the BM25 arm down toward the dense arm. And an existing, queryable index is marked hybrid-ready before the best-effort optimize(), so an optimize() failure logs a warning instead of disabling hybrid search corpus-wide. A retrieval benchmark on BEIR confirmed the optimize bug: every hybrid config scored identically to the vector-only baseline until it was fixed.

tobocop2 added 4 commits July 17, 2026 01:09
The lexical index covered only chunk text, so BM25 could never match a
document's title or filename. Every chunk now carries a nullable title
(extraction metadata when available, cleaned filename stem otherwise)
with its own FTS index, queried as a third RRF arm at a configurable
weight when title_search is on (off by default pending eval evidence).
Existing chunk-FTS call sites pin fts_columns explicitly: once a second
FTS index exists, an unpinned query searches every indexed column, which
would silently widen BM25 semantics. Old indexes keep working: schemas
evolve in place and the title arm contributes nothing until reindex.
Source rows also persist extraction title/authors/created_at for future
filtering.

bb-t8pr
lilbee passes raw user query text to the lexical arm, so a query LanceDB
parses as a phrase (e.g. a quoted span) reaches it as a phrase query. The
chunk and title FTS indexes were created without positions, so those
queries raised and the arm silently returned nothing. Create both indexes
with_position=True. Existing indexes need a reindex to gain phrase support;
they degrade to vector-only until then rather than erroring.

bb-e4qw

(cherry picked from commit a2cd684)
Rank fusion has weighed the vector and BM25 arms equally, which dilutes a
strong dense embedder on corpora where the lexical arm adds more noise than
signal. Add lexical_fusion_weight (default 1.0, so the equal-weight behaviour
is byte-for-byte unchanged) to scale the BM25 arm's share of the fused score
down toward the dense arm. The right value is corpus-dependent and meant to be
set from the retrieval benchmark, not guessed. Wired through the config field,
its HTTP/MCP/env surfaces, the TUI settings map, and fuse_arms.
ensure_fts_index() runs table.optimize() whenever the FTS index already
exists. On a large corpus that call can hit a LanceDB encoding bug and raise
(Arrow list offset overflow), and the failure fell through to leave _fts_ready
False -- silently dropping every subsequent query to vector-only with no error
surfaced. The index itself is complete and still serves queries, so mark hybrid
ready before the best-effort optimize and downgrade an optimize() failure to a
warning. Found while benchmarking retrieval: every hybrid config collapsed to
the vector-only baseline until this was fixed.

(cherry picked from commit 6aedffb)
@tobocop2

Copy link
Copy Markdown
Owner Author

Superseded by the consolidated retrieval PR #557 — review and merge there. Converted to draft to keep it off the review queue; the branch and history stay intact for reference.

@tobocop2
tobocop2 marked this pull request as draft July 18, 2026 04:55
@tobocop2

Copy link
Copy Markdown
Owner Author

Superseded by the consolidated retrieval PR #557, which contains this change. Closing; the branch stays intact for reference.

@tobocop2 tobocop2 closed this Jul 18, 2026
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.

1 participant