field_dict : Vec<String> // small, sorted, ~10 entries
field_id_of : SmallMap<String, u16> // built once
doc_to_pos : Vec<u32> // length = max_internal_id + 1
// (or RoaringBitmap → range coded)
─ doc_to_pos[ord(field_id, doc_id)] = position in payload column ─
Round-3 perf push sub-issue (tracked under umbrella #535).
[M] Replace
HashMap<(u64, String), u32>field/doc->position index with field-id + dense ordinal lookupWhere:
laurus/src/vector/index/quantized_storage.rs:45,77-93andlaurus/src/vector/index/pq_storage.rs:43,72-93.Current behaviour:
field_index: HashMap<String, Arc<HashMap<u64, u32>>>. Looking up a candidate's position requires hashing theStringfield name once per search (cached, OK) then hashingu64doc_id per candidate. Hot loop calls
record_at(pos); the inner mapis
HashMap<u64, u32>which is ~28 B per entry plus rehash growth.Why it might be a bottleneck:
HashMap<u64, u32>is 3-4x largerthan the underlying ids, and dirties cache lines that the SIMD
distance kernel just brought in. For a 10 M segment, ~280 MB of map
overhead.
Reference precedent: Lucene uses dense
intordinals (aint[]indexed by docID gives the vector ordinal). Qdrant uses dense
IdTrackerwithVec<PointOffset>keyed by internal id.Current data structure:
Proposed layout:
Use a perfect-hash or sorted
(field_id, doc_id) → u32array formulti-field. For single-field (the common case) the index collapses
to
Vec<u32>indexed directly by internal id.Suggested direction: combine with Issue Add parallel index #2 columnar layout so the
reader builds this dense map from the
doc_ids+field_idscolumnsin one O(N) pass. Hot loop becomes one indexed load (cache friendly)
instead of a hash lookup.
Risk / scope: needs internal-id allocation strategy (mapping
external u64 doc_ids to dense u32 ordinals); a no-op when external
IDs are already dense.
ID:
VI-08— see~/.claude/tasks/laurus/20260523_perf_round3_audit/task_list.mdfor the full Round-3 issue list.