yo_vector/collection.rs
1//! A collection of vectors under keys, which is what both doors reach.
2//!
3//! [`Partitions`] is the index and it knows about ids, dimensions and codes. It
4//! does not know that a client calls a vector `doc:1`, that a collection has a
5//! metric, or where the full precision vectors live, because none of those are
6//! the index's business and putting them there would make one of the two callers
7//! wrong. This is the piece above it that answers all three, so `db.vectors()`
8//! and `VADD` off a socket are two doors into one store (Y23) rather than two
9//! stores that agree for now.
10//!
11//! ```
12//! use yo_vector::Collection;
13//! use yo_shape::Metric;
14//!
15//! let mut c = Collection::new(3, Metric::L2)?;
16//! c.put(b"a", &[1.0, 0.0, 0.0])?;
17//! c.put(b"b", &[0.0, 1.0, 0.0])?;
18//!
19//! let hits = c.search(&[0.9, 0.1, 0.0], 1, None)?;
20//! assert_eq!(hits[0].key, b"a");
21//! # Ok::<(), yo_common::Error>(())
22//! ```
23//!
24//! # The metric decides what is stored
25//!
26//! [`Metric::L2`] stores the vector it was given. [`Metric::Cosine`] stores the
27//! unit vector, because the index measures distance and on unit vectors the
28//! nearest by distance is the nearest by angle, so cosine costs one
29//! normalisation on the way in rather than a second index. That is also what
30//! comes back out of [`Collection::get`], and it is the same answer Redis gives
31//! for a cosine vector set.
32//!
33//! [`Metric::Ip`] and [`Metric::Hamming`] are refused rather than approximated.
34//! Inner product is not a distance, so ordering by it is not ordering by
35//! nearness and the partitions would be built around the wrong question.
36//! Hamming wants binary vectors that a collection of floats does not hold.
37//!
38//! # Where the vectors live
39//!
40//! Beside the index, one flat run of floats with a slot per id, which the last
41//! step of every search reads and which the index itself never holds. An id is
42//! never anything but the slot it names, so the rerank is an offset rather than
43//! a lookup, and a slot comes back for reuse when its key is removed. `06` puts
44//! them in
45//! the record log at kind 3 and `yo_format::vector` is that record already
46//! written down. Until the file lands the run is in memory, which is where every
47//! other collection in this build is too, and nothing on this page changes when
48//! it moves.
49
50use yo_common::{Code, Error, Result};
51use yo_kv::Elements;
52use yo_shape::Metric;
53
54use crate::partition::{Partitions, Tuning, Vectors};
55use crate::rabitq::Bits;
56
57/// The largest dimension a collection can hold.
58///
59/// Taken from the record format rather than picked again here, because a
60/// collection that accepted a vector the log cannot hold would be one that works
61/// until the file lands.
62pub use yo_format::vector::MAX_DIM;
63
64/// The seed the rotation is built from.
65///
66/// Fixed rather than random, because the codes in a collection are only
67/// comparable to each other if they were rotated the same way, so this is a
68/// property of the collection and it belongs in the catalogue when the file
69/// lands. Until then a constant is the honest version of the same thing: two
70/// runs of the same program build the same index.
71const SEED: u64 = 0x596F_5F76_6563_0001;
72
73/// How many vectors one write is willing to have maintenance touch.
74///
75/// Splits and merges run inside the write that made them necessary, because a
76/// build with no maintenance slice has nowhere else to run them. A budget of
77/// four postings means a write that triggers a split pays for that split and
78/// not for a backlog, and a collection that has fallen behind catches up over
79/// the writes that follow rather than in one pause. A server with a maintenance
80/// slice calls [`Collection::maintain`] with a bigger number and this stays as
81/// the floor under it.
82const BUDGET: usize = 1024;
83
84/// One answer from a search.
85#[derive(Debug, Clone, PartialEq)]
86pub struct Match {
87 /// The key the vector was put under.
88 pub key: Vec<u8>,
89 /// How far it is, measured against the full precision vector rather than
90 /// against its code.
91 ///
92 /// For [`Metric::L2`] that is the euclidean distance. For
93 /// [`Metric::Cosine`] it is one minus the cosine similarity, so 0 is the
94 /// same direction and 2 is the opposite one. Both are distances, so nearer
95 /// is smaller and the answers come back in that order.
96 pub distance: f32,
97}
98
99/// Vectors under keys: the index, the vectors it reranks against, and the table
100/// that turns one into the other.
101#[derive(Debug)]
102pub struct Collection {
103 /// The RaBitQ codes under partitions that split and merge in place.
104 index: Partitions,
105 /// The full precision vectors, which the last step of every search measures
106 /// against and which the index itself never holds.
107 raw: Raw,
108 /// Key to the id the index knows it by.
109 ids: Elements<u64>,
110 metric: Metric,
111}
112
113impl Collection {
114 /// An empty collection that has allocated nothing yet.
115 ///
116 /// # Errors
117 ///
118 /// [`Code::Invalid`] for a dimension of zero or past [`MAX_DIM`], and
119 /// [`Code::Unsupported`] for a metric this build does not measure.
120 pub fn new(dim: usize, metric: Metric) -> Result<Collection> {
121 width(dim)?;
122 check_metric(metric)?;
123 Ok(Collection {
124 // One bit rather than four. Four bits costs four times the scan and
125 // on both public datasets it reaches the same recall to four
126 // decimal places once the rerank is 16 wide, which says the
127 // estimator is not what limits recall on real embeddings.
128 index: Partitions::new(dim, Bits::One, SEED, Tuning::default()),
129 raw: Raw {
130 dim,
131 data: Vec::new(),
132 owner: Vec::new(),
133 free: Vec::new(),
134 },
135 ids: Elements::new(),
136 metric,
137 })
138 }
139
140 /// How many coordinates a vector here has.
141 #[must_use]
142 pub fn dim(&self) -> usize {
143 self.raw.dim
144 }
145
146 /// What nearness means here.
147 #[must_use]
148 pub fn metric(&self) -> Metric {
149 self.metric
150 }
151
152 /// How many vectors are in the collection.
153 #[must_use]
154 pub fn len(&self) -> usize {
155 self.ids.len()
156 }
157
158 /// Whether there are none.
159 #[must_use]
160 pub fn is_empty(&self) -> bool {
161 self.ids.is_empty()
162 }
163
164 /// How many partitions the collection has grown to.
165 #[must_use]
166 pub fn partitions(&self) -> usize {
167 self.index.partitions()
168 }
169
170 /// How many coded members the partitions hold between them, which is more
171 /// than [`Collection::len`] by however many boundary copies
172 /// [`Tuning::spill`] has made.
173 #[must_use]
174 pub fn entries(&self) -> usize {
175 self.index.entries()
176 }
177
178 /// The knobs the index is searched with.
179 #[must_use]
180 pub fn tuning(&self) -> Tuning {
181 self.index.tuning()
182 }
183
184 /// Change them, which is what `EF_RUNTIME` on the wire means.
185 pub fn retune(&mut self, tuning: Tuning) {
186 self.index.retune(tuning);
187 }
188
189 /// Whether the collection holds a vector under `key`.
190 #[must_use]
191 pub fn contains(&self, key: &[u8]) -> bool {
192 self.ids.contains(key)
193 }
194
195 /// The vector under `key`, where it lies.
196 #[must_use]
197 pub fn get(&self, key: &[u8]) -> Option<&[f32]> {
198 let id = *self.ids.get(key)?;
199 Some(self.raw.at(id))
200 }
201
202 /// Every key in the collection, in no order worth relying on.
203 pub fn keys(&self) -> impl Iterator<Item = &[u8]> {
204 self.ids.iter().map(|(key, _)| key)
205 }
206
207 /// The `n`th key, counting from zero, in that same order.
208 ///
209 /// For a caller that wants one member and not all of them, which is what
210 /// `VRANDMEMBER` is and what walking the whole table to throw it away would
211 /// be the wrong way to answer.
212 #[must_use]
213 pub fn key_at(&self, n: usize) -> Option<&[u8]> {
214 self.ids.at(n).map(|(key, _)| key)
215 }
216
217 /// The id the index knows `key` by.
218 ///
219 /// An id is the slot the vector sits in. It is stable while the key is
220 /// there, it changes if the key is removed and written again, and it is
221 /// handed out so that a caller keeping something else per vector can key it
222 /// by a small integer rather than by a second copy of the key. The attribute
223 /// a vector set holds is the first of those and a pushed down filter's tag
224 /// will be the next.
225 #[must_use]
226 pub fn id(&self, key: &[u8]) -> Option<u64> {
227 self.ids.get(key).copied()
228 }
229
230 /// Put a vector in under `key`, and say whether the key is new.
231 ///
232 /// Replacing is the same call. The old code comes out of its partition and
233 /// the new one goes into whichever partition it belongs to now, so nothing
234 /// accumulates and there is no rebuild waiting at the end of it.
235 ///
236 /// # Errors
237 ///
238 /// [`Code::Invalid`] when the vector is not [`Collection::dim`] long, when
239 /// a coordinate is not a number, or when a cosine collection is handed a
240 /// vector of length zero, which has no direction to store. [`Code::Full`]
241 /// for a key past the length limit.
242 pub fn put(&mut self, key: &[u8], v: &[f32]) -> Result<bool> {
243 self.put_tagged(key, v, 0)
244 }
245
246 /// The same, with the tag a filtered search will meet in the posting scan.
247 ///
248 /// A tag is 64 bits and it travels beside the code rather than beside the
249 /// vector, which is the whole reason a filter here costs nothing: the scan
250 /// is already reading that cache line to get at the code, so testing the
251 /// tag is one instruction on a word that has arrived anyway. See
252 /// [`Signature`](crate::Signature) for how a set of field and value pairs
253 /// becomes one, and [`Collection::search_where`] for the other end of it.
254 ///
255 /// A tag of zero passes no filter except [`Any`](crate::Any), which is
256 /// what an untagged collection wants: [`Collection::put`] is this with a
257 /// zero and every search over it is unfiltered.
258 ///
259 /// # Errors
260 ///
261 /// As [`Collection::put`].
262 pub fn put_tagged(&mut self, key: &[u8], v: &[f32], tag: u64) -> Result<bool> {
263 let ready = self.ready(v)?;
264
265 let new = match self.ids.get(key) {
266 Some(&id) => {
267 self.raw.write(id, &ready);
268 self.index.insert_tagged(id, &ready, tag);
269 false
270 }
271 None => {
272 let id = self.raw.take(key, &ready);
273 if self.ids.insert(key, id).is_err() {
274 self.raw.release(id);
275 return Err(Error::new(
276 Code::Full,
277 "that key is too long for a vector collection",
278 ));
279 }
280 self.index.insert_tagged(id, &ready, tag);
281 true
282 }
283 };
284
285 self.catch_up();
286 Ok(new)
287 }
288
289 /// The tag `key` was stored with, if it is here.
290 #[must_use]
291 pub fn tag(&self, key: &[u8]) -> Option<u64> {
292 self.index.tag(*self.ids.get(key)?)
293 }
294
295 /// Change the tag under `key` without touching the vector, and say whether
296 /// there was one.
297 ///
298 /// The tag summarises something outside the vector, so it can go stale while
299 /// the vector is still right. Rewriting it is one store into the posting,
300 /// with no requantisation and no maintenance, which is what makes it cheap
301 /// enough to redo every tag in a collection when the summary changes.
302 pub fn retag(&mut self, key: &[u8], tag: u64) -> bool {
303 let Some(&id) = self.ids.get(key) else {
304 return false;
305 };
306 self.index.retag(id, tag)
307 }
308
309 /// Take a vector out, saying whether it was there.
310 ///
311 /// A delete here is a delete and not a tombstone: the member leaves its
312 /// posting and the last member of that posting moves into the hole.
313 pub fn remove(&mut self, key: &[u8]) -> bool {
314 let Some(id) = self.ids.remove(key) else {
315 return false;
316 };
317 self.index.remove(id);
318 self.raw.release(id);
319 self.catch_up();
320 true
321 }
322
323 /// The `k` nearest keys to `q`, nearest first, with `skip` left out.
324 ///
325 /// `skip` is what makes a more-like-this search work: the vector already
326 /// stored under a key is always nearest to itself, and nobody asked what a
327 /// thing is most similar to itself.
328 ///
329 /// # Errors
330 ///
331 /// [`Code::Invalid`] when `q` is not [`Collection::dim`] long or holds a
332 /// coordinate that is not a number.
333 pub fn search(&self, q: &[f32], k: usize, skip: Option<&[u8]>) -> Result<Vec<Match>> {
334 self.search_where(q, k, skip, &crate::Any)
335 }
336
337 /// The same, over only the members whose tag `filter` allows.
338 ///
339 /// The filter runs inside the posting scan and not on the answers, which is
340 /// the difference between a filtered search and a search followed by a
341 /// filter. A filter matching one member in a thousand, applied to the
342 /// nearest ten, returns nothing almost every time; applied in the scan it
343 /// keeps reading further partitions until it has `k` or until it has spent
344 /// [`Tuning::widen`], so it returns the nearest ten that pass.
345 ///
346 /// It can still come back with fewer than `k`. That is the trade every
347 /// engine makes here and it is the right one, because the alternative to
348 /// giving up after a bounded widen is reading the whole collection for a
349 /// query that was going to find nothing anyway.
350 ///
351 /// # Errors
352 ///
353 /// As [`Collection::search`].
354 pub fn search_where(
355 &self,
356 q: &[f32],
357 k: usize,
358 skip: Option<&[u8]>,
359 filter: &impl crate::Filter,
360 ) -> Result<Vec<Match>> {
361 // The query is checked before the collection is looked at, so that a
362 // query of the wrong length says so rather than answering nothing at
363 // all while the collection happens to be empty.
364 let ready = self.ready(q)?;
365 if k == 0 || self.index.is_empty() {
366 return Ok(Vec::new());
367 }
368
369 // One more than asked for when a key is being left out, so that leaving
370 // it out does not cost an answer. It is the nearest one and it is
371 // therefore always in the shortlist.
372 let want = if skip.is_some() { k + 1 } else { k };
373 let hits = self.index.search_where(&ready, want, filter, &self.raw);
374
375 let mut out = Vec::with_capacity(hits.len().min(k));
376 for hit in hits {
377 let key = self.raw.owner(hit.id);
378 if skip == Some(key) {
379 continue;
380 }
381 out.push(Match {
382 key: key.to_vec(),
383 distance: self.report(hit.distance),
384 });
385 if out.len() == k {
386 break;
387 }
388 }
389 Ok(out)
390 }
391
392 /// The same answer, arrived at by measuring every vector in the collection.
393 ///
394 /// This is what the index is an approximation of, so it is the thing recall
395 /// is measured against, and it is what `VSIM ... TRUTH` asks for. It reads
396 /// no codes at all: the estimator exists to avoid this walk and there is
397 /// nothing it can contribute to a walk that is happening anyway.
398 ///
399 /// Linear in the collection, which is the point. A client asking for it on a
400 /// million vectors is asking for a million distances and should get them
401 /// rather than a refusal, because the reason to ask is to find out what the
402 /// index missed.
403 ///
404 /// # Errors
405 ///
406 /// As [`Collection::search`].
407 pub fn search_exact(&self, q: &[f32], k: usize, skip: Option<&[u8]>) -> Result<Vec<Match>> {
408 self.search_exact_where(q, k, skip, &crate::Any)
409 }
410
411 /// The same walk, over only the members the filter allows.
412 ///
413 /// There is no scan to push the filter into here, because there is no scan:
414 /// this measures everything. It exists so that a client asking for the exact
415 /// answer and asking for a filter gets the exact answer to the question it
416 /// asked, rather than being told the two options do not go together.
417 ///
418 /// # Errors
419 ///
420 /// As [`Collection::search`].
421 pub fn search_exact_where(
422 &self,
423 q: &[f32],
424 k: usize,
425 skip: Option<&[u8]>,
426 filter: &impl crate::Filter,
427 ) -> Result<Vec<Match>> {
428 let ready = self.ready(q)?;
429 if k == 0 {
430 return Ok(Vec::new());
431 }
432 let mut hits: Vec<(f32, &[u8])> = Vec::with_capacity(self.ids.len());
433 for (key, &id) in self.ids.iter() {
434 if skip == Some(key) {
435 continue;
436 }
437 if !filter.allows(self.index.tag(id).unwrap_or(0)) || !filter.exact(id) {
438 continue;
439 }
440 hits.push((crate::dist::sqdist(&ready, self.raw.at(id)), key));
441 }
442 // By distance and then by key, so that vectors at the same distance come
443 // back in an order that does not depend on where the table put them.
444 hits.sort_by(|a, b| a.0.total_cmp(&b.0).then_with(|| a.1.cmp(b.1)));
445 hits.truncate(k);
446 Ok(hits
447 .into_iter()
448 .map(|(sq, key)| Match {
449 key: key.to_vec(),
450 distance: self.report(sq),
451 })
452 .collect())
453 }
454
455 /// Do bounded maintenance, and say how many vectors it looked at.
456 ///
457 /// A caller with a maintenance slice runs this until it returns less than
458 /// the budget. A caller without one gets what [`Collection::put`] does on
459 /// its own, which is the same work in smaller pieces.
460 pub fn maintain(&mut self, budget: usize) -> usize {
461 self.index.maintain(&self.raw, budget)
462 }
463
464 /// What the collection is holding: the vectors, the codes and the keys.
465 #[must_use]
466 pub fn memory_bytes(&self) -> usize {
467 self.raw.memory_bytes() + self.index.code_bytes() + self.ids.memory_bytes()
468 }
469
470 /// The searchable size of the collection, which is the number the 32x claim
471 /// is about.
472 #[must_use]
473 pub fn code_bytes(&self) -> usize {
474 self.index.code_bytes()
475 }
476
477 // -- what an image is made of -------------------------------------------
478 //
479 // [`crate::image`] is the other half of these: it writes a collection down
480 // and reads it back, and it needs at the parts that nothing else outside
481 // this file has any business touching. A load is not a sequence of writes,
482 // so it cannot go through [`Collection::put`]: putting a vector back would
483 // requantise it, and requantising every vector is the rebuild an image
484 // exists to avoid.
485
486 /// The index, for the half of an image that is codes and centroids.
487 pub(crate) fn index(&self) -> &Partitions {
488 &self.index
489 }
490
491 /// Key to id, which is the half of an image that is names.
492 pub(crate) fn id_table(&self) -> &Elements<u64> {
493 &self.ids
494 }
495
496 /// How far the ids go, which is not how many there are.
497 ///
498 /// An id is a slot, so a collection that has had members removed has holes
499 /// and the live count says nothing about the highest id in use. This is the
500 /// number an image writes so that a load can allocate the table once.
501 pub(crate) fn slots(&self) -> usize {
502 self.raw.owner.len()
503 }
504
505 /// Whether the index has a member under `id`.
506 pub(crate) fn holds(&self, id: u64) -> bool {
507 self.index.contains(id)
508 }
509
510 /// An empty collection around an index that is already built, with room for
511 /// `slots` vectors and not one of them written yet.
512 pub(crate) fn from_image(index: Partitions, metric: Metric, slots: usize) -> Collection {
513 let dim = index.dim();
514 Collection {
515 index,
516 raw: Raw {
517 dim,
518 data: vec![0.0; slots * dim],
519 owner: vec![None; slots],
520 free: Vec::new(),
521 },
522 ids: Elements::new(),
523 metric,
524 }
525 }
526
527 /// Put a vector back in the slot the image says it was in.
528 ///
529 /// The index already has the member, so this is the other two tables only:
530 /// the vector into its slot and the key into the id table. No maintenance
531 /// runs, because nothing moved.
532 ///
533 /// # Errors
534 ///
535 /// [`Code::Full`] for a key the id table will not take, which for an image
536 /// written by this build cannot happen and for a damaged one is the honest
537 /// answer.
538 pub(crate) fn restore(&mut self, key: &[u8], id: u64, v: &[f32]) -> Result<()> {
539 self.raw.owner[id as usize] = Some(key.into());
540 self.raw.write(id, v);
541 self.ids
542 .insert(key, id)
543 .map_err(|_| Error::new(Code::Full, "that key is too long for a vector collection"))?;
544 Ok(())
545 }
546
547 /// Drop a member the store could not produce a vector for.
548 pub(crate) fn forget(&mut self, id: u64) {
549 self.index.remove(id);
550 }
551
552 /// Say a load is over, so the free list can be built from what is left.
553 ///
554 /// In reverse, so that the lowest free slot is on top and the next write
555 /// takes it. That is what an insert path that has been running normally
556 /// leaves behind, and a collection that has just been loaded should not be
557 /// distinguishable from one that has not.
558 pub(crate) fn seal(&mut self) {
559 self.raw.free.clear();
560 for id in (0..self.raw.owner.len()).rev() {
561 if self.raw.owner[id].is_none() {
562 self.raw.free.push(id as u64);
563 }
564 }
565 }
566
567 /// A vector this collection can take, in the form it stores.
568 fn ready(&self, v: &[f32]) -> Result<Vec<f32>> {
569 if v.len() != self.raw.dim {
570 return Err(Error::fmt(
571 Code::Invalid,
572 format_args!(
573 "this collection holds {} dimensional vectors and was handed {}",
574 self.raw.dim,
575 v.len()
576 ),
577 ));
578 }
579 if let Some(at) = v.iter().position(|x| !x.is_finite()) {
580 return Err(Error::fmt(
581 Code::Invalid,
582 format_args!(
583 "coordinate {at} of that vector is {}, and a distance to it would be one too",
584 v[at]
585 ),
586 ));
587 }
588 let mut ready = v.to_vec();
589 if self.metric == Metric::Cosine {
590 normalize(&mut ready)?;
591 }
592 Ok(ready)
593 }
594
595 /// Run the splits and merges the last write owes, if it owes any.
596 ///
597 /// Inside the write rather than after a threshold of them, because a split
598 /// that is owed is a partition already twice the size it wants to be and
599 /// every search until it happens reads all of it.
600 fn catch_up(&mut self) {
601 if self.index.needs_maintenance() {
602 self.index.maintain(&self.raw, BUDGET);
603 }
604 }
605
606 /// The distance to report for the squared one the index measured.
607 ///
608 /// The index works in squared euclidean distance because a square root
609 /// changes no ordering and costs one per candidate. The caller asked in the
610 /// metric the collection was opened with, so the square root happens here,
611 /// `k` times rather than once per candidate scanned.
612 fn report(&self, sq: f32) -> f32 {
613 match self.metric {
614 // On unit vectors the squared distance is 2 - 2cos, so half of it
615 // is one minus the cosine similarity, which is the number every
616 // cosine API in the world reports.
617 Metric::Cosine => (sq / 2.0).clamp(0.0, 2.0),
618 _ => sq.max(0.0).sqrt(),
619 }
620 }
621}
622
623/// The full precision vectors, one flat run of floats with a slot per id.
624///
625/// A slot is `dim` floats at `id * dim` and an id is never anything but the slot
626/// it names, so reading a vector for the rerank is an offset rather than a
627/// lookup. Slots come back for reuse when a key is removed, which is what keeps
628/// a collection that is rewritten forever from growing forever.
629#[derive(Debug)]
630struct Raw {
631 dim: usize,
632 data: Vec<f32>,
633 /// The key each slot holds, and `None` for a slot that is free.
634 ///
635 /// This is the second copy of a key, the first being the one in
636 /// [`Collection::ids`], and it is here because a search comes back with ids
637 /// and has to answer in keys. A key is tens of bytes against a vector's
638 /// thousands, so the copy is worth more than the indirection that would
639 /// avoid it.
640 owner: Vec<Option<Box<[u8]>>>,
641 free: Vec<u64>,
642}
643
644impl Raw {
645 /// The vector in slot `id`.
646 fn at(&self, id: u64) -> &[f32] {
647 let at = id as usize * self.dim;
648 &self.data[at..at + self.dim]
649 }
650
651 /// The key slot `id` was taken for.
652 fn owner(&self, id: u64) -> &[u8] {
653 self.owner[id as usize]
654 .as_deref()
655 .expect("a live id has a key, and a search only answers with live ids")
656 }
657
658 /// Put `v` in a free slot for `key`, or in a new one, and say which.
659 fn take(&mut self, key: &[u8], v: &[f32]) -> u64 {
660 let id = match self.free.pop() {
661 Some(id) => id,
662 None => {
663 self.data.resize(self.data.len() + self.dim, 0.0);
664 self.owner.push(None);
665 (self.owner.len() - 1) as u64
666 }
667 };
668 self.owner[id as usize] = Some(key.into());
669 self.write(id, v);
670 id
671 }
672
673 /// Overwrite the vector in a slot that is already taken.
674 fn write(&mut self, id: u64, v: &[f32]) {
675 let at = id as usize * self.dim;
676 self.data[at..at + self.dim].copy_from_slice(v);
677 }
678
679 /// Give a slot back.
680 fn release(&mut self, id: u64) {
681 self.owner[id as usize] = None;
682 self.free.push(id);
683 }
684
685 fn memory_bytes(&self) -> usize {
686 self.data.capacity() * size_of::<f32>()
687 + self.owner.capacity() * size_of::<Option<Box<[u8]>>>()
688 + self
689 .owner
690 .iter()
691 .map(|k| k.as_ref().map_or(0, |k| k.len()))
692 .sum::<usize>()
693 + self.free.capacity() * size_of::<u64>()
694 }
695}
696
697impl Vectors for Raw {
698 fn get(&self, id: u64, into: &mut [f32]) -> bool {
699 let Some(Some(_)) = self.owner.get(id as usize) else {
700 return false;
701 };
702 into.copy_from_slice(self.at(id));
703 true
704 }
705}
706
707/// Turn a vector into the unit vector pointing the same way.
708fn normalize(v: &mut [f32]) -> Result<()> {
709 let norm = v.iter().map(|x| f64::from(*x) * f64::from(*x)).sum::<f64>();
710 if norm <= 0.0 {
711 return Err(Error::new(
712 Code::Invalid,
713 "a cosine collection compares directions and a vector of length zero has none",
714 ));
715 }
716 #[allow(clippy::cast_possible_truncation)]
717 let scale = norm.sqrt().recip() as f32;
718 for x in v.iter_mut() {
719 *x *= scale;
720 }
721 Ok(())
722}
723
724/// `dim` as the shape grammar writes it, if it is a dimension a collection can
725/// be opened with.
726///
727/// The width comes back rather than a bare yes, because every caller that has to
728/// ask this also has to write the dimension into the collection's description,
729/// and the check is what makes the conversion safe.
730///
731/// # Errors
732///
733/// [`Code::Invalid`], with the range in the message.
734pub fn width(dim: usize) -> Result<u32> {
735 if dim == 0 || dim > MAX_DIM {
736 return Err(Error::fmt(
737 Code::Invalid,
738 format_args!(
739 "a vector collection holds between 1 and {MAX_DIM} dimensions, and {dim} is not one of them"
740 ),
741 ));
742 }
743 u32::try_from(dim).map_err(|_| Error::new(Code::Invalid, "that dimension does not fit"))
744}
745
746/// Whether `metric` is one this build can measure.
747///
748/// # Errors
749///
750/// [`Code::Unsupported`], saying what to do instead.
751pub fn check_metric(metric: Metric) -> Result<()> {
752 match metric {
753 Metric::L2 | Metric::Cosine => Ok(()),
754 Metric::Ip => Err(Error::new(
755 Code::Unsupported,
756 "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",
757 )),
758 Metric::Hamming => Err(Error::new(
759 Code::Unsupported,
760 "hamming distance is for binary vectors and this collection holds floats",
761 )),
762 }
763}
764
765#[cfg(test)]
766mod tests {
767 use super::*;
768
769 fn axes() -> Collection {
770 let mut c = Collection::new(3, Metric::L2).unwrap();
771 c.put(b"x", &[1.0, 0.0, 0.0]).unwrap();
772 c.put(b"y", &[0.0, 1.0, 0.0]).unwrap();
773 c.put(b"z", &[0.0, 0.0, 1.0]).unwrap();
774 c
775 }
776
777 #[test]
778 fn a_vector_comes_back_the_way_it_went_in() {
779 let mut c = Collection::new(3, Metric::L2).unwrap();
780 assert!(c.is_empty());
781 assert!(c.put(b"x", &[1.0, 2.0, 3.0]).unwrap(), "the key is new");
782 assert!(
783 !c.put(b"x", &[1.0, 2.0, 3.0]).unwrap(),
784 "and then it is not"
785 );
786 assert_eq!(c.get(b"x"), Some(&[1.0, 2.0, 3.0][..]));
787 assert_eq!(c.len(), 1);
788 assert!(c.contains(b"x"));
789 assert_eq!(c.get(b"nobody"), None);
790 assert_eq!(c.keys().collect::<Vec<_>>(), vec![&b"x"[..]]);
791 assert!(c.memory_bytes() > 0);
792 }
793
794 #[test]
795 fn the_nearest_answer_is_the_nearest_vector() {
796 let c = axes();
797 let hits = c.search(&[0.9, 0.2, 0.1], 3, None).unwrap();
798 let keys: Vec<&[u8]> = hits.iter().map(|h| h.key.as_slice()).collect();
799 assert_eq!(keys, vec![&b"x"[..], &b"y"[..], &b"z"[..]]);
800 // The exact euclidean distance and not the estimate the codes gave,
801 // which is the point of reranking against the stored vector.
802 let want = (0.01f32 + 0.04 + 0.01).sqrt();
803 assert!((hits[0].distance - want).abs() < 1e-6, "{hits:?}");
804 }
805
806 #[test]
807 fn a_removed_vector_is_not_an_answer_and_its_slot_comes_back() {
808 let mut c = axes();
809 assert!(c.remove(b"x"));
810 assert!(!c.remove(b"x"), "twice is not there twice");
811 assert_eq!(c.len(), 2);
812
813 let hits = c.search(&[1.0, 0.0, 0.0], 3, None).unwrap();
814 assert_eq!(hits.len(), 2);
815 assert!(hits.iter().all(|h| h.key != b"x"));
816
817 c.put(b"w", &[1.0, 0.0, 0.0]).unwrap();
818 let hits = c.search(&[1.0, 0.0, 0.0], 1, None).unwrap();
819 assert_eq!(hits[0].key, b"w", "the reused slot answers as w");
820 }
821
822 /// Replacing has to take the old code out of its partition as well as
823 /// writing the new vector, or a search answers with a key whose vector
824 /// moved somewhere else.
825 #[test]
826 fn a_replaced_vector_is_searched_at_its_new_place() {
827 let mut c = axes();
828 c.put(b"x", &[0.0, 0.0, 1.0]).unwrap();
829 assert_eq!(c.len(), 3, "a replacement is not a second key");
830
831 let hits = c.search(&[1.0, 0.0, 0.0], 1, None).unwrap();
832 assert_eq!(hits[0].key, b"y", "x moved away from that corner");
833 }
834
835 #[test]
836 fn a_search_can_leave_one_key_out() {
837 let mut c = axes();
838 c.put(b"x2", &[0.9, 0.1, 0.0]).unwrap();
839 let hits = c.search(&[1.0, 0.0, 0.0], 2, Some(b"x")).unwrap();
840 assert_eq!(hits.len(), 2);
841 assert_eq!(hits[0].key, b"x2");
842 assert!(hits.iter().all(|h| h.key != b"x"));
843 }
844
845 #[test]
846 fn a_cosine_collection_stores_the_direction_and_reports_the_angle() {
847 let mut c = Collection::new(2, Metric::Cosine).unwrap();
848 c.put(b"east", &[7.0, 0.0]).unwrap();
849 c.put(b"north", &[0.0, 3.0]).unwrap();
850 c.put(b"west", &[-2.0, 0.0]).unwrap();
851 assert_eq!(c.get(b"east"), Some(&[1.0, 0.0][..]));
852
853 // Length is nothing to a cosine collection, so a long east and a short
854 // east are the same vector and both are nearer than north.
855 let hits = c.search(&[100.0, 0.0], 3, None).unwrap();
856 assert_eq!(hits[0].key, b"east");
857 assert!(hits[0].distance.abs() < 1e-6, "{hits:?}");
858 assert!(
859 (hits[1].distance - 1.0).abs() < 1e-6,
860 "north is a right angle"
861 );
862 assert!(
863 (hits[2].distance - 2.0).abs() < 1e-6,
864 "west is the opposite"
865 );
866
867 let e = c.put(b"nowhere", &[0.0, 0.0]).expect_err("no direction");
868 assert_eq!(e.code(), Code::Invalid);
869 }
870
871 #[test]
872 fn a_vector_of_the_wrong_length_or_shape_is_refused() {
873 let mut c = Collection::new(3, Metric::L2).unwrap();
874 let e = c.put(b"x", &[1.0, 2.0]).expect_err("two is not three");
875 assert_eq!(e.code(), Code::Invalid);
876 assert!(e.message().contains("3 dimensional"), "{e}");
877
878 let e = c
879 .put(b"x", &[1.0, f32::NAN, 2.0])
880 .expect_err("not a number");
881 assert_eq!(e.code(), Code::Invalid);
882 assert!(e.message().contains("coordinate 1"), "{e}");
883
884 // On an empty collection too, where there is nothing to search and the
885 // easy thing would be to answer nothing.
886 let e = c.search(&[1.0], 1, None).expect_err("one is not three");
887 assert_eq!(e.code(), Code::Invalid);
888 }
889
890 #[test]
891 fn a_dimension_or_a_metric_the_build_cannot_hold_is_refused() {
892 assert_eq!(
893 Collection::new(0, Metric::L2).unwrap_err().code(),
894 Code::Invalid
895 );
896 assert_eq!(
897 Collection::new(MAX_DIM + 1, Metric::L2).unwrap_err().code(),
898 Code::Invalid
899 );
900 let e = Collection::new(8, Metric::Ip).unwrap_err();
901 assert_eq!(e.code(), Code::Unsupported);
902 assert!(e.message().contains("cosine"), "{e}");
903 assert_eq!(
904 Collection::new(8, Metric::Hamming).unwrap_err().code(),
905 Code::Unsupported
906 );
907 }
908
909 /// Everything above runs inside one partition, and one partition is a scan
910 /// rather than an index, so this is the one that actually exercises the
911 /// splits and the maintenance the writes pay for.
912 #[test]
913 fn recall_holds_once_the_index_has_split() {
914 let mut c = Collection::new(8, Metric::L2).unwrap();
915 let mut seed = 0x2026u64;
916 let mut next = move || {
917 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
918 ((seed >> 33) as f32 / (1u64 << 31) as f32) - 0.5
919 };
920
921 let mut all: Vec<Vec<f32>> = Vec::new();
922 for i in 0..2000usize {
923 let x: Vec<f32> = (0..8).map(|_| next()).collect();
924 c.put(format!("k{i}").as_bytes(), &x).unwrap();
925 all.push(x);
926 }
927 assert!(c.partitions() > 1, "nothing ever split");
928
929 let mut found = 0;
930 for (i, q) in all.iter().enumerate().step_by(50) {
931 let hits = c.search(q, 1, None).unwrap();
932 if hits[0].key == format!("k{i}").into_bytes() {
933 found += 1;
934 }
935 }
936 assert!(found >= 39, "{found} of 40 queries found their own vector");
937 assert_eq!(c.maintain(1 << 20), 0, "the writes left nothing owed");
938 }
939}