Skip to main content

diskann_quantization/multi_vector/distance/
max_sim.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! MaxSim and Chamfer distance types for multi-vector representations.
7
8use thiserror::Error;
9
10/// Error type for [`MaxSim`] operations.
11#[derive(Clone, Debug, Copy, Error)]
12pub enum MaxSimError {
13    #[error("Trying to access score in index {0} for output of size {1}")]
14    IndexOutOfBounds(usize, usize),
15    #[error("Invalid buffer length {0} for query size {1}")]
16    InvalidBufferLength(usize, usize),
17    #[error("doc-vector dim {0} does not equal query-vector dim {1}")]
18    UnequalDim(usize, usize),
19}
20
21////////////
22// MaxSim //
23////////////
24
25/// Computes per-query-vector maximum similarities to document vectors.
26///
27/// For each query vector `qᵢ`, computes the negated maximum inner product
28/// to any document vector:
29///
30/// ```text
31/// scores[i] = minⱼ -IP(qᵢ, dⱼ)
32/// ```
33///
34/// Implements `DistanceFnMut` for various matrix types
35/// (e.g., [`MatRef<Standard<f32>>`](crate::multi_vector::MatRef)).
36///
37/// # Usage
38/// - Create with [`MaxSim::new`], providing a mutable scores buffer.
39/// - Call `DistanceFnMut::evaluate` with query and document matrices.
40/// - Read results from the scores buffer.
41#[derive(Debug)]
42pub struct MaxSim<'a> {
43    pub(crate) scores: &'a mut [f32],
44}
45
46impl<'a> MaxSim<'a> {
47    /// Creates a new [`MaxSim`] wrapping the provided scores buffer.
48    pub fn new(scores: &'a mut [f32]) -> Self {
49        Self { scores }
50    }
51
52    /// Returns the number of score slots in the buffer.
53    #[inline]
54    pub fn size(&self) -> usize {
55        self.scores.len()
56    }
57
58    /// Returns the score at index `i`.
59    #[inline(always)]
60    pub fn get(&self, i: usize) -> Result<f32, MaxSimError> {
61        self.scores
62            .get(i)
63            .copied()
64            .ok_or_else(|| MaxSimError::IndexOutOfBounds(i, self.size()))
65    }
66
67    /// Sets the score at index `i`.
68    #[inline(always)]
69    pub fn set(&mut self, i: usize, x: f32) -> Result<(), MaxSimError> {
70        let size = self.size();
71
72        let s = self
73            .scores
74            .get_mut(i)
75            .ok_or(MaxSimError::IndexOutOfBounds(i, size))?;
76
77        *s = x;
78        Ok(())
79    }
80
81    /// Returns a mutable reference to the internal buffer of scores.
82    ///
83    /// This is useful for implementations external to crate as well as
84    /// optimized implementations to access the buffer if needed.
85    pub fn scores_mut(&mut self) -> &mut [f32] {
86        self.scores
87    }
88}
89
90/////////////
91// Chamfer //
92/////////////
93
94/// Asymmetric Chamfer distance for multi-vector similarity.
95///
96/// Computes the sum of per-query-vector maximum similarities:
97///
98/// ```text
99/// Chamfer(Q, D) = Σᵢ minⱼ -IP(qᵢ, dⱼ)
100/// ```
101///
102/// Implements [`PureDistanceFunction`](diskann_vector::PureDistanceFunction)
103/// for matrix view types.
104#[derive(Debug, Clone, Copy)]
105pub struct Chamfer;
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    /// Test fixture providing common buffer sizes for testing
112    struct TestFixture {
113        buffer: Vec<f32>,
114    }
115
116    impl TestFixture {
117        fn new(size: usize) -> Self {
118            Self {
119                buffer: vec![0.0; size],
120            }
121        }
122
123        fn with_values(values: &[f32]) -> Self {
124            Self {
125                buffer: values.to_vec(),
126            }
127        }
128
129        fn max_sim(&mut self) -> MaxSim<'_> {
130            MaxSim::new(&mut self.buffer)
131        }
132    }
133
134    mod max_sim_new {
135        use super::*;
136
137        #[test]
138        fn returns_correct_size() {
139            let sizes = [1, 2, 5, 100, 1000];
140            for size in sizes {
141                let mut fixture = TestFixture::new(size);
142                let mut max_sim = fixture.max_sim();
143                assert_eq!(max_sim.size(), size, "size mismatch for buffer of {}", size);
144
145                let scores = max_sim.scores_mut();
146                assert_eq!(scores.len(), max_sim.size());
147            }
148        }
149    }
150
151    mod max_sim_get {
152        use super::*;
153
154        #[test]
155        fn returns_value_at_valid_index() {
156            let mut fixture = TestFixture::with_values(&[1.0, 2.0, 3.0]);
157            let max_sim = fixture.max_sim();
158
159            assert_eq!(max_sim.get(0).unwrap(), 1.0);
160            assert_eq!(max_sim.get(1).unwrap(), 2.0);
161            assert_eq!(max_sim.get(2).unwrap(), 3.0);
162        }
163
164        #[test]
165        fn fails_at_out_of_bounds_index() {
166            let mut fixture = TestFixture::new(3);
167            let max_sim = fixture.max_sim();
168
169            let result = max_sim.get(3);
170            assert!(matches!(result, Err(MaxSimError::IndexOutOfBounds(3, 3))));
171
172            let result = max_sim.get(100);
173            assert!(matches!(result, Err(MaxSimError::IndexOutOfBounds(100, 3))));
174        }
175    }
176
177    mod max_sim_set {
178        use super::*;
179
180        #[test]
181        fn sets_value_at_valid_index() {
182            let mut fixture = TestFixture::new(3);
183            let mut max_sim = fixture.max_sim();
184
185            max_sim.set(0, 10.0).unwrap();
186            max_sim.set(1, 20.0).unwrap();
187            max_sim.set(2, 30.0).unwrap();
188
189            assert_eq!(max_sim.get(0).unwrap(), 10.0);
190            assert_eq!(max_sim.get(1).unwrap(), 20.0);
191            assert_eq!(max_sim.get(2).unwrap(), 30.0);
192        }
193
194        #[test]
195        fn fails_at_out_of_bounds_index() {
196            let mut fixture = TestFixture::new(3);
197            let mut max_sim = fixture.max_sim();
198
199            let result = max_sim.set(3, 999.0);
200            assert!(matches!(result, Err(MaxSimError::IndexOutOfBounds(3, 3))));
201        }
202
203        #[test]
204        fn overwrites_existing_value() {
205            let mut fixture = TestFixture::with_values(&[1.0, 2.0, 3.0]);
206            let mut max_sim = fixture.max_sim();
207
208            max_sim.set(1, 99.0).unwrap();
209
210            assert_eq!(max_sim.get(0).unwrap(), 1.0); // unchanged
211            assert_eq!(max_sim.get(1).unwrap(), 99.0); // changed
212            assert_eq!(max_sim.get(2).unwrap(), 3.0); // unchanged
213        }
214
215        #[test]
216        fn handles_special_float_values() {
217            let mut fixture = TestFixture::new(4);
218            let mut max_sim = fixture.max_sim();
219
220            max_sim.set(0, f32::INFINITY).unwrap();
221            max_sim.set(1, f32::NEG_INFINITY).unwrap();
222            max_sim.set(2, f32::NAN).unwrap();
223            max_sim.set(3, -0.0).unwrap();
224
225            assert_eq!(max_sim.get(0).unwrap(), f32::INFINITY);
226            assert_eq!(max_sim.get(1).unwrap(), f32::NEG_INFINITY);
227            assert!(max_sim.get(2).unwrap().is_nan());
228            assert!(max_sim.get(3).unwrap().is_sign_negative());
229        }
230
231        #[test]
232        fn writes_through_to_underlying_buffer() {
233            let mut buffer = vec![0.0f32; 3];
234            {
235                let mut max_sim = MaxSim::new(&mut buffer);
236                max_sim.set(0, 1.0).unwrap();
237                max_sim.set(1, 2.0).unwrap();
238            }
239            // After MaxSim is dropped, buffer reflects the changes
240            assert_eq!(buffer, vec![1.0, 2.0, 0.0]);
241        }
242    }
243}