GYOM15
Deploy the RAG comparison app
45d0949
Raw
History Blame Contribute Delete
2.99 kB
"""Cross-encoder reranking: step 2 of a two-stage retrieval.
The retriever (vector / hybrid / graph) outputs a **large top-N**; the
cross-encoder, which reads the (query, document) pair **together**, re-scores
them finely and we keep the **top-k**. More accurate than a bi-encoder (which encodes the
query and the document *separately*), but too slow for the whole corpus -- hence
the two stages: we only run it on the already-filtered candidates.
"""
from functools import lru_cache
@lru_cache(maxsize=2)
def _model(name: str):
"""Load (and cache) the cross-encoder; lazy import."""
from sentence_transformers import CrossEncoder
return CrossEncoder(name)
class CrossEncoderReranker:
"""Re-score candidates with a cross-encoder and return the reordered top-k."""
def __init__(self, model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"):
self.model_name = model_name
def rerank(
self, query: str, candidates: list[dict], top_k: int,
mode: str = "replace", rrf_k: int = 60,
) -> list[dict]:
"""Reorder `candidates` (dicts with a "text" key) and return the top-k.
- `replace`: we follow the cross-encoder alone (it **replaces** the base ranking).
- `fusion` : RRF between the base rank (input position) and the
cross-encoder rank -- each ranking "votes", which **protects** an
already-strong retriever from being dragged down.
"""
if not candidates:
return []
scores = _model(self.model_name).predict([(query, c["text"]) for c in candidates])
ce_order = sorted(range(len(candidates)), key=lambda i: scores[i], reverse=True)
if mode == "fusion":
ce_rank = [0] * len(candidates)
for rank, i in enumerate(ce_order):
ce_rank[i] = rank
order = sorted(
range(len(candidates)),
key=lambda i: 1.0 / (rrf_k + i) + 1.0 / (rrf_k + ce_rank[i]),
reverse=True,
)
else:
order = ce_order
return [candidates[i] for i in order[:top_k]]
class RerankedRetriever:
"""Decorator: add a reranking stage to any retriever.
Exposes the **same** `search(query, k)`: retrieves an enlarged top-N via the
inner retriever, then the cross-encoder reorders and we return the top-k.
Transparent for the RAG and the application -- we wrap the retriever, nothing
else changes (single responsibility). Disabled by default in the pipeline.
"""
def __init__(self, inner, reranker: CrossEncoderReranker,
mode: str = "replace", candidates: int = 30):
self.inner = inner
self.reranker = reranker
self.mode = mode
self.candidates = candidates
def search(self, query: str, k: int = 5) -> list[dict]:
results = self.inner.search(query, k=max(self.candidates, k))
return self.reranker.rerank(query, results, top_k=k, mode=self.mode)