Skip to main content

diskann_quantization/multi_vector/distance/
max_sim.rs

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