use yo_common::{Code, Error, Result};
use yo_kv::Elements;
use yo_shape::Metric;
use crate::partition::{Partitions, Tuning, Vectors};
use crate::rabitq::Bits;
pub use yo_format::vector::MAX_DIM;
const SEED: u64 = 0x596F_5F76_6563_0001;
const BUDGET: usize = 1024;
#[derive(Debug, Clone, PartialEq)]
pub struct Match {
pub key: Vec<u8>,
pub distance: f32,
}
#[derive(Debug)]
pub struct Collection {
index: Partitions,
raw: Raw,
ids: Elements<u64>,
metric: Metric,
}
impl Collection {
pub fn new(dim: usize, metric: Metric) -> Result<Collection> {
width(dim)?;
check_metric(metric)?;
Ok(Collection {
index: Partitions::new(dim, Bits::One, SEED, Tuning::default()),
raw: Raw {
dim,
data: Vec::new(),
owner: Vec::new(),
free: Vec::new(),
},
ids: Elements::new(),
metric,
})
}
#[must_use]
pub fn dim(&self) -> usize {
self.raw.dim
}
#[must_use]
pub fn metric(&self) -> Metric {
self.metric
}
#[must_use]
pub fn len(&self) -> usize {
self.ids.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
#[must_use]
pub fn partitions(&self) -> usize {
self.index.partitions()
}
#[must_use]
pub fn entries(&self) -> usize {
self.index.entries()
}
#[must_use]
pub fn tuning(&self) -> Tuning {
self.index.tuning()
}
pub fn retune(&mut self, tuning: Tuning) {
self.index.retune(tuning);
}
#[must_use]
pub fn contains(&self, key: &[u8]) -> bool {
self.ids.contains(key)
}
#[must_use]
pub fn get(&self, key: &[u8]) -> Option<&[f32]> {
let id = *self.ids.get(key)?;
Some(self.raw.at(id))
}
pub fn keys(&self) -> impl Iterator<Item = &[u8]> {
self.ids.iter().map(|(key, _)| key)
}
#[must_use]
pub fn key_at(&self, n: usize) -> Option<&[u8]> {
self.ids.at(n).map(|(key, _)| key)
}
#[must_use]
pub fn id(&self, key: &[u8]) -> Option<u64> {
self.ids.get(key).copied()
}
pub fn put(&mut self, key: &[u8], v: &[f32]) -> Result<bool> {
self.put_tagged(key, v, 0)
}
pub fn put_tagged(&mut self, key: &[u8], v: &[f32], tag: u64) -> Result<bool> {
let ready = self.ready(v)?;
let new = match self.ids.get(key) {
Some(&id) => {
self.raw.write(id, &ready);
self.index.insert_tagged(id, &ready, tag);
false
}
None => {
let id = self.raw.take(key, &ready);
if self.ids.insert(key, id).is_err() {
self.raw.release(id);
return Err(Error::new(
Code::Full,
"that key is too long for a vector collection",
));
}
self.index.insert_tagged(id, &ready, tag);
true
}
};
self.catch_up();
Ok(new)
}
#[must_use]
pub fn tag(&self, key: &[u8]) -> Option<u64> {
self.index.tag(*self.ids.get(key)?)
}
pub fn retag(&mut self, key: &[u8], tag: u64) -> bool {
let Some(&id) = self.ids.get(key) else {
return false;
};
self.index.retag(id, tag)
}
pub fn remove(&mut self, key: &[u8]) -> bool {
let Some(id) = self.ids.remove(key) else {
return false;
};
self.index.remove(id);
self.raw.release(id);
self.catch_up();
true
}
pub fn search(&self, q: &[f32], k: usize, skip: Option<&[u8]>) -> Result<Vec<Match>> {
self.search_where(q, k, skip, &crate::Any)
}
pub fn search_where(
&self,
q: &[f32],
k: usize,
skip: Option<&[u8]>,
filter: &impl crate::Filter,
) -> Result<Vec<Match>> {
let ready = self.ready(q)?;
if k == 0 || self.index.is_empty() {
return Ok(Vec::new());
}
let want = if skip.is_some() { k + 1 } else { k };
let hits = self.index.search_where(&ready, want, filter, &self.raw);
let mut out = Vec::with_capacity(hits.len().min(k));
for hit in hits {
let key = self.raw.owner(hit.id);
if skip == Some(key) {
continue;
}
out.push(Match {
key: key.to_vec(),
distance: self.report(hit.distance),
});
if out.len() == k {
break;
}
}
Ok(out)
}
pub fn search_exact(&self, q: &[f32], k: usize, skip: Option<&[u8]>) -> Result<Vec<Match>> {
self.search_exact_where(q, k, skip, &crate::Any)
}
pub fn search_exact_where(
&self,
q: &[f32],
k: usize,
skip: Option<&[u8]>,
filter: &impl crate::Filter,
) -> Result<Vec<Match>> {
let ready = self.ready(q)?;
if k == 0 {
return Ok(Vec::new());
}
let mut hits: Vec<(f32, &[u8])> = Vec::with_capacity(self.ids.len());
for (key, &id) in self.ids.iter() {
if skip == Some(key) {
continue;
}
if !filter.allows(self.index.tag(id).unwrap_or(0)) || !filter.exact(id) {
continue;
}
hits.push((crate::dist::sqdist(&ready, self.raw.at(id)), key));
}
hits.sort_by(|a, b| a.0.total_cmp(&b.0).then_with(|| a.1.cmp(b.1)));
hits.truncate(k);
Ok(hits
.into_iter()
.map(|(sq, key)| Match {
key: key.to_vec(),
distance: self.report(sq),
})
.collect())
}
pub fn maintain(&mut self, budget: usize) -> usize {
self.index.maintain(&self.raw, budget)
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.raw.memory_bytes() + self.index.code_bytes() + self.ids.memory_bytes()
}
#[must_use]
pub fn code_bytes(&self) -> usize {
self.index.code_bytes()
}
pub(crate) fn index(&self) -> &Partitions {
&self.index
}
pub(crate) fn id_table(&self) -> &Elements<u64> {
&self.ids
}
pub(crate) fn slots(&self) -> usize {
self.raw.owner.len()
}
pub(crate) fn holds(&self, id: u64) -> bool {
self.index.contains(id)
}
pub(crate) fn from_image(index: Partitions, metric: Metric, slots: usize) -> Collection {
let dim = index.dim();
Collection {
index,
raw: Raw {
dim,
data: vec![0.0; slots * dim],
owner: vec![None; slots],
free: Vec::new(),
},
ids: Elements::new(),
metric,
}
}
pub(crate) fn restore(&mut self, key: &[u8], id: u64, v: &[f32]) -> Result<()> {
self.raw.owner[id as usize] = Some(key.into());
self.raw.write(id, v);
self.ids
.insert(key, id)
.map_err(|_| Error::new(Code::Full, "that key is too long for a vector collection"))?;
Ok(())
}
pub(crate) fn forget(&mut self, id: u64) {
self.index.remove(id);
}
pub(crate) fn seal(&mut self) {
self.raw.free.clear();
for id in (0..self.raw.owner.len()).rev() {
if self.raw.owner[id].is_none() {
self.raw.free.push(id as u64);
}
}
}
fn ready(&self, v: &[f32]) -> Result<Vec<f32>> {
if v.len() != self.raw.dim {
return Err(Error::fmt(
Code::Invalid,
format_args!(
"this collection holds {} dimensional vectors and was handed {}",
self.raw.dim,
v.len()
),
));
}
if let Some(at) = v.iter().position(|x| !x.is_finite()) {
return Err(Error::fmt(
Code::Invalid,
format_args!(
"coordinate {at} of that vector is {}, and a distance to it would be one too",
v[at]
),
));
}
let mut ready = v.to_vec();
if self.metric == Metric::Cosine {
normalize(&mut ready)?;
}
Ok(ready)
}
fn catch_up(&mut self) {
if self.index.needs_maintenance() {
self.index.maintain(&self.raw, BUDGET);
}
}
fn report(&self, sq: f32) -> f32 {
match self.metric {
Metric::Cosine => (sq / 2.0).clamp(0.0, 2.0),
_ => sq.max(0.0).sqrt(),
}
}
}
#[derive(Debug)]
struct Raw {
dim: usize,
data: Vec<f32>,
owner: Vec<Option<Box<[u8]>>>,
free: Vec<u64>,
}
impl Raw {
fn at(&self, id: u64) -> &[f32] {
let at = id as usize * self.dim;
&self.data[at..at + self.dim]
}
fn owner(&self, id: u64) -> &[u8] {
self.owner[id as usize]
.as_deref()
.expect("a live id has a key, and a search only answers with live ids")
}
fn take(&mut self, key: &[u8], v: &[f32]) -> u64 {
let id = match self.free.pop() {
Some(id) => id,
None => {
self.data.resize(self.data.len() + self.dim, 0.0);
self.owner.push(None);
(self.owner.len() - 1) as u64
}
};
self.owner[id as usize] = Some(key.into());
self.write(id, v);
id
}
fn write(&mut self, id: u64, v: &[f32]) {
let at = id as usize * self.dim;
self.data[at..at + self.dim].copy_from_slice(v);
}
fn release(&mut self, id: u64) {
self.owner[id as usize] = None;
self.free.push(id);
}
fn memory_bytes(&self) -> usize {
self.data.capacity() * size_of::<f32>()
+ self.owner.capacity() * size_of::<Option<Box<[u8]>>>()
+ self
.owner
.iter()
.map(|k| k.as_ref().map_or(0, |k| k.len()))
.sum::<usize>()
+ self.free.capacity() * size_of::<u64>()
}
}
impl Vectors for Raw {
fn get(&self, id: u64, into: &mut [f32]) -> bool {
let Some(Some(_)) = self.owner.get(id as usize) else {
return false;
};
into.copy_from_slice(self.at(id));
true
}
}
fn normalize(v: &mut [f32]) -> Result<()> {
let norm = v.iter().map(|x| f64::from(*x) * f64::from(*x)).sum::<f64>();
if norm <= 0.0 {
return Err(Error::new(
Code::Invalid,
"a cosine collection compares directions and a vector of length zero has none",
));
}
#[allow(clippy::cast_possible_truncation)]
let scale = norm.sqrt().recip() as f32;
for x in v.iter_mut() {
*x *= scale;
}
Ok(())
}
pub fn width(dim: usize) -> Result<u32> {
if dim == 0 || dim > MAX_DIM {
return Err(Error::fmt(
Code::Invalid,
format_args!(
"a vector collection holds between 1 and {MAX_DIM} dimensions, and {dim} is not one of them"
),
));
}
u32::try_from(dim).map_err(|_| Error::new(Code::Invalid, "that dimension does not fit"))
}
pub fn check_metric(metric: Metric) -> Result<()> {
match metric {
Metric::L2 | Metric::Cosine => Ok(()),
Metric::Ip => Err(Error::new(
Code::Unsupported,
"inner product is not a distance, so a partition index cannot be built around it, and a collection that ordered by it would not be ordering by nearness. Normalise the vectors and use cosine, which is the same ranking",
)),
Metric::Hamming => Err(Error::new(
Code::Unsupported,
"hamming distance is for binary vectors and this collection holds floats",
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn axes() -> Collection {
let mut c = Collection::new(3, Metric::L2).unwrap();
c.put(b"x", &[1.0, 0.0, 0.0]).unwrap();
c.put(b"y", &[0.0, 1.0, 0.0]).unwrap();
c.put(b"z", &[0.0, 0.0, 1.0]).unwrap();
c
}
#[test]
fn a_vector_comes_back_the_way_it_went_in() {
let mut c = Collection::new(3, Metric::L2).unwrap();
assert!(c.is_empty());
assert!(c.put(b"x", &[1.0, 2.0, 3.0]).unwrap(), "the key is new");
assert!(
!c.put(b"x", &[1.0, 2.0, 3.0]).unwrap(),
"and then it is not"
);
assert_eq!(c.get(b"x"), Some(&[1.0, 2.0, 3.0][..]));
assert_eq!(c.len(), 1);
assert!(c.contains(b"x"));
assert_eq!(c.get(b"nobody"), None);
assert_eq!(c.keys().collect::<Vec<_>>(), vec![&b"x"[..]]);
assert!(c.memory_bytes() > 0);
}
#[test]
fn the_nearest_answer_is_the_nearest_vector() {
let c = axes();
let hits = c.search(&[0.9, 0.2, 0.1], 3, None).unwrap();
let keys: Vec<&[u8]> = hits.iter().map(|h| h.key.as_slice()).collect();
assert_eq!(keys, vec![&b"x"[..], &b"y"[..], &b"z"[..]]);
let want = (0.01f32 + 0.04 + 0.01).sqrt();
assert!((hits[0].distance - want).abs() < 1e-6, "{hits:?}");
}
#[test]
fn a_removed_vector_is_not_an_answer_and_its_slot_comes_back() {
let mut c = axes();
assert!(c.remove(b"x"));
assert!(!c.remove(b"x"), "twice is not there twice");
assert_eq!(c.len(), 2);
let hits = c.search(&[1.0, 0.0, 0.0], 3, None).unwrap();
assert_eq!(hits.len(), 2);
assert!(hits.iter().all(|h| h.key != b"x"));
c.put(b"w", &[1.0, 0.0, 0.0]).unwrap();
let hits = c.search(&[1.0, 0.0, 0.0], 1, None).unwrap();
assert_eq!(hits[0].key, b"w", "the reused slot answers as w");
}
#[test]
fn a_replaced_vector_is_searched_at_its_new_place() {
let mut c = axes();
c.put(b"x", &[0.0, 0.0, 1.0]).unwrap();
assert_eq!(c.len(), 3, "a replacement is not a second key");
let hits = c.search(&[1.0, 0.0, 0.0], 1, None).unwrap();
assert_eq!(hits[0].key, b"y", "x moved away from that corner");
}
#[test]
fn a_search_can_leave_one_key_out() {
let mut c = axes();
c.put(b"x2", &[0.9, 0.1, 0.0]).unwrap();
let hits = c.search(&[1.0, 0.0, 0.0], 2, Some(b"x")).unwrap();
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].key, b"x2");
assert!(hits.iter().all(|h| h.key != b"x"));
}
#[test]
fn a_cosine_collection_stores_the_direction_and_reports_the_angle() {
let mut c = Collection::new(2, Metric::Cosine).unwrap();
c.put(b"east", &[7.0, 0.0]).unwrap();
c.put(b"north", &[0.0, 3.0]).unwrap();
c.put(b"west", &[-2.0, 0.0]).unwrap();
assert_eq!(c.get(b"east"), Some(&[1.0, 0.0][..]));
let hits = c.search(&[100.0, 0.0], 3, None).unwrap();
assert_eq!(hits[0].key, b"east");
assert!(hits[0].distance.abs() < 1e-6, "{hits:?}");
assert!(
(hits[1].distance - 1.0).abs() < 1e-6,
"north is a right angle"
);
assert!(
(hits[2].distance - 2.0).abs() < 1e-6,
"west is the opposite"
);
let e = c.put(b"nowhere", &[0.0, 0.0]).expect_err("no direction");
assert_eq!(e.code(), Code::Invalid);
}
#[test]
fn a_vector_of_the_wrong_length_or_shape_is_refused() {
let mut c = Collection::new(3, Metric::L2).unwrap();
let e = c.put(b"x", &[1.0, 2.0]).expect_err("two is not three");
assert_eq!(e.code(), Code::Invalid);
assert!(e.message().contains("3 dimensional"), "{e}");
let e = c
.put(b"x", &[1.0, f32::NAN, 2.0])
.expect_err("not a number");
assert_eq!(e.code(), Code::Invalid);
assert!(e.message().contains("coordinate 1"), "{e}");
let e = c.search(&[1.0], 1, None).expect_err("one is not three");
assert_eq!(e.code(), Code::Invalid);
}
#[test]
fn a_dimension_or_a_metric_the_build_cannot_hold_is_refused() {
assert_eq!(
Collection::new(0, Metric::L2).unwrap_err().code(),
Code::Invalid
);
assert_eq!(
Collection::new(MAX_DIM + 1, Metric::L2).unwrap_err().code(),
Code::Invalid
);
let e = Collection::new(8, Metric::Ip).unwrap_err();
assert_eq!(e.code(), Code::Unsupported);
assert!(e.message().contains("cosine"), "{e}");
assert_eq!(
Collection::new(8, Metric::Hamming).unwrap_err().code(),
Code::Unsupported
);
}
#[test]
fn recall_holds_once_the_index_has_split() {
let mut c = Collection::new(8, Metric::L2).unwrap();
let mut seed = 0x2026u64;
let mut next = move || {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
((seed >> 33) as f32 / (1u64 << 31) as f32) - 0.5
};
let mut all: Vec<Vec<f32>> = Vec::new();
for i in 0..2000usize {
let x: Vec<f32> = (0..8).map(|_| next()).collect();
c.put(format!("k{i}").as_bytes(), &x).unwrap();
all.push(x);
}
assert!(c.partitions() > 1, "nothing ever split");
let mut found = 0;
for (i, q) in all.iter().enumerate().step_by(50) {
let hits = c.search(q, 1, None).unwrap();
if hits[0].key == format!("k{i}").into_bytes() {
found += 1;
}
}
assert!(found >= 39, "{found} of 40 queries found their own vector");
assert_eq!(c.maintain(1 << 20), 0, "the writes left nothing owed");
}
}