1use std::sync::mpsc;
20use std::thread::JoinHandle;
21
22use tracing::{debug, info, warn};
23
24use crate::collection::{BuildComplete, BuildRequest};
25use crate::error::VectorError;
26use crate::hnsw::HnswIndex;
27
28pub type BuildSender = mpsc::SyncSender<BuildRequest>;
30
31pub type CompleteReceiver = mpsc::Receiver<BuildComplete>;
33
34pub const BUILD_QUEUE_CAPACITY: usize = 4;
37
38pub fn spawn_builder(
42 core_id: usize,
43 capacity: usize,
44) -> std::io::Result<(BuildSender, CompleteReceiver, JoinHandle<()>)> {
45 let (request_tx, request_rx) = mpsc::sync_channel::<BuildRequest>(capacity);
46 let (complete_tx, complete_rx) = mpsc::sync_channel::<BuildComplete>(capacity);
47
48 let handle = std::thread::Builder::new()
49 .name(format!("hnsw-builder-{core_id}"))
50 .spawn(move || {
51 info!(core_id, "HNSW builder thread started");
52 builder_loop(core_id, request_rx, complete_tx);
53 info!(core_id, "HNSW builder thread stopped");
54 })?;
55
56 Ok((request_tx, complete_rx, handle))
57}
58
59fn builder_loop(
60 core_id: usize,
61 rx: mpsc::Receiver<BuildRequest>,
62 tx: mpsc::SyncSender<BuildComplete>,
63) {
64 while let Ok(req) = rx.recv() {
65 debug!(
66 core_id,
67 key = %req.key,
68 segment_id = req.segment_id,
69 vectors = req.vectors.len(),
70 dim = req.dim,
71 "building HNSW index"
72 );
73 let start = std::time::Instant::now();
74 let (key, segment_id, kind) = (req.key.clone(), req.segment_id, req.kind);
75 let result = build(core_id, req);
76 match &result {
77 Ok(index) => info!(
78 core_id,
79 key = %key,
80 segment_id,
81 vectors = index.len(),
82 elapsed_ms = start.elapsed().as_millis() as u64,
83 "HNSW index built"
84 ),
85 Err(e) => warn!(core_id, key = %key, segment_id, error = %e, "HNSW build failed"),
86 }
87 let complete = BuildComplete {
88 key,
89 segment_id,
90 kind,
91 result,
92 };
93 if tx.send(complete).is_err() {
94 warn!(core_id, "builder: core channel closed, stopping");
95 break;
96 }
97 }
98}
99
100fn build(core_id: usize, req: BuildRequest) -> Result<HnswIndex, VectorError> {
104 let seed = (core_id as u64 + 1) * 1000 + u64::from(req.segment_id);
105 let mut index = HnswIndex::with_seed(req.dim, req.params, seed);
106 for vector in req.vectors {
107 index.insert(vector)?;
108 }
109 Ok(index)
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::collection::BuildKind;
116 use crate::hnsw::HnswParams;
117
118 fn request(segment_id: u32, vectors: Vec<Vec<f32>>) -> BuildRequest {
119 BuildRequest {
120 key: "k".into(),
121 segment_id,
122 kind: BuildKind::Seal,
123 vectors,
124 dim: 2,
125 params: HnswParams::default(),
126 }
127 }
128
129 #[test]
130 fn builds_keep_one_node_per_vector_and_report_errors() {
131 let (tx, rx, handle) = spawn_builder(0, 1).unwrap();
132 tx.send(request(1, vec![vec![1.0, 0.0], vec![0.0, 1.0]]))
133 .unwrap();
134 tx.send(request(2, vec![vec![1.0, 0.0], vec![1.0]]))
135 .unwrap();
136
137 let first = rx.recv().unwrap();
138 assert_eq!(first.segment_id, 1);
139 assert_eq!(first.result.unwrap().len(), 2);
140 let second = rx.recv().unwrap();
141 assert!(matches!(
142 second.result,
143 Err(VectorError::DimensionMismatch { .. })
144 ));
145
146 drop(tx);
147 handle.join().unwrap();
148 }
149
150 #[test]
151 fn a_full_request_queue_refuses_without_blocking() {
152 let (tx, _rx, _handle) = spawn_builder(0, 1).unwrap();
153 let mut refused = false;
156 for id in 0..8 {
157 if let Err(mpsc::TrySendError::Full(_)) = tx.try_send(request(id, vec![vec![1.0, 0.0]]))
158 {
159 refused = true;
160 break;
161 }
162 }
163 assert!(refused, "a bounded queue must refuse once full");
164 }
165}