manifoldb_vector/index/config.rs
1//! HNSW index configuration.
2
3/// Configuration parameters for an HNSW index.
4///
5/// # Parameters
6///
7/// * `m` - Maximum number of connections per node in each layer.
8/// Typical values: 16-64. Higher values give better recall but use more memory.
9///
10/// * `m_max0` - Maximum number of connections in layer 0 (the densest layer).
11/// Typically set to `2 * m`.
12///
13/// * `ef_construction` - Beam width during index construction.
14/// Higher values give better index quality but slower construction.
15/// Typical values: 100-500.
16///
17/// * `ef_search` - Default beam width during search.
18/// Higher values give better recall but slower search.
19/// Can be overridden per-query. Typical values: 10-500.
20///
21/// * `ml` - Level multiplier for determining max level.
22/// Typically `1 / ln(m)`. Affects the distribution of nodes across layers.
23///
24/// # Product Quantization (PQ) for Compression
25///
26/// When `pq_segments` is set (non-zero), vectors are compressed using Product Quantization:
27///
28/// * `pq_segments` - Number of subspaces to divide vectors into.
29/// Must divide the vector dimension evenly. Typical values: 8, 16, 32.
30/// Higher values = better accuracy but larger codes.
31///
32/// * `pq_centroids` - Number of centroids per subspace. Default: 256 (8-bit codes).
33///
34/// PQ reduces memory usage by 4-8x with minimal recall loss. During search:
35/// - Full precision vectors are used for the first candidate selection
36/// - Compressed vectors are stored in memory for efficient distance computation
37/// - Original vectors can be retrieved from storage for final reranking
38#[derive(Debug, Clone)]
39pub struct HnswConfig {
40 /// Maximum number of connections per node (M parameter).
41 pub m: usize,
42 /// Maximum connections in layer 0 (typically 2 * M).
43 pub m_max0: usize,
44 /// Beam width for construction.
45 pub ef_construction: usize,
46 /// Default beam width for search.
47 pub ef_search: usize,
48 /// Level multiplier (1 / ln(M)).
49 pub ml: f64,
50 /// Number of PQ segments (0 = disabled). Must divide vector dimension evenly.
51 pub pq_segments: usize,
52 /// Number of centroids per PQ segment. Default: 256.
53 pub pq_centroids: usize,
54 /// Minimum vectors required before PQ training. Default: 1000.
55 pub pq_training_samples: usize,
56}
57
58impl HnswConfig {
59 /// Create a new HNSW configuration with the specified M parameter.
60 ///
61 /// Other parameters are set to sensible defaults:
62 /// - `m_max0` = 2 * m
63 /// - `ef_construction` = 200
64 /// - `ef_search` = 50
65 /// - `ml` = 1 / ln(m)
66 /// - `pq_segments` = 0 (disabled)
67 #[must_use]
68 #[allow(clippy::cast_precision_loss)] // m is typically small (16-64), so no precision loss
69 pub fn new(m: usize) -> Self {
70 let m = m.max(2); // Ensure at least 2 connections
71 Self {
72 m,
73 m_max0: m * 2,
74 ef_construction: 200,
75 ef_search: 50,
76 ml: 1.0 / (m as f64).ln(),
77 pq_segments: 0,
78 pq_centroids: 256,
79 pq_training_samples: 1000,
80 }
81 }
82
83 /// Set the beam width for construction.
84 #[must_use]
85 pub const fn with_ef_construction(mut self, ef: usize) -> Self {
86 self.ef_construction = ef;
87 self
88 }
89
90 /// Set the default beam width for search.
91 #[must_use]
92 pub const fn with_ef_search(mut self, ef: usize) -> Self {
93 self.ef_search = ef;
94 self
95 }
96
97 /// Set the maximum connections in layer 0.
98 #[must_use]
99 pub const fn with_m_max0(mut self, m_max0: usize) -> Self {
100 self.m_max0 = m_max0;
101 self
102 }
103
104 /// Enable Product Quantization with the specified number of segments.
105 ///
106 /// The vector dimension must be divisible by `segments`.
107 /// This reduces memory usage by approximately `dimension * 4 / segments` bytes per vector.
108 ///
109 /// # Arguments
110 ///
111 /// - `segments`: Number of subspaces. Common values: 8, 16, 32.
112 ///
113 /// # Example
114 ///
115 /// ```ignore
116 /// // For 128-dim vectors with 8 segments = 16x compression
117 /// let config = HnswConfig::new(16).with_pq(8);
118 /// ```
119 #[must_use]
120 pub const fn with_pq(mut self, segments: usize) -> Self {
121 self.pq_segments = segments;
122 self
123 }
124
125 /// Set the number of centroids per PQ segment.
126 ///
127 /// Default is 256 (8-bit codes). Higher values give better accuracy
128 /// but require more memory for codebooks.
129 #[must_use]
130 pub const fn with_pq_centroids(mut self, centroids: usize) -> Self {
131 self.pq_centroids = centroids;
132 self
133 }
134
135 /// Set the minimum number of vectors required before training PQ.
136 ///
137 /// PQ training requires enough samples for k-means clustering.
138 /// Default is 1000.
139 #[must_use]
140 pub const fn with_pq_training_samples(mut self, samples: usize) -> Self {
141 self.pq_training_samples = samples;
142 self
143 }
144
145 /// Check if Product Quantization is enabled.
146 #[must_use]
147 pub const fn pq_enabled(&self) -> bool {
148 self.pq_segments > 0
149 }
150}
151
152impl Default for HnswConfig {
153 /// Create a default HNSW configuration.
154 ///
155 /// Uses M=16, which is a good balance between recall and speed.
156 fn default() -> Self {
157 Self::new(16)
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_default_config() {
167 let config = HnswConfig::default();
168 assert_eq!(config.m, 16);
169 assert_eq!(config.m_max0, 32);
170 assert_eq!(config.ef_construction, 200);
171 assert_eq!(config.ef_search, 50);
172 assert!((config.ml - 1.0 / 16_f64.ln()).abs() < 1e-10);
173 assert_eq!(config.pq_segments, 0);
174 assert!(!config.pq_enabled());
175 }
176
177 #[test]
178 fn test_custom_config() {
179 let config =
180 HnswConfig::new(32).with_ef_construction(400).with_ef_search(100).with_m_max0(48);
181
182 assert_eq!(config.m, 32);
183 assert_eq!(config.m_max0, 48);
184 assert_eq!(config.ef_construction, 400);
185 assert_eq!(config.ef_search, 100);
186 }
187
188 #[test]
189 fn test_minimum_m() {
190 let config = HnswConfig::new(1);
191 assert_eq!(config.m, 2); // Should be at least 2
192 }
193
194 #[test]
195 fn test_pq_config() {
196 let config =
197 HnswConfig::new(16).with_pq(8).with_pq_centroids(512).with_pq_training_samples(2000);
198
199 assert!(config.pq_enabled());
200 assert_eq!(config.pq_segments, 8);
201 assert_eq!(config.pq_centroids, 512);
202 assert_eq!(config.pq_training_samples, 2000);
203 }
204}