Skip to content

Index file format v2 with a written specification #212

Open

Description

@gouyt13

Problem

The current index files grew one field at a time and have no written specification. IVF-RaBitQ
(format 2) and SymphonyQG (format 1) start with a magic number and version; HNSW-RaBitQ files
start directly with max_elements, and the hnswlib sidecar that IVF-RaBitQ writes for HNSW
routing (<file>.hnsw) has no version either. None of them can store what 0.9 needs: the
cosine metric, the rotation/graph seed and the ID width (#211).

RaBitQ-SSD, planned to move into this library,
adds requirements the format should allow from the start, so it changes only once:

  • 64-bit point IDs for more than 2^32 - 1 vectors. RaBitQ-SSD selects them at compile time
    (RABITQ_PID64, its base format version 3) and each build reads only its own files; here the
    ID width must be a field of the file.
  • Companion files. Its index is a metadata file plus <base>.ssd, a page-packed inverted
    list read with O_DIRECT (offsets, lengths and buffers aligned to 4,096 bytes), plus a
    swappable coarse-quantizer file paired with the base by a fingerprint of the rotated
    centroids.
  • Raw element type. Raw records can be float32 or uint8; IVF-RaBitQ's raw mode
    (nbits=32) stores float32 today.

Proposal

A version-2 format shared by all three indexes:

  • a fixed-width header: magic, format version, index kind, metric (including cosine), original
    and padded dimensions, nbits, rotator type, seed, ID width (32 or 64), raw element type
    and point count;
  • a section table (kind, offset, size) followed by sections aligned to 64 bytes;
  • room for an external-ID section, so caller-provided IDs can be added later without another format change;
  • companion files recorded in the main file by name, size and a fingerprint, so a mismatched
    or missing file is rejected on load. The IVF HNSW-routing sidecar becomes a section or a
    recorded companion. Page alignment applies inside a companion file only (RaBitQ-SSD's
    .ssd), not to the main file;
  • a written specification in the docs.

Files from every earlier released version must stay readable (frozen fixtures already cover
them); new files are written as v2. A memory-mapped loader is not planned; 64-byte section
alignment keeps it possible later.

Also in this issue

metric="cosine": normalize data and queries internally and report 1 - cos distances. It
must be persisted, or a reloaded index would silently stop normalizing.
tests/python/test_import.py::test_invalid_metric_raises currently asserts that cosine is
rejected and changes with it.

Open questions

  • Should the IVF HNSW-routing graph be embedded as a section (one file to copy) or stay a
    companion file?
  • Fingerprint: RaBitQ-SSD uses 64-bit FNV-1a over the rotated centroids. Reuse it, or hash
    each companion file's header?

Done when

The spec page exists, v2 round-trips are tested for all indexes and metrics (including 64-bit
IDs and companion-file mismatches), and the historical-persistence tests still load every older
fixture.

Activity

  1. added this to the 0.9 milestone on Nov 2, 2026
  2. gouyt13 commented on Nov 6, 2026

    @gouyt13
    MemberAuthor

    A draft specification and the container layer are up for review in #222 (draft; not merged on its own). The spec is in docs/formats.md, next to the rabitqlib.quant byte formats.

    Answers to the open questions above:

    • The IVF HNSW-routing graph is embedded as a section, byte for byte the current sidecar layout, so an IVF index is one file again. Companion files stay for RaBitQ-SSD's .ssd lists.
    • The fingerprint reuses RaBitQ-SSD's 64-bit FNV-1a over the rotated centroids. It is deterministic, so saving the same index twice gives identical bytes. The main file also records each companion's size, and .ssd gets a 4 KiB header page.

    Also settled in the draft:

    • ID width. In-memory indexes keep 32-bit row numbers inside and write id_bits = 32; Python returns int64 (64-bit IDs, with -1 for missing results #211). IVF-SSD writes 64-bit IDs.
    • Later features. ext4-style incompat / ro_compat / compat masks and required/optional sections let later features (for example caller-provided IDs) be added without another format break. Older releases reject what they cannot read, with an error naming it.

    Feedback is welcome on the RaBitQ-SSD parts in particular: the companion layout, the fingerprint, and the coarse quantizer recording the base fingerprint instead of being a companion.

  3. added a commit that references this issue on Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions