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}