Skip to main content

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    #[cfg_attr(
914        miri,
915        ignore = "the count is the claim: recall over two thousand writes, and two thousand writes is also the only reason there is more than one partition to measure"
916    )]
917    fn recall_holds_once_the_index_has_split() {
918        let mut c = Collection::new(8, Metric::L2).unwrap();
919        let mut seed = 0x2026u64;
920        let mut next = move || {
921            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
922            ((seed >> 33) as f32 / (1u64 << 31) as f32) - 0.5
923        };
924
925        let mut all: Vec<Vec<f32>> = Vec::new();
926        for i in 0..2000usize {
927            let x: Vec<f32> = (0..8).map(|_| next()).collect();
928            c.put(format!("k{i}").as_bytes(), &x).unwrap();
929            all.push(x);
930        }
931        assert!(c.partitions() > 1, "nothing ever split");
932
933        let mut found = 0;
934        for (i, q) in all.iter().enumerate().step_by(50) {
935            let hits = c.search(q, 1, None).unwrap();
936            if hits[0].key == format!("k{i}").into_bytes() {
937                found += 1;
938            }
939        }
940        assert!(found >= 39, "{found} of 40 queries found their own vector");
941        assert_eq!(c.maintain(1 << 20), 0, "the writes left nothing owed");
942    }
943}