qa(retrieval): _search_titles ORDER BY token-hit score; word-boundary post-filter

Fox-surfaced 2026-05-02 case:
  Q: "what date did back to the future come out?"
  → top-8 sources: trilogy, ride, animated-series, The Future,
    Future Husband, The Day You Come, Ratchet & Clank Future,
    Future plc — film article entirely absent.

Two compounding defects in the title-LIKE retrieval path:

(1) `_search_titles` had no ORDER BY. SQL `LIKE '%token%' OR ...`
    returned rows in arbitrary internal order; LIMIT 32 truncated
    before genuine matches. The film article `Back to the Future`
    (title contains both `back` AND `future`) lost the LIMIT race to
    substring-match junk like `Out (poker)` (single-token match on
    `out`), `Aberdeen, South Dakota` (`south` contains `out`),
    `Backplane` (`back` substring), etc.

    Fix: SQL builds a token-hit score via CASE WHEN per token and
    ORDERs BY (title_score DESC, LENGTH(title) ASC). The film
    article's score=2 (back+future) ranks above garbage's score=1.
    Tie-break on title length favors title purity (shorter title +
    same overlap = higher per-token signal).

(2) Caller's post-filter check used substring match
    (`if t in title_lower`) — same defect as the SQL. "south"
    contains "out", "outline" contains "out", "backbone" contains
    "back", etc. all passed the no-op overlap check.

    Fix: tokenize the title via `_title_query_tokens` (already
    drops stopwords + small tokens) and stem-aware-intersect with
    the query stems. Word-boundary semantics; only genuine title
    tokens count.

Verified live:
  Pre-fix:  trilogy at #1, film article absent from top-8.
  Post-fix: film article at #1, trilogy at #2, junk gone.

Verdict on the BTTF query stays POINTER-LINKED-PARTIAL · warrant
missing because the model picked the box-office chunk (mentions
"1985" but not "July") instead of the infobox/release-date chunk.
That's correct warrant behavior: claim asserts both anchors;
cited span only has one. The retrieval-side defect was a separate
gap that this fix closed; chunk-relevance ranking for cited
content is downstream work.
This commit is contained in:
russell@unturf.com 2026-05-01 20:41:12 -04:00
parent 9ec9469c4d
commit 00528456ff
No known key found for this signature in database

View file

@ -579,15 +579,41 @@ def _search_titles(conn, qtokens: list[str], limit: int) -> list[tuple]:
"""SQL LIKE over documents.title — finds the HTTP article that FTS5
misses because list-pages with many URLs have higher 'http' term
frequency than the actual protocol article. Returns rows shaped
to match the FTS5 hit tuple."""
to match the FTS5 hit tuple.
Bug fix 2026-05-02 (fox case: ``"what date did back to the
future come out?"``): the previous SQL had no ``ORDER BY``, so
SQLite returned rows in arbitrary internal order and ``LIMIT``
truncated before the actual title-token-overlap winners landed.
The film article ``"Back to the Future"`` (title contains
``back`` AND ``future``) lost the LIMIT race to substring-match
junk like ``"Out (poker)"`` (matches ``out`` substring),
``"Aberdeen, South Dakota"`` (``south`` contains ``out``),
``"Backplane"`` (``back`` substring), etc.
Fix: score each row by how many query tokens its title contains
(substring-level the post-filter in the caller does
word-boundary verification), and ``ORDER BY`` that score
descending with title length ascending as tiebreaker (shorter
title = higher title purity per char). The legitimate
multi-token matches surface above single-substring junk before
LIMIT truncates.
"""
if not qtokens:
return []
clauses = " OR ".join(["LOWER(title) LIKE ?"] * len(qtokens))
params = [f"%{t.lower()}%" for t in qtokens]
params.append(limit)
score_expr = " + ".join(
["CASE WHEN LOWER(title) LIKE ? THEN 1 ELSE 0 END"] * len(qtokens)
)
likes = [f"%{t.lower()}%" for t in qtokens]
# Two passes of params: once for the score-CASE-WHEN block,
# once for the WHERE clause.
params = likes + likes + [limit]
rows = conn.execute(
f"SELECT document_root, document_uri, title FROM documents "
f"WHERE {clauses} LIMIT ?",
f"SELECT document_root, document_uri, title, "
f"({score_expr}) AS title_score FROM documents "
f"WHERE {clauses} "
f"ORDER BY title_score DESC, LENGTH(title) ASC LIMIT ?",
params,
).fetchall()
return rows
@ -918,9 +944,29 @@ def _search_corpus(
# CPU?". Now that contribution is the same scale as FTS5 body
# BM25, which lets Pentium_4 (high body relevance, no title
# overlap) win on its actual topical fit.
# Word-boundary overlap check (was substring-match,
# which falsely passed `"out" in "south"`,
# `"date" in "candidate"`, `"come" in "outcome"`, etc.).
# 2026-05-02 case fox surfaced: "what date did back to
# the future come out?" — the substring check admitted
# "Aberdeen, South Dakota" (south contains "out"),
# "Backplane" (back), "Outline of biology" (out), and
# consumed the over_fetch budget so the legitimate
# `Back to the Future` film article never made the
# rerank cut. Tokenizing the title via
# `_title_query_tokens` + stem-aware comparison keeps
# the title-LIKE pass returning only docs whose title
# has an actual matching word.
accept_stems = {
_stem_token_for_match(t) for t in accept_tokens
}
for r in _search_titles(conn, list(accept_tokens), over_fetch):
title_lower = (r["title"] or "").lower().replace("_", " ")
overlap = sum(1 for t in accept_tokens if t in title_lower)
title_norm = (r["title"] or "").replace("_", " ")
title_stems = {
_stem_token_for_match(t)
for t in _title_query_tokens(title_norm)
}
overlap = len(accept_stems & title_stems)
if overlap == 0:
continue
title_score = overlap * 10.0