qa/corpus_query: wire filter_by_title_relevance into multi_route

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).
This commit is contained in:
russell@unturf.com 2026-05-31 12:52:08 -04:00
parent 5fdd573c0a
commit 6c2ec1b6c0
No known key found for this signature in database

View file

@ -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)