From 6c2ec1b6c088ea4e0ea0fb6d0480afad430bdc90 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sun, 31 May 2026 12:52:08 -0400 Subject: [PATCH] qa/corpus_query: wire filter_by_title_relevance into multi_route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When policy.multi_route fans across body+title+phrase+core_keyword and merges by MIN bm25, the resulting set over-recalls on noisy titles (e.g. "East Asia" outranks "Nineteen Eighty-Four" on the Orwell phrase query because raw bm25 over the merged set doesn't know about accept-path semantics). The 5-accept-path filter arborist.qa.retrieval_routes.filter_by_title_relevance (landed step 3) gates the merged set on: title-overlap / synonym / TFIDF-core match / verbatim-phrase match / hyphen-fold anchor. Wired so accept-paths 2 and 4 actually fire — core_match_roots and phrase_match_roots are passed through from this turn's core_hits / phrase_hits, so docs surfaced via those routes pass the filter even when titles miss. Smoke (multi-shard wiki corpus, "has oceania always been at war with east asia"): Nineteen Eighty-Four now ranks #1 (was rank 3 before filter). Other phrase-route hits like "Nineteen Eighty-Four in popular media" stay; the geographic "East Asia" / "Oceania" title hits get filtered out. 264 tests pass (was 263 + 1 multi-route test from 6c that confirms filter path doesn't blow up on the Anarchism fixture either). --- arborist/qa/corpus_query.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/arborist/qa/corpus_query.py b/arborist/qa/corpus_query.py index f82dbef..a08afc8 100644 --- a/arborist/qa/corpus_query.py +++ b/arborist/qa/corpus_query.py @@ -192,7 +192,10 @@ def run_query( ts = _time.time() multi_route = bool(policy and policy.get("multi_route", False)) if multi_route: - from arborist.qa.retrieval_routes import question_phrases + from arborist.qa.retrieval_routes import ( + filter_by_title_relevance, + question_phrases, + ) per_route_limit = top_k * 4 body_hits = _safe_route(corpus, "fts_body", question, per_route_limit) title_hits = _safe_route(corpus, "fts_title", question, per_route_limit) @@ -200,8 +203,6 @@ def run_query( phrase_hits = _safe_phrase_route( corpus, phrases, per_route_limit, ) if phrases else [] - # core_keyword needs qtokens; reuse the title_query_tokens helper - # since legacy query() does the same thing. from arborist.qa.query import _title_query_tokens qtokens = list(_title_query_tokens(question)) core_hits = _safe_core_route( @@ -210,6 +211,22 @@ def run_query( hits = _merge_routes_min_bm25( body_hits, title_hits, phrase_hits, core_hits, ) + # 5-accept-path filter from arborist.qa.retrieval_routes — + # gates the merged set on title-overlap / synonym / TFIDF-core + # match / verbatim-phrase match / hyphen-fold anchor. Without + # this, multi-route over-recalls on noisy title overlaps (e.g. + # "East Asia" outranks "Nineteen Eighty-Four" for the Orwell + # phrase query). core_match_roots and phrase_match_roots are + # passed through so accept-paths 2 + 4 fire on docs that + # surfaced via those routes even when titles miss. + core_match_roots = {h.document_root for h in core_hits} + phrase_match_roots = {h.document_root for h in phrase_hits} + hits = filter_by_title_relevance( + hits, question, + core_match_roots=core_match_roots, + phrase_match_roots=phrase_match_roots, + fallback_top_n=top_k, + ) else: try: hits = corpus.fts_body(question, limit=top_k * 4)