MedSpace / src /retrieval /reranker.py
kbsss's picture
Upload folder using huggingface_hub
f373e2b verified
Raw
History Blame Contribute Delete
6.29 kB
"""
Cross-encoder reranking for improved retrieval precision.
Based on RAG skill patterns for production-grade reranking.
"""
from typing import List, Tuple, Optional
from dataclasses import dataclass
# Optional torch import
try:
import torch
TORCH_AVAILABLE = True
except ImportError:
TORCH_AVAILABLE = False
@dataclass
class RerankResult:
"""Result from reranking."""
text: str
score: float
original_index: int
metadata: dict
class CrossEncoderReranker:
"""
Rerank documents using cross-encoder model.
Cross-encoders jointly encode query-document pairs for more
accurate relevance scoring than bi-encoder similarity.
"""
SUPPORTED_MODELS = {
"ms-marco-mini": "cross-encoder/ms-marco-MiniLM-L-6-v2",
"ms-marco-base": "cross-encoder/ms-marco-TinyBERT-L-2-v2",
"bge-reranker": "BAAI/bge-reranker-base"
}
def __init__(
self,
model_name: str = "ms-marco-mini",
device: str = None,
batch_size: int = 32
):
"""
Initialize reranker.
Args:
model_name: Name of cross-encoder model or full HF path
device: Device to use (cuda/cpu), auto-detected if None
batch_size: Batch size for scoring
"""
from sentence_transformers import CrossEncoder
if device:
self.device = device
elif TORCH_AVAILABLE:
self.device = "cuda" if torch.cuda.is_available() else "cpu"
else:
self.device = "cpu"
self.batch_size = batch_size
# Resolve model name
if model_name in self.SUPPORTED_MODELS:
model_path = self.SUPPORTED_MODELS[model_name]
else:
model_path = model_name
self.model = CrossEncoder(model_path, device=self.device)
self.model_name = model_name
print(f"✅ Reranker initialized: {model_path} on {self.device}")
def rerank(
self,
query: str,
documents: List,
top_k: int = 5,
return_scores: bool = False
) -> List:
"""
Rerank documents by relevance to query.
Args:
query: Search query
documents: List of documents (strings or objects with .content)
top_k: Number of top documents to return
return_scores: If True, return RerankResult objects
Returns:
Reranked documents (top_k)
"""
if not documents:
return []
# Extract text content
doc_texts = []
for doc in documents:
if hasattr(doc, 'content'):
doc_texts.append(doc.content)
elif isinstance(doc, dict):
doc_texts.append(doc.get('content', str(doc)))
else:
doc_texts.append(str(doc))
# Create query-document pairs
pairs = [[query, doc_text] for doc_text in doc_texts]
# Get relevance scores
scores = self.model.predict(pairs, batch_size=self.batch_size)
# Create indexed scores
indexed_scores = list(enumerate(scores))
# Sort by score descending
indexed_scores.sort(key=lambda x: x[1], reverse=True)
# Take top k
top_results = indexed_scores[:top_k]
if return_scores:
# Return RerankResult objects
results = []
for idx, score in top_results:
doc = documents[idx]
metadata = {}
if hasattr(doc, 'metadata'):
metadata = doc.metadata
elif isinstance(doc, dict):
metadata = {k: v for k, v in doc.items() if k != 'content'}
results.append(RerankResult(
text=doc_texts[idx],
score=float(score),
original_index=idx,
metadata=metadata
))
return results
else:
# Return original documents in new order
return [documents[idx] for idx, _ in top_results]
def score_pair(self, query: str, document: str) -> float:
"""Score a single query-document pair."""
return float(self.model.predict([[query, document]])[0])
class CohereReranker:
"""
Reranker using Cohere API (optional, requires API key).
"""
def __init__(
self,
api_key: str = None,
model: str = "rerank-english-v3.0"
):
"""
Initialize Cohere reranker.
Args:
api_key: Cohere API key (or set COHERE_API_KEY env var)
model: Cohere rerank model name
"""
import os
try:
import cohere
except ImportError:
raise ImportError("Install cohere: pip install cohere")
self.api_key = api_key or os.environ.get("COHERE_API_KEY")
if not self.api_key:
raise ValueError("Cohere API key required")
self.client = cohere.Client(api_key=self.api_key)
self.model = model
def rerank(
self,
query: str,
documents: List,
top_k: int = 5
) -> List[RerankResult]:
"""Rerank using Cohere API."""
# Extract text content
doc_texts = []
for doc in documents:
if hasattr(doc, 'content'):
doc_texts.append(doc.content)
elif isinstance(doc, dict):
doc_texts.append(doc.get('content', str(doc)))
else:
doc_texts.append(str(doc))
response = self.client.rerank(
query=query,
documents=doc_texts,
top_n=top_k,
model=self.model,
return_documents=True
)
results = []
for result in response.results:
results.append(RerankResult(
text=result.document.text,
score=result.relevance_score,
original_index=result.index,
metadata={}
))
return results