Skip to Content
DocumentationSerialization Format

Serialization Format

toBytes() / fromBytes() use a single, versioned, little-endian binary format (QVEC). It is the only format quantvec ships (no legacy readers); pre-1.0 changes bump the version and rewrite. An index is fully reconstructable from (dim, bits, seed) — which regenerate the rotation and codebook — so only the compact per-vector data (and, for the id-keyed index, the ids) is stored.

Layout

All multi-byte fields are little-endian. The header is 24 bytes:

OffsetSizeFieldNotes
04magic"QVEC" (0x51 0x56 0x45 0x43)
41version1
51kind0 = positional, 1 = id-keyed
61metric0 = dot, 1 = cosine, 2 = euclidean
71bits2, 3, or 4
84dimu32, positive multiple of 8
124nu32, live vector count
168seedf64, RNG seed of the rotation

Body, immediately after the header:

codes : ⌈n·dim·bits/8⌉ bytes (tightly bit-packed, LSB-first; dim is a multiple of 8 so this is exact — no padding waste) scales : n · f32 (per-vector RaBitQ scale) norms : n · f32 (per-vector ‖v‖) ids : n × tagged id (id-keyed only)

Codes are stored at true 2/3/4 bits per coordinate, so the serialized index is 7.9–15.7× smaller than float32 (on par with native TurboQuant implementations).

Each id is tag (u8) then payload:

tagtypepayload
0numberf64
1stringu32 length + UTF-8 bytes
2bigintu32 length + UTF-8 of the canonical decimal string

Untrusted-input hardening

fromBytes treats the buffer as untrusted. Before any bulk read or allocation it validates:

  • the magic and version;
  • kind, metric, and bits are known values;
  • dim is a positive multiple of 8 and seed is finite;
  • the declared body size fits within the buffer (so a crafted huge n can’t trigger an out-of-bounds read or an out-of-memory allocation — it’s rejected first);
  • every id: bounds-checked length, valid UTF-8 (fatal decode), canonical bigint decimal, and no duplicate ids (a collision would silently break the id↔slot bijection).

Loading the wrong kind (e.g. positional bytes into IdMapIndex.fromBytes) throws WRONG_KIND; any structural problem throws a DeserializeError with a specific .code (BAD_MAGIC, BAD_VERSION, BAD_KIND, BAD_METRIC, BAD_BITS, BAD_DIM, BAD_SEED, BAD_LENGTH, BAD_ID, TOO_SHORT).

Compatibility notes

  • The id type is not stored; pass it to IdMapIndex.fromBytes<Id> and ensure it matches.
  • The on-disk codes section is bit-packed; the index still holds one byte per code in memory. In-memory packing (and a SIMD scan over packed codes) is on the roadmap.
Last updated on