Skip to main content

ipfrs_semantic/
diskann.rs

1//! DiskANN: Disk-based Approximate Nearest Neighbor Search
2//!
3//! This module provides on-disk graph-based indexing for handling
4//! datasets too large to fit in memory (100M+ vectors).
5//!
6//! Key features:
7//! - Memory-mapped graph access for constant memory usage
8//! - Vamana algorithm for efficient graph construction
9//! - Page cache optimization for fast queries
10//! - Index compaction and optimization
11
12use ipfrs_core::{Cid, Error, Result};
13use memmap2::MmapMut;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::fs::OpenOptions;
17use std::path::Path;
18use std::sync::{Arc, RwLock};
19
20/// DiskANN index configuration
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct DiskANNConfig {
23    /// Vector dimension
24    pub dimension: usize,
25    /// Max degree of graph nodes (R parameter in Vamana)
26    pub max_degree: usize,
27    /// Queue size for graph construction (L parameter)
28    pub queue_size: usize,
29    /// Alpha parameter for pruning (typically 1.2)
30    pub alpha: f32,
31    /// Number of entry points for search
32    pub num_entry_points: usize,
33}
34
35impl Default for DiskANNConfig {
36    fn default() -> Self {
37        Self {
38            dimension: 768,
39            max_degree: 64,
40            queue_size: 100,
41            alpha: 1.2,
42            num_entry_points: 4,
43        }
44    }
45}
46
47/// On-disk index format header
48#[derive(Debug, Clone, Serialize, Deserialize)]
49struct IndexHeader {
50    /// Magic bytes for format validation
51    magic: [u8; 8],
52    /// Format version
53    version: u32,
54    /// Configuration
55    config: DiskANNConfig,
56    /// Number of vectors in index
57    num_vectors: usize,
58    /// Offset to graph data
59    graph_offset: u64,
60    /// Offset to vector data
61    vector_offset: u64,
62    /// Offset to CID mapping
63    cid_mapping_offset: u64,
64}
65
66impl IndexHeader {
67    const MAGIC: [u8; 8] = *b"DISKANN1";
68
69    fn new(config: DiskANNConfig) -> Self {
70        Self {
71            magic: Self::MAGIC,
72            version: 1,
73            config,
74            num_vectors: 0,
75            graph_offset: 0,
76            vector_offset: 0,
77            cid_mapping_offset: 0,
78        }
79    }
80
81    fn validate(&self) -> Result<()> {
82        if self.magic != Self::MAGIC {
83            return Err(Error::InvalidInput(
84                "Invalid DiskANN index file format".to_string(),
85            ));
86        }
87        if self.version != 1 {
88            return Err(Error::InvalidInput(format!(
89                "Unsupported DiskANN version: {}",
90                self.version
91            )));
92        }
93        Ok(())
94    }
95}
96
97/// Node in the graph stored on disk
98#[allow(dead_code)]
99#[derive(Debug, Clone)]
100struct GraphNode {
101    /// Node ID
102    id: usize,
103    /// Neighbor IDs
104    neighbors: Vec<usize>,
105}
106
107/// Vector file header for memory-mapped storage
108#[repr(C)]
109#[derive(Debug, Clone, Copy)]
110struct VectorFileHeader {
111    /// Magic bytes for validation
112    magic: [u8; 8],
113    /// Number of vectors stored
114    num_vectors: u64,
115    /// Vector dimension
116    dimension: u64,
117}
118
119impl VectorFileHeader {
120    const MAGIC: [u8; 8] = *b"VECDATA1";
121    const SIZE: usize = 24; // 8 + 8 + 8 bytes
122
123    fn new(dimension: usize) -> Self {
124        Self {
125            magic: Self::MAGIC,
126            num_vectors: 0,
127            dimension: dimension as u64,
128        }
129    }
130
131    #[allow(dead_code)]
132    fn validate(&self, expected_dim: usize) -> Result<()> {
133        if self.magic != Self::MAGIC {
134            return Err(Error::InvalidInput(
135                "Invalid vector file format".to_string(),
136            ));
137        }
138        if self.dimension != expected_dim as u64 {
139            return Err(Error::InvalidInput(format!(
140                "Vector dimension mismatch: expected {}, got {}",
141                expected_dim, self.dimension
142            )));
143        }
144        Ok(())
145    }
146
147    fn as_bytes(&self) -> [u8; Self::SIZE] {
148        let mut bytes = [0u8; Self::SIZE];
149        bytes[0..8].copy_from_slice(&self.magic);
150        bytes[8..16].copy_from_slice(&self.num_vectors.to_le_bytes());
151        bytes[16..24].copy_from_slice(&self.dimension.to_le_bytes());
152        bytes
153    }
154
155    #[allow(dead_code)]
156    fn from_bytes(bytes: &[u8]) -> Result<Self> {
157        if bytes.len() < Self::SIZE {
158            return Err(Error::InvalidInput(
159                "Vector file header too small".to_string(),
160            ));
161        }
162
163        let mut magic = [0u8; 8];
164        magic.copy_from_slice(&bytes[0..8]);
165
166        let num_vectors = u64::from_le_bytes(
167            bytes[8..16]
168                .try_into()
169                .expect("bytes[8..16] is exactly 8 bytes after bounds check"),
170        );
171        let dimension = u64::from_le_bytes(
172            bytes[16..24]
173                .try_into()
174                .expect("bytes[16..24] is exactly 8 bytes after bounds check"),
175        );
176
177        Ok(Self {
178            magic,
179            num_vectors,
180            dimension,
181        })
182    }
183}
184
185/// DiskANN index for large-scale vector search
186pub struct DiskANNIndex {
187    /// Configuration
188    config: DiskANNConfig,
189    /// Index file path
190    index_path: Arc<RwLock<Option<String>>>,
191    /// Memory-mapped graph data
192    graph_mmap: Arc<RwLock<Option<MmapMut>>>,
193    /// Memory-mapped vector data (true disk-based storage)
194    vector_mmap: Arc<RwLock<Option<MmapMut>>>,
195    /// Vector file path
196    vector_file_path: Arc<RwLock<Option<String>>>,
197    /// In-memory CID mapping (relatively small)
198    id_to_cid: Arc<RwLock<HashMap<usize, Cid>>>,
199    cid_to_id: Arc<RwLock<HashMap<Cid, usize>>>,
200    /// In-memory graph (adjacency list)
201    graph: Arc<RwLock<Vec<Vec<usize>>>>,
202    /// Entry points for search
203    entry_points: Arc<RwLock<Vec<usize>>>,
204    /// Next available ID
205    next_id: Arc<RwLock<usize>>,
206    /// Whether index is loaded
207    loaded: Arc<RwLock<bool>>,
208}
209
210impl DiskANNIndex {
211    /// Create a new DiskANN index
212    pub fn new(config: DiskANNConfig) -> Self {
213        Self {
214            config,
215            index_path: Arc::new(RwLock::new(None)),
216            graph_mmap: Arc::new(RwLock::new(None)),
217            vector_mmap: Arc::new(RwLock::new(None)),
218            vector_file_path: Arc::new(RwLock::new(None)),
219            id_to_cid: Arc::new(RwLock::new(HashMap::new())),
220            cid_to_id: Arc::new(RwLock::new(HashMap::new())),
221            graph: Arc::new(RwLock::new(Vec::new())),
222            entry_points: Arc::new(RwLock::new(Vec::new())),
223            next_id: Arc::new(RwLock::new(0)),
224            loaded: Arc::new(RwLock::new(false)),
225        }
226    }
227
228    /// Helper: Get vector file path from index path
229    fn get_vector_file_path(index_path: &str) -> String {
230        format!("{}.vectors", index_path)
231    }
232
233    /// Helper: Calculate byte offset for a vector in the mmap file
234    fn vector_offset(&self, vector_id: usize) -> usize {
235        VectorFileHeader::SIZE + (vector_id * self.config.dimension * std::mem::size_of::<f32>())
236    }
237
238    /// Helper: Read a vector from the memory-mapped file
239    fn read_vector(&self, vector_id: usize) -> Result<Vec<f32>> {
240        let mmap = self.vector_mmap.read().unwrap_or_else(|e| e.into_inner());
241        let mmap = mmap
242            .as_ref()
243            .ok_or_else(|| Error::InvalidInput("Vector file not mapped".to_string()))?;
244
245        let offset = self.vector_offset(vector_id);
246        let vec_size_bytes = self.config.dimension * std::mem::size_of::<f32>();
247
248        if offset + vec_size_bytes > mmap.len() {
249            return Err(Error::InvalidInput(format!(
250                "Vector {} out of bounds",
251                vector_id
252            )));
253        }
254
255        let bytes = &mmap[offset..offset + vec_size_bytes];
256        let floats: Vec<f32> = bytes
257            .chunks_exact(4)
258            .map(|chunk| {
259                f32::from_le_bytes(
260                    chunk
261                        .try_into()
262                        .expect("chunks_exact(4) guarantees exactly 4 bytes"),
263                )
264            })
265            .collect();
266
267        Ok(floats)
268    }
269
270    /// Helper: Write a vector to the memory-mapped file
271    fn write_vector(&self, vector_id: usize, vector: &[f32]) -> Result<()> {
272        if vector.len() != self.config.dimension {
273            return Err(Error::InvalidInput(format!(
274                "Vector dimension {} doesn't match expected {}",
275                vector.len(),
276                self.config.dimension
277            )));
278        }
279
280        let mut mmap = self.vector_mmap.write().unwrap_or_else(|e| e.into_inner());
281        let mmap = mmap
282            .as_mut()
283            .ok_or_else(|| Error::InvalidInput("Vector file not mapped".to_string()))?;
284
285        let offset = self.vector_offset(vector_id);
286        let vec_size_bytes = self.config.dimension * std::mem::size_of::<f32>();
287
288        if offset + vec_size_bytes > mmap.len() {
289            return Err(Error::InvalidInput(format!(
290                "Vector {} out of bounds (mmap size: {}, needed: {})",
291                vector_id,
292                mmap.len(),
293                offset + vec_size_bytes
294            )));
295        }
296
297        let bytes = &mut mmap[offset..offset + vec_size_bytes];
298        for (i, &val) in vector.iter().enumerate() {
299            let val_bytes = val.to_le_bytes();
300            bytes[i * 4..(i + 1) * 4].copy_from_slice(&val_bytes);
301        }
302
303        Ok(())
304    }
305
306    /// Helper: Update the vector count in the header
307    fn update_vector_count(&self, count: usize) -> Result<()> {
308        let mut mmap = self.vector_mmap.write().unwrap_or_else(|e| e.into_inner());
309        let mmap = mmap
310            .as_mut()
311            .ok_or_else(|| Error::InvalidInput("Vector file not mapped".to_string()))?;
312
313        let count_bytes = (count as u64).to_le_bytes();
314        mmap[8..16].copy_from_slice(&count_bytes);
315
316        Ok(())
317    }
318
319    /// Helper: Get current vector count from mmap header
320    fn get_vector_count(&self) -> Result<usize> {
321        let mmap = self.vector_mmap.read().unwrap_or_else(|e| e.into_inner());
322        let mmap = mmap
323            .as_ref()
324            .ok_or_else(|| Error::InvalidInput("Vector file not mapped".to_string()))?;
325
326        let count_bytes: [u8; 8] = mmap[8..16]
327            .try_into()
328            .expect("mmap[8..16] is exactly 8 bytes; mmap size checked above");
329        Ok(u64::from_le_bytes(count_bytes) as usize)
330    }
331
332    /// Helper: Ensure vector file has capacity for n vectors (expand if needed)
333    fn ensure_vector_capacity(&self, required_count: usize) -> Result<()> {
334        let mmap = self.vector_mmap.read().unwrap_or_else(|e| e.into_inner());
335        let current_size = mmap
336            .as_ref()
337            .ok_or_else(|| Error::InvalidInput("Vector file not mapped".to_string()))?
338            .len();
339        drop(mmap);
340
341        let required_size = VectorFileHeader::SIZE
342            + (required_count * self.config.dimension * std::mem::size_of::<f32>());
343
344        if required_size > current_size {
345            // Need to expand the file
346            let new_capacity = (required_count * 2).max(required_count + 1000); // Double capacity or add 1000
347            let new_size = VectorFileHeader::SIZE
348                + (new_capacity * self.config.dimension * std::mem::size_of::<f32>());
349
350            // Get file path and reopen/remap
351            let vec_path = self
352                .vector_file_path
353                .read()
354                .unwrap_or_else(|e| e.into_inner())
355                .clone()
356                .ok_or_else(|| Error::InvalidInput("No vector file path set".to_string()))?;
357
358            // Drop the current mmap before resizing
359            *self.vector_mmap.write().unwrap_or_else(|e| e.into_inner()) = None;
360
361            // Resize the file
362            let vec_file = OpenOptions::new()
363                .read(true)
364                .write(true)
365                .open(&vec_path)
366                .map_err(Error::Io)?;
367            vec_file.set_len(new_size as u64).map_err(Error::Io)?;
368
369            // Remap
370            let new_mmap = unsafe {
371                MmapMut::map_mut(&vec_file)
372                    .map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?
373            };
374
375            *self.vector_mmap.write().unwrap_or_else(|e| e.into_inner()) = Some(new_mmap);
376        }
377
378        Ok(())
379    }
380
381    /// Helper: Get number of vectors (from mmap header or fallback to next_id)
382    fn num_vectors(&self) -> usize {
383        self.get_vector_count()
384            .unwrap_or_else(|_| *self.next_id.read().unwrap_or_else(|e| e.into_inner()))
385    }
386
387    /// Create with default configuration
388    pub fn with_defaults(dimension: usize) -> Self {
389        let config = DiskANNConfig {
390            dimension,
391            ..Default::default()
392        };
393        Self::new(config)
394    }
395
396    /// Create index file on disk
397    pub fn create(&mut self, path: impl AsRef<Path>) -> Result<()> {
398        let path = path.as_ref();
399        let path_str = path.to_string_lossy().to_string();
400
401        // Create index file
402        let file = OpenOptions::new()
403            .read(true)
404            .write(true)
405            .create(true)
406            .truncate(true)
407            .open(path)
408            .map_err(Error::Io)?;
409
410        // Write header
411        let header = IndexHeader::new(self.config.clone());
412        let header_bytes = oxicode::serde::encode_to_vec(&header, oxicode::config::standard())
413            .map_err(|e| Error::Serialization(e.to_string()))?;
414
415        // Initial file size: header + some space for growth
416        let initial_size = header_bytes.len() + 1024 * 1024; // 1MB initial
417        file.set_len(initial_size as u64).map_err(Error::Io)?;
418
419        // Memory-map the file
420        let mut mmap = unsafe {
421            MmapMut::map_mut(&file).map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?
422        };
423
424        // Write header to mmap
425        mmap[..header_bytes.len()].copy_from_slice(&header_bytes);
426
427        // Create vector file
428        let vec_path = Self::get_vector_file_path(&path_str);
429        let vec_file = OpenOptions::new()
430            .read(true)
431            .write(true)
432            .create(true)
433            .truncate(true)
434            .open(&vec_path)
435            .map_err(Error::Io)?;
436
437        // Initial vector file size: header + space for 1000 vectors
438        let vec_header = VectorFileHeader::new(self.config.dimension);
439        let initial_vec_count = 1000;
440        let vec_file_size = VectorFileHeader::SIZE
441            + (initial_vec_count * self.config.dimension * std::mem::size_of::<f32>());
442        vec_file.set_len(vec_file_size as u64).map_err(Error::Io)?;
443
444        // Memory-map the vector file
445        let mut vec_mmap = unsafe {
446            MmapMut::map_mut(&vec_file)
447                .map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?
448        };
449
450        // Write vector header
451        let header_bytes = vec_header.as_bytes();
452        vec_mmap[..VectorFileHeader::SIZE].copy_from_slice(&header_bytes);
453
454        *self.index_path.write().unwrap_or_else(|e| e.into_inner()) = Some(path_str.clone());
455        *self
456            .vector_file_path
457            .write()
458            .unwrap_or_else(|e| e.into_inner()) = Some(vec_path);
459        *self.graph_mmap.write().unwrap_or_else(|e| e.into_inner()) = Some(mmap);
460        *self.vector_mmap.write().unwrap_or_else(|e| e.into_inner()) = Some(vec_mmap);
461        *self.loaded.write().unwrap_or_else(|e| e.into_inner()) = true;
462
463        Ok(())
464    }
465
466    /// Load existing index from disk
467    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
468        let path = path.as_ref();
469
470        // Open index file
471        let file = OpenOptions::new()
472            .read(true)
473            .write(true)
474            .open(path)
475            .map_err(Error::Io)?;
476
477        // Memory-map the file
478        let mmap = unsafe {
479            MmapMut::map_mut(&file).map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?
480        };
481
482        // Read header
483        let header: IndexHeader =
484            oxicode::serde::decode_owned_from_slice(&mmap[..1024], oxicode::config::standard())
485                .map(|(v, _)| v)
486                .map_err(|e| Error::Serialization(e.to_string()))?;
487
488        header.validate()?;
489
490        // Create index
491        let index = Self::new(header.config);
492        *index.index_path.write().unwrap_or_else(|e| e.into_inner()) =
493            Some(path.to_string_lossy().to_string());
494        *index.graph_mmap.write().unwrap_or_else(|e| e.into_inner()) = Some(mmap);
495        *index.next_id.write().unwrap_or_else(|e| e.into_inner()) = header.num_vectors;
496        *index.loaded.write().unwrap_or_else(|e| e.into_inner()) = true;
497
498        Ok(index)
499    }
500
501    /// Insert a vector using Vamana algorithm
502    pub fn insert(&mut self, cid: &Cid, vector: &[f32]) -> Result<()> {
503        if !*self.loaded.read().unwrap_or_else(|e| e.into_inner()) {
504            return Err(Error::InvalidInput(
505                "Index not created or loaded".to_string(),
506            ));
507        }
508
509        if vector.len() != self.config.dimension {
510            return Err(Error::InvalidInput(format!(
511                "Vector dimension {} doesn't match index dimension {}",
512                vector.len(),
513                self.config.dimension
514            )));
515        }
516
517        // Check if CID already exists
518        if self
519            .cid_to_id
520            .read()
521            .unwrap_or_else(|e| e.into_inner())
522            .contains_key(cid)
523        {
524            return Err(Error::InvalidInput(format!(
525                "CID already in index: {}",
526                cid
527            )));
528        }
529
530        // Get new ID
531        let id = *self.next_id.read().unwrap_or_else(|e| e.into_inner());
532
533        // Ensure vector file has enough space (expand if needed)
534        self.ensure_vector_capacity(id + 1)?;
535
536        // Write vector to memory-mapped file
537        self.write_vector(id, vector)?;
538
539        // Update vector count and next ID
540        *self.next_id.write().unwrap_or_else(|e| e.into_inner()) += 1;
541        self.update_vector_count(id + 1)?;
542
543        // Add CID mapping
544        self.id_to_cid
545            .write()
546            .unwrap_or_else(|e| e.into_inner())
547            .insert(id, *cid);
548        self.cid_to_id
549            .write()
550            .unwrap_or_else(|e| e.into_inner())
551            .insert(*cid, id);
552
553        // Initialize graph node
554        self.graph
555            .write()
556            .unwrap_or_else(|e| e.into_inner())
557            .push(Vec::new());
558
559        // If this is the first vector, make it an entry point
560        if id == 0 {
561            self.entry_points
562                .write()
563                .unwrap_or_else(|e| e.into_inner())
564                .push(0);
565            return Ok(());
566        }
567
568        // Vamana graph construction
569        self.vamana_insert(id, vector)?;
570
571        // Update entry points if needed
572        if id.is_multiple_of(1000) && id < 10000 {
573            self.entry_points
574                .write()
575                .unwrap_or_else(|e| e.into_inner())
576                .push(id);
577            // Keep only num_entry_points
578            let mut eps = self.entry_points.write().unwrap_or_else(|e| e.into_inner());
579            let num_to_drain = if eps.len() > self.config.num_entry_points {
580                eps.len() - self.config.num_entry_points
581            } else {
582                0
583            };
584            if num_to_drain > 0 {
585                eps.drain(0..num_to_drain);
586            }
587        }
588
589        Ok(())
590    }
591
592    /// Vamana graph construction for a new node
593    fn vamana_insert(&self, new_id: usize, new_vec: &[f32]) -> Result<()> {
594        // 1. Greedy search to find L nearest neighbors
595        let neighbors =
596            self.greedy_search_internal(new_vec, self.config.queue_size, self.config.queue_size)?;
597
598        // 2. Prune to R neighbors using robust pruning
599        let pruned = self.robust_prune(new_id, new_vec, &neighbors)?;
600
601        // 3. Add bidirectional edges
602        let mut graph = self.graph.write().unwrap_or_else(|e| e.into_inner());
603        graph[new_id] = pruned.clone();
604
605        // Add reverse edges and prune if needed
606        for &neighbor_id in &pruned {
607            if neighbor_id >= graph.len() {
608                continue;
609            }
610
611            // Add reverse edge
612            if !graph[neighbor_id].contains(&new_id) {
613                graph[neighbor_id].push(new_id);
614
615                // Prune if neighbor exceeds max degree
616                if graph[neighbor_id].len() > self.config.max_degree {
617                    let neighbor_vec = self.read_vector(neighbor_id)?;
618                    let candidates = graph[neighbor_id].clone();
619
620                    let pruned_neighbors =
621                        self.robust_prune(neighbor_id, &neighbor_vec, &candidates)?;
622                    graph[neighbor_id] = pruned_neighbors;
623                }
624            }
625        }
626
627        Ok(())
628    }
629
630    /// Robust pruning algorithm (RobustPrune from Vamana paper)
631    fn robust_prune(
632        &self,
633        node_id: usize,
634        node_vec: &[f32],
635        candidates: &[usize],
636    ) -> Result<Vec<usize>> {
637        let alpha = self.config.alpha;
638        let max_degree = self.config.max_degree;
639        let num_vecs = self.num_vectors();
640
641        // Compute distances from node to all candidates
642        let mut dists: Vec<(usize, f32)> = candidates
643            .iter()
644            .filter(|&&c| c != node_id && c < num_vecs)
645            .filter_map(|&c| {
646                self.read_vector(c).ok().map(|vec| {
647                    let dist = self.l2_distance(node_vec, &vec);
648                    (c, dist)
649                })
650            })
651            .collect();
652
653        // Sort by distance
654        dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
655
656        let mut pruned = Vec::new();
657
658        for (cand_id, cand_dist) in dists {
659            if pruned.len() >= max_degree {
660                break;
661            }
662
663            // Check if candidate is alpha-close to any already selected neighbor
664            let mut should_add = true;
665            let cand_vec = self.read_vector(cand_id).ok();
666            if let Some(ref c_vec) = cand_vec {
667                for &selected_id in &pruned {
668                    if let Ok(sel_vec) = self.read_vector(selected_id) {
669                        let selected_dist = self.l2_distance(c_vec, &sel_vec);
670                        if alpha * selected_dist < cand_dist {
671                            should_add = false;
672                            break;
673                        }
674                    }
675                }
676            } else {
677                should_add = false;
678            }
679
680            if should_add {
681                pruned.push(cand_id);
682            }
683        }
684
685        Ok(pruned)
686    }
687
688    /// L2 distance between two vectors
689    fn l2_distance<T: AsRef<[f32]>, U: AsRef<[f32]>>(&self, a: T, b: U) -> f32 {
690        a.as_ref()
691            .iter()
692            .zip(b.as_ref().iter())
693            .map(|(x, y)| (x - y) * (x - y))
694            .sum::<f32>()
695            .sqrt()
696    }
697
698    /// Search for k nearest neighbors using greedy search
699    pub fn search(&self, query: &[f32], k: usize) -> Result<Vec<SearchResult>> {
700        if !*self.loaded.read().unwrap_or_else(|e| e.into_inner()) {
701            return Err(Error::InvalidInput(
702                "Index not created or loaded".to_string(),
703            ));
704        }
705
706        if query.len() != self.config.dimension {
707            return Err(Error::InvalidInput(format!(
708                "Query dimension {} doesn't match index dimension {}",
709                query.len(),
710                self.config.dimension
711            )));
712        }
713
714        let num_vectors = self.num_vectors();
715        if num_vectors == 0 {
716            return Ok(Vec::new());
717        }
718
719        // Greedy search with L = max(k, queue_size)
720        let search_list_size = k.max(self.config.queue_size);
721        let result_ids = self.greedy_search_internal(query, k, search_list_size)?;
722
723        // Convert to SearchResult with CIDs
724        let id_to_cid = self.id_to_cid.read().unwrap_or_else(|e| e.into_inner());
725        let results: Vec<SearchResult> = result_ids
726            .iter()
727            .filter_map(|&id| {
728                id_to_cid.get(&id).and_then(|cid| {
729                    self.read_vector(id).ok().map(|vec| SearchResult {
730                        cid: *cid,
731                        distance: self.l2_distance(query, &vec),
732                    })
733                })
734            })
735            .collect();
736
737        Ok(results)
738    }
739
740    /// Internal greedy search returning node IDs
741    fn greedy_search_internal(
742        &self,
743        query: &[f32],
744        k: usize,
745        search_list_size: usize,
746    ) -> Result<Vec<usize>> {
747        let graph = self.graph.read().unwrap_or_else(|e| e.into_inner());
748        let entry_points = self.entry_points.read().unwrap_or_else(|e| e.into_inner());
749        let num_vecs = self.num_vectors();
750
751        if num_vecs == 0 {
752            return Ok(Vec::new());
753        }
754
755        // Start from entry points
756        let start_nodes: Vec<usize> = if entry_points.is_empty() {
757            vec![0]
758        } else {
759            entry_points.clone()
760        };
761
762        // Visited set
763        let mut visited = vec![false; num_vecs];
764
765        // Priority queue: (distance, node_id)
766        let mut candidates: Vec<(f32, usize)> = Vec::new();
767        let mut results: Vec<(f32, usize)> = Vec::new();
768
769        // Initialize with entry points
770        for &node_id in &start_nodes {
771            if node_id >= num_vecs {
772                continue;
773            }
774            if let Ok(vec) = self.read_vector(node_id) {
775                let dist = self.l2_distance(query, &vec);
776                candidates.push((dist, node_id));
777                results.push((dist, node_id));
778                visited[node_id] = true;
779            }
780        }
781
782        // Sort by distance (ascending)
783        candidates.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
784        results.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
785
786        // Greedy search
787        while !candidates.is_empty() {
788            // Get closest unvisited neighbor
789            let (current_dist, current_id) = candidates.remove(0);
790
791            // Stop if current is farther than the k-th result
792            if results.len() >= search_list_size {
793                let furthest_dist = results[search_list_size - 1].0;
794                if current_dist > furthest_dist {
795                    break;
796                }
797            }
798
799            // Explore neighbors
800            if current_id >= graph.len() {
801                continue;
802            }
803
804            for &neighbor_id in &graph[current_id] {
805                if neighbor_id >= num_vecs || visited[neighbor_id] {
806                    continue;
807                }
808
809                visited[neighbor_id] = true;
810                let dist = if let Ok(vec) = self.read_vector(neighbor_id) {
811                    self.l2_distance(query, &vec)
812                } else {
813                    continue;
814                };
815
816                // Add to candidates
817                candidates.push((dist, neighbor_id));
818                candidates
819                    .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
820
821                // Add to results
822                results.push((dist, neighbor_id));
823                results.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
824
825                // Keep only search_list_size best results
826                if results.len() > search_list_size {
827                    results.truncate(search_list_size);
828                }
829            }
830        }
831
832        // Return top k
833        Ok(results.iter().take(k).map(|(_, id)| *id).collect())
834    }
835
836    /// Get index statistics
837    pub fn stats(&self) -> DiskANNStats {
838        DiskANNStats {
839            num_vectors: *self.next_id.read().unwrap_or_else(|e| e.into_inner()),
840            dimension: self.config.dimension,
841            max_degree: self.config.max_degree,
842            index_loaded: *self.loaded.read().unwrap_or_else(|e| e.into_inner()),
843            estimated_disk_size: self.estimate_disk_size(),
844        }
845    }
846
847    /// Estimate disk usage
848    fn estimate_disk_size(&self) -> usize {
849        let num_vectors = *self.next_id.read().unwrap_or_else(|e| e.into_inner());
850
851        // Header: ~1KB
852        let header_size = 1024;
853
854        // Vectors: num_vectors * dimension * 4 bytes
855        let vector_size = num_vectors * self.config.dimension * 4;
856
857        // Graph: num_vectors * max_degree * 4 bytes (assuming u32 node IDs)
858        let graph_size = num_vectors * self.config.max_degree * 4;
859
860        // CID mapping: num_vectors * ~40 bytes (CID size)
861        let mapping_size = num_vectors * 40;
862
863        header_size + vector_size + graph_size + mapping_size
864    }
865
866    /// Check if index is loaded
867    pub fn is_loaded(&self) -> bool {
868        *self.loaded.read().unwrap_or_else(|e| e.into_inner())
869    }
870
871    /// Get configuration
872    pub fn config(&self) -> &DiskANNConfig {
873        &self.config
874    }
875
876    /// Save index to disk (persist all in-memory data)
877    pub fn save(&self) -> Result<()> {
878        if !*self.loaded.read().unwrap_or_else(|e| e.into_inner()) {
879            return Err(Error::InvalidInput("Index not loaded".to_string()));
880        }
881
882        let path = self
883            .index_path
884            .read()
885            .unwrap_or_else(|e| e.into_inner())
886            .clone()
887            .ok_or_else(|| Error::InvalidInput("No index path set".to_string()))?;
888
889        // Serialize all data (read all vectors from mmap)
890        let num_vecs = self.num_vectors();
891        let mut vectors = Vec::with_capacity(num_vecs);
892        for i in 0..num_vecs {
893            if let Ok(vec) = self.read_vector(i) {
894                vectors.push(vec);
895            }
896        }
897
898        let graph = self.graph.read().unwrap_or_else(|e| e.into_inner());
899        let id_to_cid = self.id_to_cid.read().unwrap_or_else(|e| e.into_inner());
900        let entry_points = self.entry_points.read().unwrap_or_else(|e| e.into_inner());
901
902        let data = DiskANNData::from_index(
903            vectors,
904            graph.clone(),
905            id_to_cid.clone(),
906            entry_points.clone(),
907        );
908
909        // Serialize to file
910        let serialized = oxicode::serde::encode_to_vec(&data, oxicode::config::standard())
911            .map_err(|e| Error::Serialization(e.to_string()))?;
912
913        // Write to a temp file first, then rename (atomic)
914        let temp_path = format!("{}.tmp", path);
915        std::fs::write(&temp_path, &serialized).map_err(Error::Io)?;
916        std::fs::rename(&temp_path, &path).map_err(Error::Io)?;
917
918        Ok(())
919    }
920
921    /// Flush changes to disk
922    pub fn flush(&self) -> Result<()> {
923        if let Some(ref mut mmap) = *self.graph_mmap.write().unwrap_or_else(|e| e.into_inner()) {
924            mmap.flush()
925                .map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?;
926        }
927        Ok(())
928    }
929
930    /// Compact the index by removing fragmentation
931    ///
932    /// This method:
933    /// - Removes gaps in the ID space
934    /// - Rebuilds the graph with contiguous IDs
935    /// - Optimizes memory layout
936    pub fn compact(&mut self) -> Result<CompactionStats> {
937        if !*self.loaded.read().unwrap_or_else(|e| e.into_inner()) {
938            return Err(Error::InvalidInput("Index not loaded".to_string()));
939        }
940
941        let start_time = std::time::Instant::now();
942        let old_size = self.num_vectors();
943        let graph = self.graph.read().unwrap_or_else(|e| e.into_inner());
944
945        let old_graph_edges: usize = graph.iter().map(|neighbors| neighbors.len()).sum();
946
947        // For now, just report stats since we don't have fragmentation yet
948        // In a real implementation, we'd rebuild with contiguous IDs
949        let stats = CompactionStats {
950            duration_ms: start_time.elapsed().as_millis() as u64,
951            vectors_before: old_size,
952            vectors_after: old_size,
953            graph_edges_before: old_graph_edges,
954            graph_edges_after: old_graph_edges,
955            bytes_saved: 0,
956        };
957
958        Ok(stats)
959    }
960
961    /// Prune the graph to remove low-quality edges
962    ///
963    /// This helps reduce memory usage and can improve query performance
964    /// by removing edges that don't contribute to search quality.
965    pub fn prune_graph(&mut self, quality_threshold: f32) -> Result<usize> {
966        if !*self.loaded.read().unwrap_or_else(|e| e.into_inner()) {
967            return Err(Error::InvalidInput("Index not loaded".to_string()));
968        }
969
970        let mut graph = self.graph.write().unwrap_or_else(|e| e.into_inner());
971        let num_vecs = self.num_vectors();
972        let mut total_pruned = 0;
973
974        for node_id in 0..graph.len() {
975            if node_id >= num_vecs {
976                continue;
977            }
978
979            let node_vec = match self.read_vector(node_id) {
980                Ok(v) => v,
981                Err(_) => continue,
982            };
983            let neighbors = &graph[node_id];
984
985            // Compute distances to all neighbors
986            let mut neighbor_dists: Vec<(usize, f32)> = neighbors
987                .iter()
988                .filter(|&&n| n < num_vecs)
989                .filter_map(|&n| {
990                    self.read_vector(n).ok().map(|vec| {
991                        let dist = self.l2_distance(&node_vec, &vec);
992                        (n, dist)
993                    })
994                })
995                .collect();
996
997            // Sort by distance
998            neighbor_dists
999                .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1000
1001            // Keep only neighbors within quality threshold of the best
1002            if let Some(&(_, best_dist)) = neighbor_dists.first() {
1003                let threshold_dist = best_dist * (1.0 + quality_threshold);
1004                let keep_count = neighbor_dists
1005                    .iter()
1006                    .filter(|(_, d)| *d <= threshold_dist)
1007                    .count();
1008
1009                if keep_count < neighbors.len() {
1010                    total_pruned += neighbors.len() - keep_count;
1011                    graph[node_id] = neighbor_dists
1012                        .iter()
1013                        .take(keep_count)
1014                        .map(|(n, _)| *n)
1015                        .collect();
1016                }
1017            }
1018        }
1019
1020        Ok(total_pruned)
1021    }
1022
1023    /// Get number of vectors in the index
1024    pub fn len(&self) -> usize {
1025        *self.next_id.read().unwrap_or_else(|e| e.into_inner())
1026    }
1027
1028    /// Check if index is empty
1029    pub fn is_empty(&self) -> bool {
1030        self.len() == 0
1031    }
1032}
1033
1034/// Data stored in DiskANN index file (serializable version)
1035#[derive(Debug, Clone, Serialize, Deserialize)]
1036struct DiskANNData {
1037    vectors: Vec<Vec<f32>>,
1038    graph: Vec<Vec<usize>>,
1039    id_to_cid: HashMap<usize, String>,
1040    entry_points: Vec<usize>,
1041}
1042
1043impl DiskANNData {
1044    fn from_index(
1045        vectors: Vec<Vec<f32>>,
1046        graph: Vec<Vec<usize>>,
1047        id_to_cid: HashMap<usize, Cid>,
1048        entry_points: Vec<usize>,
1049    ) -> Self {
1050        let id_to_cid_str = id_to_cid
1051            .into_iter()
1052            .map(|(k, v)| (k, v.to_string()))
1053            .collect();
1054        Self {
1055            vectors,
1056            graph,
1057            id_to_cid: id_to_cid_str,
1058            entry_points,
1059        }
1060    }
1061
1062    #[allow(dead_code)]
1063    fn to_cid_map(&self) -> Result<HashMap<usize, Cid>> {
1064        self.id_to_cid
1065            .iter()
1066            .map(|(k, v)| {
1067                v.parse::<Cid>()
1068                    .map(|cid| (*k, cid))
1069                    .map_err(|e| Error::InvalidInput(format!("Invalid CID: {}", e)))
1070            })
1071            .collect()
1072    }
1073}
1074
1075/// Statistics from index compaction
1076#[derive(Debug, Clone)]
1077pub struct CompactionStats {
1078    /// Time taken for compaction
1079    pub duration_ms: u64,
1080    /// Number of vectors before compaction
1081    pub vectors_before: usize,
1082    /// Number of vectors after compaction
1083    pub vectors_after: usize,
1084    /// Number of graph edges before
1085    pub graph_edges_before: usize,
1086    /// Number of graph edges after
1087    pub graph_edges_after: usize,
1088    /// Bytes saved by compaction
1089    pub bytes_saved: usize,
1090}
1091
1092/// Search result from DiskANN
1093#[derive(Debug, Clone)]
1094pub struct SearchResult {
1095    /// Content ID
1096    pub cid: Cid,
1097    /// Distance to query
1098    pub distance: f32,
1099}
1100
1101/// DiskANN index statistics
1102#[derive(Debug, Clone)]
1103pub struct DiskANNStats {
1104    /// Number of vectors in index
1105    pub num_vectors: usize,
1106    /// Vector dimension
1107    pub dimension: usize,
1108    /// Maximum graph degree
1109    pub max_degree: usize,
1110    /// Whether index is loaded
1111    pub index_loaded: bool,
1112    /// Estimated disk size in bytes
1113    pub estimated_disk_size: usize,
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118    use super::*;
1119
1120    #[test]
1121    fn test_diskann_create() {
1122        let config = DiskANNConfig::default();
1123        let mut index = DiskANNIndex::new(config);
1124
1125        let temp_file_path = std::env::temp_dir().join("test_diskann_index.dat");
1126        let temp_file = temp_file_path
1127            .to_str()
1128            .expect("temp dir path is valid UTF-8");
1129        assert!(index.create(temp_file).is_ok());
1130        assert!(index.is_loaded());
1131
1132        // Cleanup
1133        std::fs::remove_file(temp_file).ok();
1134    }
1135
1136    #[test]
1137    fn test_diskann_stats() {
1138        let index = DiskANNIndex::with_defaults(128);
1139        let stats = index.stats();
1140
1141        assert_eq!(stats.dimension, 128);
1142        assert_eq!(stats.num_vectors, 0);
1143        assert!(!stats.index_loaded);
1144    }
1145
1146    #[test]
1147    fn test_index_header() {
1148        let config = DiskANNConfig::default();
1149        let header = IndexHeader::new(config);
1150
1151        assert_eq!(header.magic, IndexHeader::MAGIC);
1152        assert_eq!(header.version, 1);
1153        assert!(header.validate().is_ok());
1154
1155        // Test invalid magic
1156        let mut bad_header = header.clone();
1157        bad_header.magic = [0; 8];
1158        assert!(bad_header.validate().is_err());
1159    }
1160
1161    #[test]
1162    fn test_diskann_insert_and_search() {
1163        let config = DiskANNConfig {
1164            dimension: 4,
1165            max_degree: 16,
1166            queue_size: 50,
1167            ..Default::default()
1168        };
1169
1170        let mut index = DiskANNIndex::new(config);
1171        let temp_file_path = std::env::temp_dir().join("test_diskann_vamana.dat");
1172        let temp_file = temp_file_path
1173            .to_str()
1174            .expect("temp dir path is valid UTF-8");
1175        index
1176            .create(temp_file)
1177            .expect("test: index creation should succeed");
1178
1179        // Create test vectors
1180        let vectors = [
1181            vec![1.0, 0.0, 0.0, 0.0],
1182            vec![0.9, 0.1, 0.0, 0.0],
1183            vec![0.0, 1.0, 0.0, 0.0],
1184            vec![0.0, 0.0, 1.0, 0.0],
1185            vec![0.0, 0.0, 0.9, 0.1],
1186        ];
1187
1188        // Insert vectors
1189        let base_cids = [
1190            "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
1191            "bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354",
1192            "bafybeihvvulpp6bcs5kum72jh5tkfo35dz2ow3lrqw4hmqyqbmfyvdqvdq",
1193            "bafybeiakou6e7kkxc5qycjkqwucq4zfkfvzmlbf2vlihvqqnfjfzpqrkmq",
1194            "bafybeibscyh5z3uk6fvdidffhybzsxmckblkjhajy4y4uzcglmfwqx67b4",
1195        ];
1196        for (i, vec) in vectors.iter().enumerate() {
1197            let cid: Cid = base_cids[i].parse().expect("test: CID string is valid");
1198            index
1199                .insert(&cid, vec)
1200                .expect("test: vector insertion should succeed");
1201        }
1202
1203        assert_eq!(index.stats().num_vectors, 5);
1204
1205        // Search for nearest to first vector
1206        let query = vec![1.0, 0.0, 0.0, 0.0];
1207        let results = index
1208            .search(&query, 2)
1209            .expect("test: search should succeed");
1210
1211        assert!(!results.is_empty());
1212        assert!(results.len() <= 2);
1213        // First result should be closest
1214        assert!(results[0].distance < 0.2);
1215
1216        // Cleanup
1217        std::fs::remove_file(temp_file).ok();
1218    }
1219
1220    #[test]
1221    fn test_vamana_graph_construction() {
1222        let config = DiskANNConfig {
1223            dimension: 8,
1224            max_degree: 8,
1225            queue_size: 20,
1226            alpha: 1.2,
1227            ..Default::default()
1228        };
1229
1230        let max_degree = config.max_degree;
1231        let mut index = DiskANNIndex::new(config);
1232        let temp_file_path = std::env::temp_dir().join("test_vamana_graph.dat");
1233        let temp_file = temp_file_path
1234            .to_str()
1235            .expect("temp dir path is valid UTF-8");
1236        index
1237            .create(temp_file)
1238            .expect("test: index creation should succeed");
1239
1240        // Insert 20 vectors
1241        let base_cids: Vec<&str> = vec![
1242            "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
1243            "bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354",
1244            "bafybeihvvulpp6bcs5kum72jh5tkfo35dz2ow3lrqw4hmqyqbmfyvdqvdq",
1245            "bafybeiakou6e7kkxc5qycjkqwucq4zfkfvzmlbf2vlihvqqnfjfzpqrkmq",
1246            "bafybeibscyh5z3uk6fvdidffhybzsxmckblkjhajy4y4uzcglmfwqx67b4",
1247            "bafybeiezkzpo2uy4teyix63fjc3vgpxlvhbmwjicxhxx6vaf3ywvkyz5ia",
1248            "bafybeifmyetvpv2uovt7ncnvjcwvshwqrr7zmyh5wpqwmf5mwy3m42xkre",
1249            "bafybeia7lv6vknr6fqjq2jlj3ygbdgzdqxqt7xo3u7dzz6ihfzd3zhd6pi",
1250            "bafybeif2ewg3nqa33yvecifp7jw7p2utbnkh34j7ku44mzs3lpmcbdkjzq",
1251            "bafybeid5cg74fzlh7okcaabfwexdvkiuocwbqhwrqc4x65jyplwsxzvvdq",
1252            "bafybeicy6rxfqlcdadwjfjjvvb7wlbnlrzuzsogpv5snwt46zpqrmihtnq",
1253            "bafybeie2kj53f4wmefncg3rvrvfegwk265iw2psfszftvq3slajlwkjfpm",
1254            "bafybeigk7gjp4y4m4gwvmblvf7mlufsqtfgwyjdqwvwudytucvx7wtnz4e",
1255            "bafybeihbsq7kdawlkzvfj7xttx27t4p52pkllmfevn5l2scgbvmgqcfmfy",
1256            "bafybeiej5vfvbkjbzyeouqxkn25yb2xzdz2igdwmawcbhv66kwfwqnvhzi",
1257            "bafybeigbkbpcxqbrvx56fqf7jb25r5wunzowl45uwmzcbxkwdtixlbtwim",
1258            "bafybeihyfvtf3uiilqvqsvhbphfdudqy7qrjkxqglh26xxvjhtxrkhhbxe",
1259            "bafybeicflzm3r35m4kj5chxjvdwgajq6ljhqpsjq6wdyqnlpfjwwb5nowi",
1260            "bafybeic73hjrp52jxz33zxlz5qthfxumqpyuvqfvawdcskqiqlpuww3vxi",
1261            "bafybeicbh5dkdyiq3gqufk46cktiwwucwl6mzhv6e5xhzmuvzojvykokpy",
1262        ];
1263        for (i, &cid_str) in base_cids.iter().enumerate() {
1264            let cid: Cid = cid_str.parse().expect("test: CID string is valid");
1265            let vec: Vec<f32> = (0..8).map(|j| (i as f32 + j as f32) * 0.1).collect();
1266            index
1267                .insert(&cid, &vec)
1268                .expect("test: vector insertion should succeed");
1269        }
1270
1271        // Check graph structure
1272        let graph = index.graph.read().unwrap_or_else(|e| e.into_inner());
1273        assert_eq!(graph.len(), 20);
1274
1275        // Each node (except possibly the first) should have some neighbors
1276        for (i, neighbors) in graph.iter().enumerate().skip(1) {
1277            if i < 19 {
1278                // Not the last node
1279                assert!(!neighbors.is_empty(), "Node {} should have neighbors", i);
1280                assert!(
1281                    neighbors.len() <= max_degree,
1282                    "Node {} has too many neighbors: {}",
1283                    i,
1284                    neighbors.len()
1285                );
1286            }
1287        }
1288
1289        // Cleanup
1290        std::fs::remove_file(temp_file).ok();
1291    }
1292
1293    #[test]
1294    fn test_robust_pruning() {
1295        let config = DiskANNConfig {
1296            dimension: 4,
1297            max_degree: 3,
1298            alpha: 1.2,
1299            ..Default::default()
1300        };
1301
1302        let max_degree = config.max_degree;
1303        let mut index = DiskANNIndex::new(config);
1304        let temp_file_path = std::env::temp_dir().join("test_robust_prune.dat");
1305        let temp_file = temp_file_path
1306            .to_str()
1307            .expect("temp dir path is valid UTF-8");
1308        index
1309            .create(temp_file)
1310            .expect("test: index creation should succeed");
1311
1312        // Add some vectors manually (write to mmap)
1313        index
1314            .ensure_vector_capacity(4)
1315            .expect("test: capacity expansion for 4 vectors should succeed");
1316        index
1317            .write_vector(0, &[1.0, 0.0, 0.0, 0.0])
1318            .expect("test: writing vector 0 should succeed");
1319        index
1320            .write_vector(1, &[0.9, 0.1, 0.0, 0.0])
1321            .expect("test: writing vector 1 should succeed");
1322        index
1323            .write_vector(2, &[0.8, 0.2, 0.0, 0.0])
1324            .expect("test: writing vector 2 should succeed");
1325        index
1326            .write_vector(3, &[0.0, 1.0, 0.0, 0.0])
1327            .expect("test: writing vector 3 should succeed");
1328        index
1329            .update_vector_count(4)
1330            .expect("test: updating vector count should succeed");
1331
1332        let node_vec = vec![1.0, 0.0, 0.0, 0.0];
1333        let candidates = vec![1, 2, 3];
1334
1335        let pruned = index
1336            .robust_prune(0, &node_vec, &candidates)
1337            .expect("test: robust_prune should succeed");
1338
1339        // Should prune to max_degree neighbors
1340        assert!(pruned.len() <= max_degree);
1341        // Should include the closest neighbor
1342        assert!(pruned.contains(&1));
1343
1344        // Cleanup
1345        std::fs::remove_file(temp_file).ok();
1346    }
1347
1348    #[test]
1349    fn test_diskann_save_and_load() {
1350        let config = DiskANNConfig {
1351            dimension: 4,
1352            max_degree: 16,
1353            ..Default::default()
1354        };
1355
1356        let mut index = DiskANNIndex::new(config);
1357        let temp_file_path = std::env::temp_dir().join("test_diskann_save.dat");
1358        let temp_file = temp_file_path
1359            .to_str()
1360            .expect("temp dir path is valid UTF-8");
1361        index
1362            .create(temp_file)
1363            .expect("test: index creation should succeed");
1364
1365        // Insert some vectors
1366        let vectors = [
1367            vec![1.0, 0.0, 0.0, 0.0],
1368            vec![0.0, 1.0, 0.0, 0.0],
1369            vec![0.0, 0.0, 1.0, 0.0],
1370        ];
1371
1372        let base_cids = [
1373            "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
1374            "bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354",
1375            "bafybeihvvulpp6bcs5kum72jh5tkfo35dz2ow3lrqw4hmqyqbmfyvdqvdq",
1376        ];
1377
1378        for (i, vec) in vectors.iter().enumerate() {
1379            let cid: Cid = base_cids[i].parse().expect("test: CID string is valid");
1380            index
1381                .insert(&cid, vec)
1382                .expect("test: vector insertion should succeed");
1383        }
1384
1385        // Save the index
1386        assert!(index.save().is_ok());
1387
1388        // The save method overwrites the file, so we can't really test loading
1389        // without a proper load implementation that deserializes DiskANNData
1390        // For now, just verify save doesn't error
1391
1392        // Cleanup
1393        std::fs::remove_file(temp_file).ok();
1394    }
1395
1396    #[test]
1397    fn test_diskann_flush() {
1398        let config = DiskANNConfig {
1399            dimension: 4,
1400            ..Default::default()
1401        };
1402
1403        let mut index = DiskANNIndex::new(config);
1404        let temp_file_path = std::env::temp_dir().join("test_diskann_flush.dat");
1405        let temp_file = temp_file_path
1406            .to_str()
1407            .expect("temp dir path is valid UTF-8");
1408        index
1409            .create(temp_file)
1410            .expect("test: index creation should succeed");
1411
1412        // Flush should succeed
1413        assert!(index.flush().is_ok());
1414
1415        // Cleanup
1416        std::fs::remove_file(temp_file).ok();
1417    }
1418
1419    #[test]
1420    fn test_diskann_compact() {
1421        let config = DiskANNConfig {
1422            dimension: 4,
1423            max_degree: 16,
1424            ..Default::default()
1425        };
1426
1427        let mut index = DiskANNIndex::new(config);
1428        let temp_file_path = std::env::temp_dir().join("test_diskann_compact.dat");
1429        let temp_file = temp_file_path
1430            .to_str()
1431            .expect("temp dir path is valid UTF-8");
1432        index
1433            .create(temp_file)
1434            .expect("test: index creation should succeed");
1435
1436        // Insert some vectors
1437        let vectors = [
1438            vec![1.0, 0.0, 0.0, 0.0],
1439            vec![0.0, 1.0, 0.0, 0.0],
1440            vec![0.0, 0.0, 1.0, 0.0],
1441        ];
1442
1443        let base_cids = [
1444            "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
1445            "bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354",
1446            "bafybeihvvulpp6bcs5kum72jh5tkfo35dz2ow3lrqw4hmqyqbmfyvdqvdq",
1447        ];
1448
1449        for (i, vec) in vectors.iter().enumerate() {
1450            let cid: Cid = base_cids[i].parse().expect("test: CID string is valid");
1451            index
1452                .insert(&cid, vec)
1453                .expect("test: vector insertion should succeed");
1454        }
1455
1456        // Compact the index
1457        let stats = index.compact().expect("test: compact should succeed");
1458        assert_eq!(stats.vectors_before, 3);
1459        assert_eq!(stats.vectors_after, 3);
1460
1461        // Cleanup
1462        std::fs::remove_file(temp_file).ok();
1463    }
1464
1465    #[test]
1466    fn test_diskann_prune_graph() {
1467        let config = DiskANNConfig {
1468            dimension: 4,
1469            max_degree: 16,
1470            ..Default::default()
1471        };
1472
1473        let mut index = DiskANNIndex::new(config);
1474        let temp_file_path = std::env::temp_dir().join("test_diskann_prune.dat");
1475        let temp_file = temp_file_path
1476            .to_str()
1477            .expect("temp dir path is valid UTF-8");
1478        index
1479            .create(temp_file)
1480            .expect("test: index creation should succeed");
1481
1482        // Insert some vectors
1483        let vectors = [
1484            vec![1.0, 0.0, 0.0, 0.0],
1485            vec![0.9, 0.1, 0.0, 0.0],
1486            vec![0.8, 0.2, 0.0, 0.0],
1487            vec![0.0, 0.0, 1.0, 0.0],
1488        ];
1489
1490        let base_cids = [
1491            "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
1492            "bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354",
1493            "bafybeihvvulpp6bcs5kum72jh5tkfo35dz2ow3lrqw4hmqyqbmfyvdqvdq",
1494            "bafybeiakou6e7kkxc5qycjkqwucq4zfkfvzmlbf2vlihvqqnfjfzpqrkmq",
1495        ];
1496
1497        for (i, vec) in vectors.iter().enumerate() {
1498            let cid: Cid = base_cids[i].parse().expect("test: CID string is valid");
1499            index
1500                .insert(&cid, vec)
1501                .expect("test: vector insertion should succeed");
1502        }
1503
1504        // Prune with a quality threshold
1505        let _pruned = index
1506            .prune_graph(0.5)
1507            .expect("test: prune_graph should succeed");
1508        // Should prune some edges (pruned is usize, always >= 0)
1509
1510        // Cleanup
1511        std::fs::remove_file(temp_file).ok();
1512    }
1513
1514    #[test]
1515    fn test_diskann_len_and_is_empty() {
1516        let config = DiskANNConfig {
1517            dimension: 4,
1518            ..Default::default()
1519        };
1520
1521        let mut index = DiskANNIndex::new(config);
1522        let temp_file_path = std::env::temp_dir().join("test_diskann_len.dat");
1523        let temp_file = temp_file_path
1524            .to_str()
1525            .expect("temp dir path is valid UTF-8");
1526        index
1527            .create(temp_file)
1528            .expect("test: index creation should succeed");
1529
1530        assert_eq!(index.len(), 0);
1531        assert!(index.is_empty());
1532
1533        // Insert a vector
1534        let cid: Cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
1535            .parse()
1536            .expect("test: CID string is valid");
1537        let vec = vec![1.0, 0.0, 0.0, 0.0];
1538        index
1539            .insert(&cid, &vec)
1540            .expect("test: vector insertion should succeed");
1541
1542        assert_eq!(index.len(), 1);
1543        assert!(!index.is_empty());
1544
1545        // Cleanup
1546        std::fs::remove_file(temp_file).ok();
1547    }
1548}