Skip to content

perf(vector/index): intern field names in memory as (u64, u16) + per-segment dictionary (#859)#862

Merged
mosuka merged 1 commit into
mainfrom
perf/633-field-dict-intern
Jul 12, 2026
Merged

perf(vector/index): intern field names in memory as (u64, u16) + per-segment dictionary (#859)#862
mosuka merged 1 commit into
mainfrom
perf/633-field-dict-intern

Conversation

@mosuka

@mosuka mosuka commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

PR-B, the final piece of #633 (the on-disk dictionary landed in #860/#861): readers now keep one shared Arc<[Arc<str>]> field-name dictionary per segment and (doc_id, u16) pairs per record, instead of one heap String per record per retained structure.

  • FieldInterner (format.rs): v3 segments use the header dictionary (fixed, ids validated); v1/v2 segments synthesize it at load in first-appearance order — legacy files get the same interned representation, and nothing branches on version after load.
  • Readers ×3: vector_ids: Vec<(u64, u16)>; IVF cluster_to_vectors likewise (its eager path used to retain three String copies per record — the worst case found in the Phase 0 lifecycle sweep). Per-field caches and pool build inputs keep their String shapes (transient clones only; nothing per-record is retained).
  • VectorStorage::get/contains take (doc_id, &str), and the OnDemand arm resolves the name against the segment dictionary by linear scan (1–3 entries in practice) — the per-candidate field_name.to_string() on the mmap-default f32 paths is gone structurally; OnDemand offsets are keyed (u64, u16).
  • IVF searcher: probe results stay (u64, u16), the field filter resolves once per search and compares u16 per candidate; names rehydrate only for emitted top-k hits.
  • Public trait surface unchanged: vector_ids(), iterators, and the other boundary methods rehydrate from the dictionary at return time. The VectorStorage method signatures changed (pub but not part of the reader/writer traits; no external callers).

RAM accounting (structural, deterministic)

A retained per-record String costs ~24B header + heap payload + allocator overhead, ×2 retained copies (HNSW/Flat) or ×3 (IVF eager); the interned pair costs 16B inline with zero per-record heap. At 1M records with short names that is roughly 90–135MB → a few MB of id-bearing structures.

Deviation from the plan, recorded honestly: pool build inputs keep the (u64, String, …) shape — the per-record Strings there are transient (consumed by per-field grouping, not retained), so converting the pipelines was all churn for no retained-memory gain.

Testing

Closes #859
Refs #633

…segment dictionary (#859)

Final piece of the Issue #633 field-name work: readers keep one shared
Arc<[Arc<str>]> dictionary per segment and (doc_id, u16) pairs per
record instead of one heap String per record per retained structure.

- FieldInterner (format.rs): v3 segments use the header dictionary
  (fixed, ids validated); v1/v2 segments synthesize it at load in
  first-appearance order, so legacy files get the same interned
  representation with no version branching afterwards
- readers: vector_ids becomes Vec<(u64, u16)>; IVF cluster_to_vectors
  likewise (its eager path used to retain 3 String copies per record);
  per-field caches and pool build inputs keep their String shapes
  (transient clones only, nothing per-record retained)
- VectorStorage::get/contains take (doc_id, &str) and the OnDemand arm
  resolves the name against the segment dictionary by linear scan —
  the per-candidate field_name.to_string() on the mmap-default f32
  paths is gone structurally; OnDemand offsets are keyed (u64, u16)
- IVF searcher: probe results stay (u64, u16), the field filter
  resolves once per search and compares u16 per candidate, and names
  rehydrate only for emitted top-k hits
- public trait signatures unchanged: vector_ids(), iterators, and the
  other boundary methods rehydrate from the dictionary at return time

RAM accounting (structural): a retained per-record String costs ~24B
header + heap + allocator overhead, x2 retained copies (HNSW/Flat) or
x3 (IVF); the interned pair costs 16B inline with zero per-record heap.
Eager gate bench neutral (del0 33.4us / del10 29.3us, inside the
post-#854 envelope).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(vector/index): intern field names in memory as (u64, u16) + per-segment dictionary (sub-issue of #633)

1 participant