Skip to main content

rig_core/embeddings/
distance.rs

1//! Similarity and distance of embedding vectors.
2//!
3//! [`Embedding`](crate::embeddings::Embedding) reductions use fixed chunks and
4//! left-to-right summation for reproducible ordering.
5//!
6//! ```
7//! use rig_core::embeddings::{Embedding, distance::VectorDistance};
8//!
9//! let vector = Embedding { document: String::new(), vec: vec![1.0, 0.0] };
10//! assert_eq!(vector.cosine_similarity(&vector, true), 1.0);
11//! assert_eq!(vector.euclidean_distance(&vector), 0.0);
12//! ```
13
14/// Similarity and distance metrics of embedding vectors.
15/// Supply equal-length vectors; the embedding implementation pairs only their
16/// shared prefix. Unnormalized cosine requires nonzero magnitudes.
17pub trait VectorDistance {
18    /// The dot product.
19    fn dot_product(&self, other: &Self) -> f64;
20
21    /// Cosine similarity, from -1 to 1.
22    /// If `normalized` is true, the vectors are taken as unit length and the
23    /// dot product is returned.
24    fn cosine_similarity(&self, other: &Self, normalized: bool) -> f64;
25
26    /// The angle between the vectors as a fraction of pi, from 0 (same
27    /// direction) to 1 (opposite). `normalized` is as for
28    /// [`Self::cosine_similarity`].
29    fn angular_distance(&self, other: &Self, normalized: bool) -> f64 {
30        // Roundoff can push a valid cosine beyond the domain of acos.
31        self.cosine_similarity(other, normalized)
32            .clamp(-1.0, 1.0)
33            .acos()
34            / std::f64::consts::PI
35    }
36
37    /// The straight-line (L2) distance.
38    fn euclidean_distance(&self, other: &Self) -> f64;
39
40    /// The sum of the coordinates' absolute differences (L1).
41    fn manhattan_distance(&self, other: &Self) -> f64;
42
43    /// The largest absolute difference of a coordinate (L-infinity).
44    fn chebyshev_distance(&self, other: &Self) -> f64;
45}
46
47/// Reduction chunk size. Preserve left-to-right summation within and across
48/// chunks to keep floating-point results reproducible.
49const CHUNK: usize = 256;
50
51/// `term` of each pair of `a` and `b` summed in fixed chunks, left to right
52/// within and across them.
53fn pair_sum(a: &[f64], b: &[f64], term: impl Fn(f64, f64) -> f64) -> f64 {
54    a.chunks(CHUNK)
55        .zip(b.chunks(CHUNK))
56        .map(|(a, b)| a.iter().zip(b).map(|(x, y)| term(*x, *y)).sum::<f64>())
57        .sum()
58}
59
60impl VectorDistance for crate::embeddings::Embedding {
61    fn dot_product(&self, other: &Self) -> f64 {
62        dot(&self.vec, &other.vec)
63    }
64
65    fn cosine_similarity(&self, other: &Self, normalized: bool) -> f64 {
66        let dot_product = dot(&self.vec, &other.vec);
67        if normalized {
68            return dot_product;
69        }
70        dot_product / (magnitude(&self.vec) * magnitude(&other.vec))
71    }
72
73    fn euclidean_distance(&self, other: &Self) -> f64 {
74        pair_sum(&self.vec, &other.vec, |x, y| (x - y).powi(2)).sqrt()
75    }
76
77    fn manhattan_distance(&self, other: &Self) -> f64 {
78        pair_sum(&self.vec, &other.vec, |x, y| (x - y).abs())
79    }
80
81    fn chebyshev_distance(&self, other: &Self) -> f64 {
82        self.vec
83            .iter()
84            .zip(&other.vec)
85            .map(|(x, y)| (x - y).abs())
86            .fold(0.0, f64::max)
87    }
88}
89
90/// The dot product of `a` and `b`, summed as [`VectorDistance`] sums it.
91pub(crate) fn dot(a: &[f64], b: &[f64]) -> f64 {
92    pair_sum(a, b, |x, y| x * y)
93}
94
95/// The length of `vec`, summed as [`VectorDistance`] sums it.
96pub(crate) fn magnitude(vec: &[f64]) -> f64 {
97    pair_sum(vec, vec, |x, _| x.powi(2)).sqrt()
98}
99
100#[cfg(test)]
101mod tests;