use std::path::Path;
use anyhow::Result;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SearchHit {
pub id: u64,
pub score: f32,
}
impl PartialOrd for SearchHit {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(
other
.score
.total_cmp(&self.score)
.then_with(|| self.id.cmp(&other.id)),
)
}
}
pub trait VectorIndex: Send + Sync {
fn add(&mut self, vectors: &[Vec<f32>]) -> Result<()>;
fn add_with_ids(&mut self, vectors: &[Vec<f32>], ids: &[u64]) -> Result<()>;
fn remove(&mut self, ids: &[u64]) -> Result<()>;
fn search(&self, query: &[f32], k: usize) -> Result<Vec<SearchHit>>;
fn search_filtered(&self, query: &[f32], k: usize, allowlist: &[u64])
-> Result<Vec<SearchHit>>;
fn len(&self) -> usize;
fn is_empty(&self) -> bool;
fn dim(&self) -> usize;
fn save(&self, path: &Path) -> Result<()>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn search_hit_fields() {
let hit = SearchHit {
id: 42,
score: 0.95,
};
assert_eq!(hit.id, 42);
assert!((hit.score - 0.95).abs() < f32::EPSILON);
}
#[test]
fn search_hit_copy() {
let hit = SearchHit { id: 1, score: 0.5 };
let copied = hit; assert_eq!(copied.id, hit.id);
assert_eq!(copied.score, hit.score);
}
#[test]
fn search_hit_sort_descending_by_score() {
let mut hits = vec![
SearchHit { id: 1, score: 0.3 },
SearchHit { id: 2, score: 0.9 },
SearchHit { id: 3, score: 0.5 },
];
hits.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(hits[0].id, 2); assert_eq!(hits[1].id, 3);
assert_eq!(hits[2].id, 1);
}
#[test]
fn search_hit_tie_break_by_id() {
let mut hits = vec![
SearchHit { id: 30, score: 0.5 },
SearchHit { id: 10, score: 0.5 },
SearchHit { id: 20, score: 0.5 },
];
hits.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(hits[0].id, 10);
assert_eq!(hits[1].id, 20);
assert_eq!(hits[2].id, 30);
}
}