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