use std::marker::PhantomData;
use std::sync::Arc;
use async_trait::async_trait;
use khive_score::DeterministicScore;
use crate::error::{Result, RetrievalError};
use crate::hybrid::searcher::Reranker;
#[async_trait]
pub trait RerankDocumentResolver<Id>: Send + Sync
where
Id: Send + Sync + 'static,
{
async fn resolve_documents(&self, ids: &[Id]) -> Result<Vec<Option<String>>>;
}
pub trait CrossEncoderScorer: Send + Sync {
fn score_batch(&self, query: &str, documents: &[&str]) -> Vec<f32>;
}
pub struct NativeCrossEncoderReranker<Id, R, S>
where
Id: Clone + Send + Sync + 'static,
R: RerankDocumentResolver<Id>,
S: CrossEncoderScorer,
{
model: Arc<S>,
resolver: Arc<R>,
_id: PhantomData<fn() -> Id>,
}
impl<Id, R, S> NativeCrossEncoderReranker<Id, R, S>
where
Id: Clone + Send + Sync + 'static,
R: RerankDocumentResolver<Id>,
S: CrossEncoderScorer,
{
pub fn new(model: Arc<S>, resolver: Arc<R>) -> Self {
Self {
model,
resolver,
_id: PhantomData,
}
}
}
#[async_trait]
impl<Id, R, S> Reranker<Id> for NativeCrossEncoderReranker<Id, R, S>
where
Id: Clone + Send + Sync + 'static,
R: RerankDocumentResolver<Id>,
S: CrossEncoderScorer,
{
async fn rerank(
&self,
query: &str,
results: Vec<(Id, DeterministicScore)>,
top_k: usize,
) -> Result<Vec<(Id, DeterministicScore)>> {
if top_k == 0 || results.is_empty() {
return Ok(Vec::new());
}
let ids: Vec<Id> = results.iter().map(|(id, _)| id.clone()).collect();
let resolved = self.resolver.resolve_documents(&ids).await?;
if resolved.len() != results.len() {
return Err(RetrievalError::rerank(format!(
"resolver returned {} documents for {} candidates",
resolved.len(),
results.len()
)));
}
let mut documents: Vec<String> = Vec::with_capacity(resolved.len());
for (idx, opt) in resolved.into_iter().enumerate() {
let text = opt.ok_or_else(|| {
RetrievalError::rerank(format!(
"missing document text for rerank candidate at index {idx}"
))
})?;
documents.push(text);
}
let document_refs: Vec<&str> = documents.iter().map(String::as_str).collect();
let scores = self.model.score_batch(query, &document_refs);
if scores.len() != results.len() {
return Err(RetrievalError::rerank(format!(
"model returned {} scores for {} candidates",
scores.len(),
results.len()
)));
}
let mut scored: Vec<(usize, Id, f32)> = results
.into_iter()
.zip(scores)
.enumerate()
.map(|(idx, ((id, _), score))| (idx, id, score))
.collect();
scored.sort_by(|a, b| b.2.total_cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
Ok(scored
.into_iter()
.take(top_k)
.map(|(_, id, score)| (id, DeterministicScore::from_f64(score as f64)))
.collect())
}
}
#[cfg(test)]
#[path = "cross_encoder/tests.rs"]
mod tests;