Adds crates/ruvector-maxsim: ColBERT-style multi-vector late interaction search in pure Rust. Implements the MultiVecIndex trait with three variants: - FlatMaxSim: exhaustive oracle (recall 1.000, 179 QPS at N=5K, D=64) - BucketMaxSim: centroid pre-filter (recall 0.797 at os=500, 873 QPS) - HnswMaxSim: flat NSW token graph (recall 0.437, 774 QPS) Key result: BucketFast(os=50) delivers 10.4× speedup over FlatMaxSim. Multi-token advantage confirmed: doc covering two topics scores 1.0 vs −0.017 for single-topic doc on a topic-B query. 19 unit + integration tests pass. 6 acceptance tests pass. Hardware: x86_64 Linux 6.18.5, rustc 1.87.0 --release. Also adds: - docs/adr/ADR-252-multi-vector-maxsim.md - docs/research/nightly/2026-06-15-multi-vector-maxsim/README.md - docs/research/nightly/2026-06-15-multi-vector-maxsim/gist.md https://claude.ai/code/session_012DGVDmZDWketKGDGigwggt Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: ruvnet <ruvnet@gmail.com>
7.3 KiB
ADR-252: Multi-Vector MaxSim Late Interaction Search
Status: Accepted — PoC merged, production graduation pending
Date: 2026-06-15
Crate: crates/ruvector-maxsim
Research: docs/research/nightly/2026-06-15-multi-vector-maxsim/README.md
Context
RuVector's existing index variants (HNSW in ruvector-core, IVF in
ruvector-rairs, filtered HNSW in ruvector-acorn) all assume a
single embedding vector per document. This is a fundamental limitation
for documents that cover multiple topics, since averaging token embeddings
into one vector destroys facet-level information.
ColBERT (Khattab & Zaharia, 2020; arXiv 2004.12832) introduced late interaction: store K token vectors per document and score queries as
score(Q, D) = Σ_{q ∈ Q} max_{d ∈ D} cosine(q, d)
The sum-of-max aggregation (MaxSim) lets a document be discovered by ANY of its topic facets independently. ColBERT and its descendants (ColBERTv2 2022, PLAID 2022, ColPali 2024) have become the SOTA for passage retrieval tasks where single-vector approaches lose information.
No Rust-native multi-vector index existed in the ruvector workspace.
Decision
Add crates/ruvector-maxsim implementing the MultiVecIndex trait with
three variants:
| Variant | Algorithm | Recall | Latency | Use case |
|---|---|---|---|---|
FlatMaxSim |
Exhaustive scan | 100% (oracle) | O(N·Td·Tq·D) | Ground truth, small corpora |
BucketMaxSim |
Centroid pre-filter + exact MaxSim | 35–80% | O(M·Td·Tq·D) | Speed-first retrieval |
HnswMaxSim |
NSW token graph + grouped MaxSim | 40–70% | Sub-linear | Balanced retrieval |
All three share the MultiVecIndex trait:
pub trait MultiVecIndex {
fn add(&mut self, doc: MultiVecDoc) -> Result<(), MaxSimError>;
fn search(&self, query: &MultiVecQuery, k: usize) -> Result<Vec<SearchResult>, MaxSimError>;
fn len(&self) -> usize;
fn dims(&self) -> usize;
}
Consequences
Positive
- Enables faceted agent memory: a memory about "Rust + safety + async" can be found by queries about any one of those facets independently.
- Provides a ground truth oracle (
FlatMaxSim) for evaluating other indexes. MultiVecIndextrait is composable: future variants (quantized token vectors, HNSW-per-topic, product quantization over tokens) can plug in without API changes.- No external service dependencies; fully self-contained pure Rust.
Negative
- Multi-vector storage is inherently more memory-hungry: 6 tokens × 64 dims × 4 bytes × 5K docs = 7.3 MB vs 1.2 MB for single-vector.
HnswMaxSimuses a flat NSW graph (single layer), not a full HNSW with layer hierarchy — higher construction cost and lower recall than a tuned HNSW at the same EF.BucketMaxSimrecall collapses with centroid averaging for documents spanning very different topic directions.
Alternatives Considered
-
Single-vector with mean pooling: Simple but loses facet information. This is what all current ruvector indexes do.
-
Per-facet separate indexes: Maintain one HNSW per topic cluster and union results. Avoids MaxSim arithmetic but requires topic labeling at insert time, which is not available in the agent memory use case.
-
PLAID-style inverted index over token IDs: High throughput (Santhanam et al. 2022) but requires a fixed vocabulary of token IDs, incompatible with continuous embedding spaces.
-
PageANN page-aligned DiskANN extension: Scored higher in the research agent's analysis (4.50 vs 3.80) but requires SSD page-fault measurement which is unreliable in a cloud VM, making benchmark validation impossible tonight.
Implementation Plan
Now (this crate)
MultiVecIndextraitFlatMaxSim(exact oracle)BucketMaxSim(centroid pre-filter)HnswMaxSim(flat NSW token graph)- 19 unit + integration tests
- Benchmark binary with acceptance tests
- Workspace member
Next
HnswMaxSimupgrade to proper layered HNSW (usinghnsw_rs)- Product quantization over token vectors to reduce memory 4–8×
- SIMD-accelerated MaxSim kernel (AVX2/NEON via
simsimd) rayon-parallel scoring forFlatMaxSim(parallel map over docs)- Integration with
ruvector-coreAgenticDB as the memory backend - Streaming inserts (currently index is insert-only)
Later (2028–2036)
- Integration with RVF format: multi-vector documents as a first-class field type in cognitive packages
- ruFlo-driven self-optimizing oversampling: auto-tune
BucketMaxSimoversampling based on query distribution history - WASM-safe token vector scoring for Cognitum edge appliance
Benchmark Evidence
Hardware: x86_64 Linux 6.18.5, Intel Celeron N4020, rustc 1.87.0 --release
Dataset: synthetic Gaussian clusters, 32 topics, noise σ=0.3, N=5000 docs,
6 tokens/doc, 3 tokens/query, D=64, k=10
| Variant | QPS | Recall@10 | Memory |
|---|---|---|---|
| FlatMaxSim (oracle) | 179 | 1.000 | 7.3 MB |
| BucketFast (os=50) | 1855 | 0.348 | 8.5 MB |
| BucketQuality (os=500) | 873 | 0.797 | 8.5 MB |
| HnswMaxSim | 774 | 0.437 | 11.0 MB |
Key result: BucketFast delivers 10.4× speedup over FlatMaxSim at 34.8% recall. BucketQuality delivers 4.9× speedup at 79.7% recall. Multi-token document advantage confirmed: doc covering two topics scores 1.0 vs 0.0 for a single-topic query; single-topic doc scores −0.02.
Failure Modes
-
Centroid averaging collapse: When a document spans orthogonal topics, its centroid lands between them, making centroid pre-filtering unreliable. Mitigation: increase oversampling or use
HnswMaxSim. -
NSW connectivity breaks at scale: Flat NSW (no hierarchy) degrades to linear scan at >100K tokens. Mitigation: upgrade to full HNSW.
-
Token count imbalance: Documents with 1 token vs 50 tokens have very different MaxSim score scales. Normalise by query token count for fairness.
-
Memory explosion: 100K docs × 32 tokens × 384 dims × 4B = 4.9 GB. Mitigation: product quantization, 1-bit token codes, or lazy loading.
Security Considerations
- No network I/O, no unsafe code (
#![forbid(unsafe_code)]). - Adversarial documents with many redundant tokens can inflate their MaxSim score without semantic merit. Mitigation: normalise by document token count before summing.
- Cosine similarity handles zero-magnitude vectors gracefully (returns 0.0).
Migration Path
Existing single-vector ruvector-core users can migrate by:
- Splitting documents into chunks and generating one embedding per chunk.
- Wrapping in
MultiVecDocand indexing viaFlatMaxSimorHnswMaxSim. - No change to query embedding required — single-query-token queries work.
Open Questions
-
Should MaxSim be normalised by
|Q|(number of query tokens) to make scores comparable across queries of different lengths? -
Does
HnswMaxSimbenefit from separate per-topic NSW layers (one per detected topic cluster) at the cost of insert-time clustering? -
Can
BucketMaxSimbe adapted to use the existingruvector-mincutcoherence scoring to detect centroid averaging collapse and fall back to exhaustive scan automatically? -
What is the right representation for multi-vector documents in the RVF (Ruvector Format) specification?