Skip to main content

yo/
vector.rs

1//! `Vectors`, the collection of embeddings, and the search over it (`10` and
2//! `15` section 2).
3//!
4//! A vector collection is a name, a dimension and a metric. Something goes in
5//! under a key, the same thing comes back under that key, and a query vector
6//! gets the nearest keys to it. There is no index to create, no probe list to
7//! tune and no build step: the index is maintained as the collection is written
8//! to, in bounded pieces, which is what `10` section 5 is about and is the whole
9//! reason the index under here is partitions rather than a graph.
10//!
11//! ```
12//! let db = yo::open(yo::MEMORY)?;
13//! let v = db.vectors("passages", 3)?;
14//!
15//! v.put("a", &[1.0, 0.0, 0.0])?;
16//! v.put("b", &[0.0, 1.0, 0.0])?;
17//!
18//! let hits = v.search(&[0.9, 0.1, 0.0], 1)?;
19//! assert_eq!(hits[0].key, b"a".to_vec());
20//! # Ok::<(), yo::Error>(())
21//! ```
22//!
23//! # What a search costs, and why it is exact at the end
24//!
25//! The searchable form of a vector is a RaBitQ code, which is a bit per
26//! dimension, so a collection of 768 dimensional embeddings is 96 bytes a vector
27//! to scan rather than 3072. The codes pick the candidates and then the full
28//! precision vectors settle the order, so a hit's distance is the real distance
29//! and not an estimate, and the only thing quantisation can cost is a near miss
30//! that never made the shortlist. `yo-vector`'s recall tables are the measured
31//! version of that sentence.
32//!
33//! # The metric decides what is stored
34//!
35//! [`Metric::L2`] stores the vector it was given. [`Metric::Cosine`] stores the
36//! unit vector, because the index measures distance and on unit vectors the
37//! nearest by distance is the nearest by angle, which means cosine costs one
38//! normalisation on the way in rather than a different index. That is also what
39//! comes back out of [`Vectors::get`], and it is the same answer Redis gives
40//! for a cosine vector set.
41//!
42//! [`Metric::Ip`] and [`Metric::Hamming`] are refused rather than approximated.
43//! Inner product is not a distance, so ordering by it is not ordering by
44//! nearness and the partitions would be built around the wrong question, and
45//! Hamming wants binary vectors that this collection does not hold yet.
46//!
47//! # Where the vectors live
48//!
49//! Beside the index, one flat run of floats per collection, which is the shape
50//! `06` gives them: a vector is a record like any other and the rerank is a read
51//! at an address the id already resolves to. This build holds that run in
52//! memory, exactly as every other collection here is held in memory, and the
53//! record kind it becomes on disk is already written down as
54//! `yo_format::vector`. Nothing on this page changes when the file arrives.
55//!
56//! # What is on this page and what is not
57//!
58//! Only the handle. The collection itself is [`yo_vector::Collection`], one crate
59//! down, because the vector commands on the wire need the same key table, the
60//! same slab of floats and the same metric handling that this does. Two doors
61//! into one store is Y23 and it is the reason `INCR` off a socket and
62//! [`Db::counter`](crate::Db::counter) cannot drift apart either.
63
64use yo_common::{Code, Error, Result};
65use yo_shape::Metric;
66
67use crate::db::Handle;
68
69pub use yo_vector::Match;
70
71/// A collection of vectors, reached by [`Db::vectors`](crate::Db::vectors).
72///
73/// Keys are byte strings the way the keyspace's are, so anything that is bytes
74/// will do.
75#[derive(Clone)]
76pub struct Vectors {
77    pub(crate) db: Handle,
78    pub(crate) at: usize,
79    pub(crate) dim: usize,
80    pub(crate) metric: Metric,
81}
82
83impl core::fmt::Debug for Vectors {
84    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
85        f.debug_struct("Vectors")
86            .field("dim", &self.dim)
87            .field("metric", &self.metric)
88            .finish_non_exhaustive()
89    }
90}
91
92impl Vectors {
93    /// How many coordinates a vector in this collection has.
94    #[must_use]
95    pub fn dim(&self) -> usize {
96        self.dim
97    }
98
99    /// What nearness means here.
100    #[must_use]
101    pub fn metric(&self) -> Metric {
102        self.metric
103    }
104
105    /// Put a vector in under `key`, and say whether the key is new.
106    ///
107    /// Replacing is the same call, which is what makes re-embedding a document
108    /// one line. The old code comes out of its partition and the new one goes
109    /// into whichever partition it belongs to now, so nothing accumulates and
110    /// there is no rebuild waiting at the end of it.
111    ///
112    /// # Errors
113    ///
114    /// [`Code::Invalid`] when the vector is not [`Vectors::dim`] long, when it
115    /// holds a coordinate that is not a number, or when a cosine collection is
116    /// handed a vector of length zero, which has no direction to store.
117    /// [`Code::Full`] for a key past the length limit.
118    pub fn put(&self, key: impl AsRef<[u8]>, v: &[f32]) -> Result<bool> {
119        let key = key.as_ref();
120        self.db
121            .write(|inner| inner.collections[self.at].data.vectors_mut().put(key, v))
122    }
123
124    /// The vector under `key`, if there is one.
125    ///
126    /// A cosine collection hands back the unit vector it stored rather than the
127    /// one it was given. See the module note on the metric for why.
128    ///
129    /// # Errors
130    ///
131    /// [`Code::Invalid`] if called from inside a callback that is already
132    /// holding this database.
133    pub fn get(&self, key: impl AsRef<[u8]>) -> Result<Option<Vec<f32>>> {
134        self.with(key, <[f32]>::to_vec)
135    }
136
137    /// The same, handed to a closure where it lies, which copies nothing.
138    ///
139    /// The owned form is what most code wants and this is Y29's other half: the
140    /// vectors are contiguous floats already, so a caller that only wants to
141    /// measure one against something never has to allocate to do it.
142    ///
143    /// # Errors
144    ///
145    /// As [`Vectors::get`].
146    pub fn with<R>(&self, key: impl AsRef<[u8]>, f: impl FnOnce(&[f32]) -> R) -> Result<Option<R>> {
147        let key = key.as_ref();
148        self.db
149            .read(|inner| Ok(inner.collections[self.at].data.vectors().get(key).map(f)))
150    }
151
152    /// Whether the collection holds a vector under `key`.
153    ///
154    /// # Errors
155    ///
156    /// As [`Vectors::get`].
157    pub fn contains(&self, key: impl AsRef<[u8]>) -> Result<bool> {
158        let key = key.as_ref();
159        self.db
160            .read(|inner| Ok(inner.collections[self.at].data.vectors().contains(key)))
161    }
162
163    /// Take a vector out, saying whether it was there.
164    ///
165    /// A delete here is a delete and not a tombstone: the member leaves its
166    /// posting and the last member of that posting moves into the hole. That is
167    /// the difference between this and a graph index, where deletes pile up
168    /// until somebody rebuilds.
169    ///
170    /// # Errors
171    ///
172    /// As [`Vectors::get`].
173    pub fn remove(&self, key: impl AsRef<[u8]>) -> Result<bool> {
174        let key = key.as_ref();
175        self.db
176            .write(|inner| Ok(inner.collections[self.at].data.vectors_mut().remove(key)))
177    }
178
179    /// How many vectors are in the collection.
180    ///
181    /// # Errors
182    ///
183    /// As [`Vectors::get`].
184    pub fn len(&self) -> Result<usize> {
185        self.db
186            .read(|inner| Ok(inner.collections[self.at].data.vectors().len()))
187    }
188
189    /// Whether there are none.
190    ///
191    /// # Errors
192    ///
193    /// As [`Vectors::get`].
194    pub fn is_empty(&self) -> Result<bool> {
195        self.len().map(|n| n == 0)
196    }
197
198    /// The `k` nearest keys to `q`, nearest first.
199    ///
200    /// Fewer than `k` come back when the collection holds fewer than that, and
201    /// nothing comes back from an empty one.
202    ///
203    /// ```
204    /// let db = yo::open(yo::MEMORY)?;
205    /// let v = db.vectors("words", 2)?;
206    ///
207    /// v.put("north", &[0.0, 1.0])?;
208    /// v.put("east", &[1.0, 0.0])?;
209    /// v.put("west", &[-1.0, 0.0])?;
210    ///
211    /// let hits = v.search(&[0.2, 0.9], 2)?;
212    /// assert_eq!(hits[0].key, b"north".to_vec());
213    /// assert_eq!(hits.len(), 2);
214    /// # Ok::<(), yo::Error>(())
215    /// ```
216    ///
217    /// # Errors
218    ///
219    /// [`Code::Invalid`] when `q` is not [`Vectors::dim`] long or holds a
220    /// coordinate that is not a number.
221    pub fn search(&self, q: &[f32], k: usize) -> Result<Vec<Match>> {
222        self.db
223            .read(|inner| inner.collections[self.at].data.vectors().search(q, k, None))
224    }
225
226    /// The `k` nearest keys to the vector already stored under `key`, which is
227    /// the more-like-this search.
228    ///
229    /// `key` itself is never one of the answers, because it is always the
230    /// nearest and nobody asked what a thing is most similar to itself.
231    ///
232    /// # Errors
233    ///
234    /// [`Code::NotFound`] when nothing is stored under `key`, because an empty
235    /// answer would otherwise mean both "no such key" and "nothing near it".
236    pub fn near(&self, key: impl AsRef<[u8]>, k: usize) -> Result<Vec<Match>> {
237        let key = key.as_ref();
238        self.db.read(|inner| {
239            let store = inner.collections[self.at].data.vectors();
240            let Some(q) = store.get(key) else {
241                return Err(Error::fmt(
242                    Code::NotFound,
243                    format_args!(
244                        "this collection has no vector under that key, so there is nothing to be near to"
245                    ),
246                ));
247            };
248            store.search(q, k, Some(key))
249        })
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use yo_vector::collection::MAX_DIM;
256
257    use crate::db::{MEMORY, open};
258
259    use super::*;
260
261    /// Three vectors on the axes, which makes every answer obvious by eye.
262    fn axes(v: &Vectors) {
263        v.put("x", &[1.0, 0.0, 0.0]).unwrap();
264        v.put("y", &[0.0, 1.0, 0.0]).unwrap();
265        v.put("z", &[0.0, 0.0, 1.0]).unwrap();
266    }
267
268    #[test]
269    fn a_vector_comes_back_the_way_it_went_in() {
270        let db = open(MEMORY).unwrap();
271        let v = db.vectors("e", 3).unwrap();
272        assert!(v.is_empty().unwrap());
273
274        assert!(v.put("x", &[1.0, 2.0, 3.0]).unwrap(), "the key is new");
275        assert!(!v.put("x", &[1.0, 2.0, 3.0]).unwrap(), "and then it is not");
276        assert_eq!(v.get("x").unwrap(), Some(vec![1.0, 2.0, 3.0]));
277        assert_eq!(v.len().unwrap(), 1);
278        assert!(v.contains("x").unwrap());
279        assert_eq!(v.get("nobody").unwrap(), None);
280        assert_eq!(v.with("x", |x| x.len()).unwrap(), Some(3));
281    }
282
283    #[test]
284    fn the_nearest_answer_is_the_nearest_vector() {
285        let db = open(MEMORY).unwrap();
286        let v = db.vectors("e", 3).unwrap();
287        axes(&v);
288
289        let hits = v.search(&[0.9, 0.2, 0.1], 3).unwrap();
290        let keys: Vec<&[u8]> = hits.iter().map(|h| h.key.as_slice()).collect();
291        assert_eq!(keys, vec![&b"x"[..], &b"y"[..], &b"z"[..]]);
292        assert!(hits[0].distance < hits[1].distance);
293        // The exact euclidean distance and not the estimate the codes gave,
294        // which is the whole point of reranking against the stored vector.
295        let want = (0.01f32 + 0.04 + 0.01).sqrt();
296        assert!((hits[0].distance - want).abs() < 1e-6, "{hits:?}");
297    }
298
299    #[test]
300    fn asking_for_more_than_there_is_gets_what_there_is() {
301        let db = open(MEMORY).unwrap();
302        let v = db.vectors("e", 3).unwrap();
303        assert!(v.search(&[1.0, 0.0, 0.0], 4).unwrap().is_empty());
304        axes(&v);
305        assert_eq!(v.search(&[1.0, 0.0, 0.0], 10).unwrap().len(), 3);
306        assert!(v.search(&[1.0, 0.0, 0.0], 0).unwrap().is_empty());
307    }
308
309    #[test]
310    fn a_removed_vector_is_not_an_answer_and_its_slot_comes_back() {
311        let db = open(MEMORY).unwrap();
312        let v = db.vectors("e", 3).unwrap();
313        axes(&v);
314
315        assert!(v.remove("x").unwrap());
316        assert!(!v.remove("x").unwrap(), "twice is not there twice");
317        assert_eq!(v.len().unwrap(), 2);
318        assert!(!v.contains("x").unwrap());
319
320        let hits = v.search(&[1.0, 0.0, 0.0], 3).unwrap();
321        assert_eq!(hits.len(), 2);
322        assert!(hits.iter().all(|h| h.key != b"x".to_vec()));
323
324        v.put("w", &[1.0, 0.0, 0.0]).unwrap();
325        let hits = v.search(&[1.0, 0.0, 0.0], 1).unwrap();
326        assert_eq!(hits[0].key, b"w".to_vec(), "the reused slot answers as w");
327    }
328
329    /// Replacing has to take the old code out of its partition as well as
330    /// writing the new vector, or the search answers with a key whose vector
331    /// moved somewhere else.
332    #[test]
333    fn a_replaced_vector_is_searched_at_its_new_place() {
334        let db = open(MEMORY).unwrap();
335        let v = db.vectors("e", 3).unwrap();
336        axes(&v);
337
338        v.put("x", &[0.0, 0.0, 1.0]).unwrap();
339        assert_eq!(v.len().unwrap(), 3, "a replacement is not a second key");
340
341        let hits = v.search(&[1.0, 0.0, 0.0], 1).unwrap();
342        assert_eq!(hits[0].key, b"y".to_vec(), "x moved away from that corner");
343        let hits = v.search(&[0.0, 0.0, 1.0], 2).unwrap();
344        let keys: Vec<Vec<u8>> = hits.into_iter().map(|h| h.key).collect();
345        assert!(keys.contains(&b"x".to_vec()) && keys.contains(&b"z".to_vec()));
346    }
347
348    #[test]
349    fn more_like_this_leaves_the_thing_itself_out() {
350        let db = open(MEMORY).unwrap();
351        let v = db.vectors("e", 3).unwrap();
352        axes(&v);
353        v.put("x2", &[0.9, 0.1, 0.0]).unwrap();
354
355        let hits = v.near("x", 2).unwrap();
356        assert_eq!(hits.len(), 2);
357        assert_eq!(hits[0].key, b"x2".to_vec());
358        assert!(hits.iter().all(|h| h.key != b"x".to_vec()));
359
360        let e = v.near("nobody", 2).expect_err("no such key");
361        assert_eq!(e.code(), Code::NotFound);
362    }
363
364    #[test]
365    fn a_cosine_collection_stores_the_direction_and_reports_the_angle() {
366        let db = open(MEMORY).unwrap();
367        let v = db.vectors_with("e", 2, Metric::Cosine).unwrap();
368
369        v.put("east", &[7.0, 0.0]).unwrap();
370        v.put("north", &[0.0, 3.0]).unwrap();
371        v.put("west", &[-2.0, 0.0]).unwrap();
372        assert_eq!(v.get("east").unwrap(), Some(vec![1.0, 0.0]));
373
374        // Length is nothing to a cosine collection, so a long east and a short
375        // east are the same vector and both are nearer than north.
376        let hits = v.search(&[100.0, 0.0], 3).unwrap();
377        assert_eq!(hits[0].key, b"east".to_vec());
378        assert!(hits[0].distance.abs() < 1e-6, "{hits:?}");
379        assert!(
380            (hits[1].distance - 1.0).abs() < 1e-6,
381            "north is a right angle"
382        );
383        assert!(
384            (hits[2].distance - 2.0).abs() < 1e-6,
385            "west is the opposite"
386        );
387
388        let e = v.put("nowhere", &[0.0, 0.0]).expect_err("no direction");
389        assert_eq!(e.code(), Code::Invalid);
390    }
391
392    #[test]
393    fn a_vector_of_the_wrong_length_or_shape_is_refused() {
394        let db = open(MEMORY).unwrap();
395        let v = db.vectors("e", 3).unwrap();
396
397        let e = v.put("x", &[1.0, 2.0]).expect_err("two is not three");
398        assert_eq!(e.code(), Code::Invalid);
399        assert!(e.message().contains("3 dimensional"), "{e}");
400
401        let e = v.put("x", &[1.0, f32::NAN, 2.0]).expect_err("not a number");
402        assert_eq!(e.code(), Code::Invalid);
403        assert!(e.message().contains("coordinate 1"), "{e}");
404
405        let e = v.search(&[1.0], 1).expect_err("one is not three");
406        assert_eq!(e.code(), Code::Invalid);
407    }
408
409    /// Enough vectors that the index has actually split, because everything
410    /// above runs inside one partition and a one partition index is a scan.
411    #[test]
412    fn recall_holds_once_the_index_has_split() {
413        let db = open(MEMORY).unwrap();
414        let v = db.vectors("e", 8).unwrap();
415
416        let mut seed = 0x2026u64;
417        let mut next = move || {
418            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
419            ((seed >> 33) as f32 / (1u64 << 31) as f32) - 0.5
420        };
421        let mut all: Vec<Vec<f32>> = Vec::new();
422        for i in 0..2000usize {
423            let x: Vec<f32> = (0..8).map(|_| next()).collect();
424            v.put(format!("k{i}"), &x).unwrap();
425            all.push(x);
426        }
427
428        let mut found = 0;
429        for (i, q) in all.iter().enumerate().step_by(50) {
430            let hits = v.search(q, 1).unwrap();
431            if hits[0].key == format!("k{i}").into_bytes() {
432                found += 1;
433            }
434        }
435        assert!(found >= 39, "{found} of 40 queries found their own vector");
436        assert_eq!(v.len().unwrap(), 2000);
437        assert!(db.memory_bytes().unwrap() > 2000 * 8 * 4);
438    }
439
440    #[test]
441    fn a_dimension_or_a_metric_the_build_cannot_hold_is_refused_at_open() {
442        let db = open(MEMORY).unwrap();
443
444        let e = db.vectors("e", 0).expect_err("zero dimensions is nothing");
445        assert_eq!(e.code(), Code::Invalid);
446        let e = db.vectors("e", MAX_DIM + 1).expect_err("past the limit");
447        assert_eq!(e.code(), Code::Invalid);
448
449        let e = db
450            .vectors_with("e", 8, Metric::Ip)
451            .expect_err("not a distance");
452        assert_eq!(e.code(), Code::Unsupported);
453        assert!(e.message().contains("cosine"), "{e}");
454        let e = db
455            .vectors_with("e", 8, Metric::Hamming)
456            .expect_err("not floats");
457        assert_eq!(e.code(), Code::Unsupported);
458    }
459
460    /// The dimension and the metric are the collection's shape, so opening the
461    /// same name with either of them changed is the same refusal a map gets for
462    /// the wrong value type.
463    #[test]
464    fn the_dimension_and_the_metric_are_part_of_the_shape() {
465        let db = open(MEMORY).unwrap();
466        let v = db.vectors("e", 3).unwrap();
467        v.put("x", &[1.0, 0.0, 0.0]).unwrap();
468
469        let same = db.vectors("e", 3).unwrap();
470        assert_eq!(same.len().unwrap(), 1, "the same name is the same store");
471
472        let e = db.vectors("e", 4).expect_err("that is another collection");
473        assert_eq!(e.code(), Code::ShapeMismatch);
474        let e = db
475            .vectors_with("e", 3, Metric::Cosine)
476            .expect_err("and so is that");
477        assert_eq!(e.code(), Code::ShapeMismatch);
478
479        let e = db.map::<String, u64>("e").expect_err("and so is a map");
480        assert_eq!(e.code(), Code::ShapeMismatch);
481    }
482}