yo_format/image.rs
1//! The index image: what a checkpoint points at so an index does not have to be
2//! rebuilt from the records it was built from.
3//!
4//! [`CheckpointEntry::index_image_addr`](crate::superblock::CheckpointEntry)
5//! has been in the superblock since the format was first written down and
6//! nothing has pointed at anything yet. This is the first thing it points at,
7//! and it is the vector index, because the vector index is the one that costs
8//! real money to rebuild: a million vectors is a million quantisations and a few
9//! thousand partitions that arrived at their shape through a long sequence of
10//! splits, merges and sweeps. Replaying the records gives back the vectors. It
11//! does not give back the shape, and rebuilding the shape on open is the outage
12//! the whole update protocol exists to avoid.
13//!
14//! # It is chunks, not a record kind
15//!
16//! `06` fixes the record kinds and there is no kind for an index, which is
17//! deliberate: an index is derived, so a reader that has never heard of it must
18//! be able to walk straight past it. So an image is written as
19//! [`CollectionChunk`](crate::RecordKind::CollectionChunk) records through the
20//! chain in `yo-kv`, exactly the way a demoted collection is, and the only thing
21//! that knows the chunks mean an index is the checkpoint entry that points at
22//! them. A reader that ignores the checkpoint sees a run of chunks nobody claims
23//! and compaction drops them.
24//!
25//! `10` section 2 is where the shape comes from: a partition is a natural chunk.
26//! At the default posting size and 768 dimensions a partition is about 32 KiB,
27//! which is half of one chunk, so a partition is one chunk and one read almost
28//! always, and a partition that has grown past a chunk is a chain of its own
29//! rather than a special case.
30//!
31//! ```text
32//! root one per partition
33//! +------------------+ +---------------------------+
34//! | header, 100 bytes | | count | code_bytes | stuck |
35//! +------------------+ +---------------------------+
36//! | centroid chain | | ids count * 8 |
37//! | key chain | | tags count * 8 |
38//! +------------------+ | codes count * width |
39//! | partition 0 | -----> | meta count * 16 |
40//! | partition 1 | +---------------------------+
41//! | ... |
42//! +------------------+
43//! ```
44//!
45//! The four arrays inside a partition are separate runs rather than one run of
46//! structures, and that is the same reason the posting itself is laid out that
47//! way in memory: a scan that only wants the tags reads only the tags. A cold
48//! partition can be brought in one array at a time for the same reason.
49//!
50//! # What is not in here
51//!
52//! The vectors. They are records of kind 3 already (`crate::vector`), at
53//! addresses the log resolves, and G8's budget is 96 bytes of index for a 768
54//! dimensional vector with the raw copy in the log. An image that carried them
55//! as well would write every vector twice and spend the whole gate to save a
56//! walk. So loading an image is two halves: the image gives the shape and the
57//! codes, and the log gives the vectors back under the keys the image names.
58//!
59//! No checksum either, and that is not an omission. Every chunk of an image is a
60//! record, every record carries a CRC32C over its own bytes, and the chain's
61//! directory is a record too, so a second checksum inside the image would cover
62//! bytes that are already covered. What a checksum cannot catch is an image that
63//! is intact and stale, and that is what the checkpoint's log addresses are for.
64//!
65//! # The freeze
66//!
67//! This layout is frozen with the rest of the format at the end of M6. After
68//! that the only lever is `min_reader_version` (`07` section 9), so the fields
69//! that exist to be changed later exist now: [`ImageHeader::kind`] so that a
70//! document or graph index can have an image beside this one, `flags` in both
71//! headers so that a section can be added to an image a version one reader then
72//! refuses one image at a time, and `bits` and `metric` as their own bytes
73//! rather than as something a reader has to infer.
74//!
75//! An image is a cache in the end, which is the safety net under all of it: a
76//! reader that does not like an image can throw it away and rebuild from the
77//! records, slowly and correctly.
78
79use crate::{
80 get_f32, get_u8, get_u16, get_u32, get_u64, put_f32, put_u8, put_u16, put_u32, put_u64,
81};
82use yo_common::{Code, Error, Result};
83
84/// The four bytes an image starts with, so that a stray chunk is not read as
85/// one.
86pub const IMAGE_TAG: u32 = u32::from_le_bytes(*b"YOIX");
87
88/// The fixed part at the front of an image root.
89pub const IMAGE_HEADER_LEN: usize = 100;
90
91/// One line of the partition table that follows the root header.
92pub const PARTITION_ENTRY_LEN: usize = 16;
93
94/// The fixed part at the front of one partition's image.
95pub const POSTING_HEADER_LEN: usize = 16;
96
97/// What a code needs beside it: `norm`, `scale`, `lo` and `delta`, four `f32`.
98pub const META_LEN: usize = 16;
99
100/// What kind of index an image holds.
101///
102/// One value today and a byte for it, because the vector index is the first
103/// thing worth writing down and it will not be the last. A reader that meets a
104/// kind it does not know throws the image away and rebuilds, which is always
105/// available and is why this is a byte rather than a format version.
106pub mod image_kind {
107 /// The partition index over RaBitQ codes (`10`).
108 pub const VECTOR: u8 = 1;
109}
110
111/// How a vector is compared, as the byte the image stores.
112///
113/// The same four values `yo_shape::Metric` has, written down here because this
114/// crate is the format and it does not depend on the shape crate. A collection
115/// built for cosine and searched as L2 answers wrongly and quietly, so the
116/// metric travels with the image rather than being taken on trust from whatever
117/// opened it.
118pub mod metric {
119 /// Euclidean distance.
120 pub const L2: u8 = 0;
121 /// Cosine similarity, stored as unit vectors.
122 pub const COSINE: u8 = 1;
123 /// Inner product.
124 pub const IP: u8 = 2;
125 /// Hamming distance.
126 pub const HAMMING: u8 = 3;
127
128 /// Whether `b` is a metric this version has a name for.
129 #[must_use]
130 pub const fn is_known(b: u8) -> bool {
131 matches!(b, L2 | COSINE | IP | HAMMING)
132 }
133}
134
135/// The root of an image: everything the index is, apart from the members.
136///
137/// The two chains here and the partition table after it are addresses into the
138/// log, so this is small whatever the collection's size: a few dozen bytes plus
139/// sixteen per partition.
140#[derive(Debug, Clone, Copy, PartialEq, Default)]
141pub struct ImageHeader {
142 /// Which index this is an image of. See [`image_kind`].
143 pub kind: u8,
144 /// How wide one coordinate is written in a code, which is 1 or 4.
145 pub bits: u8,
146 /// What nearness means here. See [`metric`].
147 pub metric: u8,
148 /// How many coordinates a vector has.
149 pub dim: u32,
150 /// How many partitions the index has grown to.
151 pub partitions: u32,
152 /// What the rotation was built from. Without it the codes are noise.
153 pub seed: u64,
154 /// How many vectors the collection held, for a check after loading.
155 pub members: u64,
156 /// How many slots the vector table had, which is the largest id plus one.
157 ///
158 /// An id is a slot rather than a name, and a collection that has had
159 /// members removed has holes in it, so the count of live members does not
160 /// say how far the ids go. Writing it down means a loader allocates the
161 /// table once and can refuse an id that is past the end of it before it is
162 /// used to index anything.
163 pub slots: u32,
164 /// The size a partition wants.
165 pub posting: u32,
166 /// How many partitions a search scans.
167 pub probe: u32,
168 /// How many candidates are reranked per answer.
169 pub rerank: u32,
170 /// How many neighbours a split sweeps.
171 pub sweep: u32,
172 /// How much further a filtered search looks.
173 pub widen: u32,
174 /// How many partitions one vector may be written into.
175 ///
176 /// Part of the image rather than left to whatever the loader happens to be
177 /// tuned to, because it says how the collection was built. A collection
178 /// built with boundary copies and reopened without them keeps the copies it
179 /// has and stops making new ones, which is a recall curve that changes under
180 /// the caller for a reason nothing reports.
181 pub spill: u32,
182 /// How much further than the nearest centroid a copy is still made, as a
183 /// fraction.
184 pub slack: f32,
185 /// How many partitions in a row may add nothing before a search stops.
186 pub patience: u32,
187 /// Where the centroids are, and how many bytes of them there are.
188 pub centroids: Chain,
189 /// Where the key table is, and how long it is.
190 pub keys: Chain,
191}
192
193/// Where a section went and how long it is.
194///
195/// The same pair `yo_kv::cold::Chain` carries, written here as two `u64` because
196/// this crate is the layout and does not depend on the crate that walks the
197/// chunks.
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
199pub struct Chain {
200 /// The address of the single chunk, or of the directory.
201 pub at: u64,
202 /// The section's length in bytes, which is what says which of those it is.
203 pub len: u64,
204}
205
206/// How long an image root with this many partitions is.
207///
208/// # Errors
209///
210/// [`Code::Invalid`] if the partition count is large enough that the root would
211/// not fit in a `usize`, which on a 64 bit machine it never is and on a 32 bit
212/// one is a corrupt count rather than a real index.
213pub fn image_len(partitions: u32) -> Result<usize> {
214 (partitions as usize)
215 .checked_mul(PARTITION_ENTRY_LEN)
216 .and_then(|n| n.checked_add(IMAGE_HEADER_LEN))
217 .ok_or_else(|| {
218 Error::new(Code::Invalid, "that many partitions do not fit in an image")
219 .with_detail(format!("partitions={partitions}"))
220 })
221}
222
223impl ImageHeader {
224 /// Writes the header into the first [`IMAGE_HEADER_LEN`] bytes of `into`.
225 ///
226 /// # Errors
227 ///
228 /// [`Code::Invalid`] if `into` is shorter than a whole root, header and
229 /// partition table both, because a header written into a buffer that cannot
230 /// hold the table it describes is a root nobody can read.
231 pub fn encode(&self, into: &mut [u8]) -> Result<usize> {
232 let need = image_len(self.partitions)?;
233 if into.len() < need {
234 return Err(
235 Error::new(Code::Invalid, "buffer is shorter than the image root")
236 .with_detail(format!("have={} need={need}", into.len())),
237 );
238 }
239 put_u32(into, 0, IMAGE_TAG);
240 put_u8(into, 4, self.kind);
241 put_u8(into, 5, self.bits);
242 put_u8(into, 6, self.metric);
243 put_u8(into, 7, 0);
244 put_u32(into, 8, self.dim);
245 put_u32(into, 12, self.partitions);
246 put_u64(into, 16, self.seed);
247 put_u64(into, 24, self.members);
248 put_u32(into, 32, self.posting);
249 put_u32(into, 36, self.probe);
250 put_u32(into, 40, self.rerank);
251 put_u32(into, 44, self.sweep);
252 put_u32(into, 48, self.widen);
253 put_u32(into, 52, self.slots);
254 put_u64(into, 56, self.centroids.at);
255 put_u64(into, 64, self.centroids.len);
256 put_u64(into, 72, self.keys.at);
257 put_u64(into, 80, self.keys.len);
258 put_u32(into, 88, self.spill);
259 put_f32(into, 92, self.slack);
260 put_u32(into, 96, self.patience);
261 Ok(need)
262 }
263
264 /// Reads a header back and checks that the bytes behind it are a whole
265 /// partition table.
266 ///
267 /// # Errors
268 ///
269 /// [`Code::Corrupt`] if the tag is wrong, if a reserved field is set, if the
270 /// dimension or the code width is not one this version writes, or if the
271 /// buffer is not as long as the partition count says it is.
272 pub fn decode(bytes: &[u8]) -> Result<ImageHeader> {
273 if bytes.len() < IMAGE_HEADER_LEN {
274 return Err(Error::new(Code::Corrupt, "shorter than an image header")
275 .with_detail(format!("len={}", bytes.len())));
276 }
277 let tag = get_u32(bytes, 0);
278 if tag != IMAGE_TAG {
279 return Err(Error::new(Code::Corrupt, "not an index image")
280 .with_detail(format!("tag={tag:#010x}")));
281 }
282 // Reserved bytes are checked rather than ignored, as everywhere else in
283 // this crate: anything in them was written by something that did not
284 // agree with this layout.
285 if get_u8(bytes, 7) != 0 {
286 return Err(Error::new(
287 Code::Corrupt,
288 "reserved image header bytes are set",
289 ));
290 }
291 let h = ImageHeader {
292 kind: get_u8(bytes, 4),
293 bits: get_u8(bytes, 5),
294 metric: get_u8(bytes, 6),
295 dim: get_u32(bytes, 8),
296 partitions: get_u32(bytes, 12),
297 seed: get_u64(bytes, 16),
298 members: get_u64(bytes, 24),
299 posting: get_u32(bytes, 32),
300 probe: get_u32(bytes, 36),
301 rerank: get_u32(bytes, 40),
302 sweep: get_u32(bytes, 44),
303 widen: get_u32(bytes, 48),
304 slots: get_u32(bytes, 52),
305 spill: get_u32(bytes, 88),
306 slack: get_f32(bytes, 92),
307 patience: get_u32(bytes, 96),
308 centroids: Chain {
309 at: get_u64(bytes, 56),
310 len: get_u64(bytes, 64),
311 },
312 keys: Chain {
313 at: get_u64(bytes, 72),
314 len: get_u64(bytes, 80),
315 },
316 };
317 if h.dim == 0 || h.dim as usize > crate::vector::MAX_DIM {
318 return Err(Error::new(Code::Corrupt, "image dimension out of range")
319 .with_detail(format!("dim={}", h.dim)));
320 }
321 if h.bits != 1 && h.bits != 4 {
322 return Err(Error::new(Code::Corrupt, "unknown code width")
323 .with_detail(format!("bits={}", h.bits)));
324 }
325 if h.members > u64::from(h.slots) {
326 return Err(
327 Error::new(Code::Corrupt, "more members than the table has slots")
328 .with_detail(format!("members={} slots={}", h.members, h.slots)),
329 );
330 }
331 if !metric::is_known(h.metric) {
332 return Err(Error::new(Code::Corrupt, "unknown metric")
333 .with_detail(format!("metric={}", h.metric)));
334 }
335 let need = image_len(h.partitions)?;
336 if bytes.len() != need {
337 return Err(
338 Error::new(Code::Corrupt, "the image root is not the length it says")
339 .with_detail(format!("len={} need={need}", bytes.len())),
340 );
341 }
342 // The centroids are the one section whose size the header already
343 // implies, so a disagreement there is worth catching before anything
344 // tries to cut the section into partitions.
345 let want = u64::from(h.partitions) * u64::from(h.dim) * 4;
346 if h.centroids.len != want {
347 return Err(
348 Error::new(Code::Corrupt, "the centroid section is the wrong size")
349 .with_detail(format!("len={} want={want}", h.centroids.len)),
350 );
351 }
352 Ok(h)
353 }
354}
355
356/// Writes partition `i`'s chain into an encoded root.
357///
358/// # Errors
359///
360/// [`Code::Invalid`] if `i` is past the table.
361pub fn put_partition(root: &mut [u8], i: u32, chain: Chain) -> Result<()> {
362 let at =
363 partition_offset(root.len(), i).ok_or_else(|| missing(i, root.len(), Code::Invalid))?;
364 put_u64(root, at, chain.at);
365 put_u64(root, at + 8, chain.len);
366 Ok(())
367}
368
369/// Reads partition `i`'s chain back out of an encoded root.
370///
371/// # Errors
372///
373/// [`Code::Corrupt`] if `i` is past the table.
374pub fn get_partition(root: &[u8], i: u32) -> Result<Chain> {
375 let at =
376 partition_offset(root.len(), i).ok_or_else(|| missing(i, root.len(), Code::Corrupt))?;
377 Ok(Chain {
378 at: get_u64(root, at),
379 len: get_u64(root, at + 8),
380 })
381}
382
383fn partition_offset(root_len: usize, i: u32) -> Option<usize> {
384 let at = IMAGE_HEADER_LEN + (i as usize) * PARTITION_ENTRY_LEN;
385 (at + PARTITION_ENTRY_LEN <= root_len).then_some(at)
386}
387
388/// A partition the table does not have, which is a caller's mistake on the way
389/// in and a broken root on the way out.
390fn missing(i: u32, root_len: usize, code: Code) -> Error {
391 Error::new(code, "no such partition in the image")
392 .with_detail(format!("partition={i} root={root_len}"))
393}
394
395/// The fixed part at the front of one partition's image.
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
397pub struct PostingHeader {
398 /// How many members the partition holds.
399 pub count: u32,
400 /// How many bytes one code takes.
401 ///
402 /// Derivable from the root's `dim` and `bits`, and stored anyway for the
403 /// same reason `dim` is stored in a vector record: a partition can then be
404 /// cut into its four arrays without the root, which is what a reader
405 /// checking a file rather than opening one has.
406 pub code_bytes: u32,
407 /// The size at which a split was tried and found there was no cut.
408 ///
409 /// Part of the image because it is not derivable: it is the memory of a
410 /// split that failed, and an image that dropped it would have every
411 /// partition of identical vectors try to split again on the first write
412 /// after an open.
413 pub stuck: u32,
414}
415
416/// How long one partition's image is.
417///
418/// # Errors
419///
420/// [`Code::Invalid`] if the arithmetic overflows, which means a corrupt count
421/// rather than a real partition.
422pub fn posting_len(count: u32, code_bytes: u32) -> Result<usize> {
423 let count = count as usize;
424 let per = 8usize
425 .checked_add(8)
426 .and_then(|n| n.checked_add(code_bytes as usize))
427 .and_then(|n| n.checked_add(META_LEN))
428 .ok_or_else(|| Error::new(Code::Invalid, "a member of that size does not fit"))?;
429 count
430 .checked_mul(per)
431 .and_then(|n| n.checked_add(POSTING_HEADER_LEN))
432 .ok_or_else(|| {
433 Error::new(Code::Invalid, "that many members do not fit in a partition")
434 .with_detail(format!("count={count} code_bytes={code_bytes}"))
435 })
436}
437
438impl PostingHeader {
439 /// Writes the header into the front of `into`.
440 ///
441 /// # Errors
442 ///
443 /// [`Code::Invalid`] if `into` is not long enough for the whole partition.
444 pub fn encode(&self, into: &mut [u8]) -> Result<usize> {
445 let need = posting_len(self.count, self.code_bytes)?;
446 if into.len() < need {
447 return Err(
448 Error::new(Code::Invalid, "buffer is shorter than the posting")
449 .with_detail(format!("have={} need={need}", into.len())),
450 );
451 }
452 put_u32(into, 0, self.count);
453 put_u32(into, 4, self.code_bytes);
454 put_u32(into, 8, self.stuck);
455 put_u32(into, 12, 0);
456 Ok(need)
457 }
458
459 /// Reads a partition's header back and checks the bytes behind it.
460 ///
461 /// # Errors
462 ///
463 /// [`Code::Corrupt`] if a reserved field is set or if the buffer is not
464 /// exactly the length the header describes.
465 pub fn decode(bytes: &[u8]) -> Result<PostingHeader> {
466 if bytes.len() < POSTING_HEADER_LEN {
467 return Err(Error::new(Code::Corrupt, "shorter than a posting header")
468 .with_detail(format!("len={}", bytes.len())));
469 }
470 if get_u32(bytes, 12) != 0 {
471 return Err(Error::new(
472 Code::Corrupt,
473 "reserved posting header bytes are set",
474 ));
475 }
476 let h = PostingHeader {
477 count: get_u32(bytes, 0),
478 code_bytes: get_u32(bytes, 4),
479 stuck: get_u32(bytes, 8),
480 };
481 let Ok(need) = posting_len(h.count, h.code_bytes) else {
482 return Err(
483 Error::new(Code::Corrupt, "that many members do not fit in a partition")
484 .with_detail(format!("count={} code_bytes={}", h.count, h.code_bytes)),
485 );
486 };
487 if bytes.len() != need {
488 return Err(
489 Error::new(Code::Corrupt, "the posting is not the length it says")
490 .with_detail(format!("len={} need={need}", bytes.len())),
491 );
492 }
493 Ok(h)
494 }
495
496 /// Where the ids start.
497 #[must_use]
498 pub const fn ids_at(&self) -> usize {
499 POSTING_HEADER_LEN
500 }
501
502 /// Where the tags start.
503 #[must_use]
504 pub const fn tags_at(&self) -> usize {
505 self.ids_at() + self.count as usize * 8
506 }
507
508 /// Where the codes start.
509 #[must_use]
510 pub const fn codes_at(&self) -> usize {
511 self.tags_at() + self.count as usize * 8
512 }
513
514 /// Where the meta starts.
515 #[must_use]
516 pub const fn meta_at(&self) -> usize {
517 self.codes_at() + self.count as usize * self.code_bytes as usize
518 }
519}
520
521/// Writes floats into `into` end to end and says how many bytes that took.
522///
523/// Every float in an image goes through here: the centroid section is one long
524/// run of them, and the four numbers beside a code are a run of four. Both are
525/// bit for bit, because a centroid that comes back nearly right puts members
526/// under the wrong partition and a code's scale that comes back nearly right
527/// reorders the answers.
528///
529/// # Errors
530///
531/// [`Code::Invalid`] if `into` is too short.
532pub fn put_floats(into: &mut [u8], values: &[f32]) -> Result<usize> {
533 let need = values.len() * 4;
534 if into.len() < need {
535 return Err(
536 Error::new(Code::Invalid, "buffer is shorter than the floats")
537 .with_detail(format!("have={} need={need}", into.len())),
538 );
539 }
540 for (i, v) in values.iter().enumerate() {
541 crate::put_f32(into, i * 4, *v);
542 }
543 Ok(need)
544}
545
546/// Reads floats back out of `bytes` into `out`, which says how many.
547///
548/// # Errors
549///
550/// [`Code::Corrupt`] if there are not that many floats there, which is what a
551/// section that disagrees with the header it was described by looks like.
552pub fn get_floats(bytes: &[u8], out: &mut [f32]) -> Result<()> {
553 let need = out.len() * 4;
554 if bytes.len() < need {
555 return Err(
556 Error::new(Code::Corrupt, "the section is shorter than its floats")
557 .with_detail(format!("len={} need={need}", bytes.len())),
558 );
559 }
560 for (i, slot) in out.iter_mut().enumerate() {
561 *slot = crate::get_f32(bytes, i * 4);
562 }
563 Ok(())
564}
565
566/// How many bytes a key of `klen` takes in the key table.
567///
568/// # Errors
569///
570/// [`Code::Invalid`] if the key is longer than a record's key can be, which is
571/// the same limit for the same reason: a key is a thing you look up by.
572pub fn key_entry_len(klen: usize) -> Result<usize> {
573 if klen > crate::record::MAX_KEY_LEN {
574 return Err(
575 Error::new(Code::Invalid, "the key is longer than 65535 bytes")
576 .with_detail(format!("klen={klen}")),
577 );
578 }
579 Ok(10 + klen)
580}
581
582/// Writes one key table entry and says how long it was.
583///
584/// The table is a run of `id`, `klen`, key, with no padding and no order worth
585/// relying on, because the only thing that reads it reads all of it. A key is
586/// tens of bytes and the alignment would cost more than the sequential read
587/// saves.
588///
589/// # Errors
590///
591/// [`Code::Invalid`] if the key is too long or `into` is too short.
592pub fn put_key(into: &mut [u8], id: u64, key: &[u8]) -> Result<usize> {
593 let need = key_entry_len(key.len())?;
594 if into.len() < need {
595 return Err(
596 Error::new(Code::Invalid, "buffer is shorter than the key entry")
597 .with_detail(format!("have={} need={need}", into.len())),
598 );
599 }
600 put_u64(into, 0, id);
601 put_u16(into, 8, key.len() as u16);
602 into[10..need].copy_from_slice(key);
603 Ok(need)
604}
605
606/// The key table, one entry at a time.
607///
608/// Stops at the first entry that does not fit, which is what a truncated
609/// section looks like, and the caller compares the count it got against the
610/// member count in the header rather than being told twice.
611#[derive(Debug, Clone)]
612pub struct Keys<'a> {
613 rest: &'a [u8],
614}
615
616impl<'a> Keys<'a> {
617 /// Walks the entries in `bytes`.
618 #[must_use]
619 pub const fn new(bytes: &'a [u8]) -> Keys<'a> {
620 Keys { rest: bytes }
621 }
622
623 /// Whether every byte handed in was accounted for.
624 ///
625 /// False after a short entry, which is the one thing walking the table
626 /// cannot tell a caller by ending.
627 #[must_use]
628 pub const fn done(&self) -> bool {
629 self.rest.is_empty()
630 }
631}
632
633impl<'a> Iterator for Keys<'a> {
634 type Item = (u64, &'a [u8]);
635
636 fn next(&mut self) -> Option<(u64, &'a [u8])> {
637 if self.rest.len() < 10 {
638 return None;
639 }
640 let id = get_u64(self.rest, 0);
641 let klen = get_u16(self.rest, 8) as usize;
642 let end = 10 + klen;
643 if self.rest.len() < end {
644 return None;
645 }
646 let key = &self.rest[10..end];
647 self.rest = &self.rest[end..];
648 Some((id, key))
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 fn header(partitions: u32, dim: u32) -> ImageHeader {
657 ImageHeader {
658 kind: image_kind::VECTOR,
659 bits: 1,
660 metric: metric::COSINE,
661 dim,
662 partitions,
663 seed: 0x0102_0304_0506_0708,
664 members: 9999,
665 slots: 10_000,
666 posting: 256,
667 probe: 8,
668 rerank: 4,
669 sweep: 4,
670 widen: 8,
671 spill: 4,
672 slack: 0.10,
673 patience: 3,
674 centroids: Chain {
675 at: 4096,
676 len: u64::from(partitions) * u64::from(dim) * 4,
677 },
678 keys: Chain { at: 8192, len: 123 },
679 }
680 }
681
682 #[test]
683 fn a_root_comes_back_field_for_field() {
684 let h = header(3, 128);
685 let mut buf = vec![0u8; image_len(3).unwrap()];
686 let wrote = h.encode(&mut buf).unwrap();
687 assert_eq!(wrote, buf.len());
688 assert_eq!(ImageHeader::decode(&buf).unwrap(), h);
689 }
690
691 #[test]
692 fn the_partition_table_is_addressed_and_not_walked() {
693 let h = header(4, 8);
694 let mut buf = vec![0u8; image_len(4).unwrap()];
695 h.encode(&mut buf).unwrap();
696 for i in 0..4 {
697 let chain = Chain {
698 at: 1000 + u64::from(i),
699 len: 64 + u64::from(i),
700 };
701 put_partition(&mut buf, i, chain).unwrap();
702 }
703 for i in 0..4 {
704 assert_eq!(
705 get_partition(&buf, i).unwrap(),
706 Chain {
707 at: 1000 + u64::from(i),
708 len: 64 + u64::from(i)
709 }
710 );
711 }
712 assert!(get_partition(&buf, 4).is_err(), "there is no fifth");
713 assert!(put_partition(&mut buf, 9, Chain::default()).is_err());
714 }
715
716 #[test]
717 fn a_root_that_is_not_a_root_is_refused() {
718 let h = header(2, 16);
719 let mut buf = vec![0u8; image_len(2).unwrap()];
720 h.encode(&mut buf).unwrap();
721 assert!(ImageHeader::decode(&buf).is_ok());
722
723 let mut wrong = buf.clone();
724 put_u32(&mut wrong, 0, 0xdead_beef);
725 assert_eq!(
726 ImageHeader::decode(&wrong).unwrap_err().code(),
727 Code::Corrupt,
728 "a chunk that is not an image was read as one"
729 );
730
731 let mut set = buf.clone();
732 set[7] = 1;
733 assert!(
734 ImageHeader::decode(&set).is_err(),
735 "byte 7 is reserved and a writer that set it disagreed with this layout"
736 );
737
738 let mut short = buf.clone();
739 put_u32(&mut short, 52, 3);
740 assert!(
741 ImageHeader::decode(&short).is_err(),
742 "a table with fewer slots than members cannot hold them"
743 );
744
745 let mut bits = buf.clone();
746 put_u8(&mut bits, 5, 2);
747 assert!(ImageHeader::decode(&bits).is_err(), "no two bit codes");
748
749 let mut met = buf.clone();
750 put_u8(&mut met, 6, 9);
751 assert!(ImageHeader::decode(&met).is_err(), "no ninth metric");
752
753 let mut dim = buf.clone();
754 put_u32(&mut dim, 8, 0);
755 assert!(ImageHeader::decode(&dim).is_err(), "no zero dimension");
756
757 // The count and the buffer have to agree, because a count that is
758 // believed on its own is a count that indexes past the table.
759 let mut count = buf.clone();
760 put_u32(&mut count, 12, 99);
761 assert!(ImageHeader::decode(&count).is_err());
762
763 let mut cent = buf.clone();
764 put_u64(&mut cent, 64, 7);
765 assert!(
766 ImageHeader::decode(¢).is_err(),
767 "the centroid section has to be partitions times dim floats"
768 );
769
770 for len in 0..buf.len() {
771 assert!(
772 ImageHeader::decode(&buf[..len]).is_err(),
773 "{len} bytes decoded as a two partition image"
774 );
775 }
776 }
777
778 #[test]
779 fn a_partition_is_four_runs_that_do_not_overlap() {
780 let h = PostingHeader {
781 count: 5,
782 code_bytes: 16,
783 stuck: 12,
784 };
785 let mut buf = vec![0u8; posting_len(5, 16).unwrap()];
786 let wrote = h.encode(&mut buf).unwrap();
787 assert_eq!(wrote, buf.len());
788 assert_eq!(PostingHeader::decode(&buf).unwrap(), h);
789
790 assert_eq!(h.ids_at(), POSTING_HEADER_LEN);
791 assert_eq!(h.tags_at(), h.ids_at() + 40);
792 assert_eq!(h.codes_at(), h.tags_at() + 40);
793 assert_eq!(h.meta_at(), h.codes_at() + 80);
794 assert_eq!(h.meta_at() + 5 * META_LEN, buf.len());
795 }
796
797 #[test]
798 fn an_empty_partition_is_a_header_and_nothing_else() {
799 let h = PostingHeader {
800 count: 0,
801 code_bytes: 96,
802 stuck: 0,
803 };
804 let mut buf = vec![0u8; POSTING_HEADER_LEN];
805 h.encode(&mut buf).unwrap();
806 assert_eq!(PostingHeader::decode(&buf).unwrap(), h);
807 assert_eq!(h.meta_at(), buf.len());
808 }
809
810 #[test]
811 fn a_posting_that_is_not_the_length_it_claims_is_refused() {
812 let h = PostingHeader {
813 count: 3,
814 code_bytes: 8,
815 stuck: 0,
816 };
817 let mut buf = vec![0u8; posting_len(3, 8).unwrap()];
818 h.encode(&mut buf).unwrap();
819 for len in 0..buf.len() {
820 assert!(
821 PostingHeader::decode(&buf[..len]).is_err(),
822 "{len} bytes decoded as three members"
823 );
824 }
825 let mut set = buf.clone();
826 set[12] = 1;
827 assert!(PostingHeader::decode(&set).is_err(), "reserved");
828
829 // A count nobody could have written, which is what a corrupt header
830 // looks like, and it has to be refused before anything multiplies it out.
831 let mut huge = buf.clone();
832 put_u32(&mut huge, 0, u32::MAX);
833 put_u32(&mut huge, 4, u32::MAX);
834 assert!(PostingHeader::decode(&huge).is_err());
835 }
836
837 #[test]
838 fn floats_go_down_and_come_back_bit_for_bit() {
839 let values = [0.0f32, -0.0, 1.5, -2.25, 1e-38, 3.4e38];
840 let mut buf = vec![0u8; values.len() * 4];
841 assert_eq!(put_floats(&mut buf, &values).unwrap(), buf.len());
842 let mut back = vec![0f32; values.len()];
843 get_floats(&buf, &mut back).unwrap();
844 for (a, b) in values.iter().zip(&back) {
845 assert_eq!(a.to_bits(), b.to_bits(), "{a} came back as {b}");
846 }
847 assert!(get_floats(&buf[..4], &mut back).is_err(), "not that many");
848 assert!(put_floats(&mut buf[..4], &values).is_err());
849 }
850
851 #[test]
852 fn the_key_table_walks_back_in_order() {
853 let entries: Vec<(u64, &[u8])> = vec![
854 (0, b"a".as_slice()),
855 (7, b"".as_slice()),
856 (3, b"a rather longer key than the first one".as_slice()),
857 ];
858 let mut buf = Vec::new();
859 for (id, key) in &entries {
860 let mut one = vec![0u8; key_entry_len(key.len()).unwrap()];
861 let wrote = put_key(&mut one, *id, key).unwrap();
862 assert_eq!(wrote, one.len());
863 buf.extend_from_slice(&one);
864 }
865 let mut walk = Keys::new(&buf);
866 let got: Vec<(u64, &[u8])> = walk.by_ref().collect();
867 assert_eq!(got, entries);
868 assert!(walk.done(), "the walk left bytes behind");
869 }
870
871 #[test]
872 fn a_truncated_key_table_stops_rather_than_reading_past_it() {
873 let mut buf = vec![0u8; key_entry_len(4).unwrap()];
874 put_key(&mut buf, 1, b"abcd").unwrap();
875 assert!(Keys::new(&[]).done(), "no bytes is an empty table");
876 for len in 1..buf.len() {
877 let mut walk = Keys::new(&buf[..len]);
878 assert_eq!(walk.by_ref().count(), 0, "{len} bytes gave a whole key");
879 assert!(!walk.done(), "a short entry is not a finished table");
880 }
881 assert!(key_entry_len(70_000).is_err());
882 }
883
884 #[test]
885 fn the_layout_is_the_one_written_down() {
886 // The numbers in the module diagram, so that a change to any of them is
887 // a change to a test rather than a silent change to the format.
888 assert_eq!(IMAGE_HEADER_LEN, 100);
889 assert_eq!(PARTITION_ENTRY_LEN, 16);
890 assert_eq!(POSTING_HEADER_LEN, 16);
891 assert_eq!(META_LEN, 16);
892 assert_eq!(IMAGE_TAG, u32::from_le_bytes(*b"YOIX"));
893 assert_eq!(image_len(0).unwrap(), IMAGE_HEADER_LEN);
894 assert_eq!(image_len(1).unwrap(), IMAGE_HEADER_LEN + 16);
895 // A partition at the default posting size and 768 dimensions, which is
896 // the case the chunk size was chosen for.
897 assert!(
898 posting_len(256, 96).unwrap() < 64 * 1024,
899 "a partition should be one chunk at the sizes it is tuned for"
900 );
901 }
902}