Skip to main content

oxirs_vec/diskann/
storage.rs

1//! Storage backends for DiskANN
2//!
3//! Provides abstractions for storing vectors and graph structures on disk
4//! with support for memory-mapped I/O and buffered access.
5//!
6//! ## Storage Layout
7//! - Vectors: Raw f32 arrays or PQ-compressed codes
8//! - Graph: Adjacency lists with neighbor IDs
9//! - Metadata: Index configuration and statistics
10//!
11//! ## Backends
12//! - **DiskStorage**: Standard file I/O with buffering
13//! - **MemoryMappedStorage**: Memory-mapped files for fast access
14//! - **CachedStorage**: Hybrid with LRU caching
15
16use crate::diskann::config::DiskAnnConfig;
17use crate::diskann::graph::VamanaGraph;
18use crate::diskann::types::{DiskAnnError, DiskAnnResult, VectorId};
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21use std::fs::{File, OpenOptions};
22use std::io::{BufReader, BufWriter, Read, Write};
23use std::path::{Path, PathBuf};
24
25/// Storage backend trait
26pub trait StorageBackend: Send + Sync {
27    /// Write a vector to storage
28    fn write_vector(&mut self, vector_id: &VectorId, vector: &[f32]) -> DiskAnnResult<()>;
29
30    /// Read a vector from storage
31    fn read_vector(&self, vector_id: &VectorId) -> DiskAnnResult<Vec<f32>>;
32
33    /// Write graph structure
34    fn write_graph(&mut self, graph: &VamanaGraph) -> DiskAnnResult<()>;
35
36    /// Read graph structure
37    fn read_graph(&self) -> DiskAnnResult<VamanaGraph>;
38
39    /// Write metadata
40    fn write_metadata(&mut self, metadata: &StorageMetadata) -> DiskAnnResult<()>;
41
42    /// Read metadata
43    fn read_metadata(&self) -> DiskAnnResult<StorageMetadata>;
44
45    /// Delete all data
46    fn clear(&mut self) -> DiskAnnResult<()>;
47
48    /// Flush any pending writes
49    fn flush(&mut self) -> DiskAnnResult<()>;
50
51    /// Get storage size in bytes
52    fn size(&self) -> DiskAnnResult<u64>;
53}
54
55/// Storage metadata
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct StorageMetadata {
58    pub version: String,
59    pub config: DiskAnnConfig,
60    pub num_vectors: usize,
61    pub created_at: chrono::DateTime<chrono::Utc>,
62    pub updated_at: chrono::DateTime<chrono::Utc>,
63}
64
65impl StorageMetadata {
66    pub fn new(config: DiskAnnConfig) -> Self {
67        let now = chrono::Utc::now();
68        Self {
69            version: env!("CARGO_PKG_VERSION").to_string(),
70            config,
71            num_vectors: 0,
72            created_at: now,
73            updated_at: now,
74        }
75    }
76
77    pub fn update_timestamp(&mut self) {
78        self.updated_at = chrono::Utc::now();
79    }
80}
81
82/// Standard disk storage with buffered I/O
83#[derive(Debug)]
84pub struct DiskStorage {
85    base_path: PathBuf,
86    vector_file: Option<PathBuf>,
87    graph_file: Option<PathBuf>,
88    metadata_file: Option<PathBuf>,
89    dimension: usize,
90    vector_cache: HashMap<VectorId, Vec<f32>>,
91    cache_limit: usize,
92}
93
94impl DiskStorage {
95    /// Create new disk storage at given path
96    pub fn new<P: AsRef<Path>>(base_path: P, dimension: usize) -> DiskAnnResult<Self> {
97        let base_path = base_path.as_ref().to_path_buf();
98
99        // Create directory if it doesn't exist
100        if !base_path.exists() {
101            std::fs::create_dir_all(&base_path).map_err(|e| DiskAnnError::IoError {
102                message: format!("Failed to create directory: {}", e),
103            })?;
104        }
105
106        let vector_file = Some(base_path.join("vectors.bin"));
107        let graph_file = Some(base_path.join("graph.bin"));
108        let metadata_file = Some(base_path.join("metadata.json"));
109
110        Ok(Self {
111            base_path,
112            vector_file,
113            graph_file,
114            metadata_file,
115            dimension,
116            vector_cache: HashMap::new(),
117            cache_limit: 1000,
118        })
119    }
120
121    /// Set cache limit (number of vectors to keep in memory)
122    pub fn with_cache_limit(mut self, limit: usize) -> Self {
123        self.cache_limit = limit;
124        self
125    }
126
127    /// Get vector file path
128    pub fn vector_file_path(&self) -> &Option<PathBuf> {
129        &self.vector_file
130    }
131
132    /// Get graph file path
133    pub fn graph_file_path(&self) -> &Option<PathBuf> {
134        &self.graph_file
135    }
136
137    /// Evict old entries from cache if needed
138    fn evict_cache_if_needed(&mut self) {
139        if self.vector_cache.len() > self.cache_limit {
140            // Simple eviction: remove first entry
141            if let Some(key) = self.vector_cache.keys().next().cloned() {
142                self.vector_cache.remove(&key);
143            }
144        }
145    }
146}
147
148impl Clone for DiskStorage {
149    fn clone(&self) -> Self {
150        Self {
151            base_path: self.base_path.clone(),
152            vector_file: self.vector_file.clone(),
153            graph_file: self.graph_file.clone(),
154            metadata_file: self.metadata_file.clone(),
155            dimension: self.dimension,
156            vector_cache: HashMap::new(), // Don't clone cache
157            cache_limit: self.cache_limit,
158        }
159    }
160}
161
162impl StorageBackend for DiskStorage {
163    fn write_vector(&mut self, vector_id: &VectorId, vector: &[f32]) -> DiskAnnResult<()> {
164        if vector.len() != self.dimension {
165            return Err(DiskAnnError::DimensionMismatch {
166                expected: self.dimension,
167                actual: vector.len(),
168            });
169        }
170
171        // Add to cache
172        self.vector_cache.insert(vector_id.clone(), vector.to_vec());
173        self.evict_cache_if_needed();
174
175        // Append to vector file
176        if let Some(path) = &self.vector_file {
177            let file = OpenOptions::new()
178                .create(true)
179                .append(true)
180                .open(path)
181                .map_err(|e| DiskAnnError::IoError {
182                    message: format!("Failed to open vector file: {}", e),
183                })?;
184
185            let mut writer = BufWriter::new(file);
186
187            // Write vector ID length and ID
188            let id_bytes = vector_id.as_bytes();
189            writer
190                .write_all(&(id_bytes.len() as u32).to_le_bytes())
191                .map_err(|e| DiskAnnError::IoError {
192                    message: format!("Failed to write vector ID length: {}", e),
193                })?;
194            writer
195                .write_all(id_bytes)
196                .map_err(|e| DiskAnnError::IoError {
197                    message: format!("Failed to write vector ID: {}", e),
198                })?;
199
200            // Write vector data
201            for &value in vector {
202                writer
203                    .write_all(&value.to_le_bytes())
204                    .map_err(|e| DiskAnnError::IoError {
205                        message: format!("Failed to write vector data: {}", e),
206                    })?;
207            }
208
209            writer.flush().map_err(|e| DiskAnnError::IoError {
210                message: format!("Failed to flush vector file: {}", e),
211            })?;
212        }
213
214        Ok(())
215    }
216
217    fn read_vector(&self, vector_id: &VectorId) -> DiskAnnResult<Vec<f32>> {
218        // Check cache first
219        if let Some(vector) = self.vector_cache.get(vector_id) {
220            return Ok(vector.clone());
221        }
222
223        // Read from disk
224        if let Some(path) = &self.vector_file {
225            if !path.exists() {
226                return Err(DiskAnnError::VectorNotFound {
227                    id: vector_id.clone(),
228                });
229            }
230
231            let file = File::open(path).map_err(|e| DiskAnnError::IoError {
232                message: format!("Failed to open vector file: {}", e),
233            })?;
234            let mut reader = BufReader::new(file);
235
236            // Sequential scan (inefficient, but simple for now)
237            loop {
238                // Read ID length
239                let mut id_len_bytes = [0u8; 4];
240                if reader.read_exact(&mut id_len_bytes).is_err() {
241                    break; // End of file
242                }
243                let id_len = u32::from_le_bytes(id_len_bytes) as usize;
244
245                // Read ID
246                let mut id_bytes = vec![0u8; id_len];
247                reader
248                    .read_exact(&mut id_bytes)
249                    .map_err(|e| DiskAnnError::IoError {
250                        message: format!("Failed to read vector ID: {}", e),
251                    })?;
252                let id = String::from_utf8(id_bytes).map_err(|e| DiskAnnError::IoError {
253                    message: format!("Invalid UTF-8 in vector ID: {}", e),
254                })?;
255
256                // Read vector data
257                let mut vector = vec![0.0f32; self.dimension];
258                for value in &mut vector {
259                    let mut bytes = [0u8; 4];
260                    reader
261                        .read_exact(&mut bytes)
262                        .map_err(|e| DiskAnnError::IoError {
263                            message: format!("Failed to read vector data: {}", e),
264                        })?;
265                    *value = f32::from_le_bytes(bytes);
266                }
267
268                if &id == vector_id {
269                    return Ok(vector);
270                }
271            }
272
273            Err(DiskAnnError::VectorNotFound {
274                id: vector_id.clone(),
275            })
276        } else {
277            Err(DiskAnnError::VectorNotFound {
278                id: vector_id.clone(),
279            })
280        }
281    }
282
283    fn write_graph(&mut self, graph: &VamanaGraph) -> DiskAnnResult<()> {
284        if let Some(path) = &self.graph_file {
285            let file = File::create(path).map_err(|e| DiskAnnError::IoError {
286                message: format!("Failed to create graph file: {}", e),
287            })?;
288
289            let mut writer = BufWriter::new(file);
290            oxicode::serde::encode_into_std_write(graph, &mut writer, oxicode::config::standard())?;
291
292            // Explicitly flush the buffer and fsync, propagating any error. A
293            // `BufWriter` flushes on drop but *swallows* the error, so under I/O
294            // pressure a short/failed write would otherwise leave a truncated
295            // graph file behind while this call still returned `Ok`, making a
296            // subsequent `read_graph` fail to decode. Mirrors `write_metadata`.
297            let file = writer.into_inner().map_err(|e| DiskAnnError::IoError {
298                message: format!("Failed to flush graph file: {}", e),
299            })?;
300            file.sync_all().map_err(|e| DiskAnnError::IoError {
301                message: format!("Failed to sync graph file: {}", e),
302            })?;
303        }
304        Ok(())
305    }
306
307    fn read_graph(&self) -> DiskAnnResult<VamanaGraph> {
308        if let Some(path) = &self.graph_file {
309            if !path.exists() {
310                return Err(DiskAnnError::StorageError {
311                    message: "Graph file does not exist".to_string(),
312                });
313            }
314
315            let file = File::open(path).map_err(|e| DiskAnnError::IoError {
316                message: format!("Failed to open graph file: {}", e),
317            })?;
318
319            let mut reader = BufReader::new(file);
320            let (graph, _) =
321                oxicode::serde::decode_from_std_read(&mut reader, oxicode::config::standard())?;
322            Ok(graph)
323        } else {
324            Err(DiskAnnError::StorageError {
325                message: "Graph file path not set".to_string(),
326            })
327        }
328    }
329
330    fn write_metadata(&mut self, metadata: &StorageMetadata) -> DiskAnnResult<()> {
331        if let Some(path) = &self.metadata_file {
332            let mut file = File::create(path).map_err(|e| DiskAnnError::IoError {
333                message: format!("Failed to create metadata file: {}", e),
334            })?;
335
336            serde_json::to_writer_pretty(&mut file, metadata).map_err(|e| {
337                DiskAnnError::SerializationError {
338                    message: format!("Failed to serialize metadata: {}", e),
339                }
340            })?;
341
342            // Explicitly sync to disk
343            file.sync_all().map_err(|e| DiskAnnError::IoError {
344                message: format!("Failed to sync metadata file: {}", e),
345            })?;
346        }
347        Ok(())
348    }
349
350    fn read_metadata(&self) -> DiskAnnResult<StorageMetadata> {
351        if let Some(path) = &self.metadata_file {
352            if !path.exists() {
353                return Err(DiskAnnError::StorageError {
354                    message: "Metadata file does not exist".to_string(),
355                });
356            }
357
358            let file = File::open(path).map_err(|e| DiskAnnError::IoError {
359                message: format!("Failed to open metadata file: {}", e),
360            })?;
361
362            let metadata =
363                serde_json::from_reader(file).map_err(|e| DiskAnnError::SerializationError {
364                    message: format!("Failed to deserialize metadata: {}", e),
365                })?;
366
367            Ok(metadata)
368        } else {
369            Err(DiskAnnError::StorageError {
370                message: "Metadata file path not set".to_string(),
371            })
372        }
373    }
374
375    fn clear(&mut self) -> DiskAnnResult<()> {
376        self.vector_cache.clear();
377
378        if let Some(path) = &self.vector_file {
379            if path.exists() {
380                std::fs::remove_file(path).map_err(|e| DiskAnnError::IoError {
381                    message: format!("Failed to remove vector file: {}", e),
382                })?;
383            }
384        }
385
386        if let Some(path) = &self.graph_file {
387            if path.exists() {
388                std::fs::remove_file(path).map_err(|e| DiskAnnError::IoError {
389                    message: format!("Failed to remove graph file: {}", e),
390                })?;
391            }
392        }
393
394        if let Some(path) = &self.metadata_file {
395            if path.exists() {
396                std::fs::remove_file(path).map_err(|e| DiskAnnError::IoError {
397                    message: format!("Failed to remove metadata file: {}", e),
398                })?;
399            }
400        }
401
402        Ok(())
403    }
404
405    fn flush(&mut self) -> DiskAnnResult<()> {
406        // All writes are immediately flushed in this implementation
407        Ok(())
408    }
409
410    fn size(&self) -> DiskAnnResult<u64> {
411        let mut total_size = 0u64;
412
413        if let Some(path) = &self.vector_file {
414            if path.exists() {
415                total_size += std::fs::metadata(path)
416                    .map_err(|e| DiskAnnError::IoError {
417                        message: format!("Failed to get vector file size: {}", e),
418                    })?
419                    .len();
420            }
421        }
422
423        if let Some(path) = &self.graph_file {
424            if path.exists() {
425                total_size += std::fs::metadata(path)
426                    .map_err(|e| DiskAnnError::IoError {
427                        message: format!("Failed to get graph file size: {}", e),
428                    })?
429                    .len();
430            }
431        }
432
433        if let Some(path) = &self.metadata_file {
434            if path.exists() {
435                total_size += std::fs::metadata(path)
436                    .map_err(|e| DiskAnnError::IoError {
437                        message: format!("Failed to get metadata file size: {}", e),
438                    })?
439                    .len();
440            }
441        }
442
443        Ok(total_size)
444    }
445}
446
447/// Memory-mapped storage (stub for future implementation)
448#[derive(Debug, Clone, Serialize, Deserialize)]
449pub struct MemoryMappedStorage {
450    base_path: PathBuf,
451    dimension: usize,
452}
453
454impl MemoryMappedStorage {
455    pub fn new<P: AsRef<Path>>(base_path: P, dimension: usize) -> DiskAnnResult<Self> {
456        Ok(Self {
457            base_path: base_path.as_ref().to_path_buf(),
458            dimension,
459        })
460    }
461}
462
463impl StorageBackend for MemoryMappedStorage {
464    fn write_vector(&mut self, _vector_id: &VectorId, _vector: &[f32]) -> DiskAnnResult<()> {
465        Err(DiskAnnError::StorageError {
466            message: "MemoryMappedStorage not yet implemented".to_string(),
467        })
468    }
469
470    fn read_vector(&self, _vector_id: &VectorId) -> DiskAnnResult<Vec<f32>> {
471        Err(DiskAnnError::StorageError {
472            message: "MemoryMappedStorage not yet implemented".to_string(),
473        })
474    }
475
476    fn write_graph(&mut self, _graph: &VamanaGraph) -> DiskAnnResult<()> {
477        Err(DiskAnnError::StorageError {
478            message: "MemoryMappedStorage not yet implemented".to_string(),
479        })
480    }
481
482    fn read_graph(&self) -> DiskAnnResult<VamanaGraph> {
483        Err(DiskAnnError::StorageError {
484            message: "MemoryMappedStorage not yet implemented".to_string(),
485        })
486    }
487
488    fn write_metadata(&mut self, _metadata: &StorageMetadata) -> DiskAnnResult<()> {
489        Err(DiskAnnError::StorageError {
490            message: "MemoryMappedStorage not yet implemented".to_string(),
491        })
492    }
493
494    fn read_metadata(&self) -> DiskAnnResult<StorageMetadata> {
495        Err(DiskAnnError::StorageError {
496            message: "MemoryMappedStorage not yet implemented".to_string(),
497        })
498    }
499
500    fn clear(&mut self) -> DiskAnnResult<()> {
501        Ok(())
502    }
503
504    fn flush(&mut self) -> DiskAnnResult<()> {
505        Ok(())
506    }
507
508    fn size(&self) -> DiskAnnResult<u64> {
509        Ok(0)
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
516    use super::*;
517    use crate::diskann::config::PruningStrategy;
518    use std::env;
519
520    fn temp_dir() -> PathBuf {
521        use std::sync::atomic::{AtomicU64, Ordering};
522        static COUNTER: AtomicU64 = AtomicU64::new(0);
523        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
524        env::temp_dir().join(format!(
525            "diskann_storage_test_{}_{}_{}",
526            std::process::id(),
527            chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0),
528            id
529        ))
530    }
531
532    #[test]
533    fn test_disk_storage_vector_write_read() -> Result<()> {
534        let dir = temp_dir();
535        let mut storage = DiskStorage::new(&dir, 3)?;
536
537        let vector = vec![1.0, 2.0, 3.0];
538        storage.write_vector(&"vec1".to_string(), &vector)?;
539
540        let read_vector = storage.read_vector(&"vec1".to_string())?;
541        assert_eq!(read_vector, vector);
542
543        storage.clear()?;
544        Ok(())
545    }
546
547    #[test]
548    fn test_disk_storage_dimension_mismatch() -> Result<()> {
549        let dir = temp_dir();
550        let mut storage = DiskStorage::new(&dir, 3)?;
551
552        let vector = vec![1.0, 2.0]; // Wrong dimension
553        let result = storage.write_vector(&"vec1".to_string(), &vector);
554
555        assert!(result.is_err());
556        storage.clear()?;
557        Ok(())
558    }
559
560    #[test]
561    fn test_disk_storage_graph() -> Result<()> {
562        let dir = temp_dir();
563        std::fs::remove_dir_all(&dir).ok(); // Clean up if exists
564        let mut storage = DiskStorage::new(&dir, 3)?;
565
566        let mut graph = VamanaGraph::new(3, PruningStrategy::Alpha, 1.2);
567        graph.add_node("v1".to_string())?;
568        graph.add_node("v2".to_string())?;
569
570        storage.write_graph(&graph)?;
571        let read_graph = storage.read_graph()?;
572
573        assert_eq!(read_graph.num_nodes(), 2);
574        storage.clear()?;
575        std::fs::remove_dir_all(&dir).ok();
576        Ok(())
577    }
578
579    #[test]
580    fn test_disk_storage_metadata() -> Result<()> {
581        let dir = temp_dir();
582        std::fs::remove_dir_all(&dir).ok(); // Clean up if exists
583        let mut storage = DiskStorage::new(&dir, 128)?;
584
585        let config = DiskAnnConfig::default_config(128);
586        let metadata = StorageMetadata::new(config);
587
588        storage.write_metadata(&metadata)?;
589        let read_metadata = storage.read_metadata()?;
590
591        assert_eq!(read_metadata.config.dimension, 128);
592        storage.clear()?;
593        std::fs::remove_dir_all(&dir).ok();
594        Ok(())
595    }
596
597    #[test]
598    fn test_disk_storage_size() -> Result<()> {
599        let dir = temp_dir();
600        let mut storage = DiskStorage::new(&dir, 3)?;
601
602        let initial_size = storage.size()?;
603        assert_eq!(initial_size, 0);
604
605        let vector = vec![1.0, 2.0, 3.0];
606        storage.write_vector(&"vec1".to_string(), &vector)?;
607
608        let after_write = storage.size()?;
609        assert!(after_write > initial_size);
610
611        storage.clear()?;
612        Ok(())
613    }
614
615    #[test]
616    fn test_disk_storage_cache() -> Result<()> {
617        let dir = temp_dir();
618        std::fs::remove_dir_all(&dir).ok(); // Clean up if exists
619        let mut storage = DiskStorage::new(&dir, 3)?.with_cache_limit(2);
620
621        storage.write_vector(&"v1".to_string(), &[1.0, 2.0, 3.0])?;
622        storage.write_vector(&"v2".to_string(), &[4.0, 5.0, 6.0])?;
623        storage.write_vector(&"v3".to_string(), &[7.0, 8.0, 9.0])?;
624
625        // Cache should have at most 2 entries
626        assert!(storage.vector_cache.len() <= 2);
627
628        storage.clear()?;
629        std::fs::remove_dir_all(&dir).ok();
630        Ok(())
631    }
632
633    #[test]
634    fn test_vector_not_found() -> Result<()> {
635        let dir = temp_dir();
636        let storage = DiskStorage::new(&dir, 3)?;
637
638        let result = storage.read_vector(&"nonexistent".to_string());
639        assert!(result.is_err());
640        Ok(())
641    }
642
643    #[test]
644    fn test_storage_clear() -> Result<()> {
645        let dir = temp_dir();
646        std::fs::remove_dir_all(&dir).ok(); // Clean up if exists
647        let mut storage = DiskStorage::new(&dir, 3)?;
648
649        storage.write_vector(&"v1".to_string(), &[1.0, 2.0, 3.0])?;
650
651        // Verify file was created
652        let vector_file = storage
653            .vector_file
654            .as_ref()
655            .ok_or("vector_file is None")?
656            .clone();
657        assert!(
658            vector_file.exists(),
659            "Vector file should exist after write: {:?}",
660            vector_file
661        );
662
663        storage.clear()?;
664        assert!(
665            !vector_file.exists(),
666            "Vector file should not exist after clear: {:?}",
667            vector_file
668        );
669
670        // Cleanup
671        std::fs::remove_dir_all(&dir).ok();
672        Ok(())
673    }
674}