use crate::search::BM25SearchResult;
use anyhow::{Context, Result};
use ndarray::Array2;
use ort::session::{builder::GraphOptimizationLevel, Session, SessionInputValue};
use ort::value::Value;
use std::borrow::Cow;
use std::path::Path;
use tokenizers::{EncodeInput, InputSequence, Tokenizer};
const MAX_LENGTH: usize = 512;
pub struct Reranker {
session: Session,
tokenizer: Tokenizer,
}
impl Reranker {
pub fn new(model_path: &Path) -> Result<Self> {
let model_file = model_path.join("model.onnx");
let tokenizer_file = model_path.join("tokenizer.json");
let session = Session::builder()?
.with_optimization_level(GraphOptimizationLevel::Level3)?
.commit_from_file(&model_file)
.context("Failed to load ONNX model")?;
let tokenizer = Tokenizer::from_file(tokenizer_file)
.map_err(|e| anyhow::anyhow!("Failed to load tokenizer: {}", e))?;
Ok(Self { session, tokenizer })
}
pub fn rerank(
&mut self,
query: &str,
mut results: Vec<BM25SearchResult>,
limit: usize,
) -> Result<Vec<crate::search::SearchResult>> {
if results.is_empty() {
return Ok(vec![]);
}
let pairs: Vec<(String, String)> = results
.iter()
.map(|r| {
let mut doc_parts = vec![r.file_path.clone()];
if let Some(sig) = &r.signature {
doc_parts.push(sig.clone());
}
if let Some(doc) = &r.doc_comment {
doc_parts.push(doc.clone());
}
let prefix_len: usize =
doc_parts.iter().map(|s| s.chars().count()).sum::<usize>() + doc_parts.len(); let remaining = 500usize.saturating_sub(prefix_len);
if remaining > 50 {
let body: String = r.content.chars().take(remaining).collect();
doc_parts.push(body);
}
let doc_text: String = doc_parts.join("\n").chars().take(500).collect();
(query.to_string(), doc_text)
})
.collect();
let scores = self.score_pairs(&pairs)?;
for (result, score) in results.iter_mut().zip(scores.iter()) {
result.score = *score;
}
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
Ok(results.into_iter().take(limit).map(|r| r.into()).collect())
}
fn score_pairs(&mut self, pairs: &[(String, String)]) -> Result<Vec<f32>> {
let batch_size = 8;
let mut all_scores = Vec::new();
for batch in pairs.chunks(batch_size) {
let inputs: Vec<EncodeInput> = batch
.iter()
.map(|(q, d)| {
EncodeInput::Dual(
InputSequence::Raw(q.clone().into()),
InputSequence::Raw(d.clone().into()),
)
})
.collect();
let encodings = self
.tokenizer
.encode_batch(inputs, true)
.map_err(|e| anyhow::anyhow!("Tokenization failed: {}", e))?;
let batch_len = encodings.len();
let max_len = encodings
.iter()
.map(|e| e.len())
.max()
.unwrap_or(0)
.min(MAX_LENGTH);
let mut input_ids_vec = vec![0i64; batch_len * max_len];
let mut attention_mask_vec = vec![0i64; batch_len * max_len];
let mut token_type_ids_vec = vec![0i64; batch_len * max_len];
for (i, encoding) in encodings.iter().enumerate() {
let ids = encoding.get_ids();
let mask = encoding.get_attention_mask();
let type_ids = encoding.get_type_ids();
let len = ids.len().min(max_len);
for j in 0..len {
input_ids_vec[i * max_len + j] = ids[j] as i64;
attention_mask_vec[i * max_len + j] = mask[j] as i64;
token_type_ids_vec[i * max_len + j] = type_ids[j] as i64;
}
}
let input_ids: Array2<i64> =
Array2::from_shape_vec((batch_len, max_len), input_ids_vec)?;
let attention_mask: Array2<i64> =
Array2::from_shape_vec((batch_len, max_len), attention_mask_vec)?;
let token_type_ids: Array2<i64> =
Array2::from_shape_vec((batch_len, max_len), token_type_ids_vec)?;
let input_ids_value = Value::from_array(input_ids)?;
let attention_mask_value = Value::from_array(attention_mask)?;
let token_type_ids_value = Value::from_array(token_type_ids)?;
let inputs: Vec<(Cow<'_, str>, SessionInputValue<'_>)> = vec![
(
Cow::Borrowed("input_ids"),
SessionInputValue::from(&input_ids_value),
),
(
Cow::Borrowed("attention_mask"),
SessionInputValue::from(&attention_mask_value),
),
(
Cow::Borrowed("token_type_ids"),
SessionInputValue::from(&token_type_ids_value),
),
];
let outputs = self.session.run(inputs)?;
let output = outputs
.get("logits")
.or_else(|| outputs.get("output"))
.context("Model output not found")?;
let output_array = output.try_extract_array::<f32>()?;
let shape: Vec<usize> = output_array.shape().to_vec();
if shape.len() == 2 && shape[1] == 1 {
for i in 0..batch_len {
all_scores.push(output_array[[i, 0]]);
}
} else if shape.len() == 1 {
for i in 0..batch_len {
all_scores.push(output_array[[i]]);
}
} else {
for i in 0..batch_len {
let score = if shape.len() == 2 {
output_array[[i, 0]]
} else {
let mut sum = 0.0f32;
let dim = shape.get(1).copied().unwrap_or(1);
for j in 0..dim {
sum += output_array[[i, j]];
}
sum / dim as f32
};
all_scores.push(score);
}
}
}
Ok(all_scores)
}
}