1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct DiskANNConfig {
23 pub dimension: usize,
25 pub max_degree: usize,
27 pub queue_size: usize,
29 pub alpha: f32,
31 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#[derive(Debug, Clone, Serialize, Deserialize)]
49struct IndexHeader {
50 magic: [u8; 8],
52 version: u32,
54 config: DiskANNConfig,
56 num_vectors: usize,
58 graph_offset: u64,
60 vector_offset: u64,
62 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#[allow(dead_code)]
99#[derive(Debug, Clone)]
100struct GraphNode {
101 id: usize,
103 neighbors: Vec<usize>,
105}
106
107#[repr(C)]
109#[derive(Debug, Clone, Copy)]
110struct VectorFileHeader {
111 magic: [u8; 8],
113 num_vectors: u64,
115 dimension: u64,
117}
118
119impl VectorFileHeader {
120 const MAGIC: [u8; 8] = *b"VECDATA1";
121 const SIZE: usize = 24; 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
185pub struct DiskANNIndex {
187 config: DiskANNConfig,
189 index_path: Arc<RwLock<Option<String>>>,
191 graph_mmap: Arc<RwLock<Option<MmapMut>>>,
193 vector_mmap: Arc<RwLock<Option<MmapMut>>>,
195 vector_file_path: Arc<RwLock<Option<String>>>,
197 id_to_cid: Arc<RwLock<HashMap<usize, Cid>>>,
199 cid_to_id: Arc<RwLock<HashMap<Cid, usize>>>,
200 graph: Arc<RwLock<Vec<Vec<usize>>>>,
202 entry_points: Arc<RwLock<Vec<usize>>>,
204 next_id: Arc<RwLock<usize>>,
206 loaded: Arc<RwLock<bool>>,
208}
209
210impl DiskANNIndex {
211 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 fn get_vector_file_path(index_path: &str) -> String {
230 format!("{}.vectors", index_path)
231 }
232
233 fn vector_offset(&self, vector_id: usize) -> usize {
235 VectorFileHeader::SIZE + (vector_id * self.config.dimension * std::mem::size_of::<f32>())
236 }
237
238 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 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 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 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 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 let new_capacity = (required_count * 2).max(required_count + 1000); let new_size = VectorFileHeader::SIZE
348 + (new_capacity * self.config.dimension * std::mem::size_of::<f32>());
349
350 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 *self.vector_mmap.write().unwrap_or_else(|e| e.into_inner()) = None;
360
361 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 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 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 pub fn with_defaults(dimension: usize) -> Self {
389 let config = DiskANNConfig {
390 dimension,
391 ..Default::default()
392 };
393 Self::new(config)
394 }
395
396 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 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 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 let initial_size = header_bytes.len() + 1024 * 1024; file.set_len(initial_size as u64).map_err(Error::Io)?;
418
419 let mut mmap = unsafe {
421 MmapMut::map_mut(&file).map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?
422 };
423
424 mmap[..header_bytes.len()].copy_from_slice(&header_bytes);
426
427 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 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 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 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 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
468 let path = path.as_ref();
469
470 let file = OpenOptions::new()
472 .read(true)
473 .write(true)
474 .open(path)
475 .map_err(Error::Io)?;
476
477 let mmap = unsafe {
479 MmapMut::map_mut(&file).map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?
480 };
481
482 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 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 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 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 let id = *self.next_id.read().unwrap_or_else(|e| e.into_inner());
532
533 self.ensure_vector_capacity(id + 1)?;
535
536 self.write_vector(id, vector)?;
538
539 *self.next_id.write().unwrap_or_else(|e| e.into_inner()) += 1;
541 self.update_vector_count(id + 1)?;
542
543 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 self.graph
555 .write()
556 .unwrap_or_else(|e| e.into_inner())
557 .push(Vec::new());
558
559 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 self.vamana_insert(id, vector)?;
570
571 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 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 fn vamana_insert(&self, new_id: usize, new_vec: &[f32]) -> Result<()> {
594 let neighbors =
596 self.greedy_search_internal(new_vec, self.config.queue_size, self.config.queue_size)?;
597
598 let pruned = self.robust_prune(new_id, new_vec, &neighbors)?;
600
601 let mut graph = self.graph.write().unwrap_or_else(|e| e.into_inner());
603 graph[new_id] = pruned.clone();
604
605 for &neighbor_id in &pruned {
607 if neighbor_id >= graph.len() {
608 continue;
609 }
610
611 if !graph[neighbor_id].contains(&new_id) {
613 graph[neighbor_id].push(new_id);
614
615 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 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 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 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 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 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 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 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 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 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 let start_nodes: Vec<usize> = if entry_points.is_empty() {
757 vec![0]
758 } else {
759 entry_points.clone()
760 };
761
762 let mut visited = vec![false; num_vecs];
764
765 let mut candidates: Vec<(f32, usize)> = Vec::new();
767 let mut results: Vec<(f32, usize)> = Vec::new();
768
769 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 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 while !candidates.is_empty() {
788 let (current_dist, current_id) = candidates.remove(0);
790
791 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 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 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 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 if results.len() > search_list_size {
827 results.truncate(search_list_size);
828 }
829 }
830 }
831
832 Ok(results.iter().take(k).map(|(_, id)| *id).collect())
834 }
835
836 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 fn estimate_disk_size(&self) -> usize {
849 let num_vectors = *self.next_id.read().unwrap_or_else(|e| e.into_inner());
850
851 let header_size = 1024;
853
854 let vector_size = num_vectors * self.config.dimension * 4;
856
857 let graph_size = num_vectors * self.config.max_degree * 4;
859
860 let mapping_size = num_vectors * 40;
862
863 header_size + vector_size + graph_size + mapping_size
864 }
865
866 pub fn is_loaded(&self) -> bool {
868 *self.loaded.read().unwrap_or_else(|e| e.into_inner())
869 }
870
871 pub fn config(&self) -> &DiskANNConfig {
873 &self.config
874 }
875
876 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 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 let serialized = oxicode::serde::encode_to_vec(&data, oxicode::config::standard())
911 .map_err(|e| Error::Serialization(e.to_string()))?;
912
913 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 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 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 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 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 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 neighbor_dists
999 .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1000
1001 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 pub fn len(&self) -> usize {
1025 *self.next_id.read().unwrap_or_else(|e| e.into_inner())
1026 }
1027
1028 pub fn is_empty(&self) -> bool {
1030 self.len() == 0
1031 }
1032}
1033
1034#[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#[derive(Debug, Clone)]
1077pub struct CompactionStats {
1078 pub duration_ms: u64,
1080 pub vectors_before: usize,
1082 pub vectors_after: usize,
1084 pub graph_edges_before: usize,
1086 pub graph_edges_after: usize,
1088 pub bytes_saved: usize,
1090}
1091
1092#[derive(Debug, Clone)]
1094pub struct SearchResult {
1095 pub cid: Cid,
1097 pub distance: f32,
1099}
1100
1101#[derive(Debug, Clone)]
1103pub struct DiskANNStats {
1104 pub num_vectors: usize,
1106 pub dimension: usize,
1108 pub max_degree: usize,
1110 pub index_loaded: bool,
1112 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 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 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 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 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 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 assert!(results[0].distance < 0.2);
1215
1216 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 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 let graph = index.graph.read().unwrap_or_else(|e| e.into_inner());
1273 assert_eq!(graph.len(), 20);
1274
1275 for (i, neighbors) in graph.iter().enumerate().skip(1) {
1277 if i < 19 {
1278 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 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 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 assert!(pruned.len() <= max_degree);
1341 assert!(pruned.contains(&1));
1343
1344 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 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 assert!(index.save().is_ok());
1387
1388 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 assert!(index.flush().is_ok());
1414
1415 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 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 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 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 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 let _pruned = index
1506 .prune_graph(0.5)
1507 .expect("test: prune_graph should succeed");
1508 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 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 std::fs::remove_file(temp_file).ok();
1547 }
1548}