Skip to main content

rightkit_embed/
resident.rs

1//! Resident int8 vector index: one model's vectors held in memory, quantised per row.
2//!
3//! Each row is stored as `i8` codes with one `f32` scale (`absmax / 127`), so the index costs
4//! about a quarter of the f32 matrix. Scores are cosine similarities against the dequantised
5//! rows, so the only error is the quantisation of each row. Built from a [`VectorStore`]
6//! snapshot with [`VectorStore::resident_index`]; later writes to the store are not seen.
7//!
8//! [`VectorStore`]: crate::VectorStore
9
10use crate::vector::{validate, Nearest, VectorError};
11use std::cmp::Ordering;
12
13/// An in-memory int8 snapshot of the vectors of one model. See the module documentation.
14#[derive(Debug, Clone, PartialEq)]
15pub struct ResidentIndex {
16    dimension: usize,
17    /// Keys, ascending; the row order of `codes`, `scales` and `norms`.
18    keys: Vec<String>,
19    /// Row-major `i8` codes, `keys.len() * dimension` values.
20    codes: Vec<i8>,
21    /// Per row: `absmax / 127`, or 0 for an all-zero row.
22    scales: Vec<f32>,
23    /// Per row: the L2 norm of the dequantised row, in f64 so large finite scales cannot overflow.
24    norms: Vec<f64>,
25}
26
27impl ResidentIndex {
28    /// Quantises `vectors` (key and vector pairs, keys unique). Each vector must have
29    /// `dimension` finite values.
30    pub(crate) fn from_vectors(
31        dimension: usize,
32        mut vectors: Vec<(String, Vec<f32>)>,
33    ) -> Result<Self, VectorError> {
34        vectors.sort_by(|left, right| left.0.cmp(&right.0));
35        let mut index = Self {
36            dimension,
37            keys: Vec::with_capacity(vectors.len()),
38            codes: Vec::with_capacity(vectors.len() * dimension),
39            scales: Vec::with_capacity(vectors.len()),
40            norms: Vec::with_capacity(vectors.len()),
41        };
42        for (key, vector) in vectors {
43            validate(&vector, dimension)?;
44            let absmax = vector
45                .iter()
46                .fold(0.0_f32, |max, value| max.max(value.abs()));
47            let scale = absmax / 127.0;
48            let mut sum_squares = 0.0_f64;
49            if scale > 0.0 {
50                for value in &vector {
51                    let code = (value / scale).round().clamp(-127.0, 127.0) as i8;
52                    index.codes.push(code);
53                    sum_squares += f64::from(code) * f64::from(code);
54                }
55            } else {
56                index.codes.resize(index.codes.len() + dimension, 0);
57            }
58            index.scales.push(scale);
59            index.norms.push(f64::from(scale) * sum_squares.sqrt());
60            index.keys.push(key);
61        }
62        Ok(index)
63    }
64
65    /// Vector width of every row.
66    pub fn dimension(&self) -> usize {
67        self.dimension
68    }
69
70    /// Number of rows.
71    pub fn len(&self) -> usize {
72        self.keys.len()
73    }
74
75    /// True when the index holds no rows.
76    pub fn is_empty(&self) -> bool {
77        self.keys.is_empty()
78    }
79
80    /// The `limit` rows with the highest cosine similarity to `query`, best first. Ties are
81    /// ordered by key, as [`VectorStore::nearest`] orders them. A zero query or zero row scores
82    /// 0. `Nearest::vector` is the dequantised row.
83    ///
84    /// [`VectorStore::nearest`]: crate::VectorStore::nearest
85    pub fn nearest(&self, query: &[f32], limit: usize) -> Result<Vec<Nearest>, VectorError> {
86        validate(query, self.dimension)?;
87        if limit == 0 || self.keys.is_empty() {
88            return Ok(Vec::new());
89        }
90        let query_norm = query
91            .iter()
92            .map(|value| f64::from(*value) * f64::from(*value))
93            .sum::<f64>()
94            .sqrt();
95        let mut scored: Vec<(usize, f32)> = (0..self.keys.len())
96            .map(|row| {
97                let codes = &self.codes[row * self.dimension..(row + 1) * self.dimension];
98                let dot: f64 = query
99                    .iter()
100                    .zip(codes)
101                    .map(|(value, code)| f64::from(*value) * f64::from(*code))
102                    .sum();
103                let denominator = query_norm * self.norms[row];
104                let score = if denominator > 0.0 {
105                    (dot * f64::from(self.scales[row]) / denominator) as f32
106                } else {
107                    0.0
108                };
109                (row, score)
110            })
111            .collect();
112        let rank = |left: &(usize, f32), right: &(usize, f32)| -> Ordering {
113            right
114                .1
115                .total_cmp(&left.1)
116                .then_with(|| self.keys[left.0].cmp(&self.keys[right.0]))
117        };
118        if limit < scored.len() {
119            scored.select_nth_unstable_by(limit - 1, &rank);
120            scored.truncate(limit);
121        }
122        scored.sort_by(&rank);
123        Ok(scored
124            .into_iter()
125            .map(|(row, score)| Nearest {
126                key: self.keys[row].clone(),
127                score,
128                vector: self.dequantise(row),
129            })
130            .collect())
131    }
132
133    fn dequantise(&self, row: usize) -> Vec<f32> {
134        let scale = self.scales[row];
135        self.codes[row * self.dimension..(row + 1) * self.dimension]
136            .iter()
137            .map(|code| f32::from(*code) * scale)
138            .collect()
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    fn index(rows: &[(&str, Vec<f32>)]) -> ResidentIndex {
147        let owned = rows
148            .iter()
149            .map(|(key, vector)| ((*key).to_owned(), vector.clone()))
150            .collect();
151        ResidentIndex::from_vectors(rows[0].1.len(), owned).unwrap()
152    }
153
154    #[test]
155    fn rows_quantise_to_absmax_over_127_and_dequantise_back() {
156        let index = index(&[("a", vec![0.5, -1.0, 0.25])]);
157        assert_eq!(index.codes, vec![64, -127, 32]);
158        assert!((index.scales[0] - 1.0 / 127.0).abs() < 1e-9);
159        let back = index.dequantise(0);
160        assert!((back[1] + 1.0).abs() < 1e-6);
161        assert!((back[0] - 0.5).abs() < 0.01);
162    }
163
164    #[test]
165    fn nearest_ranks_by_cosine_and_matches_the_f32_order() {
166        let index = index(&[
167            ("far", vec![0.0, 1.0, 0.0]),
168            ("near", vec![0.9, 0.1, 0.0]),
169            ("same", vec![1.0, 0.0, 0.0]),
170        ]);
171        let hits = index.nearest(&[1.0, 0.0, 0.0], 2).unwrap();
172        let keys: Vec<&str> = hits.iter().map(|hit| hit.key.as_str()).collect();
173        assert_eq!(keys, vec!["same", "near"]);
174        assert!(hits[0].score > 0.999);
175        assert_eq!(hits[1].vector.len(), 3);
176    }
177
178    #[test]
179    fn large_finite_vectors_score_by_direction_not_overflow() {
180        let index = index(&[("big", vec![1e20, 1e20]), ("other", vec![1e20, -1e20])]);
181        let hits = index.nearest(&[1e20, 1e20], 2).unwrap();
182        assert_eq!(hits[0].key, "big");
183        assert!(
184            (hits[0].score - 1.0).abs() < 1e-3,
185            "score {}",
186            hits[0].score
187        );
188        assert!(hits[1].score.abs() < 1e-3, "score {}", hits[1].score);
189        let hits = index.nearest(&[f32::MAX, f32::MAX], 1).unwrap();
190        assert!(
191            (hits[0].score - 1.0).abs() < 1e-3,
192            "score {}",
193            hits[0].score
194        );
195    }
196
197    #[test]
198    fn zero_rows_and_zero_queries_score_zero_and_limits_are_honoured() {
199        let index = index(&[("zero", vec![0.0, 0.0]), ("one", vec![1.0, 0.0])]);
200        let hits = index.nearest(&[1.0, 0.0], 10).unwrap();
201        assert_eq!(hits.len(), 2);
202        assert_eq!(hits[1].key, "zero");
203        assert_eq!(hits[1].score, 0.0);
204        let hits = index.nearest(&[0.0, 0.0], 1).unwrap();
205        assert_eq!(hits[0].score, 0.0);
206        assert!(index.nearest(&[1.0, 0.0], 0).unwrap().is_empty());
207    }
208
209    #[test]
210    fn dimension_and_non_finite_input_are_refused() {
211        let index = index(&[("a", vec![1.0, 0.0])]);
212        assert!(matches!(
213            index.nearest(&[1.0], 1),
214            Err(VectorError::Dimension {
215                expected: 2,
216                actual: 1
217            })
218        ));
219        assert!(matches!(
220            index.nearest(&[f32::NAN, 0.0], 1),
221            Err(VectorError::NonFinite)
222        ));
223        let bad = ResidentIndex::from_vectors(2, vec![("b".into(), vec![1.0])]);
224        assert!(bad.is_err());
225    }
226}