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