1use std::io::Write;
8use std::sync::Arc;
9
10use rustc_hash::FxHashMap;
11
12use crate::directories::DirectoryWriter;
13use crate::dsl::{DenseVectorConfig, Field, FieldType, VectorIndexType};
14use crate::error::{Error, Result};
15use crate::segment::{SegmentId, SegmentReader};
16
17use super::IndexWriter;
18
19const MAX_IVF_CLUSTERS: usize = 4096;
22
23struct TrainedFieldUpdate {
24 field_id: u32,
25 index_type: VectorIndexType,
26 vector_count: usize,
27 num_clusters: usize,
28 centroids_file: String,
29 codebook_file: Option<String>,
30}
31
32struct SizeLimitedWriter<'a, W: Write + ?Sized> {
37 inner: &'a mut W,
38 written: usize,
39 limit: usize,
40}
41
42impl<'a, W: Write + ?Sized> SizeLimitedWriter<'a, W> {
43 fn new(inner: &'a mut W, limit: usize) -> Self {
44 Self {
45 inner,
46 written: 0,
47 limit,
48 }
49 }
50}
51
52impl<W: Write + ?Sized> Write for SizeLimitedWriter<'_, W> {
53 fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
54 let next_size = self
55 .written
56 .checked_add(buffer.len())
57 .ok_or_else(|| std::io::Error::other("trained artifact size overflow"))?;
58 if next_size > self.limit {
59 return Err(std::io::Error::new(
60 std::io::ErrorKind::InvalidData,
61 format!(
62 "trained artifact exceeds the {}-byte safety limit",
63 self.limit
64 ),
65 ));
66 }
67 let written = self.inner.write(buffer)?;
68 self.written += written;
69 Ok(written)
70 }
71
72 fn flush(&mut self) -> std::io::Result<()> {
73 self.inner.flush()
74 }
75}
76
77fn validate_explicit_ivf_num_clusters(config: &DenseVectorConfig) -> Result<()> {
78 match config.num_clusters {
79 Some(0) => Err(Error::Schema(
80 "dense vector num_clusters must be at least 1".to_string(),
81 )),
82 Some(value) if value > MAX_IVF_CLUSTERS => Err(Error::Schema(format!(
83 "dense vector num_clusters must not exceed {MAX_IVF_CLUSTERS}, got {value}"
84 ))),
85 _ => Ok(()),
86 }
87}
88
89fn effective_ivf_num_clusters(
96 config: &DenseVectorConfig,
97 corpus_count: usize,
98 sample_count: usize,
99) -> Result<usize> {
100 if sample_count == 0 {
101 return Err(Error::Schema(
102 "cannot train an IVF vector index without sample vectors".to_string(),
103 ));
104 }
105
106 validate_explicit_ivf_num_clusters(config)?;
107 let requested = match config.num_clusters {
108 Some(value) => value,
109 None => config.optimal_num_clusters(corpus_count),
110 };
111
112 Ok(requested.min(sample_count))
113}
114
115impl<D: DirectoryWriter + 'static> IndexWriter<D> {
116 pub async fn build_vector_index(&self) -> Result<()> {
126 let dense_fields = self.get_dense_vector_fields();
127 if dense_fields.is_empty() {
128 log::info!("No dense vector fields configured for ANN indexing");
129 return Ok(());
130 }
131
132 let artifact_update = self.segment_manager.begin_vector_artifact_update().await?;
133 self.build_vector_index_locked(&dense_fields, &artifact_update)
134 .await
135 }
136
137 async fn build_vector_index_locked(
139 &self,
140 dense_fields: &[(Field, DenseVectorConfig)],
141 artifact_update: &crate::merge::VectorArtifactUpdateGuard,
142 ) -> Result<()> {
143 let fields_to_build = self.get_fields_to_build(dense_fields).await;
145 if fields_to_build.is_empty() {
146 log::info!("All vector fields already built, skipping training");
147 return Ok(());
148 }
149
150 for (_, config) in &fields_to_build {
153 if config.uses_ivf() {
154 validate_explicit_ivf_num_clusters(config)?;
155 }
156 }
157
158 let snapshot = self.segment_manager.acquire_snapshot().await;
160 let segment_ids = snapshot.segment_ids();
161 if segment_ids.is_empty() {
162 return Ok(());
163 }
164
165 let (all_vectors, total_vectors) = self
167 .collect_vectors_for_training(segment_ids, &fields_to_build)
168 .await?;
169
170 let mut updates = Vec::with_capacity(fields_to_build.len());
174 for (field, config) in &fields_to_build {
175 if let Some(update) = self
176 .train_field_index(*field, config, &all_vectors, &total_vectors)
177 .await?
178 {
179 updates.push(update);
180 }
181 }
182
183 if updates.is_empty() {
184 log::info!("No vectors available for trained vector-index fields");
185 return Ok(());
186 }
187
188 self.segment_manager
191 .update_vector_metadata_and_publish(artifact_update, |meta| {
192 for update in &updates {
193 meta.init_field(update.field_id, update.index_type);
194 meta.mark_field_built(
195 update.field_id,
196 update.vector_count,
197 update.num_clusters,
198 update.centroids_file.clone(),
199 update.codebook_file.clone(),
200 );
201 }
202 })
203 .await?;
204
205 log::info!("Vector index training complete, ANN will be built during merges");
206
207 Ok(())
208 }
209
210 pub async fn rebuild_vector_index(&self) -> Result<()> {
217 let dense_fields = self.get_dense_vector_fields();
218 if dense_fields.is_empty() {
219 return Ok(());
220 }
221
222 let artifact_update = self.segment_manager.begin_vector_artifact_update().await?;
226 let snapshot = self.segment_manager.acquire_snapshot().await;
227 let field_ids: Vec<u32> = dense_fields.iter().map(|(field, _)| field.0).collect();
228 self.reject_rebuild_with_ann_segments(snapshot.segment_ids(), &field_ids)
229 .await?;
230
231 self.segment_manager
235 .update_vector_metadata_and_publish(&artifact_update, |meta| {
236 for field_id in &field_ids {
237 if let Some(field_meta) = meta.vector_fields.get_mut(field_id) {
238 field_meta.state = super::VectorIndexState::Flat;
239 field_meta.centroids_file = None;
240 field_meta.codebook_file = None;
241 }
242 }
243 meta.refresh_total_vectors();
244 })
245 .await?;
246
247 log::info!("Reset vector index state to Flat, triggering rebuild...");
248
249 self.build_vector_index_locked(&dense_fields, &artifact_update)
250 .await
251 }
252
253 async fn reject_rebuild_with_ann_segments(
258 &self,
259 segment_ids: &[String],
260 field_ids: &[u32],
261 ) -> Result<()> {
262 for id_str in segment_ids {
263 let segment_id = SegmentId::from_hex(id_str)
264 .ok_or_else(|| Error::Corruption(format!("Invalid segment ID: {id_str}")))?;
265 let reader = SegmentReader::open_with_cache_blocks(
266 self.directory.as_ref(),
267 segment_id,
268 Arc::clone(&self.schema),
269 self.config.term_cache_blocks,
270 self.config.store_cache_blocks,
271 )
272 .await?;
273 Self::reject_ann_in_reader(&reader, id_str, field_ids)?;
274 }
275 Ok(())
276 }
277
278 fn reject_ann_in_reader(reader: &SegmentReader, id_str: &str, field_ids: &[u32]) -> Result<()> {
279 for &field_id in field_ids {
280 if matches!(
281 reader.vector_indexes().get(&field_id),
282 Some(crate::segment::VectorIndex::IVF(_))
283 | Some(crate::segment::VectorIndex::ScaNN(_))
284 ) {
285 return Err(Error::Schema(format!(
286 "cannot retrain vector artifacts for field {field_id}: segment {id_str} \
287 already contains an IVF/ScaNN index built with the current generation; \
288 rebuild requires all committed segments for the field to be flat"
289 )));
290 }
291 }
292 Ok(())
293 }
294
295 fn get_dense_vector_fields(&self) -> Vec<(Field, DenseVectorConfig)> {
297 self.schema
298 .fields()
299 .filter_map(|(field, entry)| {
300 if entry.field_type == FieldType::DenseVector && entry.indexed {
301 entry
302 .dense_vector_config
303 .as_ref()
304 .filter(|c| c.uses_ivf())
309 .map(|c| (field, c.clone()))
310 } else {
311 None
312 }
313 })
314 .collect()
315 }
316
317 async fn get_fields_to_build(
319 &self,
320 dense_fields: &[(Field, DenseVectorConfig)],
321 ) -> Vec<(Field, DenseVectorConfig)> {
322 let field_ids: Vec<u32> = dense_fields.iter().map(|(f, _)| f.0).collect();
323 let built: Vec<u32> = self
324 .segment_manager
325 .read_metadata(|meta| {
326 field_ids
327 .iter()
328 .filter(|fid| meta.is_field_built(**fid))
329 .copied()
330 .collect()
331 })
332 .await;
333 dense_fields
334 .iter()
335 .filter(|(field, _)| !built.contains(&field.0))
336 .cloned()
337 .collect()
338 }
339
340 async fn collect_vectors_for_training(
345 &self,
346 segment_ids: &[String],
347 fields_to_build: &[(Field, DenseVectorConfig)],
348 ) -> Result<(FxHashMap<u32, Vec<Vec<f32>>>, FxHashMap<u32, usize>)> {
349 const MAX_TRAINING_VECTORS: usize = 100_000;
351
352 let mut all_vectors: FxHashMap<u32, Vec<Vec<f32>>> = FxHashMap::default();
353 let mut total_vectors: FxHashMap<u32, usize> = FxHashMap::default();
354 let mut total_skipped = 0usize;
355 let field_ids: Vec<u32> = fields_to_build.iter().map(|(field, _)| field.0).collect();
356
357 for id_str in segment_ids {
358 let segment_id = SegmentId::from_hex(id_str)
359 .ok_or_else(|| Error::Corruption(format!("Invalid segment ID: {}", id_str)))?;
360 let reader = SegmentReader::open_with_cache_blocks(
361 self.directory.as_ref(),
362 segment_id,
363 Arc::clone(&self.schema),
364 self.config.term_cache_blocks,
365 self.config.store_cache_blocks,
366 )
367 .await?;
368
369 Self::reject_ann_in_reader(&reader, id_str, &field_ids)?;
375
376 for (field_id, lazy_flat) in reader.flat_vectors() {
377 if !fields_to_build.iter().any(|(f, _)| f.0 == *field_id) {
378 continue;
379 }
380 let total = total_vectors.entry(*field_id).or_default();
381 *total = total.saturating_add(lazy_flat.num_vectors);
382 let entry = all_vectors.entry(*field_id).or_default();
383 let remaining = MAX_TRAINING_VECTORS.saturating_sub(entry.len());
384
385 if remaining == 0 {
386 total_skipped += lazy_flat.num_vectors;
387 continue;
388 }
389
390 let n = lazy_flat.num_vectors;
391 let dim = lazy_flat.dim;
392 let quant = lazy_flat.quantization;
393
394 let indices: Vec<usize> = if n <= remaining {
396 (0..n).collect()
397 } else {
398 let step = (n / remaining).max(1);
399 (0..n).step_by(step).take(remaining).collect()
400 };
401
402 if indices.len() < n {
403 total_skipped += n - indices.len();
404 }
405
406 const BATCH: usize = 1024;
408 let mut f32_buf = vec![0f32; BATCH * dim];
409 for chunk in indices.chunks(BATCH) {
410 let start = chunk[0];
412 let end = *chunk.last().unwrap();
413 if end - start + 1 == chunk.len() {
414 if let Ok(batch_bytes) =
416 lazy_flat.read_vectors_batch(start, chunk.len()).await
417 {
418 let floats = chunk.len() * dim;
419 f32_buf.resize(floats, 0.0);
420 crate::segment::dequantize_raw(
421 batch_bytes.as_slice(),
422 quant,
423 floats,
424 &mut f32_buf,
425 )
426 .map_err(crate::Error::Io)?;
427 for i in 0..chunk.len() {
428 entry.push(f32_buf[i * dim..(i + 1) * dim].to_vec());
429 }
430 }
431 } else {
432 f32_buf.resize(dim, 0.0);
434 for &idx in chunk {
435 if let Ok(()) = lazy_flat.read_vector_into(idx, &mut f32_buf).await {
436 entry.push(f32_buf[..dim].to_vec());
437 }
438 }
439 }
440 }
441 }
442 }
443
444 if total_skipped > 0 {
445 let collected: usize = all_vectors.values().map(|v| v.len()).sum();
446 log::info!(
447 "Sampled {} vectors for training (skipped {}, max {} per field)",
448 collected,
449 total_skipped,
450 MAX_TRAINING_VECTORS,
451 );
452 }
453
454 Ok((all_vectors, total_vectors))
455 }
456
457 async fn train_field_index(
459 &self,
460 field: Field,
461 config: &DenseVectorConfig,
462 all_vectors: &FxHashMap<u32, Vec<Vec<f32>>>,
463 total_vectors: &FxHashMap<u32, usize>,
464 ) -> Result<Option<TrainedFieldUpdate>> {
465 let field_id = field.0;
466 let vectors = match all_vectors.get(&field_id) {
467 Some(v) if !v.is_empty() => v,
468 _ => return Ok(None),
469 };
470
471 let dim = config.dim;
472 let sample_count = vectors.len();
473 let corpus_count = total_vectors
474 .get(&field_id)
475 .copied()
476 .unwrap_or(sample_count);
477 if !matches!(
480 config.index_type,
481 VectorIndexType::IvfRaBitQ | VectorIndexType::ScaNN
482 ) {
483 return Ok(None);
484 }
485
486 let num_clusters = effective_ivf_num_clusters(config, corpus_count, sample_count)?;
487
488 log::info!(
489 "Training vector index for field {} with {} sampled / {} total vectors, {} clusters (dim={})",
490 field_id,
491 sample_count,
492 corpus_count,
493 num_clusters,
494 dim,
495 );
496
497 let centroids_filename = format!("field_{}_centroids.bin", field_id);
498 let mut codebook_filename: Option<String> = None;
499
500 let actual_num_clusters = match config.index_type {
501 VectorIndexType::IvfRaBitQ => {
502 self.train_ivf_rabitq(
503 field_id,
504 dim,
505 num_clusters,
506 config.soar.clone(),
507 vectors,
508 ¢roids_filename,
509 )
510 .await?
511 }
512 VectorIndexType::ScaNN => {
513 codebook_filename = Some(format!("field_{}_codebook.bin", field_id));
514 self.train_scann(
515 field_id,
516 dim,
517 num_clusters,
518 config.soar.clone(),
519 vectors,
520 ¢roids_filename,
521 codebook_filename.as_ref().unwrap(),
522 )
523 .await?
524 }
525 _ => unreachable!("non-IVF vector index returned above"),
526 };
527
528 Ok(Some(TrainedFieldUpdate {
529 field_id,
530 index_type: config.index_type,
531 vector_count: corpus_count,
532 num_clusters: actual_num_clusters,
533 centroids_file: centroids_filename,
534 codebook_file: codebook_filename,
535 }))
536 }
537
538 async fn save_trained_artifact(
540 &self,
541 artifact: &impl serde::Serialize,
542 filename: &str,
543 ) -> Result<()> {
544 let temp_filename = format!("{filename}.tmp");
545 let temp_path = std::path::Path::new(&temp_filename);
546 let final_path = std::path::Path::new(filename);
547 let mut writer = self.directory.streaming_writer(temp_path).await?;
548 let encode_result = {
549 let mut limited = SizeLimitedWriter::new(
550 writer.as_mut(),
551 super::metadata::MAX_TRAINED_ARTIFACT_BYTES,
552 );
553 bincode::serde::encode_into_std_write(
554 artifact,
555 &mut limited,
556 bincode::config::standard(),
557 )
558 };
559 if let Err(error) = encode_result {
560 drop(writer);
561 let _ = self.directory.delete(temp_path).await;
562 return Err(Error::Serialization(format!(
563 "failed to serialize trained artifact '{filename}': {error}"
564 )));
565 }
566 if let Err(error) = writer.finish() {
567 let _ = self.directory.delete(temp_path).await;
568 return Err(Error::Io(error));
569 }
570 if let Err(error) = self.directory.rename(temp_path, final_path).await {
571 let _ = self.directory.delete(temp_path).await;
572 return Err(Error::Io(error));
573 }
574 self.directory.sync().await?;
575 Ok(())
576 }
577
578 async fn train_ivf_rabitq(
580 &self,
581 field_id: u32,
582 dim: usize,
583 num_clusters: usize,
584 soar: Option<crate::structures::SoarConfig>,
585 vectors: &[Vec<f32>],
586 centroids_filename: &str,
587 ) -> Result<usize> {
588 let mut coarse_config = crate::structures::CoarseConfig::new(dim, num_clusters);
589 if let Some(soar) = soar {
590 coarse_config = coarse_config.with_soar(soar);
591 }
592 let centroids = crate::structures::CoarseCentroids::train(&coarse_config, vectors);
593 self.save_trained_artifact(¢roids, centroids_filename)
594 .await?;
595
596 log::info!(
597 "Saved IVF-RaBitQ centroids for field {} ({} clusters, soar={})",
598 field_id,
599 centroids.num_clusters,
600 centroids.soar_config.is_some()
601 );
602 Ok(centroids.num_clusters as usize)
603 }
604
605 #[allow(clippy::too_many_arguments)]
607 async fn train_scann(
608 &self,
609 field_id: u32,
610 dim: usize,
611 num_clusters: usize,
612 soar: Option<crate::structures::SoarConfig>,
613 vectors: &[Vec<f32>],
614 centroids_filename: &str,
615 codebook_filename: &str,
616 ) -> Result<usize> {
617 let mut coarse_config = crate::structures::CoarseConfig::new(dim, num_clusters);
618 if let Some(soar) = soar {
619 coarse_config = coarse_config.with_soar(soar);
620 }
621 let centroids = crate::structures::CoarseCentroids::train(&coarse_config, vectors);
622 self.save_trained_artifact(¢roids, centroids_filename)
623 .await?;
624
625 let pq_config = crate::structures::PQConfig::new(dim);
626 let codebook = crate::structures::PQCodebook::train(pq_config, vectors, 10);
627 self.save_trained_artifact(&codebook, codebook_filename)
628 .await?;
629
630 log::info!(
631 "Saved ScaNN centroids and codebook for field {} ({} clusters)",
632 field_id,
633 centroids.num_clusters
634 );
635 Ok(centroids.num_clusters as usize)
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642
643 fn ivf_config(num_clusters: Option<usize>) -> DenseVectorConfig {
644 DenseVectorConfig::with_ivf(8, num_clusters, 4)
645 }
646
647 #[test]
648 fn effective_clusters_follow_corpus_heuristic_but_fit_sample() {
649 let config = ivf_config(None);
650
651 assert_eq!(
652 effective_ivf_num_clusters(&config, 1_000_000, 73).unwrap(),
653 73
654 );
655 assert_eq!(
656 effective_ivf_num_clusters(&config, 10_000, 1_000).unwrap(),
657 100
658 );
659 }
660
661 #[test]
662 fn effective_clusters_clamp_explicit_value_to_sample() {
663 let config = ivf_config(Some(256));
664 assert_eq!(
665 effective_ivf_num_clusters(&config, 1_000_000, 17).unwrap(),
666 17
667 );
668 }
669
670 #[test]
671 fn effective_clusters_reject_invalid_explicit_bounds() {
672 let zero = effective_ivf_num_clusters(&ivf_config(Some(0)), 10_000, 100)
673 .unwrap_err()
674 .to_string();
675 assert!(zero.contains("at least 1"));
676
677 let too_many =
678 effective_ivf_num_clusters(&ivf_config(Some(MAX_IVF_CLUSTERS + 1)), 10_000, 100)
679 .unwrap_err()
680 .to_string();
681 assert!(too_many.contains("must not exceed 4096"));
682 }
683
684 #[test]
685 fn effective_clusters_reject_empty_training_sample() {
686 let error = effective_ivf_num_clusters(&ivf_config(None), 10_000, 0)
687 .unwrap_err()
688 .to_string();
689 assert!(error.contains("without sample vectors"));
690 }
691
692 #[test]
693 fn artifact_writer_enforces_limit_without_writing_past_it() {
694 let mut output = Vec::new();
695 let mut writer = SizeLimitedWriter::new(&mut output, 3);
696 writer.write_all(&[1, 2]).unwrap();
697 let error = writer.write_all(&[3, 4]).unwrap_err().to_string();
698 assert!(error.contains("3-byte safety limit"), "{error}");
699 assert_eq!(output, vec![1, 2]);
700 }
701}