use super::Embedding;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DistanceMetric {
Cosine { normalized: bool },
Angular { normalized: bool },
Euclidean,
Manhattan,
Chebyshev,
DotProduct,
}
impl DistanceMetric {
pub fn score(&self, a: &Embedding, b: &Embedding) -> f64 {
match self {
Self::Cosine { normalized } => a.cosine_similarity(b, *normalized),
Self::Angular { normalized } => a.angular_distance(b, *normalized),
Self::Euclidean => a.euclidean_distance(b),
Self::Manhattan => a.manhattan_distance(b),
Self::Chebyshev => a.chebyshev_distance(b),
Self::DotProduct => a.dot_product(b),
}
}
pub fn higher_is_better(&self) -> bool {
matches!(self, Self::Cosine { .. } | Self::DotProduct)
}
}
pub trait VectorDistance {
fn dot_product(&self, other: &Self) -> f64;
fn cosine_similarity(&self, other: &Self, normalized: bool) -> f64;
fn angular_distance(&self, other: &Self, normalized: bool) -> f64;
fn euclidean_distance(&self, other: &Self) -> f64;
fn manhattan_distance(&self, other: &Self) -> f64;
fn chebyshev_distance(&self, other: &Self) -> f64;
}
impl VectorDistance for Embedding {
fn dot_product(&self, other: &Self) -> f64 {
self.vec.iter().zip(&other.vec).map(|(a, b)| a * b).sum()
}
fn cosine_similarity(&self, other: &Self, normalized: bool) -> f64 {
let dot = self.dot_product(other);
if normalized {
dot
} else {
let mag_a: f64 = self.vec.iter().map(|x| x * x).sum::<f64>().sqrt();
let mag_b: f64 = other.vec.iter().map(|x| x * x).sum::<f64>().sqrt();
dot / (mag_a * mag_b)
}
}
fn angular_distance(&self, other: &Self, normalized: bool) -> f64 {
self.cosine_similarity(other, normalized).acos() / std::f64::consts::PI
}
fn euclidean_distance(&self, other: &Self) -> f64 {
self.vec
.iter()
.zip(&other.vec)
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt()
}
fn manhattan_distance(&self, other: &Self) -> f64 {
self.vec.iter().zip(&other.vec).map(|(a, b)| (a - b).abs()).sum()
}
fn chebyshev_distance(&self, other: &Self) -> f64 {
self.vec
.iter()
.zip(&other.vec)
.map(|(a, b)| (a - b).abs())
.fold(0.0_f64, f64::max)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pair() -> (Embedding, Embedding) {
(
Embedding { document: "a".into(), vec: vec![1.0, 2.0, 3.0] },
Embedding { document: "b".into(), vec: vec![1.0, 5.0, 7.0] },
)
}
#[test]
fn dot_product() {
let (a, b) = pair();
assert_eq!(a.dot_product(&b), 32.0);
}
#[test]
fn cosine_similarity() {
let (a, b) = pair();
assert!((a.cosine_similarity(&b, false) - 0.9875414397573881).abs() < 1e-10);
}
#[test]
fn euclidean_distance() {
let (a, b) = pair();
assert_eq!(a.euclidean_distance(&b), 5.0);
}
#[test]
fn manhattan_distance() {
let (a, b) = pair();
assert_eq!(a.manhattan_distance(&b), 7.0);
}
#[test]
fn chebyshev_distance() {
let (a, b) = pair();
assert_eq!(a.chebyshev_distance(&b), 4.0);
}
}