| """Cross-encoder reranking of a semantic pool (free: local GPU sidecar).
|
|
|
| Why this stage exists: IRIS measured the cross-encoder as the dominant
|
| ranking channel behind its semantic doubling (0.121 -> 0.244), fused at ~3.3x
|
| the weight of any retrieval channel. The sidecar costs nothing per query
|
| (owned/rented GPU, already running), so on the score-per-dollar objective it
|
| is free accuracy: the one lever that improves the numerator without touching
|
| the denominator.
|
|
|
| Contract: wraps any SemPool (duck-typed: .ranked(), .evidence(cid),
|
| .texts(cid), plus optional .title/.year accessors) and returns an object with
|
| the same surface whose .ranked() is CE-reordered. Soft-fails to the original
|
| pool on any transport error, so a dropped tunnel degrades rather than breaks.
|
| """
|
|
|
| from __future__ import annotations
|
|
|
| import json
|
| import os
|
| import urllib.error
|
| import urllib.request
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| RERANK_CAP = int((os.environ.get("PFBMAX_CE_CAP") or "400").strip() or 400)
|
| DOC_CHARS = 1400
|
| TIMEOUT_S = 180
|
|
|
|
|
| def _post(url: str, payload: dict, timeout: float) -> dict:
|
| req = urllib.request.Request(
|
| url.rstrip("/") + "/rerank",
|
| data=json.dumps(payload).encode("utf-8"),
|
| headers={"Content-Type": "application/json"},
|
| )
|
| with urllib.request.urlopen(req, timeout=timeout) as resp:
|
| return json.loads(resp.read().decode("utf-8"))
|
|
|
|
|
| def rerank_scores(url: str, query: str, docs: list[dict],
|
| timeout: float = TIMEOUT_S) -> dict[str, float]:
|
| """{id: score} from the sidecar; {} on any failure (caller keeps order)."""
|
| if not url or not docs:
|
| return {}
|
| try:
|
| body = _post(url, {"query": query, "documents": docs}, timeout)
|
| except (urllib.error.URLError, OSError, ValueError, TimeoutError):
|
| return {}
|
| out: dict[str, float] = {}
|
| for row in body.get("scores") or []:
|
| if isinstance(row, dict) and "id" in row:
|
| try:
|
| out[str(row["id"])] = float(row["score"])
|
| except (TypeError, ValueError):
|
| continue
|
| return out
|
|
|
|
|
| class RerankedPool:
|
| """SemPool passthrough with a CE-reordered .ranked()."""
|
|
|
| def __init__(self, pool, order: list[str], trace: dict | None = None):
|
| self._pool = pool
|
| self._order = order
|
| self.ce_trace = trace or {}
|
|
|
| def ranked(self) -> list[str]:
|
| return list(self._order)
|
|
|
| def __getattr__(self, name):
|
| return getattr(self._pool, name)
|
|
|
|
|
| def ce_rerank_pool(query: str, criteria: list[str], pool, url: str | None = None,
|
| cap: int = RERANK_CAP, trace: dict | None = None):
|
| """Reorder the pool's head by cross-encoder score, keeping the tail in
|
| fused order. The CE query carries the criteria: PFB relevance is
|
| conjunctive (a paper must satisfy every criterion to earn the scorer's
|
| top label), so scoring against the bare query alone under-weights the
|
| constraints that decide the label."""
|
| url = url or os.environ.get("IRIS_ASTA_RERANKER_URL", "")
|
| tr = trace if trace is not None else {}
|
| order = list(pool.ranked())
|
| if not url or not order:
|
| tr["ce"] = {"status": "skipped-no-url" if not url else "empty-pool"}
|
| return pool
|
|
|
| head = order[:cap]
|
| docs = []
|
| for cid in head:
|
| try:
|
| text = pool.evidence(cid) or ""
|
| except Exception:
|
| text = ""
|
| title = ""
|
| for attr in ("title",):
|
| getter = getattr(pool, attr, None)
|
| if callable(getter):
|
| try:
|
| title = getter(cid) or ""
|
| except Exception:
|
| title = ""
|
| blob = (title + "\n" + text).strip()[:DOC_CHARS]
|
| if blob:
|
| docs.append({"id": cid, "text": blob})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| use_crit = os.environ.get("PFBMAX_CE_CRITERIA", "1").strip() in ("1", "true", "yes")
|
| ce_query = (query + " || " + " || ".join(criteria[:4])
|
| if (use_crit and criteria) else query)
|
| scores = rerank_scores(url, ce_query, docs)
|
| if not scores:
|
| tr["ce"] = {"status": "error", "sent": len(docs)}
|
| return pool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| url2 = os.environ.get("PFBMAX_RERANKER_URL_2", "").strip()
|
| fused_order = None
|
| if url2:
|
| scores2 = rerank_scores(url2, ce_query, docs)
|
| if scores2:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| band = float(os.environ.get("PFBMAX_CE_BAND", "0.10") or 0.10)
|
| fused_order = sorted(
|
| (c for c in head if c in scores or c in scores2),
|
| key=lambda c: (-round(scores.get(c, -1e9) / band),
|
| -scores2.get(c, -1e9)))
|
| tr.setdefault("ce_fuse", {}).update(
|
| {"mode": "lex", "band": band, "n2": len(scores2)})
|
|
|
| if fused_order is not None:
|
| scored = fused_order
|
| unscored = [c for c in head if c not in set(fused_order)]
|
| new_order = scored + unscored + order[cap:]
|
| tr["ce"] = {"status": "ok-lex", "sent": len(docs), "scored": len(scored)}
|
| return RerankedPool(pool, new_order, tr["ce"])
|
|
|
| scored = [c for c in head if c in scores]
|
| unscored = [c for c in head if c not in scores]
|
| scored.sort(key=lambda c: -scores[c])
|
| new_order = scored + unscored + order[cap:]
|
| tr["ce"] = {"status": "ok", "sent": len(docs), "scored": len(scored),
|
| "moved_into_top50": len([c for c in scored[:50]
|
| if order.index(c) >= 50])}
|
| return RerankedPool(pool, new_order, tr["ce"])
|
|
|