use ipfrs_core::{Cid, Error, Result};
use memmap2::MmapMut;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::path::Path;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiskANNConfig {
pub dimension: usize,
pub max_degree: usize,
pub queue_size: usize,
pub alpha: f32,
pub num_entry_points: usize,
}
impl Default for DiskANNConfig {
fn default() -> Self {
Self {
dimension: 768,
max_degree: 64,
queue_size: 100,
alpha: 1.2,
num_entry_points: 4,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct IndexHeader {
magic: [u8; 8],
version: u32,
config: DiskANNConfig,
num_vectors: usize,
graph_offset: u64,
vector_offset: u64,
cid_mapping_offset: u64,
}
impl IndexHeader {
const MAGIC: [u8; 8] = *b"DISKANN1";
fn new(config: DiskANNConfig) -> Self {
Self {
magic: Self::MAGIC,
version: 1,
config,
num_vectors: 0,
graph_offset: 0,
vector_offset: 0,
cid_mapping_offset: 0,
}
}
fn validate(&self) -> Result<()> {
if self.magic != Self::MAGIC {
return Err(Error::InvalidInput(
"Invalid DiskANN index file format".to_string(),
));
}
if self.version != 1 {
return Err(Error::InvalidInput(format!(
"Unsupported DiskANN version: {}",
self.version
)));
}
Ok(())
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct GraphNode {
id: usize,
neighbors: Vec<usize>,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct VectorFileHeader {
magic: [u8; 8],
num_vectors: u64,
dimension: u64,
}
impl VectorFileHeader {
const MAGIC: [u8; 8] = *b"VECDATA1";
const SIZE: usize = 24;
fn new(dimension: usize) -> Self {
Self {
magic: Self::MAGIC,
num_vectors: 0,
dimension: dimension as u64,
}
}
#[allow(dead_code)]
fn validate(&self, expected_dim: usize) -> Result<()> {
if self.magic != Self::MAGIC {
return Err(Error::InvalidInput(
"Invalid vector file format".to_string(),
));
}
if self.dimension != expected_dim as u64 {
return Err(Error::InvalidInput(format!(
"Vector dimension mismatch: expected {}, got {}",
expected_dim, self.dimension
)));
}
Ok(())
}
fn as_bytes(&self) -> [u8; Self::SIZE] {
let mut bytes = [0u8; Self::SIZE];
bytes[0..8].copy_from_slice(&self.magic);
bytes[8..16].copy_from_slice(&self.num_vectors.to_le_bytes());
bytes[16..24].copy_from_slice(&self.dimension.to_le_bytes());
bytes
}
#[allow(dead_code)]
fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() < Self::SIZE {
return Err(Error::InvalidInput(
"Vector file header too small".to_string(),
));
}
let mut magic = [0u8; 8];
magic.copy_from_slice(&bytes[0..8]);
let num_vectors = u64::from_le_bytes(
bytes[8..16]
.try_into()
.expect("bytes[8..16] is exactly 8 bytes after bounds check"),
);
let dimension = u64::from_le_bytes(
bytes[16..24]
.try_into()
.expect("bytes[16..24] is exactly 8 bytes after bounds check"),
);
Ok(Self {
magic,
num_vectors,
dimension,
})
}
}
pub struct DiskANNIndex {
config: DiskANNConfig,
index_path: Arc<RwLock<Option<String>>>,
graph_mmap: Arc<RwLock<Option<MmapMut>>>,
vector_mmap: Arc<RwLock<Option<MmapMut>>>,
vector_file_path: Arc<RwLock<Option<String>>>,
id_to_cid: Arc<RwLock<HashMap<usize, Cid>>>,
cid_to_id: Arc<RwLock<HashMap<Cid, usize>>>,
graph: Arc<RwLock<Vec<Vec<usize>>>>,
entry_points: Arc<RwLock<Vec<usize>>>,
next_id: Arc<RwLock<usize>>,
loaded: Arc<RwLock<bool>>,
}
impl DiskANNIndex {
pub fn new(config: DiskANNConfig) -> Self {
Self {
config,
index_path: Arc::new(RwLock::new(None)),
graph_mmap: Arc::new(RwLock::new(None)),
vector_mmap: Arc::new(RwLock::new(None)),
vector_file_path: Arc::new(RwLock::new(None)),
id_to_cid: Arc::new(RwLock::new(HashMap::new())),
cid_to_id: Arc::new(RwLock::new(HashMap::new())),
graph: Arc::new(RwLock::new(Vec::new())),
entry_points: Arc::new(RwLock::new(Vec::new())),
next_id: Arc::new(RwLock::new(0)),
loaded: Arc::new(RwLock::new(false)),
}
}
fn get_vector_file_path(index_path: &str) -> String {
format!("{}.vectors", index_path)
}
fn vector_offset(&self, vector_id: usize) -> usize {
VectorFileHeader::SIZE + (vector_id * self.config.dimension * std::mem::size_of::<f32>())
}
fn read_vector(&self, vector_id: usize) -> Result<Vec<f32>> {
let mmap = self.vector_mmap.read().unwrap_or_else(|e| e.into_inner());
let mmap = mmap
.as_ref()
.ok_or_else(|| Error::InvalidInput("Vector file not mapped".to_string()))?;
let offset = self.vector_offset(vector_id);
let vec_size_bytes = self.config.dimension * std::mem::size_of::<f32>();
if offset + vec_size_bytes > mmap.len() {
return Err(Error::InvalidInput(format!(
"Vector {} out of bounds",
vector_id
)));
}
let bytes = &mmap[offset..offset + vec_size_bytes];
let floats: Vec<f32> = bytes
.chunks_exact(4)
.map(|chunk| {
f32::from_le_bytes(
chunk
.try_into()
.expect("chunks_exact(4) guarantees exactly 4 bytes"),
)
})
.collect();
Ok(floats)
}
fn write_vector(&self, vector_id: usize, vector: &[f32]) -> Result<()> {
if vector.len() != self.config.dimension {
return Err(Error::InvalidInput(format!(
"Vector dimension {} doesn't match expected {}",
vector.len(),
self.config.dimension
)));
}
let mut mmap = self.vector_mmap.write().unwrap_or_else(|e| e.into_inner());
let mmap = mmap
.as_mut()
.ok_or_else(|| Error::InvalidInput("Vector file not mapped".to_string()))?;
let offset = self.vector_offset(vector_id);
let vec_size_bytes = self.config.dimension * std::mem::size_of::<f32>();
if offset + vec_size_bytes > mmap.len() {
return Err(Error::InvalidInput(format!(
"Vector {} out of bounds (mmap size: {}, needed: {})",
vector_id,
mmap.len(),
offset + vec_size_bytes
)));
}
let bytes = &mut mmap[offset..offset + vec_size_bytes];
for (i, &val) in vector.iter().enumerate() {
let val_bytes = val.to_le_bytes();
bytes[i * 4..(i + 1) * 4].copy_from_slice(&val_bytes);
}
Ok(())
}
fn update_vector_count(&self, count: usize) -> Result<()> {
let mut mmap = self.vector_mmap.write().unwrap_or_else(|e| e.into_inner());
let mmap = mmap
.as_mut()
.ok_or_else(|| Error::InvalidInput("Vector file not mapped".to_string()))?;
let count_bytes = (count as u64).to_le_bytes();
mmap[8..16].copy_from_slice(&count_bytes);
Ok(())
}
fn get_vector_count(&self) -> Result<usize> {
let mmap = self.vector_mmap.read().unwrap_or_else(|e| e.into_inner());
let mmap = mmap
.as_ref()
.ok_or_else(|| Error::InvalidInput("Vector file not mapped".to_string()))?;
let count_bytes: [u8; 8] = mmap[8..16]
.try_into()
.expect("mmap[8..16] is exactly 8 bytes; mmap size checked above");
Ok(u64::from_le_bytes(count_bytes) as usize)
}
fn ensure_vector_capacity(&self, required_count: usize) -> Result<()> {
let mmap = self.vector_mmap.read().unwrap_or_else(|e| e.into_inner());
let current_size = mmap
.as_ref()
.ok_or_else(|| Error::InvalidInput("Vector file not mapped".to_string()))?
.len();
drop(mmap);
let required_size = VectorFileHeader::SIZE
+ (required_count * self.config.dimension * std::mem::size_of::<f32>());
if required_size > current_size {
let new_capacity = (required_count * 2).max(required_count + 1000); let new_size = VectorFileHeader::SIZE
+ (new_capacity * self.config.dimension * std::mem::size_of::<f32>());
let vec_path = self
.vector_file_path
.read()
.unwrap_or_else(|e| e.into_inner())
.clone()
.ok_or_else(|| Error::InvalidInput("No vector file path set".to_string()))?;
*self.vector_mmap.write().unwrap_or_else(|e| e.into_inner()) = None;
let vec_file = OpenOptions::new()
.read(true)
.write(true)
.open(&vec_path)
.map_err(Error::Io)?;
vec_file.set_len(new_size as u64).map_err(Error::Io)?;
let new_mmap = unsafe {
MmapMut::map_mut(&vec_file)
.map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?
};
*self.vector_mmap.write().unwrap_or_else(|e| e.into_inner()) = Some(new_mmap);
}
Ok(())
}
fn num_vectors(&self) -> usize {
self.get_vector_count()
.unwrap_or_else(|_| *self.next_id.read().unwrap_or_else(|e| e.into_inner()))
}
pub fn with_defaults(dimension: usize) -> Self {
let config = DiskANNConfig {
dimension,
..Default::default()
};
Self::new(config)
}
pub fn create(&mut self, path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();
let path_str = path.to_string_lossy().to_string();
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(Error::Io)?;
let header = IndexHeader::new(self.config.clone());
let header_bytes = oxicode::serde::encode_to_vec(&header, oxicode::config::standard())
.map_err(|e| Error::Serialization(e.to_string()))?;
let initial_size = header_bytes.len() + 1024 * 1024; file.set_len(initial_size as u64).map_err(Error::Io)?;
let mut mmap = unsafe {
MmapMut::map_mut(&file).map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?
};
mmap[..header_bytes.len()].copy_from_slice(&header_bytes);
let vec_path = Self::get_vector_file_path(&path_str);
let vec_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&vec_path)
.map_err(Error::Io)?;
let vec_header = VectorFileHeader::new(self.config.dimension);
let initial_vec_count = 1000;
let vec_file_size = VectorFileHeader::SIZE
+ (initial_vec_count * self.config.dimension * std::mem::size_of::<f32>());
vec_file.set_len(vec_file_size as u64).map_err(Error::Io)?;
let mut vec_mmap = unsafe {
MmapMut::map_mut(&vec_file)
.map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?
};
let header_bytes = vec_header.as_bytes();
vec_mmap[..VectorFileHeader::SIZE].copy_from_slice(&header_bytes);
*self.index_path.write().unwrap_or_else(|e| e.into_inner()) = Some(path_str.clone());
*self
.vector_file_path
.write()
.unwrap_or_else(|e| e.into_inner()) = Some(vec_path);
*self.graph_mmap.write().unwrap_or_else(|e| e.into_inner()) = Some(mmap);
*self.vector_mmap.write().unwrap_or_else(|e| e.into_inner()) = Some(vec_mmap);
*self.loaded.write().unwrap_or_else(|e| e.into_inner()) = true;
Ok(())
}
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let file = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(Error::Io)?;
let mmap = unsafe {
MmapMut::map_mut(&file).map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?
};
let header: IndexHeader =
oxicode::serde::decode_owned_from_slice(&mmap[..1024], oxicode::config::standard())
.map(|(v, _)| v)
.map_err(|e| Error::Serialization(e.to_string()))?;
header.validate()?;
let index = Self::new(header.config);
*index.index_path.write().unwrap_or_else(|e| e.into_inner()) =
Some(path.to_string_lossy().to_string());
*index.graph_mmap.write().unwrap_or_else(|e| e.into_inner()) = Some(mmap);
*index.next_id.write().unwrap_or_else(|e| e.into_inner()) = header.num_vectors;
*index.loaded.write().unwrap_or_else(|e| e.into_inner()) = true;
Ok(index)
}
pub fn insert(&mut self, cid: &Cid, vector: &[f32]) -> Result<()> {
if !*self.loaded.read().unwrap_or_else(|e| e.into_inner()) {
return Err(Error::InvalidInput(
"Index not created or loaded".to_string(),
));
}
if vector.len() != self.config.dimension {
return Err(Error::InvalidInput(format!(
"Vector dimension {} doesn't match index dimension {}",
vector.len(),
self.config.dimension
)));
}
if self
.cid_to_id
.read()
.unwrap_or_else(|e| e.into_inner())
.contains_key(cid)
{
return Err(Error::InvalidInput(format!(
"CID already in index: {}",
cid
)));
}
let id = *self.next_id.read().unwrap_or_else(|e| e.into_inner());
self.ensure_vector_capacity(id + 1)?;
self.write_vector(id, vector)?;
*self.next_id.write().unwrap_or_else(|e| e.into_inner()) += 1;
self.update_vector_count(id + 1)?;
self.id_to_cid
.write()
.unwrap_or_else(|e| e.into_inner())
.insert(id, *cid);
self.cid_to_id
.write()
.unwrap_or_else(|e| e.into_inner())
.insert(*cid, id);
self.graph
.write()
.unwrap_or_else(|e| e.into_inner())
.push(Vec::new());
if id == 0 {
self.entry_points
.write()
.unwrap_or_else(|e| e.into_inner())
.push(0);
return Ok(());
}
self.vamana_insert(id, vector)?;
if id.is_multiple_of(1000) && id < 10000 {
self.entry_points
.write()
.unwrap_or_else(|e| e.into_inner())
.push(id);
let mut eps = self.entry_points.write().unwrap_or_else(|e| e.into_inner());
let num_to_drain = if eps.len() > self.config.num_entry_points {
eps.len() - self.config.num_entry_points
} else {
0
};
if num_to_drain > 0 {
eps.drain(0..num_to_drain);
}
}
Ok(())
}
fn vamana_insert(&self, new_id: usize, new_vec: &[f32]) -> Result<()> {
let neighbors =
self.greedy_search_internal(new_vec, self.config.queue_size, self.config.queue_size)?;
let pruned = self.robust_prune(new_id, new_vec, &neighbors)?;
let mut graph = self.graph.write().unwrap_or_else(|e| e.into_inner());
graph[new_id] = pruned.clone();
for &neighbor_id in &pruned {
if neighbor_id >= graph.len() {
continue;
}
if !graph[neighbor_id].contains(&new_id) {
graph[neighbor_id].push(new_id);
if graph[neighbor_id].len() > self.config.max_degree {
let neighbor_vec = self.read_vector(neighbor_id)?;
let candidates = graph[neighbor_id].clone();
let pruned_neighbors =
self.robust_prune(neighbor_id, &neighbor_vec, &candidates)?;
graph[neighbor_id] = pruned_neighbors;
}
}
}
Ok(())
}
fn robust_prune(
&self,
node_id: usize,
node_vec: &[f32],
candidates: &[usize],
) -> Result<Vec<usize>> {
let alpha = self.config.alpha;
let max_degree = self.config.max_degree;
let num_vecs = self.num_vectors();
let mut dists: Vec<(usize, f32)> = candidates
.iter()
.filter(|&&c| c != node_id && c < num_vecs)
.filter_map(|&c| {
self.read_vector(c).ok().map(|vec| {
let dist = self.l2_distance(node_vec, &vec);
(c, dist)
})
})
.collect();
dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let mut pruned = Vec::new();
for (cand_id, cand_dist) in dists {
if pruned.len() >= max_degree {
break;
}
let mut should_add = true;
let cand_vec = self.read_vector(cand_id).ok();
if let Some(ref c_vec) = cand_vec {
for &selected_id in &pruned {
if let Ok(sel_vec) = self.read_vector(selected_id) {
let selected_dist = self.l2_distance(c_vec, &sel_vec);
if alpha * selected_dist < cand_dist {
should_add = false;
break;
}
}
}
} else {
should_add = false;
}
if should_add {
pruned.push(cand_id);
}
}
Ok(pruned)
}
fn l2_distance<T: AsRef<[f32]>, U: AsRef<[f32]>>(&self, a: T, b: U) -> f32 {
a.as_ref()
.iter()
.zip(b.as_ref().iter())
.map(|(x, y)| (x - y) * (x - y))
.sum::<f32>()
.sqrt()
}
pub fn search(&self, query: &[f32], k: usize) -> Result<Vec<SearchResult>> {
if !*self.loaded.read().unwrap_or_else(|e| e.into_inner()) {
return Err(Error::InvalidInput(
"Index not created or loaded".to_string(),
));
}
if query.len() != self.config.dimension {
return Err(Error::InvalidInput(format!(
"Query dimension {} doesn't match index dimension {}",
query.len(),
self.config.dimension
)));
}
let num_vectors = self.num_vectors();
if num_vectors == 0 {
return Ok(Vec::new());
}
let search_list_size = k.max(self.config.queue_size);
let result_ids = self.greedy_search_internal(query, k, search_list_size)?;
let id_to_cid = self.id_to_cid.read().unwrap_or_else(|e| e.into_inner());
let results: Vec<SearchResult> = result_ids
.iter()
.filter_map(|&id| {
id_to_cid.get(&id).and_then(|cid| {
self.read_vector(id).ok().map(|vec| SearchResult {
cid: *cid,
distance: self.l2_distance(query, &vec),
})
})
})
.collect();
Ok(results)
}
fn greedy_search_internal(
&self,
query: &[f32],
k: usize,
search_list_size: usize,
) -> Result<Vec<usize>> {
let graph = self.graph.read().unwrap_or_else(|e| e.into_inner());
let entry_points = self.entry_points.read().unwrap_or_else(|e| e.into_inner());
let num_vecs = self.num_vectors();
if num_vecs == 0 {
return Ok(Vec::new());
}
let start_nodes: Vec<usize> = if entry_points.is_empty() {
vec![0]
} else {
entry_points.clone()
};
let mut visited = vec![false; num_vecs];
let mut candidates: Vec<(f32, usize)> = Vec::new();
let mut results: Vec<(f32, usize)> = Vec::new();
for &node_id in &start_nodes {
if node_id >= num_vecs {
continue;
}
if let Ok(vec) = self.read_vector(node_id) {
let dist = self.l2_distance(query, &vec);
candidates.push((dist, node_id));
results.push((dist, node_id));
visited[node_id] = true;
}
}
candidates.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
results.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
while !candidates.is_empty() {
let (current_dist, current_id) = candidates.remove(0);
if results.len() >= search_list_size {
let furthest_dist = results[search_list_size - 1].0;
if current_dist > furthest_dist {
break;
}
}
if current_id >= graph.len() {
continue;
}
for &neighbor_id in &graph[current_id] {
if neighbor_id >= num_vecs || visited[neighbor_id] {
continue;
}
visited[neighbor_id] = true;
let dist = if let Ok(vec) = self.read_vector(neighbor_id) {
self.l2_distance(query, &vec)
} else {
continue;
};
candidates.push((dist, neighbor_id));
candidates
.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
results.push((dist, neighbor_id));
results.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
if results.len() > search_list_size {
results.truncate(search_list_size);
}
}
}
Ok(results.iter().take(k).map(|(_, id)| *id).collect())
}
pub fn stats(&self) -> DiskANNStats {
DiskANNStats {
num_vectors: *self.next_id.read().unwrap_or_else(|e| e.into_inner()),
dimension: self.config.dimension,
max_degree: self.config.max_degree,
index_loaded: *self.loaded.read().unwrap_or_else(|e| e.into_inner()),
estimated_disk_size: self.estimate_disk_size(),
}
}
fn estimate_disk_size(&self) -> usize {
let num_vectors = *self.next_id.read().unwrap_or_else(|e| e.into_inner());
let header_size = 1024;
let vector_size = num_vectors * self.config.dimension * 4;
let graph_size = num_vectors * self.config.max_degree * 4;
let mapping_size = num_vectors * 40;
header_size + vector_size + graph_size + mapping_size
}
pub fn is_loaded(&self) -> bool {
*self.loaded.read().unwrap_or_else(|e| e.into_inner())
}
pub fn config(&self) -> &DiskANNConfig {
&self.config
}
pub fn save(&self) -> Result<()> {
if !*self.loaded.read().unwrap_or_else(|e| e.into_inner()) {
return Err(Error::InvalidInput("Index not loaded".to_string()));
}
let path = self
.index_path
.read()
.unwrap_or_else(|e| e.into_inner())
.clone()
.ok_or_else(|| Error::InvalidInput("No index path set".to_string()))?;
let num_vecs = self.num_vectors();
let mut vectors = Vec::with_capacity(num_vecs);
for i in 0..num_vecs {
if let Ok(vec) = self.read_vector(i) {
vectors.push(vec);
}
}
let graph = self.graph.read().unwrap_or_else(|e| e.into_inner());
let id_to_cid = self.id_to_cid.read().unwrap_or_else(|e| e.into_inner());
let entry_points = self.entry_points.read().unwrap_or_else(|e| e.into_inner());
let data = DiskANNData::from_index(
vectors,
graph.clone(),
id_to_cid.clone(),
entry_points.clone(),
);
let serialized = oxicode::serde::encode_to_vec(&data, oxicode::config::standard())
.map_err(|e| Error::Serialization(e.to_string()))?;
let temp_path = format!("{}.tmp", path);
std::fs::write(&temp_path, &serialized).map_err(Error::Io)?;
std::fs::rename(&temp_path, &path).map_err(Error::Io)?;
Ok(())
}
pub fn flush(&self) -> Result<()> {
if let Some(ref mut mmap) = *self.graph_mmap.write().unwrap_or_else(|e| e.into_inner()) {
mmap.flush()
.map_err(|e| Error::Io(std::io::Error::other(e.to_string())))?;
}
Ok(())
}
pub fn compact(&mut self) -> Result<CompactionStats> {
if !*self.loaded.read().unwrap_or_else(|e| e.into_inner()) {
return Err(Error::InvalidInput("Index not loaded".to_string()));
}
let start_time = std::time::Instant::now();
let old_size = self.num_vectors();
let graph = self.graph.read().unwrap_or_else(|e| e.into_inner());
let old_graph_edges: usize = graph.iter().map(|neighbors| neighbors.len()).sum();
let stats = CompactionStats {
duration_ms: start_time.elapsed().as_millis() as u64,
vectors_before: old_size,
vectors_after: old_size,
graph_edges_before: old_graph_edges,
graph_edges_after: old_graph_edges,
bytes_saved: 0,
};
Ok(stats)
}
pub fn prune_graph(&mut self, quality_threshold: f32) -> Result<usize> {
if !*self.loaded.read().unwrap_or_else(|e| e.into_inner()) {
return Err(Error::InvalidInput("Index not loaded".to_string()));
}
let mut graph = self.graph.write().unwrap_or_else(|e| e.into_inner());
let num_vecs = self.num_vectors();
let mut total_pruned = 0;
for node_id in 0..graph.len() {
if node_id >= num_vecs {
continue;
}
let node_vec = match self.read_vector(node_id) {
Ok(v) => v,
Err(_) => continue,
};
let neighbors = &graph[node_id];
let mut neighbor_dists: Vec<(usize, f32)> = neighbors
.iter()
.filter(|&&n| n < num_vecs)
.filter_map(|&n| {
self.read_vector(n).ok().map(|vec| {
let dist = self.l2_distance(&node_vec, &vec);
(n, dist)
})
})
.collect();
neighbor_dists
.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
if let Some(&(_, best_dist)) = neighbor_dists.first() {
let threshold_dist = best_dist * (1.0 + quality_threshold);
let keep_count = neighbor_dists
.iter()
.filter(|(_, d)| *d <= threshold_dist)
.count();
if keep_count < neighbors.len() {
total_pruned += neighbors.len() - keep_count;
graph[node_id] = neighbor_dists
.iter()
.take(keep_count)
.map(|(n, _)| *n)
.collect();
}
}
}
Ok(total_pruned)
}
pub fn len(&self) -> usize {
*self.next_id.read().unwrap_or_else(|e| e.into_inner())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DiskANNData {
vectors: Vec<Vec<f32>>,
graph: Vec<Vec<usize>>,
id_to_cid: HashMap<usize, String>,
entry_points: Vec<usize>,
}
impl DiskANNData {
fn from_index(
vectors: Vec<Vec<f32>>,
graph: Vec<Vec<usize>>,
id_to_cid: HashMap<usize, Cid>,
entry_points: Vec<usize>,
) -> Self {
let id_to_cid_str = id_to_cid
.into_iter()
.map(|(k, v)| (k, v.to_string()))
.collect();
Self {
vectors,
graph,
id_to_cid: id_to_cid_str,
entry_points,
}
}
#[allow(dead_code)]
fn to_cid_map(&self) -> Result<HashMap<usize, Cid>> {
self.id_to_cid
.iter()
.map(|(k, v)| {
v.parse::<Cid>()
.map(|cid| (*k, cid))
.map_err(|e| Error::InvalidInput(format!("Invalid CID: {}", e)))
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct CompactionStats {
pub duration_ms: u64,
pub vectors_before: usize,
pub vectors_after: usize,
pub graph_edges_before: usize,
pub graph_edges_after: usize,
pub bytes_saved: usize,
}
#[derive(Debug, Clone)]
pub struct SearchResult {
pub cid: Cid,
pub distance: f32,
}
#[derive(Debug, Clone)]
pub struct DiskANNStats {
pub num_vectors: usize,
pub dimension: usize,
pub max_degree: usize,
pub index_loaded: bool,
pub estimated_disk_size: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diskann_create() {
let config = DiskANNConfig::default();
let mut index = DiskANNIndex::new(config);
let temp_file_path = std::env::temp_dir().join("test_diskann_index.dat");
let temp_file = temp_file_path
.to_str()
.expect("temp dir path is valid UTF-8");
assert!(index.create(temp_file).is_ok());
assert!(index.is_loaded());
std::fs::remove_file(temp_file).ok();
}
#[test]
fn test_diskann_stats() {
let index = DiskANNIndex::with_defaults(128);
let stats = index.stats();
assert_eq!(stats.dimension, 128);
assert_eq!(stats.num_vectors, 0);
assert!(!stats.index_loaded);
}
#[test]
fn test_index_header() {
let config = DiskANNConfig::default();
let header = IndexHeader::new(config);
assert_eq!(header.magic, IndexHeader::MAGIC);
assert_eq!(header.version, 1);
assert!(header.validate().is_ok());
let mut bad_header = header.clone();
bad_header.magic = [0; 8];
assert!(bad_header.validate().is_err());
}
#[test]
fn test_diskann_insert_and_search() {
let config = DiskANNConfig {
dimension: 4,
max_degree: 16,
queue_size: 50,
..Default::default()
};
let mut index = DiskANNIndex::new(config);
let temp_file_path = std::env::temp_dir().join("test_diskann_vamana.dat");
let temp_file = temp_file_path
.to_str()
.expect("temp dir path is valid UTF-8");
index
.create(temp_file)
.expect("test: index creation should succeed");
let vectors = [
vec![1.0, 0.0, 0.0, 0.0],
vec![0.9, 0.1, 0.0, 0.0],
vec![0.0, 1.0, 0.0, 0.0],
vec![0.0, 0.0, 1.0, 0.0],
vec![0.0, 0.0, 0.9, 0.1],
];
let base_cids = [
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354",
"bafybeihvvulpp6bcs5kum72jh5tkfo35dz2ow3lrqw4hmqyqbmfyvdqvdq",
"bafybeiakou6e7kkxc5qycjkqwucq4zfkfvzmlbf2vlihvqqnfjfzpqrkmq",
"bafybeibscyh5z3uk6fvdidffhybzsxmckblkjhajy4y4uzcglmfwqx67b4",
];
for (i, vec) in vectors.iter().enumerate() {
let cid: Cid = base_cids[i].parse().expect("test: CID string is valid");
index
.insert(&cid, vec)
.expect("test: vector insertion should succeed");
}
assert_eq!(index.stats().num_vectors, 5);
let query = vec![1.0, 0.0, 0.0, 0.0];
let results = index
.search(&query, 2)
.expect("test: search should succeed");
assert!(!results.is_empty());
assert!(results.len() <= 2);
assert!(results[0].distance < 0.2);
std::fs::remove_file(temp_file).ok();
}
#[test]
fn test_vamana_graph_construction() {
let config = DiskANNConfig {
dimension: 8,
max_degree: 8,
queue_size: 20,
alpha: 1.2,
..Default::default()
};
let max_degree = config.max_degree;
let mut index = DiskANNIndex::new(config);
let temp_file_path = std::env::temp_dir().join("test_vamana_graph.dat");
let temp_file = temp_file_path
.to_str()
.expect("temp dir path is valid UTF-8");
index
.create(temp_file)
.expect("test: index creation should succeed");
let base_cids: Vec<&str> = vec![
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354",
"bafybeihvvulpp6bcs5kum72jh5tkfo35dz2ow3lrqw4hmqyqbmfyvdqvdq",
"bafybeiakou6e7kkxc5qycjkqwucq4zfkfvzmlbf2vlihvqqnfjfzpqrkmq",
"bafybeibscyh5z3uk6fvdidffhybzsxmckblkjhajy4y4uzcglmfwqx67b4",
"bafybeiezkzpo2uy4teyix63fjc3vgpxlvhbmwjicxhxx6vaf3ywvkyz5ia",
"bafybeifmyetvpv2uovt7ncnvjcwvshwqrr7zmyh5wpqwmf5mwy3m42xkre",
"bafybeia7lv6vknr6fqjq2jlj3ygbdgzdqxqt7xo3u7dzz6ihfzd3zhd6pi",
"bafybeif2ewg3nqa33yvecifp7jw7p2utbnkh34j7ku44mzs3lpmcbdkjzq",
"bafybeid5cg74fzlh7okcaabfwexdvkiuocwbqhwrqc4x65jyplwsxzvvdq",
"bafybeicy6rxfqlcdadwjfjjvvb7wlbnlrzuzsogpv5snwt46zpqrmihtnq",
"bafybeie2kj53f4wmefncg3rvrvfegwk265iw2psfszftvq3slajlwkjfpm",
"bafybeigk7gjp4y4m4gwvmblvf7mlufsqtfgwyjdqwvwudytucvx7wtnz4e",
"bafybeihbsq7kdawlkzvfj7xttx27t4p52pkllmfevn5l2scgbvmgqcfmfy",
"bafybeiej5vfvbkjbzyeouqxkn25yb2xzdz2igdwmawcbhv66kwfwqnvhzi",
"bafybeigbkbpcxqbrvx56fqf7jb25r5wunzowl45uwmzcbxkwdtixlbtwim",
"bafybeihyfvtf3uiilqvqsvhbphfdudqy7qrjkxqglh26xxvjhtxrkhhbxe",
"bafybeicflzm3r35m4kj5chxjvdwgajq6ljhqpsjq6wdyqnlpfjwwb5nowi",
"bafybeic73hjrp52jxz33zxlz5qthfxumqpyuvqfvawdcskqiqlpuww3vxi",
"bafybeicbh5dkdyiq3gqufk46cktiwwucwl6mzhv6e5xhzmuvzojvykokpy",
];
for (i, &cid_str) in base_cids.iter().enumerate() {
let cid: Cid = cid_str.parse().expect("test: CID string is valid");
let vec: Vec<f32> = (0..8).map(|j| (i as f32 + j as f32) * 0.1).collect();
index
.insert(&cid, &vec)
.expect("test: vector insertion should succeed");
}
let graph = index.graph.read().unwrap_or_else(|e| e.into_inner());
assert_eq!(graph.len(), 20);
for (i, neighbors) in graph.iter().enumerate().skip(1) {
if i < 19 {
assert!(!neighbors.is_empty(), "Node {} should have neighbors", i);
assert!(
neighbors.len() <= max_degree,
"Node {} has too many neighbors: {}",
i,
neighbors.len()
);
}
}
std::fs::remove_file(temp_file).ok();
}
#[test]
fn test_robust_pruning() {
let config = DiskANNConfig {
dimension: 4,
max_degree: 3,
alpha: 1.2,
..Default::default()
};
let max_degree = config.max_degree;
let mut index = DiskANNIndex::new(config);
let temp_file_path = std::env::temp_dir().join("test_robust_prune.dat");
let temp_file = temp_file_path
.to_str()
.expect("temp dir path is valid UTF-8");
index
.create(temp_file)
.expect("test: index creation should succeed");
index
.ensure_vector_capacity(4)
.expect("test: capacity expansion for 4 vectors should succeed");
index
.write_vector(0, &[1.0, 0.0, 0.0, 0.0])
.expect("test: writing vector 0 should succeed");
index
.write_vector(1, &[0.9, 0.1, 0.0, 0.0])
.expect("test: writing vector 1 should succeed");
index
.write_vector(2, &[0.8, 0.2, 0.0, 0.0])
.expect("test: writing vector 2 should succeed");
index
.write_vector(3, &[0.0, 1.0, 0.0, 0.0])
.expect("test: writing vector 3 should succeed");
index
.update_vector_count(4)
.expect("test: updating vector count should succeed");
let node_vec = vec![1.0, 0.0, 0.0, 0.0];
let candidates = vec![1, 2, 3];
let pruned = index
.robust_prune(0, &node_vec, &candidates)
.expect("test: robust_prune should succeed");
assert!(pruned.len() <= max_degree);
assert!(pruned.contains(&1));
std::fs::remove_file(temp_file).ok();
}
#[test]
fn test_diskann_save_and_load() {
let config = DiskANNConfig {
dimension: 4,
max_degree: 16,
..Default::default()
};
let mut index = DiskANNIndex::new(config);
let temp_file_path = std::env::temp_dir().join("test_diskann_save.dat");
let temp_file = temp_file_path
.to_str()
.expect("temp dir path is valid UTF-8");
index
.create(temp_file)
.expect("test: index creation should succeed");
let vectors = [
vec![1.0, 0.0, 0.0, 0.0],
vec![0.0, 1.0, 0.0, 0.0],
vec![0.0, 0.0, 1.0, 0.0],
];
let base_cids = [
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354",
"bafybeihvvulpp6bcs5kum72jh5tkfo35dz2ow3lrqw4hmqyqbmfyvdqvdq",
];
for (i, vec) in vectors.iter().enumerate() {
let cid: Cid = base_cids[i].parse().expect("test: CID string is valid");
index
.insert(&cid, vec)
.expect("test: vector insertion should succeed");
}
assert!(index.save().is_ok());
std::fs::remove_file(temp_file).ok();
}
#[test]
fn test_diskann_flush() {
let config = DiskANNConfig {
dimension: 4,
..Default::default()
};
let mut index = DiskANNIndex::new(config);
let temp_file_path = std::env::temp_dir().join("test_diskann_flush.dat");
let temp_file = temp_file_path
.to_str()
.expect("temp dir path is valid UTF-8");
index
.create(temp_file)
.expect("test: index creation should succeed");
assert!(index.flush().is_ok());
std::fs::remove_file(temp_file).ok();
}
#[test]
fn test_diskann_compact() {
let config = DiskANNConfig {
dimension: 4,
max_degree: 16,
..Default::default()
};
let mut index = DiskANNIndex::new(config);
let temp_file_path = std::env::temp_dir().join("test_diskann_compact.dat");
let temp_file = temp_file_path
.to_str()
.expect("temp dir path is valid UTF-8");
index
.create(temp_file)
.expect("test: index creation should succeed");
let vectors = [
vec![1.0, 0.0, 0.0, 0.0],
vec![0.0, 1.0, 0.0, 0.0],
vec![0.0, 0.0, 1.0, 0.0],
];
let base_cids = [
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354",
"bafybeihvvulpp6bcs5kum72jh5tkfo35dz2ow3lrqw4hmqyqbmfyvdqvdq",
];
for (i, vec) in vectors.iter().enumerate() {
let cid: Cid = base_cids[i].parse().expect("test: CID string is valid");
index
.insert(&cid, vec)
.expect("test: vector insertion should succeed");
}
let stats = index.compact().expect("test: compact should succeed");
assert_eq!(stats.vectors_before, 3);
assert_eq!(stats.vectors_after, 3);
std::fs::remove_file(temp_file).ok();
}
#[test]
fn test_diskann_prune_graph() {
let config = DiskANNConfig {
dimension: 4,
max_degree: 16,
..Default::default()
};
let mut index = DiskANNIndex::new(config);
let temp_file_path = std::env::temp_dir().join("test_diskann_prune.dat");
let temp_file = temp_file_path
.to_str()
.expect("temp dir path is valid UTF-8");
index
.create(temp_file)
.expect("test: index creation should succeed");
let vectors = [
vec![1.0, 0.0, 0.0, 0.0],
vec![0.9, 0.1, 0.0, 0.0],
vec![0.8, 0.2, 0.0, 0.0],
vec![0.0, 0.0, 1.0, 0.0],
];
let base_cids = [
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354",
"bafybeihvvulpp6bcs5kum72jh5tkfo35dz2ow3lrqw4hmqyqbmfyvdqvdq",
"bafybeiakou6e7kkxc5qycjkqwucq4zfkfvzmlbf2vlihvqqnfjfzpqrkmq",
];
for (i, vec) in vectors.iter().enumerate() {
let cid: Cid = base_cids[i].parse().expect("test: CID string is valid");
index
.insert(&cid, vec)
.expect("test: vector insertion should succeed");
}
let _pruned = index
.prune_graph(0.5)
.expect("test: prune_graph should succeed");
std::fs::remove_file(temp_file).ok();
}
#[test]
fn test_diskann_len_and_is_empty() {
let config = DiskANNConfig {
dimension: 4,
..Default::default()
};
let mut index = DiskANNIndex::new(config);
let temp_file_path = std::env::temp_dir().join("test_diskann_len.dat");
let temp_file = temp_file_path
.to_str()
.expect("temp dir path is valid UTF-8");
index
.create(temp_file)
.expect("test: index creation should succeed");
assert_eq!(index.len(), 0);
assert!(index.is_empty());
let cid: Cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse()
.expect("test: CID string is valid");
let vec = vec![1.0, 0.0, 0.0, 0.0];
index
.insert(&cid, &vec)
.expect("test: vector insertion should succeed");
assert_eq!(index.len(), 1);
assert!(!index.is_empty());
std::fs::remove_file(temp_file).ok();
}
}