use std::hash::Hash;
use async_trait::async_trait;
use khive_score::DeterministicScore;
use crate::error::Result;
use khive_fusion::{fuse, FuseError, FusionStrategy};
use super::config::{HybridConfig, Query};
#[async_trait]
pub trait VectorSearch: Send + Sync {
type Id: Eq + Hash + Clone + Ord + Send + Sync;
async fn vector_search(
&self,
embedding: &[f32],
top_k: usize,
) -> Result<Vec<(Self::Id, DeterministicScore)>>;
}
#[async_trait]
pub trait KeywordSearch: Send + Sync {
type Id: Eq + Hash + Clone + Ord + Send + Sync;
async fn keyword_search(
&self,
text: &str,
top_k: usize,
) -> Result<Vec<(Self::Id, DeterministicScore)>>;
}
#[async_trait]
pub trait HybridSearcher: VectorSearch + KeywordSearch<Id = <Self as VectorSearch>::Id> {
async fn hybrid_search(
&self,
query: &Query,
config: &HybridConfig,
) -> Result<Vec<(<Self as VectorSearch>::Id, DeterministicScore)>>;
}
#[async_trait]
pub trait Reranker<Id: Send + Sync + 'static>: Send + Sync {
async fn rerank(
&self,
query: &str,
results: Vec<(Id, DeterministicScore)>,
top_k: usize,
) -> Result<Vec<(Id, DeterministicScore)>>;
}
pub fn fuse_search_results<Id: Eq + Hash + Clone + Ord>(
sources: Vec<Vec<(Id, DeterministicScore)>>,
config: &HybridConfig,
) -> std::result::Result<Vec<(Id, DeterministicScore)>, FuseError> {
let strategy = match &config.fusion_strategy {
FusionStrategy::Weighted { .. } => {
if sources.len() != 2 {
FusionStrategy::rrf()
} else {
let (v, k) = config.normalized_weights();
FusionStrategy::weighted(vec![v, k])
}
}
FusionStrategy::Custom { .. } => FusionStrategy::rrf(),
other => other.clone(),
};
let mut fused = fuse(sources, &strategy, config.top_k)?;
if let Some(min_score) = config.min_score {
fused.retain(|(_, score)| *score >= min_score);
}
Ok(fused)
}
pub fn fuse_search_results_checked<Id: Eq + Hash + Clone + Ord>(
sources: Vec<Vec<(Id, DeterministicScore)>>,
config: &HybridConfig,
) -> Result<Vec<(Id, DeterministicScore)>> {
match &config.fusion_strategy {
FusionStrategy::Custom { name, .. } => {
return Err(crate::error::RetrievalError::Fusion(format!(
"Custom strategy {name:?} requires runtime dispatch"
)));
}
FusionStrategy::Weighted { .. } if sources.len() != 2 => {
return Err(crate::error::RetrievalError::Fusion(format!(
"Weighted fusion requires exactly 2 sources, got {}",
sources.len()
)));
}
_ => {}
}
fuse_search_results(sources, config)
.map_err(|error| crate::error::RetrievalError::Fusion(error.to_string()))
}
#[cfg(test)]
#[path = "searcher/tests.rs"]
mod tests;