Angshul's picture
Upload 31 files
dd990bf verified
Raw History Blame Contribute Delete
5.53 kB
"""The *single* fixed graph selector used across all six reported benchmarks.
Only candidate text, its optional title, and a query-specific neural relevance
score are allowed as input. There is no training, gold-label access, or corpus-
specific parameter tuning inside this module.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping, Sequence
import re
import unicodedata
import numpy as np
def normalize_text(text: Any) -> str:
"""Match the exact normalization of the published evaluation scripts."""
return " ".join(
re.sub(r"[^\w\s]", " ", unicodedata.normalize("NFKD", str(text)).casefold()).split()
)
@dataclass(frozen=True)
class Passage:
title: str
text: str
id: str | None = None
@classmethod
def from_value(cls, value: "Passage | Mapping[str, Any] | str") -> "Passage":
if isinstance(value, Passage):
return value
if isinstance(value, str):
return cls(title="", text=value)
if not isinstance(value, Mapping):
raise TypeError("Each passage must be a string, dict, or Passage.")
text = value.get("text", value.get("body", value.get("paragraph_text", "")))
return cls(title=str(value.get("title", "") or ""), text=str(text or ""),
id=None if value.get("id") is None else str(value["id"]))
def graph_features(query: str, passages: Sequence[Passage]) -> tuple[np.ndarray, np.ndarray]:
"""Return directed source->target article-title edges and query/title matches.
This is intentionally the SAME simple string-link rule as the six-dataset
benchmarks. In particular, it does NOT introduce token overlap, semantic
similarity edges, facet decomposition, or learned relation extraction.
"""
q = normalize_text(query)
titles = [normalize_text(p.title) for p in passages]
bodies = [normalize_text(p.text) for p in passages]
q_words = set(q.split())
n = len(passages)
link = np.zeros((n, n), np.float32)
match = np.zeros(n, np.float32)
for j, title in enumerate(titles):
if not title:
continue
title_words = set(title.split())
overlap = len(title_words & q_words) / max(1, len(title_words))
match[j] = 1.0 if " " + title + " " in " " + q + " " else 0.5 * overlap
for i, body in enumerate(bodies):
if i != j and len(title) >= 4 and " " + title + " " in " " + body + " ":
link[i, j] = 1.0
return link, match
def minmax_relevance(scores: Sequence[float]) -> np.ndarray:
scores = np.asarray(scores, dtype=np.float32)
if scores.ndim != 1 or not np.isfinite(scores).all():
raise ValueError("Scores must be a one-dimensional array of finite numbers.")
if not len(scores):
raise ValueError("At least one candidate passage is required.")
lo, hi = float(scores.min()), float(scores.max())
return (scores - lo) / (hi - lo) if hi > lo + 1e-9 else np.zeros_like(scores)
def select_with_features(
scores: Sequence[float], link: np.ndarray, match: np.ndarray, k: int = 3
) -> list[int]:
"""Greedy rank with fixed coefficients .4, .5, .2 and deterministic ties.
At step t the score of candidate j is
0.4 * normalized_relevance_j
+ 0.5 * max_{i previously selected} link[i, j]
+ 0.2 * query_title_match_j.
"""
if isinstance(k, bool) or not isinstance(k, int) or k < 1:
raise ValueError("k must be a positive integer")
norm_scores = minmax_relevance(scores)
n = len(norm_scores)
link = np.asarray(link, dtype=np.float32)
match = np.asarray(match, dtype=np.float32)
if link.shape != (n, n) or match.shape != (n,):
raise ValueError("Link matrix or match vector dimensions do not match scores")
selected: list[int] = []
remaining = set(range(n))
for _ in range(min(k, n)):
nxt = max(remaining, key=lambda j: (
0.4 * norm_scores[j]
+ 0.5 * max((link[i, j] for i in selected), default=0.0)
+ 0.2 * match[j],
-j,
))
selected.append(nxt)
remaining.remove(nxt)
return selected
def rank_with_scores(query: str, passages: Sequence[Passage | Mapping[str, Any] | str],
scores: Sequence[float], k: int = 3) -> list[dict[str, Any]]:
"""Public, network-free inference API if the retriever supplies scores.
Returned `step_score` is the acquisition score at the moment of selection,
not a calibrated relevance probability.
"""
docs = [Passage.from_value(p) for p in passages]
if len(docs) != len(scores):
raise ValueError("Supply exactly one relevance score per candidate passage")
link, match = graph_features(query, docs)
normalized = minmax_relevance(scores)
order = select_with_features(scores, link, match, k)
result = []
selected = []
for idx in order:
bonus = max((float(link[i, idx]) for i in selected), default=0.0)
step = 0.4 * float(normalized[idx]) + 0.5 * bonus + 0.2 * float(match[idx])
item = docs[idx]
result.append({
"index": idx, "id": item.id, "title": item.title, "text": item.text,
"relevance_score": float(scores[idx]),
"normalized_relevance": float(normalized[idx]),
"directed_link_bonus": bonus,
"query_title_match": float(match[idx]),
"step_score": step,
})
selected.append(idx)
return result