1#[derive(Debug, Clone, thiserror::Error)]
6#[non_exhaustive]
7pub enum MathError {
8 #[error("vector length mismatch: {0} vs {1}")]
10 LengthMismatch(usize, usize),
11}
12
13pub fn cosine_similarity(a: &[f32], b: &[f32]) -> Result<f32, MathError> {
32 if a.len() != b.len() {
33 return Err(MathError::LengthMismatch(a.len(), b.len()));
34 }
35
36 let dot_product: f64 = a
38 .iter()
39 .zip(b.iter())
40 .map(|(x, y)| (*x as f64) * (*y as f64))
41 .sum();
42 let norm_a: f64 = a
43 .iter()
44 .map(|x| (*x as f64) * (*x as f64))
45 .sum::<f64>()
46 .sqrt();
47 let norm_b: f64 = b
48 .iter()
49 .map(|x| (*x as f64) * (*x as f64))
50 .sum::<f64>()
51 .sqrt();
52
53 const MIN_NORM: f64 = 1e-8;
58 if norm_a < MIN_NORM || norm_b < MIN_NORM {
59 return Ok(0.0);
60 }
61
62 Ok((dot_product / (norm_a * norm_b)) as f32)
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn test_identical_vectors() {
71 let v = vec![1.0, 2.0, 3.0];
72 let sim = cosine_similarity(&v, &v).unwrap();
73 assert!((sim - 1.0).abs() < 1e-6);
74 }
75
76 #[test]
77 fn test_orthogonal_vectors() {
78 let a = vec![1.0, 0.0, 0.0];
79 let b = vec![0.0, 1.0, 0.0];
80 let sim = cosine_similarity(&a, &b).unwrap();
81 assert!((sim - 0.0).abs() < 1e-6);
82 }
83
84 #[test]
85 fn test_opposite_vectors() {
86 let a = vec![1.0, 0.0];
87 let b = vec![-1.0, 0.0];
88 let sim = cosine_similarity(&a, &b).unwrap();
89 assert!((sim - (-1.0)).abs() < 1e-6);
90 }
91
92 #[test]
93 fn test_different_lengths_returns_error() {
94 let a = vec![1.0, 2.0];
95 let b = vec![1.0];
96 assert!(cosine_similarity(&a, &b).is_err());
97 }
98
99 #[test]
100 fn test_zero_vector() {
101 let a = vec![0.0, 0.0];
102 let b = vec![1.0, 2.0];
103 assert_eq!(cosine_similarity(&a, &b).unwrap(), 0.0);
104 }
105
106 #[test]
107 fn test_near_zero_vector_is_degenerate() {
108 let tiny = vec![1e-10_f32, 0.0, 0.0];
111 let unit = vec![1.0_f32, 0.0, 0.0];
112 assert_eq!(cosine_similarity(&tiny, &unit).unwrap(), 0.0);
113 }
114}