ic_rig/embeddings/
distance.rs1use super::Embedding;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum DistanceMetric {
24 Cosine { normalized: bool },
30
31 Angular { normalized: bool },
35
36 Euclidean,
38
39 Manhattan,
41
42 Chebyshev,
44
45 DotProduct,
47}
48
49impl DistanceMetric {
50 pub fn score(&self, a: &Embedding, b: &Embedding) -> f64 {
52 match self {
53 Self::Cosine { normalized } => a.cosine_similarity(b, *normalized),
54 Self::Angular { normalized } => a.angular_distance(b, *normalized),
55 Self::Euclidean => a.euclidean_distance(b),
56 Self::Manhattan => a.manhattan_distance(b),
57 Self::Chebyshev => a.chebyshev_distance(b),
58 Self::DotProduct => a.dot_product(b),
59 }
60 }
61
62 pub fn higher_is_better(&self) -> bool {
65 matches!(self, Self::Cosine { .. } | Self::DotProduct)
66 }
67}
68
69pub trait VectorDistance {
71 fn dot_product(&self, other: &Self) -> f64;
73
74 fn cosine_similarity(&self, other: &Self, normalized: bool) -> f64;
80
81 fn angular_distance(&self, other: &Self, normalized: bool) -> f64;
83
84 fn euclidean_distance(&self, other: &Self) -> f64;
86
87 fn manhattan_distance(&self, other: &Self) -> f64;
89
90 fn chebyshev_distance(&self, other: &Self) -> f64;
92}
93
94impl VectorDistance for Embedding {
95 fn dot_product(&self, other: &Self) -> f64 {
96 self.vec.iter().zip(&other.vec).map(|(a, b)| a * b).sum()
97 }
98
99 fn cosine_similarity(&self, other: &Self, normalized: bool) -> f64 {
100 let dot = self.dot_product(other);
101 if normalized {
102 dot
103 } else {
104 let mag_a: f64 = self.vec.iter().map(|x| x * x).sum::<f64>().sqrt();
105 let mag_b: f64 = other.vec.iter().map(|x| x * x).sum::<f64>().sqrt();
106 dot / (mag_a * mag_b)
107 }
108 }
109
110 fn angular_distance(&self, other: &Self, normalized: bool) -> f64 {
111 self.cosine_similarity(other, normalized).acos() / std::f64::consts::PI
112 }
113
114 fn euclidean_distance(&self, other: &Self) -> f64 {
115 self.vec
116 .iter()
117 .zip(&other.vec)
118 .map(|(a, b)| (a - b).powi(2))
119 .sum::<f64>()
120 .sqrt()
121 }
122
123 fn manhattan_distance(&self, other: &Self) -> f64 {
124 self.vec.iter().zip(&other.vec).map(|(a, b)| (a - b).abs()).sum()
125 }
126
127 fn chebyshev_distance(&self, other: &Self) -> f64 {
128 self.vec
129 .iter()
130 .zip(&other.vec)
131 .map(|(a, b)| (a - b).abs())
132 .fold(0.0_f64, f64::max)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 fn pair() -> (Embedding, Embedding) {
141 (
142 Embedding { document: "a".into(), vec: vec![1.0, 2.0, 3.0] },
143 Embedding { document: "b".into(), vec: vec![1.0, 5.0, 7.0] },
144 )
145 }
146
147 #[test]
148 fn dot_product() {
149 let (a, b) = pair();
150 assert_eq!(a.dot_product(&b), 32.0);
151 }
152
153 #[test]
154 fn cosine_similarity() {
155 let (a, b) = pair();
156 assert!((a.cosine_similarity(&b, false) - 0.9875414397573881).abs() < 1e-10);
157 }
158
159 #[test]
160 fn euclidean_distance() {
161 let (a, b) = pair();
162 assert_eq!(a.euclidean_distance(&b), 5.0);
163 }
164
165 #[test]
166 fn manhattan_distance() {
167 let (a, b) = pair();
168 assert_eq!(a.manhattan_distance(&b), 7.0);
169 }
170
171 #[test]
172 fn chebyshev_distance() {
173 let (a, b) = pair();
174 assert_eq!(a.chebyshev_distance(&b), 4.0);
175 }
176}