use std::collections::BTreeSet;
use crate::model::Recollection;
use crate::rerank::{RerankError, Reranker};
pub(crate) fn terms(text: &str) -> BTreeSet<String> {
text.split(|c: char| !c.is_alphanumeric())
.filter(|term| !term.is_empty())
.map(str::to_lowercase)
.collect()
}
#[allow(clippy::cast_precision_loss)] pub(crate) fn lexical_relevance(query_terms: &BTreeSet<String>, content: &str) -> f32 {
if query_terms.is_empty() {
return 0.0;
}
let content_terms = terms(content);
let overlap = query_terms.intersection(&content_terms).count();
overlap as f32 / query_terms.len() as f32
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DeterministicReranker;
impl Reranker for DeterministicReranker {
fn rerank(
&self,
query: &str,
candidates: Vec<Recollection>,
) -> Result<Vec<Recollection>, RerankError> {
let query_terms = terms(query);
let mut indexed: Vec<(usize, f32, Recollection)> = candidates
.into_iter()
.enumerate()
.map(|(position, candidate)| {
let score = lexical_relevance(&query_terms, &candidate.content);
(position, score, candidate)
})
.collect();
indexed.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
Ok(indexed
.into_iter()
.map(|(_, _, candidate)| candidate)
.collect())
}
}
#[cfg(test)]
#[path = "relevance_tests.rs"]
mod tests;