use super::{Candidate, cosine_dist};
use crate::materialize::PackedVectors;
#[allow(dead_code)]
pub(super) fn select_neighbors_simple(candidates: &[Candidate], m: usize) -> Vec<u32> {
let mut sorted: Vec<Candidate> = candidates.to_vec();
sorted.sort_by(|a, b| crate::order::cmp_dist_asc(a.dist, b.dist).then_with(|| a.id.cmp(&b.id)));
sorted.truncate(m);
sorted.into_iter().map(|c| c.id).collect()
}
#[allow(clippy::too_many_arguments)]
pub(super) fn select_neighbors_heuristic(
candidates: &[Candidate],
m: usize,
store: &PackedVectors,
dim: usize,
sa: &mut [f32],
sb: &mut [f32],
keep_pruned: bool,
) -> Vec<u32> {
if candidates.is_empty() || m == 0 {
return Vec::new();
}
let mut working: Vec<Candidate> = candidates.to_vec();
working
.sort_by(|a, b| crate::order::cmp_dist_asc(a.dist, b.dist).then_with(|| a.id.cmp(&b.id)));
let mut chosen: Vec<Candidate> = Vec::with_capacity(m);
let mut pruned: Vec<Candidate> = Vec::new();
for cand in working {
if chosen.len() >= m {
break;
}
let cand_row = store.row(cand.id as usize, dim, sa);
let mut shadowed = false;
for chosen_one in &chosen {
let chosen_row = store.row(chosen_one.id as usize, dim, sb);
let d_cc = cosine_dist(cand_row, chosen_row);
if d_cc < cand.dist {
shadowed = true;
break;
}
}
if shadowed {
pruned.push(cand);
} else {
chosen.push(cand);
}
}
if keep_pruned && chosen.len() < m {
for cand in pruned {
if chosen.len() >= m {
break;
}
chosen.push(cand);
}
}
chosen.into_iter().map(|c| c.id).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn cand(id: u32, dist: f32) -> Candidate {
Candidate { id, dist }
}
#[test]
fn simple_keeps_top_m_by_distance() {
let cs = vec![cand(0, 0.5), cand(1, 0.1), cand(2, 0.3), cand(3, 0.2)];
assert_eq!(select_neighbors_simple(&cs, 2), vec![1, 3]);
assert_eq!(select_neighbors_simple(&cs, 4), vec![1, 3, 2, 0]);
}
#[test]
fn simple_breaks_ties_by_id() {
let cs = vec![cand(2, 0.1), cand(0, 0.1), cand(1, 0.1)];
assert_eq!(select_neighbors_simple(&cs, 2), vec![0, 1]);
}
#[test]
fn heuristic_returns_at_most_m_and_includes_closest() {
let vectors = vec![
1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, -1.0, ];
let cs = vec![cand(0, 0.0), cand(1, 0.5), cand(2, 1.0), cand(3, 1.5)];
let store = PackedVectors::F32(vectors);
let mut sa = store.scratch(2);
let mut sb = store.scratch(2);
let picked = select_neighbors_heuristic(&cs, 2, &store, 2, &mut sa, &mut sb, true);
assert!(picked.len() <= 2, "len={} > 2", picked.len());
assert!(picked.contains(&0), "closest must always be picked");
}
#[test]
fn heuristic_drops_shadowed_candidate() {
let vectors = vec![
1.0, 0.0, 0.99, 0.01, 0.0, 1.0, ];
let cs = vec![cand(0, 0.05), cand(1, 0.06), cand(2, 1.0)];
let store = PackedVectors::F32(vectors);
let mut sa = store.scratch(2);
let mut sb = store.scratch(2);
let picked = select_neighbors_heuristic(&cs, 2, &store, 2, &mut sa, &mut sb, true);
assert_eq!(picked.len(), 2);
assert!(picked.contains(&0));
assert!(
picked.contains(&2),
"heuristic should reject shadowed 1 in favor of orthogonal 2: got {:?}",
picked
);
}
#[test]
fn heuristic_refills_when_under_m() {
let vectors = vec![
1.0, 0.0, 0.99, 0.01, 0.98, 0.02, ];
let cs = vec![cand(0, 0.05), cand(1, 0.06), cand(2, 0.07)];
let store = PackedVectors::F32(vectors);
let mut sa = store.scratch(2);
let mut sb = store.scratch(2);
let picked = select_neighbors_heuristic(&cs, 3, &store, 2, &mut sa, &mut sb, true);
assert_eq!(picked.len(), 3);
assert_eq!(picked, vec![0, 1, 2]);
}
}