Skip to main content

yo_format/
vector.rs

1//! What a vector record holds, which is the vector itself at full precision.
2//!
3//! `06` section 2.1 gives kind 3 to a vector and says nothing about what is
4//! inside it, and `10` section 2 says why there is anything inside it at all:
5//! the searchable form of a vector is a RaBitQ code in a posting, and the code
6//! is lossy, so the last step of a search measures the best few candidates
7//! against the real thing. Every other engine that quantises has to keep the
8//! raw vectors somewhere on purpose. Here a vector is a record like any other
9//! and the rerank is a read at an address the id already resolves to.
10//!
11//! ```text
12//! +---------+---------+---------+---------+----------------------+
13//! |   dim   | element |  flags  | reserved|  dim * width bytes   |
14//! |    4    |    1    |    1    |    2    |                      |
15//! +---------+---------+---------+---------+----------------------+
16//! ```
17//!
18//! # Why the dimension is in the record
19//!
20//! The collection knows its own dimension, it is in the catalogue, and a
21//! reader that has the catalogue could work the count out from the record
22//! length. Storing it anyway costs four bytes and buys two things. A record is
23//! checkable on its own, so `yodb check` and the independent reader can say
24//! that a vector record is malformed without loading the catalogue for the
25//! collection it belongs to, and a collection whose dimension was changed under
26//! it produces a record that disagrees with the catalogue rather than a vector
27//! that is silently reinterpreted at a different length.
28//!
29//! # Why there is an element byte when there is one element type
30//!
31//! Everything writes and reads [`Element::F32`] today. The byte is there
32//! because after the freeze at the end of M6 the only lever left is
33//! `min_reader_version`, and half precision storage is the change most likely
34//! to be wanted: it halves what the log holds for a vector collection, which is
35//! most of what a vector collection is. With the byte here that lands as a new
36//! element value which old readers refuse one record at a time. Without it, it
37//! would need a new record kind or a format version, and both of those refuse
38//! the whole file.
39//!
40//! An unknown element value is [`Code::Corrupt`], not a record to skip. Skipping
41//! is the right answer for an unknown `kind`, because a kind a reader has never
42//! heard of is a thing it was never meant to understand. An element it cannot
43//! read inside a kind it can is different: the caller asked for this vector, and
44//! quietly returning nothing would look like a vector that is not there.
45//!
46//! # What is not in here
47//!
48//! The codes, the centroids and the postings. Those are the index, the index is
49//! derived from these records, and `10` section 2 keeps them resident or in the
50//! cold tier rather than in the log. When they do get written down it will be
51//! as collection chunks under a checkpoint, because the record kinds are fixed
52//! by `06` and there is no kind for an index.
53//!
54//! The id, because that is the record's key, and the tag a filtered scan reads,
55//! because that is derived from the document the vector belongs to.
56
57use crate::{get_u8, get_u16, get_u32, put_u8, put_u16, put_u32};
58use yo_common::{Code, Error, Result};
59
60/// The fixed part at the front of a vector record's value.
61pub const VECTOR_HEADER_LEN: usize = 8;
62
63/// The largest dimension this version will write or read.
64///
65/// Sixty five thousand is far past every published embedding family, which
66/// stops at a few thousand, and it is small enough that a corrupt `dim` cannot
67/// ask a reader for a gigabyte. The real limit is lower and it is not this: a
68/// record has to fit a page, so at f32 a vector is bounded by the page size,
69/// and a collection of vectors this long would want the chunked form (`05`
70/// section 4.4) instead.
71pub const MAX_DIM: usize = 65_536;
72
73/// How one coordinate is stored.
74///
75/// The values are part of the format. See the module note on why there is a
76/// byte for this when there is only one of them.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78#[repr(u8)]
79pub enum Element {
80    /// Little endian `f32`, four bytes.
81    F32 = 0,
82}
83
84impl Element {
85    /// The byte that stands for this element type.
86    #[must_use]
87    pub const fn as_u8(self) -> u8 {
88        self as u8
89    }
90
91    /// The element type for a byte, or `None` if this version has not heard of
92    /// it.
93    #[must_use]
94    pub const fn from_u8(b: u8) -> Option<Element> {
95        match b {
96            0 => Some(Element::F32),
97            _ => None,
98        }
99    }
100
101    /// How many bytes one coordinate takes.
102    #[must_use]
103    pub const fn width(self) -> usize {
104        match self {
105            Element::F32 => 4,
106        }
107    }
108}
109
110/// How long the value of a vector record with `dim` coordinates is.
111///
112/// # Errors
113///
114/// [`Code::Invalid`] if `dim` is zero or past [`MAX_DIM`]. A zero dimensional
115/// vector is not a small vector, it is a mistake somewhere upstream, and it
116/// would sort as equidistant from everything.
117pub fn vector_len(dim: usize, of: Element) -> Result<usize> {
118    if dim == 0 || dim > MAX_DIM {
119        return Err(Error::new(Code::Invalid, "dimension out of range")
120            .with_detail(format!("dim={dim} max={MAX_DIM}")));
121    }
122    Ok(VECTOR_HEADER_LEN + dim * of.width())
123}
124
125/// A vector record's value, borrowed.
126///
127/// Decoding does not copy the coordinates and does not check them. What it
128/// checks is that the header is one this version understands and that the
129/// coordinates that the header claims are all there, which is what stops a
130/// short or corrupt record from being read as a vector of the wrong length.
131#[derive(Debug, Clone, Copy)]
132pub struct VectorBody<'a> {
133    dim: usize,
134    element: Element,
135    values: &'a [u8],
136}
137
138impl<'a> VectorBody<'a> {
139    /// Writes `values` into `into` and says how many bytes that took.
140    ///
141    /// # Errors
142    ///
143    /// [`Code::Invalid`] if the dimension is out of range, if `into` is too
144    /// short, or if any coordinate is not finite.
145    ///
146    /// The last one is a refusal at the boundary rather than a thing to sort
147    /// out later. A NaN coordinate makes every distance involving that vector a
148    /// NaN, a NaN compares false against everything, and the result is not an
149    /// error anywhere: the vector simply never wins and never loses, and it
150    /// quietly distorts the centroid of whatever partition it lands in. That is
151    /// a bug report about recall six months later, and it costs one pass over a
152    /// buffer that is being copied anyway to make it impossible.
153    pub fn encode(values: &[f32], into: &mut [u8]) -> Result<usize> {
154        let need = vector_len(values.len(), Element::F32)?;
155        if into.len() < need {
156            return Err(
157                Error::new(Code::Invalid, "buffer is shorter than the vector")
158                    .with_detail(format!("have={} need={need}", into.len())),
159            );
160        }
161        if let Some(at) = values.iter().position(|v| !v.is_finite()) {
162            return Err(Error::new(Code::Invalid, "a coordinate is not a number")
163                .with_detail(format!("at={at} value={}", values[at])));
164        }
165        put_u32(into, 0, values.len() as u32);
166        put_u8(into, 4, Element::F32.as_u8());
167        put_u8(into, 5, 0);
168        put_u16(into, 6, 0);
169        for (i, v) in values.iter().enumerate() {
170            let at = VECTOR_HEADER_LEN + i * 4;
171            into[at..at + 4].copy_from_slice(&v.to_le_bytes());
172        }
173        Ok(need)
174    }
175
176    /// Reads the header back and borrows the coordinates.
177    ///
178    /// # Errors
179    ///
180    /// [`Code::Corrupt`] if the header is not one this version understands or
181    /// if the record is not as long as its own header says it is.
182    pub fn decode(bytes: &'a [u8]) -> Result<VectorBody<'a>> {
183        if bytes.len() < VECTOR_HEADER_LEN {
184            return Err(Error::new(Code::Corrupt, "shorter than a vector header")
185                .with_detail(format!("len={}", bytes.len())));
186        }
187        let dim = get_u32(bytes, 0) as usize;
188        let raw = get_u8(bytes, 4);
189        let Some(element) = Element::from_u8(raw) else {
190            return Err(Error::new(Code::Corrupt, "unknown vector element type")
191                .with_detail(format!("element={raw}")));
192        };
193        // Reserved bytes are checked rather than ignored. A version that gives
194        // them a meaning will say so with `min_reader_version`, and until then
195        // a record with anything in them was written by something that did not
196        // agree with this layout.
197        let flags = get_u8(bytes, 5);
198        let reserved = get_u16(bytes, 6);
199        if flags != 0 || reserved != 0 {
200            return Err(
201                Error::new(Code::Corrupt, "reserved vector header bytes are set")
202                    .with_detail(format!("flags={flags:#04x} reserved={reserved:#06x}")),
203            );
204        }
205        // The same range as `vector_len` and a different code, because a
206        // dimension a caller passed in is a mistake and one that came off a
207        // disk is a broken record.
208        if dim == 0 || dim > MAX_DIM {
209            return Err(Error::new(Code::Corrupt, "vector dimension out of range")
210                .with_detail(format!("dim={dim} max={MAX_DIM}")));
211        }
212        let need = VECTOR_HEADER_LEN + dim * element.width();
213        if bytes.len() < need {
214            return Err(
215                Error::new(Code::Corrupt, "vector record is shorter than its dimension")
216                    .with_detail(format!("len={} need={need} dim={dim}", bytes.len())),
217            );
218        }
219        Ok(VectorBody {
220            dim,
221            element,
222            values: &bytes[VECTOR_HEADER_LEN..need],
223        })
224    }
225
226    /// How many coordinates the vector has.
227    #[must_use]
228    pub const fn dim(&self) -> usize {
229        self.dim
230    }
231
232    /// How the coordinates are stored.
233    #[must_use]
234    pub const fn element(&self) -> Element {
235        self.element
236    }
237
238    /// Copies the coordinates into `out`.
239    ///
240    /// # Errors
241    ///
242    /// [`Code::Invalid`] if `out` is not exactly [`dim`](Self::dim) long. Not a
243    /// prefix and not a longer buffer, because both of those are a caller that
244    /// thinks the collection has a different dimension than it does, and the
245    /// only useful moment to say so is here.
246    pub fn read_into(&self, out: &mut [f32]) -> Result<()> {
247        if out.len() != self.dim {
248            return Err(
249                Error::new(Code::Invalid, "buffer is not the vector's length")
250                    .with_detail(format!("have={} dim={}", out.len(), self.dim)),
251            );
252        }
253        match self.element {
254            Element::F32 => {
255                for (i, slot) in out.iter_mut().enumerate() {
256                    let at = i * 4;
257                    let b = &self.values[at..at + 4];
258                    *slot = f32::from_le_bytes([b[0], b[1], b[2], b[3]]);
259                }
260            }
261        }
262        Ok(())
263    }
264
265    /// One coordinate, or `None` past the end.
266    #[must_use]
267    pub fn at(&self, i: usize) -> Option<f32> {
268        if i >= self.dim {
269            return None;
270        }
271        match self.element {
272            Element::F32 => {
273                let b = self.values.get(i * 4..i * 4 + 4)?;
274                Some(f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
275            }
276        }
277    }
278
279    /// The coordinates as they are stored, for a caller that is copying a
280    /// record rather than reading it.
281    #[must_use]
282    pub const fn bytes(&self) -> &'a [u8] {
283        self.values
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    fn round_trip(values: &[f32]) -> Vec<f32> {
292        let mut buf = vec![0u8; vector_len(values.len(), Element::F32).unwrap()];
293        let wrote = VectorBody::encode(values, &mut buf).unwrap();
294        assert_eq!(
295            wrote,
296            buf.len(),
297            "encode wrote a different length than it asked for"
298        );
299        let body = VectorBody::decode(&buf).unwrap();
300        assert_eq!(body.dim(), values.len());
301        assert_eq!(body.element(), Element::F32);
302        let mut out = vec![0f32; body.dim()];
303        body.read_into(&mut out).unwrap();
304        out
305    }
306
307    #[test]
308    fn a_vector_comes_back_bit_for_bit() {
309        // Exactly, not nearly. Rerank is the step that decides the final
310        // ordering, so a vector that comes back close enough is a vector that
311        // reorders results for no reason anybody could find.
312        let values = [0.0, -0.0, 1.0, -1.0, 1e-38, 3.4e38, 0.1, 2.5];
313        assert_eq!(round_trip(&values), values);
314    }
315
316    #[test]
317    fn a_long_vector_is_fine() {
318        let values: Vec<f32> = (0..1536).map(|i| i as f32 * 0.001).collect();
319        assert_eq!(round_trip(&values), values);
320    }
321
322    #[test]
323    fn coordinates_can_be_read_one_at_a_time() {
324        let values = [3.0f32, 1.0, 4.0, 1.5];
325        let mut buf = vec![0u8; vector_len(4, Element::F32).unwrap()];
326        VectorBody::encode(&values, &mut buf).unwrap();
327        let body = VectorBody::decode(&buf).unwrap();
328        for (i, want) in values.iter().enumerate() {
329            assert_eq!(body.at(i), Some(*want));
330        }
331        assert_eq!(body.at(4), None, "past the end is not a coordinate");
332    }
333
334    #[test]
335    fn a_vector_that_is_not_a_vector_is_refused() {
336        let mut buf = vec![0u8; 64];
337        assert!(VectorBody::encode(&[], &mut buf).is_err(), "no dimension");
338        assert!(
339            VectorBody::encode(&[f32::NAN, 1.0], &mut buf).is_err(),
340            "a NaN coordinate poisons every distance it takes part in"
341        );
342        assert!(VectorBody::encode(&[f32::INFINITY], &mut buf).is_err());
343        let mut tiny = [0u8; 8];
344        assert!(
345            VectorBody::encode(&[1.0, 2.0], &mut tiny).is_err(),
346            "the header fits and the coordinates do not"
347        );
348    }
349
350    #[test]
351    fn a_record_shorter_than_it_claims_is_corrupt() {
352        let values = [1.0f32, 2.0, 3.0, 4.0];
353        let mut buf = vec![0u8; vector_len(4, Element::F32).unwrap()];
354        VectorBody::encode(&values, &mut buf).unwrap();
355        for len in 0..buf.len() {
356            assert!(
357                VectorBody::decode(&buf[..len]).is_err(),
358                "{len} bytes decoded as a four dimensional vector"
359            );
360        }
361        assert!(VectorBody::decode(&buf).is_ok());
362    }
363
364    #[test]
365    fn an_element_type_this_version_does_not_know_is_refused() {
366        let mut buf = vec![0u8; vector_len(2, Element::F32).unwrap()];
367        VectorBody::encode(&[1.0, 2.0], &mut buf).unwrap();
368        buf[4] = 1;
369        let e = VectorBody::decode(&buf).unwrap_err();
370        assert_eq!(e.code(), Code::Corrupt);
371    }
372
373    #[test]
374    fn reserved_bytes_have_to_be_zero() {
375        let values = [1.0f32, 2.0];
376        for at in [5usize, 6, 7] {
377            let mut buf = vec![0u8; vector_len(2, Element::F32).unwrap()];
378            VectorBody::encode(&values, &mut buf).unwrap();
379            buf[at] = 1;
380            assert!(
381                VectorBody::decode(&buf).is_err(),
382                "byte {at} is reserved and a set bit in it means the writer disagreed with this layout"
383            );
384        }
385    }
386
387    #[test]
388    fn a_dimension_that_could_not_fit_anywhere_is_refused_before_it_is_believed() {
389        let mut buf = vec![0u8; vector_len(2, Element::F32).unwrap()];
390        VectorBody::encode(&[1.0, 2.0], &mut buf).unwrap();
391        put_u32(&mut buf, 0, u32::MAX);
392        let e = VectorBody::decode(&buf).unwrap_err();
393        assert_eq!(e.code(), Code::Corrupt);
394        assert!(vector_len(MAX_DIM + 1, Element::F32).is_err());
395    }
396
397    #[test]
398    fn reading_into_the_wrong_length_says_so() {
399        let mut buf = vec![0u8; vector_len(3, Element::F32).unwrap()];
400        VectorBody::encode(&[1.0, 2.0, 3.0], &mut buf).unwrap();
401        let body = VectorBody::decode(&buf).unwrap();
402        assert!(body.read_into(&mut [0.0; 2]).is_err(), "too short");
403        assert!(body.read_into(&mut [0.0; 4]).is_err(), "too long");
404        assert!(body.read_into(&mut [0.0; 3]).is_ok());
405    }
406
407    #[test]
408    fn the_element_byte_maps_both_ways() {
409        assert_eq!(Element::from_u8(Element::F32.as_u8()), Some(Element::F32));
410        assert_eq!(Element::from_u8(1), None);
411        assert_eq!(Element::F32.width(), 4);
412    }
413}