use crate::vector::{validate, Nearest, VectorError};
use std::cmp::Ordering;
#[derive(Debug, Clone, PartialEq)]
pub struct ResidentIndex {
dimension: usize,
keys: Vec<String>,
codes: Vec<i8>,
scales: Vec<f32>,
norms: Vec<f64>,
}
impl ResidentIndex {
pub(crate) fn from_vectors(
dimension: usize,
mut vectors: Vec<(String, Vec<f32>)>,
) -> Result<Self, VectorError> {
vectors.sort_by(|left, right| left.0.cmp(&right.0));
let mut index = Self {
dimension,
keys: Vec::with_capacity(vectors.len()),
codes: Vec::with_capacity(vectors.len() * dimension),
scales: Vec::with_capacity(vectors.len()),
norms: Vec::with_capacity(vectors.len()),
};
for (key, vector) in vectors {
validate(&vector, dimension)?;
let absmax = vector
.iter()
.fold(0.0_f32, |max, value| max.max(value.abs()));
let scale = absmax / 127.0;
let mut sum_squares = 0.0_f64;
if scale > 0.0 {
for value in &vector {
let code = (value / scale).round().clamp(-127.0, 127.0) as i8;
index.codes.push(code);
sum_squares += f64::from(code) * f64::from(code);
}
} else {
index.codes.resize(index.codes.len() + dimension, 0);
}
index.scales.push(scale);
index.norms.push(f64::from(scale) * sum_squares.sqrt());
index.keys.push(key);
}
Ok(index)
}
pub fn dimension(&self) -> usize {
self.dimension
}
pub fn len(&self) -> usize {
self.keys.len()
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
pub fn nearest(&self, query: &[f32], limit: usize) -> Result<Vec<Nearest>, VectorError> {
validate(query, self.dimension)?;
if limit == 0 || self.keys.is_empty() {
return Ok(Vec::new());
}
let query_norm = query
.iter()
.map(|value| f64::from(*value) * f64::from(*value))
.sum::<f64>()
.sqrt();
let mut scored: Vec<(usize, f32)> = (0..self.keys.len())
.map(|row| {
let codes = &self.codes[row * self.dimension..(row + 1) * self.dimension];
let dot: f64 = query
.iter()
.zip(codes)
.map(|(value, code)| f64::from(*value) * f64::from(*code))
.sum();
let denominator = query_norm * self.norms[row];
let score = if denominator > 0.0 {
(dot * f64::from(self.scales[row]) / denominator) as f32
} else {
0.0
};
(row, score)
})
.collect();
let rank = |left: &(usize, f32), right: &(usize, f32)| -> Ordering {
right
.1
.total_cmp(&left.1)
.then_with(|| self.keys[left.0].cmp(&self.keys[right.0]))
};
if limit < scored.len() {
scored.select_nth_unstable_by(limit - 1, &rank);
scored.truncate(limit);
}
scored.sort_by(&rank);
Ok(scored
.into_iter()
.map(|(row, score)| Nearest {
key: self.keys[row].clone(),
score,
vector: self.dequantise(row),
})
.collect())
}
fn dequantise(&self, row: usize) -> Vec<f32> {
let scale = self.scales[row];
self.codes[row * self.dimension..(row + 1) * self.dimension]
.iter()
.map(|code| f32::from(*code) * scale)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn index(rows: &[(&str, Vec<f32>)]) -> ResidentIndex {
let owned = rows
.iter()
.map(|(key, vector)| ((*key).to_owned(), vector.clone()))
.collect();
ResidentIndex::from_vectors(rows[0].1.len(), owned).unwrap()
}
#[test]
fn rows_quantise_to_absmax_over_127_and_dequantise_back() {
let index = index(&[("a", vec![0.5, -1.0, 0.25])]);
assert_eq!(index.codes, vec![64, -127, 32]);
assert!((index.scales[0] - 1.0 / 127.0).abs() < 1e-9);
let back = index.dequantise(0);
assert!((back[1] + 1.0).abs() < 1e-6);
assert!((back[0] - 0.5).abs() < 0.01);
}
#[test]
fn nearest_ranks_by_cosine_and_matches_the_f32_order() {
let index = index(&[
("far", vec![0.0, 1.0, 0.0]),
("near", vec![0.9, 0.1, 0.0]),
("same", vec![1.0, 0.0, 0.0]),
]);
let hits = index.nearest(&[1.0, 0.0, 0.0], 2).unwrap();
let keys: Vec<&str> = hits.iter().map(|hit| hit.key.as_str()).collect();
assert_eq!(keys, vec!["same", "near"]);
assert!(hits[0].score > 0.999);
assert_eq!(hits[1].vector.len(), 3);
}
#[test]
fn large_finite_vectors_score_by_direction_not_overflow() {
let index = index(&[("big", vec![1e20, 1e20]), ("other", vec![1e20, -1e20])]);
let hits = index.nearest(&[1e20, 1e20], 2).unwrap();
assert_eq!(hits[0].key, "big");
assert!(
(hits[0].score - 1.0).abs() < 1e-3,
"score {}",
hits[0].score
);
assert!(hits[1].score.abs() < 1e-3, "score {}", hits[1].score);
let hits = index.nearest(&[f32::MAX, f32::MAX], 1).unwrap();
assert!(
(hits[0].score - 1.0).abs() < 1e-3,
"score {}",
hits[0].score
);
}
#[test]
fn zero_rows_and_zero_queries_score_zero_and_limits_are_honoured() {
let index = index(&[("zero", vec![0.0, 0.0]), ("one", vec![1.0, 0.0])]);
let hits = index.nearest(&[1.0, 0.0], 10).unwrap();
assert_eq!(hits.len(), 2);
assert_eq!(hits[1].key, "zero");
assert_eq!(hits[1].score, 0.0);
let hits = index.nearest(&[0.0, 0.0], 1).unwrap();
assert_eq!(hits[0].score, 0.0);
assert!(index.nearest(&[1.0, 0.0], 0).unwrap().is_empty());
}
#[test]
fn dimension_and_non_finite_input_are_refused() {
let index = index(&[("a", vec![1.0, 0.0])]);
assert!(matches!(
index.nearest(&[1.0], 1),
Err(VectorError::Dimension {
expected: 2,
actual: 1
})
));
assert!(matches!(
index.nearest(&[f32::NAN, 0.0], 1),
Err(VectorError::NonFinite)
));
let bad = ResidentIndex::from_vectors(2, vec![("b".into(), vec![1.0])]);
assert!(bad.is_err());
}
}