use std::collections::{HashMap, HashSet};
use crate::model::Recollection;
pub(crate) const POOL_FACTOR: usize = 8;
pub(crate) const POOL_MIN: usize = 64;
pub(crate) fn pool_size(k: usize) -> usize {
k.saturating_mul(POOL_FACTOR).max(POOL_MIN)
}
#[derive(Debug, Clone)]
pub(crate) struct Candidate {
pub recollection: Recollection,
pub vector_score: f64,
pub graph_weight: f64,
}
#[derive(Debug, Clone)]
pub(crate) struct ScoredCandidate {
pub recollection: Recollection,
#[cfg_attr(not(feature = "context"), allow(dead_code))]
pub vector_norm: f64,
#[cfg_attr(not(feature = "context"), allow(dead_code))]
pub graph_weight: f64,
pub fused: f64,
}
pub(crate) fn fuse(
pool: Vec<Candidate>,
reached: &[Candidate],
k: usize,
graph_boost: f64,
) -> Vec<Recollection> {
fuse_scored(pool, reached, k, graph_boost)
.into_iter()
.map(|scored| scored.recollection)
.collect()
}
pub(crate) fn fuse_scored(
pool: Vec<Candidate>,
reached: &[Candidate],
k: usize,
graph_boost: f64,
) -> Vec<ScoredCandidate> {
let weights: HashMap<u64, f64> = reached
.iter()
.map(|c| (c.recollection.id, c.graph_weight))
.collect();
let max_score = pool
.iter()
.map(|c| c.vector_score)
.fold(f64::MIN, f64::max)
.max(f64::EPSILON);
let mut candidates = pool;
let present: HashSet<u64> = candidates.iter().map(|c| c.recollection.id).collect();
candidates.extend(
reached
.iter()
.filter(|c| !present.contains(&c.recollection.id))
.cloned(),
);
let mut scored: Vec<ScoredCandidate> = candidates
.into_iter()
.map(|candidate| {
let graph_weight = weights
.get(&candidate.recollection.id)
.copied()
.unwrap_or(0.0);
let vector_norm = candidate.vector_score.max(0.0) / max_score;
ScoredCandidate {
recollection: candidate.recollection,
vector_norm,
graph_weight,
fused: vector_norm + graph_boost * graph_weight,
}
})
.collect();
scored.sort_by(|a, b| b.fused.total_cmp(&a.fused));
scored.truncate(k);
scored
}
#[cfg(test)]
#[path = "fusion_tests.rs"]
mod tests;