1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
6pub enum Distance {
7 #[default]
9 Cosine,
10 L2,
12 Ip,
14}
15
16impl Distance {
17 pub fn tag(self) -> &'static str {
19 match self {
20 Distance::Cosine => "cosine",
21 Distance::L2 => "l2",
22 Distance::Ip => "ip",
23 }
24 }
25
26 pub fn parse(raw: &[u8]) -> Option<Distance> {
28 if raw.eq_ignore_ascii_case(b"cosine") {
29 Some(Distance::Cosine)
30 } else if raw.eq_ignore_ascii_case(b"l2") {
31 Some(Distance::L2)
32 } else if raw.eq_ignore_ascii_case(b"ip") {
33 Some(Distance::Ip)
34 } else {
35 None
36 }
37 }
38
39 #[inline]
41 pub(crate) fn eval(self, a: &[f32], b: &[f32]) -> f32 {
42 match self {
43 Distance::Cosine => 1.0 - dot(a, b),
45 Distance::L2 => l2(a, b),
46 Distance::Ip => -dot(a, b),
47 }
48 }
49
50 pub(crate) fn prepare(self, v: &mut [f32]) {
52 if self == Distance::Cosine {
53 let norm = dot(v, v).sqrt();
54 if norm > 0.0 {
55 for x in v.iter_mut() {
56 *x /= norm;
57 }
58 }
59 }
60 }
61}
62
63#[cfg(target_arch = "x86_64")]
76mod avx {
77 #[target_feature(enable = "avx2,fma")]
78 pub(super) unsafe fn dot(a: &[f32], b: &[f32]) -> f32 {
79 super::dot_lanes(a, b)
80 }
81
82 #[target_feature(enable = "avx2,fma")]
83 pub(super) unsafe fn l2(a: &[f32], b: &[f32]) -> f32 {
84 super::l2_lanes(a, b)
85 }
86}
87
88#[cfg(target_arch = "x86_64")]
89#[inline]
90fn has_avx2() -> bool {
91 use std::sync::atomic::{AtomicU8, Ordering};
92 static STATE: AtomicU8 = AtomicU8::new(0);
93 match STATE.load(Ordering::Relaxed) {
94 0 => {
95 let yes = std::arch::is_x86_feature_detected!("avx2")
96 && std::arch::is_x86_feature_detected!("fma");
97 STATE.store(if yes { 2 } else { 1 }, Ordering::Relaxed);
98 yes
99 }
100 s => s == 2,
101 }
102}
103
104#[inline]
105fn dot(a: &[f32], b: &[f32]) -> f32 {
106 #[cfg(target_arch = "x86_64")]
107 if has_avx2() {
108 return unsafe { avx::dot(a, b) };
110 }
111 dot_lanes(a, b)
112}
113
114#[inline]
115fn l2(a: &[f32], b: &[f32]) -> f32 {
116 #[cfg(target_arch = "x86_64")]
117 if has_avx2() {
118 return unsafe { avx::l2(a, b) };
120 }
121 l2_lanes(a, b)
122}
123
124#[allow(clippy::inline_always)]
127#[inline(always)]
128fn dot_lanes(a: &[f32], b: &[f32]) -> f32 {
129 let mut acc = [0.0f32; 8];
130 let (ac, ar) = a.split_at(a.len() - a.len() % 8);
131 let (bc, br) = b.split_at(ac.len().min(b.len()));
132 for (ca, cb) in ac.chunks_exact(8).zip(bc.chunks_exact(8)) {
133 for i in 0..8 {
134 acc[i] += ca[i] * cb[i];
135 }
136 }
137 let mut s: f32 = acc.iter().sum();
138 for (x, y) in ar.iter().zip(br) {
139 s += x * y;
140 }
141 s
142}
143
144#[allow(clippy::inline_always)]
146#[inline(always)]
147fn l2_lanes(a: &[f32], b: &[f32]) -> f32 {
148 let mut acc = [0.0f32; 8];
149 let (ac, ar) = a.split_at(a.len() - a.len() % 8);
150 let (bc, br) = b.split_at(ac.len().min(b.len()));
151 for (ca, cb) in ac.chunks_exact(8).zip(bc.chunks_exact(8)) {
152 for i in 0..8 {
153 let d = ca[i] - cb[i];
154 acc[i] += d * d;
155 }
156 }
157 let mut s: f32 = acc.iter().sum();
158 for (x, y) in ar.iter().zip(br) {
159 let d = x - y;
160 s += d * d;
161 }
162 s
163}
164
165pub fn parse_vector(raw: &[u8], dim: usize) -> Option<Vec<f32>> {
168 if let Some(csv) = raw.strip_prefix(b"csv:") {
169 let vals: Option<Vec<f32>> = std::str::from_utf8(csv)
170 .ok()?
171 .split(',')
172 .map(|s| s.trim().parse::<f32>().ok())
173 .collect();
174 let vals = vals?;
175 return (vals.len() == dim && vals.iter().all(|x| x.is_finite())).then_some(vals);
176 }
177 if raw.len() != dim * 4 {
178 return None;
179 }
180 let mut out = Vec::with_capacity(dim);
181 for chunk in raw.chunks_exact(4) {
182 let x = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
183 if !x.is_finite() {
184 return None;
185 }
186 out.push(x);
187 }
188 Some(out)
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn metrics_smaller_is_closer() {
197 let mut a = vec![1.0, 0.0];
198 let mut b = vec![0.9, 0.1];
199 let mut c = vec![-1.0, 0.0];
200 for v in [&mut a, &mut b, &mut c] {
201 Distance::Cosine.prepare(v);
202 }
203 assert!(Distance::Cosine.eval(&a, &b) < Distance::Cosine.eval(&a, &c));
204 assert!(Distance::L2.eval(&[0.0, 0.0], &[1.0, 1.0]) > Distance::L2.eval(&[0.0, 0.0], &[0.5, 0.5]));
205 assert!(Distance::Ip.eval(&[1.0, 1.0], &[2.0, 2.0]) < Distance::Ip.eval(&[1.0, 1.0], &[0.1, 0.1]));
206 }
207
208 #[test]
209 fn wire_formats() {
210 let mut raw = Vec::new();
211 for x in [1.0f32, -2.5, 3.25] {
212 raw.extend_from_slice(&x.to_le_bytes());
213 }
214 assert_eq!(parse_vector(&raw, 3), Some(vec![1.0, -2.5, 3.25]));
215 assert_eq!(parse_vector(&raw, 4), None, "dim mismatch");
216 assert_eq!(parse_vector(b"csv:1, 2.5, -3", 3), Some(vec![1.0, 2.5, -3.0]));
217 assert_eq!(parse_vector(b"csv:1,x,3", 3), None);
218 let mut nan = Vec::new();
219 for x in [1.0f32, f32::NAN] {
220 nan.extend_from_slice(&x.to_le_bytes());
221 }
222 assert_eq!(parse_vector(&nan, 2), None, "non-finite rejected");
223 assert!(Distance::parse(b"COSINE") == Some(Distance::Cosine));
224 assert!(Distance::parse(b"nope").is_none());
225 }
226}