From 00528456ff9e0113e3e250057e938b8bca001a1f Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Fri, 1 May 2026 20:41:12 -0400 Subject: [PATCH] qa(retrieval): _search_titles ORDER BY token-hit score; word-boundary post-filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- aborist/qa/query.py | 60 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/aborist/qa/query.py b/aborist/qa/query.py index 77570fc..9202121 100644 --- a/aborist/qa/query.py +++ b/aborist/qa/query.py @@ -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