Skip to content

perf(vector): migrate to InternalId = u32 throughout vector index + search #686

Description

@mosuka

Cross-cutting data-structure rewrite tracked under #537.

Current state

Doc IDs flow through the vector index as u64:

  • HnswGraph::id_to_index: AHashMap<u64, usize> and nodes: Vec<Vec<Vec<u64>>> (vector/index/hnsw/graph.rs:21,27).
  • ConcurrentHnswGraph::nodes: HashMap<u64, Vec<RwLock<Vec<u64>>>> (vector/index/hnsw/writer.rs:40-41).
  • Candidate / ResultCandidate carry id: u64.
  • IVF inverted lists hold Vec<(u64, String, Vector)>.

A single vector segment is capped at u32 entries elsewhere (IVF / Flat headers use vector_count: u32); the u64 is unnecessary inside the index.

Proposed direction

  • Define InternalId(u32) with TryFrom<u64> at the segment boundary.
  • HNSW neighbour lists: Vec<u32> (50% saving vs Vec<u64>).
  • IVF inverted list ids: Vec<u32>.
  • Pair with the CSR HNSW migration (perf(vector/index): Round-3 roadmap — Qdrant/LanceDB parity #535 children) so the neighbour arena is a single contiguous Vec<u32>.
  • Keep external u64 doc id at the searcher result boundary.

Acceptance

  • HNSW graph RAM drops by ~50% on the neighbour arrays.
  • Candidate heap packed format (X-04) becomes natural.

References

  • Lucene uses int ordinals throughout vector index.
  • Qdrant uses u32 PointOffset.

Task list (2026-07-10 restart, full-scope route)

Context: sub-issue #709 (in-memory-only HnswGraph narrowing) was measured mixed and closed wontfix (2026-05-26). Its post-mortem concluded the remaining scope only has a chance as a bundled full-stack change (writer + searcher + IVF + on-disk format together), or as a cleanup without a perf gate. Since then, store-level VectorStore::search benchmarks (±0.06–3.5% same-code spread) give far better measurement than the raw HNSW benches #709 had to use (±6–19%).

Phase 0: Investigation & opportunity-ceiling gate

  • Full ID dataflow map (graph / writer / searcher / IVF / flat / quantized / store) with file:line, classified as in-memory build / in-memory search / on-disk / API boundary
  • On-disk format inventory: every site serializing ids, versioning/compat mechanisms, merge path
  • Public API & bindings boundary check (no u32 leak; interaction with the shared u64 RoaringTreemap deletion bitmap)
  • Capacity invariants (u32 segment caps, id monotonicity, segment-local vs global ids)
  • Opportunity ceiling: RAM accounting at n=10k/100k/1M with fire-condition line-level evidence; explain how full scope differs from perf(vector/hnsw): narrow HnswGraph internal neighbour lists from u64 to u32 (in-memory only) (sub-issue of #686) #709's failure mode
  • GO/NO-GO recommendation: perf-gated route vs no-gate cleanup route
  • Post investigation summary as an issue comment

Phase 1: Implementation plan

Phase 2+: Implementation

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions