oxirs_vec/hnsw/
parallel_construction.rs1use super::{HnswConfig, HnswIndex};
7use crate::Vector;
8use anyhow::Result;
9use parking_lot::RwLock;
10use std::sync::Arc;
11use std::time::Instant;
12
13#[derive(Debug, Clone)]
15pub struct ParallelConstructionConfig {
16 pub num_threads: usize,
18 pub batch_size: usize,
20 pub parallel_connections: bool,
22 pub lock_granularity: usize,
24}
25
26impl Default for ParallelConstructionConfig {
27 fn default() -> Self {
28 Self {
29 num_threads: 0, batch_size: 1000,
31 parallel_connections: true,
32 lock_granularity: 64,
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
39pub struct ParallelConstructionStats {
40 pub total_time_ms: f64,
42 pub vectors_processed: usize,
44 pub threads_used: usize,
46 pub avg_insertion_time_us: f64,
48 pub throughput: f64,
50}
51
52pub struct ParallelHnswBuilder {
54 config: ParallelConstructionConfig,
55 hnsw_config: HnswConfig,
56}
57
58impl ParallelHnswBuilder {
59 pub fn new(hnsw_config: HnswConfig, parallel_config: ParallelConstructionConfig) -> Self {
61 Self {
62 config: parallel_config,
63 hnsw_config,
64 }
65 }
66
67 pub fn build(
69 &self,
70 vectors: Vec<(String, Vector)>,
71 ) -> Result<(HnswIndex, ParallelConstructionStats)> {
72 let start = Instant::now();
73 let num_threads = if self.config.num_threads == 0 {
74 std::thread::available_parallelism()
75 .map(|n| n.get())
76 .unwrap_or(1)
77 } else {
78 self.config.num_threads
79 };
80
81 tracing::info!(
82 "Building HNSW index with {} threads for {} vectors",
83 num_threads,
84 vectors.len()
85 );
86
87 let hnsw_index = HnswIndex::new(self.hnsw_config.clone())?;
89 let index = Arc::new(RwLock::new(hnsw_index));
90
91 let vectors_arc = Arc::new(vectors);
93 let batch_size = self.config.batch_size;
94
95 for batch_start in (0..vectors_arc.len()).step_by(batch_size) {
97 let batch_end = (batch_start + batch_size).min(vectors_arc.len());
98 let batch_vectors = &vectors_arc[batch_start..batch_end];
99
100 for (uri, vector) in batch_vectors {
102 let mut idx = index.write();
103 idx.add_vector(uri.clone(), vector.clone())?;
104 }
105 }
106
107 if self.config.parallel_connections {
109 self.build_connections_parallel(&index, num_threads)?;
110 }
111
112 let elapsed = start.elapsed();
113 let total_time_ms = elapsed.as_secs_f64() * 1000.0;
114
115 let stats = ParallelConstructionStats {
116 total_time_ms,
117 vectors_processed: vectors_arc.len(),
118 threads_used: num_threads,
119 avg_insertion_time_us: (total_time_ms * 1000.0) / vectors_arc.len() as f64,
120 throughput: vectors_arc.len() as f64 / elapsed.as_secs_f64(),
121 };
122
123 let final_index = Arc::try_unwrap(index)
125 .map_err(|_| anyhow::anyhow!("Failed to extract index from Arc"))?
126 .into_inner();
127
128 Ok((final_index, stats))
129 }
130
131 fn build_connections_parallel(
133 &self,
134 _index: &Arc<RwLock<HnswIndex>>,
135 num_threads: usize,
136 ) -> Result<()> {
137 tracing::debug!("Building connections with {} threads", num_threads);
145
146 Ok(())
147 }
148}
149
150pub struct ParallelHnswIndexBuilder {
152 hnsw_config: HnswConfig,
153 parallel_config: ParallelConstructionConfig,
154 vectors: Vec<(String, Vector)>,
155}
156
157impl ParallelHnswIndexBuilder {
158 pub fn new() -> Self {
160 Self {
161 hnsw_config: HnswConfig::default(),
162 parallel_config: ParallelConstructionConfig::default(),
163 vectors: Vec::new(),
164 }
165 }
166
167 pub fn with_hnsw_config(mut self, config: HnswConfig) -> Self {
169 self.hnsw_config = config;
170 self
171 }
172
173 pub fn with_parallel_config(mut self, config: ParallelConstructionConfig) -> Self {
175 self.parallel_config = config;
176 self
177 }
178
179 pub fn with_threads(mut self, num_threads: usize) -> Self {
181 self.parallel_config.num_threads = num_threads;
182 self
183 }
184
185 pub fn with_batch_size(mut self, batch_size: usize) -> Self {
187 self.parallel_config.batch_size = batch_size;
188 self
189 }
190
191 pub fn add_vectors(mut self, vectors: Vec<(String, Vector)>) -> Self {
193 self.vectors = vectors;
194 self
195 }
196
197 pub fn build(self) -> Result<(HnswIndex, ParallelConstructionStats)> {
199 let builder = ParallelHnswBuilder::new(self.hnsw_config, self.parallel_config);
200 builder.build(self.vectors)
201 }
202}
203
204impl Default for ParallelHnswIndexBuilder {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 fn create_test_vectors(count: usize, dim: usize) -> Vec<(String, Vector)> {
215 (0..count)
216 .map(|i| {
217 let values = vec![i as f32 / count as f32; dim];
218 (format!("vec_{}", i), Vector::new(values))
219 })
220 .collect()
221 }
222
223 #[test]
224 fn test_parallel_construction_config() {
225 let config = ParallelConstructionConfig::default();
226 assert_eq!(config.num_threads, 0);
227 assert!(config.batch_size > 0);
228 }
229
230 #[test]
231 fn test_parallel_builder_creation() {
232 let hnsw_config = HnswConfig::default();
233 let parallel_config = ParallelConstructionConfig::default();
234 let _builder = ParallelHnswBuilder::new(hnsw_config, parallel_config);
235 }
236
237 #[test]
238 fn test_parallel_index_builder() -> Result<()> {
239 let vectors = create_test_vectors(100, 64);
240
241 let result = ParallelHnswIndexBuilder::new()
242 .with_threads(2)
243 .with_batch_size(50)
244 .add_vectors(vectors)
245 .build();
246
247 assert!(result.is_ok());
248 let (index, stats) = result?;
249
250 assert_eq!(index.len(), 100);
251 assert_eq!(stats.vectors_processed, 100);
252 assert!(stats.throughput > 0.0);
253 Ok(())
254 }
255
256 #[test]
257 fn test_different_batch_sizes() {
258 let vectors = create_test_vectors(200, 32);
259
260 let result1 = ParallelHnswIndexBuilder::new()
262 .with_batch_size(10)
263 .add_vectors(vectors.clone())
264 .build();
265 assert!(result1.is_ok());
266
267 let result2 = ParallelHnswIndexBuilder::new()
269 .with_batch_size(200)
270 .add_vectors(vectors)
271 .build();
272 assert!(result2.is_ok());
273 }
274
275 #[test]
276 fn test_multi_threaded_build() -> Result<()> {
277 let vectors = create_test_vectors(500, 128);
278
279 let result = ParallelHnswIndexBuilder::new()
280 .with_threads(4)
281 .add_vectors(vectors)
282 .build();
283
284 assert!(result.is_ok());
285 let (_index, stats) = result?;
286
287 assert_eq!(stats.vectors_processed, 500);
288 assert_eq!(stats.threads_used, 4);
289 Ok(())
290 }
291}