rust/index/src/sparse/README.md
The sparse index module implements the Block-Max WAND (Weak AND) algorithm for efficient sparse vector search. This implementation is built on top of Chroma's blockfile abstraction and provides high-performance top-k retrieval for sparse vectors, commonly used in text search and information retrieval systems.
This implementation combines the WAND (Weak AND) algorithm from Broder et al. [1] with the Block-Max optimization from Ding and Suel [2] to achieve efficient sparse vector search.
The WAND algorithm [1] introduces a two-level query evaluation process:
The core innovation is the WAND predicate, which generalizes AND and OR operations using weighted thresholds. For a set of terms with weights w₁, w₂, ..., wₖ and threshold θ, WAND returns true when the sum of weights for present terms exceeds θ.
The Block-Max WAND algorithm [2] improves upon WAND by:
This creates a piecewise upper-bound approximation that significantly reduces the number of documents that need full evaluation.
The algorithm maintains cursors for each query dimension and processes documents as follows:
Initialization: Create a cursor for each query dimension tracking:
Pivot Selection (following WAND):
Block-Level Optimization (Block-Max enhancement):
Document Evaluation:
Cursor Advancement:
According to the original papers:
The combination of WAND's pivot-based traversal with Block-Max's fine-grained upper bounds makes this implementation particularly effective for high-dimensional sparse vector search.
[1] Broder, A. Z., Carmel, D., Herscovici, M., Soffer, A., & Zien, J. (2003). Efficient query evaluation using a two-level retrieval process. In Proceedings of CIKM '03.
[2] Ding, S., & Suel, T. (2011). Faster top-k document retrieval using block-max indexes. In Proceedings of SIGIR '11.
The module consists of three main components:
types.rs)writer.rs)reader.rs)The sparse index uses two separate blockfiles to store its data. Both blockfiles follow the standard Chroma blockfile format:
Prefix (String) -> Key (K) -> Value (V)
sparse_max)Stores block-level and dimension-level maximum values for efficient pruning.
Format:
Prefix: String -> Key: u32 -> Value: f32
Entries:
a) Dimension-level maximums:
"DIMENSION"dimension_id (u32)dimension_max_value (f32)b) Block-level maximums:
encode_u32(dimension_id) (base64-encoded dimension ID)block_boundary_offset (u32)block_max_value (f32)block_boundary_offset is the exclusive upper bound of the block (i.e., one past the last document in the block)sparse_offset_value)Stores the actual sparse vector values for each document.
Format:
Prefix: String -> Key: u32 -> Value: f32
Entries:
encode_u32(dimension_id) (base64-encoded dimension ID)document_offset (u32)value (f32)For a sparse vector at document offset 42 with values {dim_5: 0.8, dim_10: 0.3} and block size 128:
Max Blockfile entries:
Prefix: "DIMENSION", Key: 5, Value: 0.9 # Global max for dimension 5
Prefix: "DIMENSION", Key: 10, Value: 0.7 # Global max for dimension 10
Prefix: encode_u32(5), Key: 128, Value: 0.9 # Block [0,128) max for dim 5
Prefix: encode_u32(10), Key: 128, Value: 0.7 # Block [0,128) max for dim 10
Offset-Value Blockfile entries:
Prefix: encode_u32(5), Key: 42, Value: 0.8 # Value for dim 5 at doc 42
Prefix: encode_u32(10), Key: 42, Value: 0.3 # Value for dim 10 at doc 42
// Writing sparse vectors
let mut delta = SparseDelta::default();
delta.create(doc_offset, vec![(dim1, val1), (dim2, val2)]);
sparse_writer.write(delta).await;
let flusher = sparse_writer.commit().await?;
flusher.flush().await?;
// Querying with WAND
let query = vec![(dim1, query_val1), (dim2, query_val2)];
let top_k_results = sparse_reader.wand(query, k).await?;