Skip to content

Public Read/Write I/O API + pub TurboQuantIndex::from_parts #70

Description

@gburd

Context

I'm the author of pg_turbovec, a PostgreSQL extension that uses turbovec as the kernel for indexed cosine / inner-product / L2 / L1 ANN search at storage sizes 10–20× smaller than pgvector. It's been carrying a small additive vendor patch on top of turbovec 0.5.0/0.6.0 to handle two needs that database extensions hit but the upstream API doesn't currently support:

  1. In-memory deserialisation. When the index payload is already in RAM (read out of a bytea column or a buffer-manager-pinned page), going through IdMapIndex::load(path) requires a tmpfile round-trip — write to /tmp, fsync, re-open, read back, parse. On a 1 M × 384-d × 4-bit index that's ≈195 MB of needless I/O. The internal parsers in src/io.rs are already generic over Read/Write; widening the public surface to expose that internal generality is a 100-line additive change.

  2. Constructing an IdMapIndex from already-decoded parts. Database storage backends sometimes have their own page format (e.g. PostgreSQL relfiles) and want to skip the .tvim wire format entirely. Promoting TurboQuantIndex::from_parts (and the small packed_codes() / scales() accessors it pairs with) from pub(crate) to pub lets external embedders construct the index from their own deserialised bytes.

Proposed API additions

All additive; no behavioural change to existing APIs; no wire-format change.

// src/io.rs
pub fn write_to<W: Write>(w: &mut W,) -> io::Result<()>
pub fn load_from<R: Read>(r: &mut R) -> io::Result<…>
pub fn write_id_map_to<W: Write>(w: &mut W,) -> io::Result<()>
pub fn load_id_map_from<R: Read>(r: &mut R) -> io::Result<…>

// src/id_map.rs
impl IdMapIndex {
    pub fn write_to_writer<W: Write>(&self, w: &mut W) -> io::Result<()>
    pub fn load_from_reader<R: Read>(r: &mut R) -> io::Result<Self>
}

// src/lib.rs — three previously-pub(crate) items become pub
pub fn TurboQuantIndex::from_parts(dim, bit_width, n_vectors, packed_codes, scales) -> Self
pub fn TurboQuantIndex::packed_codes(&self) -> &[u8]
pub fn TurboQuantIndex::scales(&self) -> &[f32]

Why this matters in the field

pg_turbovec measured a 21× cold-scan speedup (26 310 ms → 1 256 ms p50, 1 M × 1536-d real OpenAI ada-002 corpus on a single bench host) by combining:

  • The Read/Write API to read TVIM payloads straight from PG's buffer manager (no tmpfile).
  • pub fn from_parts to construct the IdMapIndex from the buffer-pinned bytes, skipping deserialisation entirely.
  • Persisting the SIMD-blocked layout into the relfile at ambuild time so the per-backend pack::repack (12–15 s on this corpus) becomes amortised constant.

The first two bullets are the API change in this issue; the third lives entirely in pg_turbovec. Without the upstream changes, pg_turbovec carries the vendor patch indefinitely, which is fine but adds a maintenance step every time turbovec releases.

Status of a draft PR

I respect the by-invitation policy in CONTRIBUTING. I've drafted the implementation against v0.6.0 on a branch in my fork — gburd/turbovec in-memory-io-and-pub-from-parts — so if you're amenable to the change and would like to invite a PR, the diff is already ready (3 files, +107 / -11 lines, all upstream cargo test --tests --release passes). Happy to take it on if you prefer; equally happy if you'd rather pick this up yourself or not at all.

Test results on the draft branch

test result: ok. 6 passed   tests/codebook
test result: ok. 5 passed   tests/concurrent_search
test result: ok. 5 passed   tests/distortion
test result: ok. 4 passed   tests/encode
test result: ok. 15 passed  tests/filtering
test result: ok. 11 passed  tests/id_map
test result: ok. 5 passed   tests/io_versioning
test result: ok. 6 passed   tests/kernel_correctness
test result: ok. 21 passed  tests/lazy_init

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions