1use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::io::Write;
16use std::path::Path;
17
18use crate::dsl::{Schema, VectorIndexType};
19use crate::error::{Error, Result};
20
21pub const INDEX_META_FILENAME: &str = "metadata.json";
23const INDEX_META_TMP_FILENAME: &str = "metadata.json.tmp";
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
28pub enum VectorIndexState {
29 #[default]
31 Flat,
32 Built {
34 vector_count: usize,
36 num_clusters: usize,
38 },
39}
40
41fn default_true() -> bool {
42 true
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct SegmentMetaInfo {
49 pub num_docs: u32,
51 pub ancestors: Vec<String>,
53 pub generation: u32,
55 #[serde(default)]
59 pub reordered: bool,
60 #[serde(default = "default_true")]
65 pub bp_converged: bool,
66 #[serde(default)]
70 pub bp_unconverged_passes: u32,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct FieldVectorMeta {
76 pub field_id: u32,
78 pub index_type: VectorIndexType,
80 pub state: VectorIndexState,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub centroids_file: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub codebook_file: Option<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct IndexMetadata {
93 pub version: u32,
95 pub schema: Schema,
97 #[serde(default)]
100 pub segment_metas: HashMap<String, SegmentMetaInfo>,
101 #[serde(default)]
103 pub vector_fields: HashMap<u32, FieldVectorMeta>,
104 #[serde(default)]
106 pub total_vectors: usize,
107}
108
109impl IndexMetadata {
110 pub fn new(schema: Schema) -> Self {
112 Self {
113 version: 1,
114 schema,
115 segment_metas: HashMap::new(),
116 vector_fields: HashMap::new(),
117 total_vectors: 0,
118 }
119 }
120
121 pub fn segment_ids(&self) -> Vec<String> {
123 let mut ids: Vec<String> = self.segment_metas.keys().cloned().collect();
124 ids.sort();
125 ids
126 }
127
128 pub fn add_segment(&mut self, segment_id: String, num_docs: u32) {
130 self.segment_metas.insert(
131 segment_id,
132 SegmentMetaInfo {
133 num_docs,
134 ancestors: Vec::new(),
135 generation: 0,
136 reordered: false,
137 bp_converged: true,
138 bp_unconverged_passes: 0,
139 },
140 );
141 }
142
143 pub fn add_merged_segment(
145 &mut self,
146 segment_id: String,
147 num_docs: u32,
148 ancestors: Vec<String>,
149 generation: u32,
150 reordered: bool,
151 bp_converged: bool,
152 ) {
153 self.add_segment_meta(
154 segment_id,
155 SegmentMetaInfo {
156 num_docs,
157 ancestors,
158 generation,
159 reordered,
160 bp_converged,
161 bp_unconverged_passes: 0,
162 },
163 );
164 }
165
166 pub(crate) fn add_segment_meta(&mut self, segment_id: String, info: SegmentMetaInfo) {
170 self.segment_metas.insert(segment_id, info);
171 }
172
173 pub fn remove_segment(&mut self, segment_id: &str) {
175 self.segment_metas.remove(segment_id);
176 }
177
178 pub fn has_segment(&self, segment_id: &str) -> bool {
180 self.segment_metas.contains_key(segment_id)
181 }
182
183 pub fn segment_doc_count(&self, segment_id: &str) -> Option<u32> {
185 self.segment_metas.get(segment_id).map(|m| m.num_docs)
186 }
187
188 pub fn is_field_built(&self, field_id: u32) -> bool {
190 self.vector_fields
191 .get(&field_id)
192 .map(|f| matches!(f.state, VectorIndexState::Built { .. }))
193 .unwrap_or(false)
194 }
195
196 pub fn get_field_meta(&self, field_id: u32) -> Option<&FieldVectorMeta> {
198 self.vector_fields.get(&field_id)
199 }
200
201 pub fn init_field(&mut self, field_id: u32, index_type: VectorIndexType) {
203 self.vector_fields
204 .entry(field_id)
205 .or_insert(FieldVectorMeta {
206 field_id,
207 index_type,
208 state: VectorIndexState::Flat,
209 centroids_file: None,
210 codebook_file: None,
211 });
212 }
213
214 pub fn mark_field_built(
216 &mut self,
217 field_id: u32,
218 vector_count: usize,
219 num_clusters: usize,
220 centroids_file: String,
221 codebook_file: Option<String>,
222 ) {
223 if let Some(field) = self.vector_fields.get_mut(&field_id) {
224 field.state = VectorIndexState::Built {
225 vector_count,
226 num_clusters,
227 };
228 field.centroids_file = Some(centroids_file);
229 field.codebook_file = codebook_file;
230 }
231 }
232
233 pub fn should_build_field(&self, field_id: u32, threshold: usize) -> bool {
235 if self.is_field_built(field_id) {
237 return false;
238 }
239 self.total_vectors >= threshold
241 }
242
243 pub async fn load<D: crate::directories::Directory>(dir: &D) -> Result<Self> {
248 let path = Path::new(INDEX_META_FILENAME);
249 match dir.open_read(path).await {
250 Ok(slice) => {
251 let bytes = slice.read_bytes().await?;
252 serde_json::from_slice(bytes.as_slice())
253 .map_err(|e| Error::Serialization(e.to_string()))
254 }
255 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
256 let tmp_path = Path::new(INDEX_META_TMP_FILENAME);
258 let slice = dir.open_read(tmp_path).await?;
259 let bytes = slice.read_bytes().await?;
260 let meta: Self = serde_json::from_slice(bytes.as_slice())
261 .map_err(|e| Error::Serialization(e.to_string()))?;
262 log::warn!("Recovered metadata from temp file (previous crash during save)");
263 Ok(meta)
264 }
265 Err(e) => Err(Error::Io(e)),
266 }
267 }
268
269 pub async fn save<D: crate::directories::DirectoryWriter>(&self, dir: &D) -> Result<()> {
274 let bytes = self.serialize_to_bytes()?;
275 Self::save_bytes(dir, &bytes).await
276 }
277
278 pub fn serialize_to_bytes(&self) -> Result<Vec<u8>> {
281 serde_json::to_vec_pretty(self).map_err(|e| Error::Serialization(e.to_string()))
282 }
283
284 pub async fn save_bytes<D: crate::directories::DirectoryWriter>(
289 dir: &D,
290 bytes: &[u8],
291 ) -> Result<()> {
292 let tmp_path = Path::new(INDEX_META_TMP_FILENAME);
293 let final_path = Path::new(INDEX_META_FILENAME);
294 let mut writer = dir.streaming_writer(tmp_path).await.map_err(Error::Io)?;
299 writer.write_all(bytes).map_err(Error::Io)?;
300 writer.finish().map_err(Error::Io)?;
301 dir.rename(tmp_path, final_path).await.map_err(Error::Io)?;
308 if let Err(error) = dir.sync().await {
309 log::error!(
310 "[metadata] directory fsync failed after committed rename: {}. \
311 Continuing with the renamed generation; crash durability is not guaranteed",
312 error,
313 );
314 }
315 Ok(())
316 }
317
318 pub async fn load_trained_from_fields<D: crate::directories::Directory>(
321 vector_fields: &HashMap<u32, FieldVectorMeta>,
322 dir: &D,
323 ) -> Option<crate::segment::TrainedVectorStructures> {
324 use std::sync::Arc;
325
326 let mut centroids = rustc_hash::FxHashMap::default();
327 let mut codebooks = rustc_hash::FxHashMap::default();
328
329 log::debug!(
330 "[trained] loading trained structures, vector_fields={:?}",
331 vector_fields.keys().collect::<Vec<_>>()
332 );
333
334 for (field_id, field_meta) in vector_fields {
335 log::debug!(
336 "[trained] field {} state={:?} centroids_file={:?} codebook_file={:?}",
337 field_id,
338 field_meta.state,
339 field_meta.centroids_file,
340 field_meta.codebook_file,
341 );
342 if !matches!(field_meta.state, VectorIndexState::Built { .. }) {
343 log::debug!("[trained] field {} skipped (not Built)", field_id);
344 continue;
345 }
346
347 match &field_meta.centroids_file {
349 None => {
350 log::warn!(
351 "[trained] field {} is Built but has no centroids_file",
352 field_id
353 );
354 }
355 Some(file) => match dir.open_read(Path::new(file)).await {
356 Err(e) => {
357 log::warn!(
358 "[trained] field {} failed to open centroids file '{}': {}",
359 field_id,
360 file,
361 e
362 );
363 }
364 Ok(slice) => match slice.read_bytes().await {
365 Err(e) => {
366 log::warn!(
367 "[trained] field {} failed to read centroids file '{}': {}",
368 field_id,
369 file,
370 e
371 );
372 }
373 Ok(bytes) => {
374 match bincode::serde::decode_from_slice::<
375 crate::structures::CoarseCentroids,
376 _,
377 >(
378 bytes.as_slice(), bincode::config::standard()
379 )
380 .map(|(v, _)| v)
381 {
382 Err(e) => {
383 log::warn!(
384 "[trained] field {} failed to deserialize centroids from '{}': {}",
385 field_id,
386 file,
387 e
388 );
389 }
390 Ok(c) => {
391 log::debug!(
392 "[trained] field {} loaded centroids ({} clusters)",
393 field_id,
394 c.num_clusters
395 );
396 centroids.insert(*field_id, Arc::new(c));
397 }
398 }
399 }
400 },
401 },
402 }
403
404 match &field_meta.codebook_file {
406 None => {} Some(file) => match dir.open_read(Path::new(file)).await {
408 Err(e) => {
409 log::warn!(
410 "[trained] field {} failed to open codebook file '{}': {}",
411 field_id,
412 file,
413 e
414 );
415 }
416 Ok(slice) => match slice.read_bytes().await {
417 Err(e) => {
418 log::warn!(
419 "[trained] field {} failed to read codebook file '{}': {}",
420 field_id,
421 file,
422 e
423 );
424 }
425 Ok(bytes) => {
426 match bincode::serde::decode_from_slice::<
427 crate::structures::PQCodebook,
428 _,
429 >(
430 bytes.as_slice(), bincode::config::standard()
431 )
432 .map(|(v, _)| v)
433 {
434 Err(e) => {
435 log::warn!(
436 "[trained] field {} failed to deserialize codebook from '{}': {}",
437 field_id,
438 file,
439 e
440 );
441 }
442 Ok(c) => {
443 log::debug!("[trained] field {} loaded codebook", field_id);
444 codebooks.insert(*field_id, Arc::new(c));
445 }
446 }
447 }
448 },
449 },
450 }
451 }
452
453 if centroids.is_empty() {
454 None
455 } else {
456 Some(crate::segment::TrainedVectorStructures {
457 centroids,
458 codebooks,
459 })
460 }
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[derive(Clone, Default)]
469 struct SyncFailDirectory(crate::directories::RamDirectory);
470
471 #[async_trait::async_trait]
472 impl crate::directories::Directory for SyncFailDirectory {
473 async fn exists(&self, path: &Path) -> std::io::Result<bool> {
474 self.0.exists(path).await
475 }
476
477 async fn file_size(&self, path: &Path) -> std::io::Result<u64> {
478 self.0.file_size(path).await
479 }
480
481 async fn open_read(&self, path: &Path) -> std::io::Result<crate::directories::FileHandle> {
482 self.0.open_read(path).await
483 }
484
485 async fn read_range(
486 &self,
487 path: &Path,
488 range: std::ops::Range<u64>,
489 ) -> std::io::Result<crate::directories::OwnedBytes> {
490 self.0.read_range(path, range).await
491 }
492
493 async fn list_files(&self, prefix: &Path) -> std::io::Result<Vec<std::path::PathBuf>> {
494 self.0.list_files(prefix).await
495 }
496
497 async fn open_lazy(&self, path: &Path) -> std::io::Result<crate::directories::FileHandle> {
498 self.0.open_lazy(path).await
499 }
500 }
501
502 #[async_trait::async_trait]
503 impl crate::directories::DirectoryWriter for SyncFailDirectory {
504 async fn write(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
505 self.0.write(path, data).await
506 }
507
508 async fn delete(&self, path: &Path) -> std::io::Result<()> {
509 self.0.delete(path).await
510 }
511
512 async fn rename(&self, from: &Path, to: &Path) -> std::io::Result<()> {
513 self.0.rename(from, to).await
514 }
515
516 async fn sync(&self) -> std::io::Result<()> {
517 Err(std::io::Error::other("injected directory fsync failure"))
518 }
519
520 async fn streaming_writer(
521 &self,
522 path: &Path,
523 ) -> std::io::Result<Box<dyn crate::directories::StreamingWriter>> {
524 self.0.streaming_writer(path).await
525 }
526 }
527
528 fn test_schema() -> Schema {
529 Schema::default()
530 }
531
532 #[test]
533 fn test_metadata_init() {
534 let mut meta = IndexMetadata::new(test_schema());
535 assert_eq!(meta.total_vectors, 0);
536 assert!(meta.segment_metas.is_empty());
537 assert!(!meta.is_field_built(0));
538
539 meta.init_field(0, VectorIndexType::IvfRaBitQ);
540 assert!(!meta.is_field_built(0));
541 assert!(meta.vector_fields.contains_key(&0));
542 }
543
544 #[tokio::test]
545 async fn save_treats_post_rename_sync_failure_as_committed() {
546 let directory = SyncFailDirectory::default();
547 let mut metadata = IndexMetadata::new(test_schema());
548 metadata.add_segment("committed".to_string(), 7);
549
550 metadata.save(&directory).await.unwrap();
551
552 let loaded = IndexMetadata::load(&directory).await.unwrap();
553 assert_eq!(loaded.segment_doc_count("committed"), Some(7));
554 }
555
556 #[test]
557 fn test_metadata_segments() {
558 let mut meta = IndexMetadata::new(test_schema());
559 meta.add_segment("abc123".to_string(), 50);
560 meta.add_segment("def456".to_string(), 100);
561 assert_eq!(meta.segment_metas.len(), 2);
562 assert_eq!(meta.segment_doc_count("abc123"), Some(50));
563 assert_eq!(meta.segment_doc_count("def456"), Some(100));
564
565 meta.add_segment("abc123".to_string(), 75);
567 assert_eq!(meta.segment_metas.len(), 2);
568 assert_eq!(meta.segment_doc_count("abc123"), Some(75));
569
570 meta.remove_segment("abc123");
571 assert_eq!(meta.segment_metas.len(), 1);
572 assert!(meta.has_segment("def456"));
573 assert!(!meta.has_segment("abc123"));
574 }
575
576 #[test]
577 fn test_mark_field_built() {
578 let mut meta = IndexMetadata::new(test_schema());
579 meta.init_field(0, VectorIndexType::IvfRaBitQ);
580 meta.total_vectors = 10000;
581
582 assert!(!meta.is_field_built(0));
583
584 meta.mark_field_built(0, 10000, 256, "field_0_centroids.bin".to_string(), None);
585
586 assert!(meta.is_field_built(0));
587 let field = meta.get_field_meta(0).unwrap();
588 assert_eq!(
589 field.centroids_file.as_deref(),
590 Some("field_0_centroids.bin")
591 );
592 }
593
594 #[test]
595 fn test_should_build_field() {
596 let mut meta = IndexMetadata::new(test_schema());
597 meta.init_field(0, VectorIndexType::IvfRaBitQ);
598
599 meta.total_vectors = 500;
601 assert!(!meta.should_build_field(0, 1000));
602
603 meta.total_vectors = 1500;
605 assert!(meta.should_build_field(0, 1000));
606
607 meta.mark_field_built(0, 1500, 256, "centroids.bin".to_string(), None);
609 assert!(!meta.should_build_field(0, 1000));
610 }
611
612 #[test]
613 fn test_serialization() {
614 let mut meta = IndexMetadata::new(test_schema());
615 meta.add_segment("seg1".to_string(), 100);
616 meta.init_field(0, VectorIndexType::IvfRaBitQ);
617 meta.total_vectors = 5000;
618
619 let json = serde_json::to_string_pretty(&meta).unwrap();
620 let loaded: IndexMetadata = serde_json::from_str(&json).unwrap();
621
622 assert_eq!(loaded.segment_ids().len(), meta.segment_ids().len());
623 assert_eq!(loaded.segment_doc_count("seg1"), Some(100));
624 assert_eq!(loaded.total_vectors, meta.total_vectors);
625 assert!(loaded.vector_fields.contains_key(&0));
626 }
627
628 #[test]
629 fn old_metadata_defaults_the_bp_retry_counter() {
630 let mut meta = IndexMetadata::new(test_schema());
631 meta.add_segment("legacy".to_string(), 10);
632 let mut json = serde_json::to_value(&meta).unwrap();
633 json["segment_metas"]["legacy"]
634 .as_object_mut()
635 .unwrap()
636 .remove("bp_unconverged_passes");
637
638 let loaded: IndexMetadata = serde_json::from_value(json).unwrap();
639 assert_eq!(loaded.segment_metas["legacy"].bp_unconverged_passes, 0);
640 }
641
642 #[test]
643 fn test_merged_segment_lineage() {
644 let mut meta = IndexMetadata::new(test_schema());
645 meta.add_segment("a".to_string(), 50);
646 meta.add_segment("b".to_string(), 75);
647
648 assert_eq!(meta.segment_metas["a"].generation, 0);
650 assert!(meta.segment_metas["a"].ancestors.is_empty());
651
652 meta.add_merged_segment(
654 "c".to_string(),
655 125,
656 vec!["a".to_string(), "b".to_string()],
657 1,
658 false,
659 true,
660 );
661 assert_eq!(meta.segment_metas["c"].generation, 1);
662 assert_eq!(meta.segment_metas["c"].ancestors, vec!["a", "b"]);
663 assert_eq!(meta.segment_doc_count("c"), Some(125));
664
665 meta.add_segment("d".to_string(), 30);
667 meta.add_merged_segment(
668 "e".to_string(),
669 155,
670 vec!["c".to_string(), "d".to_string()],
671 2,
672 false,
673 true,
674 );
675 assert_eq!(meta.segment_metas["e"].generation, 2);
676 }
677}