1use crate::vector::{validate, Nearest, VectorError};
11use std::cmp::Ordering;
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct ResidentIndex {
16 dimension: usize,
17 keys: Vec<String>,
19 codes: Vec<i8>,
21 scales: Vec<f32>,
23 norms: Vec<f64>,
25}
26
27impl ResidentIndex {
28 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 pub fn dimension(&self) -> usize {
67 self.dimension
68 }
69
70 pub fn len(&self) -> usize {
72 self.keys.len()
73 }
74
75 pub fn is_empty(&self) -> bool {
77 self.keys.is_empty()
78 }
79
80 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}