use crate::distance::DistanceMetric;
use smallvec::SmallVec;
pub trait DistanceEngine: Send + Sync {
fn distance(&self, a: &[f32], b: &[f32]) -> f32;
fn batch_distance(&self, query: &[f32], candidates: &[&[f32]]) -> Vec<f32> {
candidates.iter().map(|c| self.distance(query, c)).collect()
}
fn metric(&self) -> DistanceMetric;
#[must_use]
fn is_pre_normalized(&self) -> bool {
false
}
}
#[inline]
pub(crate) fn batch_distance_with_prefetch(
engine: &impl DistanceEngine,
query: &[f32],
candidates: &[&[f32]],
) -> SmallVec<[f32; 64]> {
let prefetch_distance = crate::simd_native::calculate_prefetch_distance(query.len());
let mut results = SmallVec::with_capacity(candidates.len());
for (i, candidate) in candidates.iter().enumerate() {
if i + prefetch_distance < candidates.len() {
crate::simd_native::prefetch_vector(candidates[i + prefetch_distance]);
}
results.push(engine.distance(query, candidate));
}
results
}
pub struct CpuDistance {
metric: DistanceMetric,
}
impl CpuDistance {
#[must_use]
pub fn new(metric: DistanceMetric) -> Self {
Self { metric }
}
}
impl DistanceEngine for CpuDistance {
fn distance(&self, a: &[f32], b: &[f32]) -> f32 {
#[cfg(feature = "internal-bench")]
crate::index::hnsw::eval_count::record_eval();
match self.metric {
DistanceMetric::Cosine => cosine_distance_scalar(a, b),
DistanceMetric::Euclidean => euclidean_distance_scalar(a, b),
DistanceMetric::DotProduct => dot_product_scalar(a, b),
DistanceMetric::Hamming => hamming_distance_scalar(a, b),
DistanceMetric::Jaccard => jaccard_distance_scalar(a, b),
}
}
fn metric(&self) -> DistanceMetric {
self.metric
}
}
pub struct CachedSimdDistance {
metric: DistanceMetric,
engine: crate::simd_native::DistanceEngine,
pre_normalized: bool,
}
impl CachedSimdDistance {
#[must_use]
pub fn new(metric: DistanceMetric, dimension: usize) -> Self {
Self {
metric,
engine: crate::simd_native::DistanceEngine::new(dimension),
pre_normalized: false,
}
}
#[must_use]
pub fn new_prenormalized(metric: DistanceMetric, dimension: usize) -> Self {
Self {
metric,
engine: crate::simd_native::DistanceEngine::new(dimension),
pre_normalized: metric == DistanceMetric::Cosine,
}
}
}
impl DistanceEngine for CachedSimdDistance {
#[allow(clippy::inline_always)] #[inline(always)]
fn distance(&self, a: &[f32], b: &[f32]) -> f32 {
#[cfg(feature = "internal-bench")]
crate::index::hnsw::eval_count::record_eval();
match self.metric {
DistanceMetric::Cosine if self.pre_normalized => {
1.0 - self.engine.dot_product(a, b).clamp(-1.0, 1.0)
}
DistanceMetric::Cosine => 1.0 - self.engine.cosine_similarity(a, b),
DistanceMetric::Euclidean => self.engine.euclidean_squared(a, b),
DistanceMetric::DotProduct => -self.engine.dot_product(a, b),
DistanceMetric::Hamming => self.engine.hamming(a, b),
DistanceMetric::Jaccard => 1.0 - self.engine.jaccard(a, b),
}
}
fn batch_distance(&self, query: &[f32], candidates: &[&[f32]]) -> Vec<f32> {
batch_distance_with_prefetch(self, query, candidates).into_vec()
}
fn metric(&self) -> DistanceMetric {
self.metric
}
fn is_pre_normalized(&self) -> bool {
self.pre_normalized
}
}
#[inline]
fn cosine_distance_scalar(a: &[f32], b: &[f32]) -> f32 {
1.0 - crate::simd_native::scalar::cosine_scalar(a, b)
}
#[inline]
fn euclidean_distance_scalar(a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).powi(2))
.sum::<f32>()
.sqrt()
}
#[inline]
fn dot_product_scalar(a: &[f32], b: &[f32]) -> f32 {
-a.iter().zip(b.iter()).map(|(x, y)| x * y).sum::<f32>()
}
#[inline]
fn hamming_distance_scalar(a: &[f32], b: &[f32]) -> f32 {
crate::simd_native::scalar::hamming_scalar(a, b)
}
#[inline]
fn jaccard_distance_scalar(a: &[f32], b: &[f32]) -> f32 {
1.0 - crate::simd_native::scalar::jaccard_scalar(a, b)
}