| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- """Score-based relevance guard before LLM rank (only when candidate pool is large)."""
- from __future__ import annotations
- from ...core.paper import Paper as LitPaper
- from ...core.search.paper_searcher import PaperSearcher, _has_any_author
- from ...utils.author_query_match import normalize_author_names
- from .method_acronym import is_method_acronym_token, title_matches_method_acronym
- from .search_plan import ResolvedSearchPlan
- _DEFAULT_THRESHOLD = 40
- _MIN_KEEP = 8
- _SCORE_TITLE = 5
- _SCORE_AUTHOR = 4
- _SCORE_VENUE = 3
- _SCORE_KEYWORD = 2
- _SCORE_YEAR = 1
- _SCORE_METHOD_ACRONYM = 6
- def apply_relevance_guard(
- candidates: list[LitPaper],
- *,
- plan: ResolvedSearchPlan,
- guard_threshold: int = _DEFAULT_THRESHOLD,
- min_keep: int = _MIN_KEEP,
- ) -> tuple[list[LitPaper], bool]:
- """候选过多时按相关性打分软过滤;过滤后过少则回退原列表。"""
- if len(candidates) <= guard_threshold:
- return candidates, False
- target_titles = [t.lower() for t in (plan.target_titles or []) if t.strip()]
- keywords = [
- k.lower()
- for k in (plan.keywords or [])
- if len(str(k).strip()) >= 2
- ]
- venues = [v for v in (plan.venues or []) if v.strip()]
- author_phrases = normalize_author_names(plan.authors or [])
- yf, yt = plan.year_from, plan.year_to
- method_acronym = (getattr(plan, "method_acronym", None) or "").strip() or None
- if not method_acronym and len(keywords) == 1 and is_method_acronym_token(keywords[0]):
- method_acronym = keywords[0]
- kept: list[LitPaper] = []
- for p in candidates:
- score = _relevance_score(
- p,
- target_titles=target_titles,
- keywords=keywords,
- venues=venues,
- author_phrases=author_phrases,
- year_from=yf,
- year_to=yt,
- method_acronym=method_acronym,
- )
- if _passes_guard_threshold(
- score,
- plan=plan,
- has_target_titles=bool(target_titles),
- has_strong_constraints=bool(venues or author_phrases or yf is not None),
- method_acronym=method_acronym,
- ):
- kept.append(p)
- if len(kept) < min_keep:
- return candidates, False
- return kept, True
- def _passes_guard_threshold(
- score: int,
- *,
- plan: ResolvedSearchPlan,
- has_target_titles: bool,
- has_strong_constraints: bool,
- method_acronym: str | None = None,
- ) -> bool:
- if method_acronym:
- return score >= _SCORE_METHOD_ACRONYM
- if has_target_titles:
- return score >= _SCORE_TITLE
- if has_strong_constraints:
- return score >= (_SCORE_VENUE + _SCORE_KEYWORD - 2) # >= 3
- return score >= (_SCORE_KEYWORD) # >= 2 for broad keyword queries
- def _relevance_score(
- p: LitPaper,
- *,
- target_titles: list[str],
- keywords: list[str],
- venues: list[str],
- author_phrases: list[str],
- year_from: int | None,
- year_to: int | None,
- method_acronym: str | None = None,
- ) -> int:
- score = 0
- title = (getattr(p, "title", None) or "").lower()
- abstract = (getattr(p, "abstract", None) or "").lower()
- journal = (getattr(p, "journal", None) or getattr(p, "venue", None) or "").lower()
- blob = f"{title} {abstract} {journal}"
- if target_titles and any(
- (len(tt) > 8 and (tt in title or title in tt)) for tt in target_titles
- ):
- score += _SCORE_TITLE
- if method_acronym:
- if title_matches_method_acronym(f"{title} {abstract}", method_acronym):
- score += _SCORE_METHOD_ACRONYM
- elif target_titles and any(
- len(tt) >= 12 and (tt in title or title in tt) for tt in target_titles
- ):
- score += _SCORE_METHOD_ACRONYM
- if author_phrases and _has_any_author(p, author_phrases):
- score += _SCORE_AUTHOR
- if venues:
- for v in venues:
- if PaperSearcher._paper_matches_venue_proceedings(p, v) or v.lower() in blob:
- score += _SCORE_VENUE
- break
- if keywords:
- kw_hits = sum(1 for kw in keywords if kw in blob)
- if kw_hits >= 2 or (kw_hits >= 1 and len(keywords) <= 3):
- score += _SCORE_KEYWORD
- elif kw_hits == 1:
- score += 1
- if year_from is not None or year_to is not None:
- try:
- py = int(getattr(p, "year", 0) or 0)
- except (TypeError, ValueError):
- py = 0
- if py:
- in_range = True
- if year_from is not None and py < int(year_from):
- in_range = False
- if year_to is not None and py > int(year_to):
- in_range = False
- if in_range:
- score += _SCORE_YEAR
- return score
|