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}