use anyhow::{anyhow, Result};
use memmap2::{Mmap, MmapMut};
use std::collections::HashSet;
use std::fs::{File, OpenOptions};
use std::io::{BufReader, Read};
use std::path::Path;
use tracing::info;
use super::vamana::{DistanceMetric, VamanaConfig, VamanaIndex, VamanaNode, VectorStorage};
const MAGIC: [u8; 4] = *b"VAMA";
const VERSION: u32 = 1;
const HEADER_SIZE: usize = 64;
const ALIGNMENT: usize = 64;
#[repr(C, packed)]
#[derive(Debug, Clone, Copy)]
struct VamanaHeader {
magic: [u8; 4],
version: u32,
num_vectors: u64,
dimension: u32,
max_degree: u32,
medoid: u32,
distance_metric: u8,
deleted_count: u32,
incremental_inserts: u64,
checksum: u64,
reserved: [u8; 15],
}
impl VamanaHeader {
fn new(
num_vectors: usize,
dimension: usize,
max_degree: usize,
medoid: u32,
distance_metric: DistanceMetric,
deleted_count: usize,
incremental_inserts: usize,
) -> Self {
Self {
magic: MAGIC,
version: VERSION,
num_vectors: num_vectors as u64,
dimension: dimension as u32,
max_degree: max_degree as u32,
medoid,
distance_metric: match distance_metric {
DistanceMetric::NormalizedDotProduct => 0,
DistanceMetric::Euclidean => 1,
DistanceMetric::Cosine => 2,
},
deleted_count: deleted_count as u32,
incremental_inserts: incremental_inserts as u64,
checksum: 0, reserved: [0u8; 15],
}
}
fn to_bytes(&self) -> [u8; HEADER_SIZE] {
let mut bytes = [0u8; HEADER_SIZE];
bytes[0..4].copy_from_slice(&self.magic);
bytes[4..8].copy_from_slice(&self.version.to_le_bytes());
bytes[8..16].copy_from_slice(&self.num_vectors.to_le_bytes());
bytes[16..20].copy_from_slice(&self.dimension.to_le_bytes());
bytes[20..24].copy_from_slice(&self.max_degree.to_le_bytes());
bytes[24..28].copy_from_slice(&self.medoid.to_le_bytes());
bytes[28] = self.distance_metric;
bytes[29..33].copy_from_slice(&self.deleted_count.to_le_bytes());
bytes[33..41].copy_from_slice(&self.incremental_inserts.to_le_bytes());
bytes[41..49].copy_from_slice(&self.checksum.to_le_bytes());
bytes
}
fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() < HEADER_SIZE {
return Err(anyhow!("Header too small"));
}
let magic: [u8; 4] = bytes[0..4].try_into()?;
if magic != MAGIC {
return Err(anyhow!("Invalid magic bytes: {:?}", magic));
}
let version = u32::from_le_bytes(bytes[4..8].try_into()?);
if version != VERSION {
return Err(anyhow!("Unsupported version: {}", version));
}
Ok(Self {
magic,
version,
num_vectors: u64::from_le_bytes(bytes[8..16].try_into()?),
dimension: u32::from_le_bytes(bytes[16..20].try_into()?),
max_degree: u32::from_le_bytes(bytes[20..24].try_into()?),
medoid: u32::from_le_bytes(bytes[24..28].try_into()?),
distance_metric: bytes[28],
deleted_count: u32::from_le_bytes(bytes[29..33].try_into()?),
incremental_inserts: u64::from_le_bytes(bytes[33..41].try_into()?),
checksum: u64::from_le_bytes(bytes[41..49].try_into()?),
reserved: [0u8; 15],
})
}
fn distance_metric_enum(&self) -> DistanceMetric {
match self.distance_metric {
0 => DistanceMetric::NormalizedDotProduct,
1 => DistanceMetric::Euclidean,
2 => DistanceMetric::Cosine,
_ => DistanceMetric::NormalizedDotProduct,
}
}
}
fn compute_checksum(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325; for byte in data {
hash ^= *byte as u64;
hash = hash.wrapping_mul(0x100000001b3); }
hash
}
fn align_to(offset: usize, alignment: usize) -> usize {
(offset + alignment - 1) & !(alignment - 1)
}
impl VamanaIndex {
pub fn save_to_file(&self, path: &Path) -> Result<()> {
let start = std::time::Instant::now();
let graph = self.graph.read();
let vectors = self.vectors.read();
let deleted_ids = self.deleted_ids.read();
let medoid = *self.medoid.read();
let num_vectors = self.num_vectors.load(std::sync::atomic::Ordering::Acquire);
let incremental_inserts = self
.incremental_inserts
.load(std::sync::atomic::Ordering::Acquire);
let vector_data: Vec<f32> = match &*vectors {
VectorStorage::Memory(vecs) => vecs.iter().flatten().copied().collect(),
VectorStorage::Mmap {
mmap,
dimension,
num_vectors,
} => {
let total_floats = dimension * num_vectors;
let bytes = &mmap[..total_floats * 4];
bytes
.chunks_exact(4)
.map(|b| f32::from_le_bytes(b.try_into().unwrap()))
.collect()
}
};
let mut header = VamanaHeader::new(
num_vectors,
self.config.dimension,
self.config.max_degree,
medoid,
self.config.distance_metric,
deleted_ids.len(),
incremental_inserts,
);
let deleted_section_size = deleted_ids.len() * 4;
let mut graph_section_size = 0;
for node in graph.iter() {
graph_section_size += 2 + node.neighbors.len() * 4; }
let vectors_offset = align_to(
HEADER_SIZE + deleted_section_size + graph_section_size,
ALIGNMENT,
);
let vectors_section_size = vector_data.len() * 4;
let total_size = vectors_offset + vectors_section_size;
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)?;
file.set_len(total_size as u64)?;
let mut mmap = unsafe { MmapMut::map_mut(&file)? };
let header_bytes = header.to_bytes();
mmap[..HEADER_SIZE].copy_from_slice(&header_bytes);
let mut offset = HEADER_SIZE;
for &id in deleted_ids.iter() {
mmap[offset..offset + 4].copy_from_slice(&id.to_le_bytes());
offset += 4;
}
for node in graph.iter() {
let count = node.neighbors.len() as u16;
mmap[offset..offset + 2].copy_from_slice(&count.to_le_bytes());
offset += 2;
for &neighbor in &node.neighbors {
mmap[offset..offset + 4].copy_from_slice(&neighbor.to_le_bytes());
offset += 4;
}
}
offset = vectors_offset;
for val in &vector_data {
mmap[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
offset += 4;
}
let checksum = compute_checksum(&mmap[HEADER_SIZE..]);
header.checksum = checksum;
let header_bytes = header.to_bytes();
mmap[..HEADER_SIZE].copy_from_slice(&header_bytes);
mmap.flush()?;
info!(
"Saved Vamana index: {} vectors, {} bytes in {:?}",
num_vectors,
total_size,
start.elapsed()
);
Ok(())
}
pub fn load_from_file(path: &Path) -> Result<Self> {
let start = std::time::Instant::now();
if !path.exists() {
return Err(anyhow!("Index file not found: {:?}", path));
}
let file = File::open(path)?;
let mmap = unsafe { Mmap::map(&file)? };
let header = VamanaHeader::from_bytes(&mmap[..HEADER_SIZE])?;
let stored_checksum = header.checksum;
let computed_checksum = compute_checksum(&mmap[HEADER_SIZE..]);
if stored_checksum != computed_checksum {
return Err(anyhow!(
"Checksum mismatch: stored={}, computed={}",
stored_checksum,
computed_checksum
));
}
let num_vectors = header.num_vectors as usize;
let dimension = header.dimension as usize;
let deleted_count = header.deleted_count as usize;
let mut offset = HEADER_SIZE;
let mut deleted_ids = HashSet::with_capacity(deleted_count);
for _ in 0..deleted_count {
let id = u32::from_le_bytes(mmap[offset..offset + 4].try_into()?);
deleted_ids.insert(id);
offset += 4;
}
let mut graph = Vec::with_capacity(num_vectors);
for node_id in 0..num_vectors {
let count = u16::from_le_bytes(mmap[offset..offset + 2].try_into()?) as usize;
offset += 2;
let mut neighbors = Vec::with_capacity(count);
for _ in 0..count {
let neighbor = u32::from_le_bytes(mmap[offset..offset + 4].try_into()?);
neighbors.push(neighbor);
offset += 4;
}
graph.push(VamanaNode {
id: node_id as u32,
neighbors,
});
}
let vectors_offset = align_to(offset, ALIGNMENT);
let vectors_bytes = &mmap[vectors_offset..];
let mut vectors = Vec::with_capacity(num_vectors);
for i in 0..num_vectors {
let start = i * dimension * 4;
let end = start + dimension * 4;
let vec: Vec<f32> = vectors_bytes[start..end]
.chunks_exact(4)
.map(|b| f32::from_le_bytes(b.try_into().unwrap()))
.collect();
vectors.push(vec);
}
let config = VamanaConfig {
max_degree: header.max_degree as usize,
search_list_size: 75, alpha: 1.2, dimension,
use_mmap: false, distance_metric: header.distance_metric_enum(),
};
let index = VamanaIndex {
config,
graph: std::sync::Arc::new(parking_lot::RwLock::new(graph)),
vectors: std::sync::Arc::new(parking_lot::RwLock::new(VectorStorage::Memory(vectors))),
medoid: std::sync::Arc::new(parking_lot::RwLock::new(header.medoid)),
num_vectors: std::sync::atomic::AtomicUsize::new(num_vectors),
storage_path: Some(path.to_path_buf()),
incremental_inserts: std::sync::atomic::AtomicUsize::new(
header.incremental_inserts as usize,
),
rebuilding: std::sync::atomic::AtomicBool::new(false),
deleted_ids: std::sync::Arc::new(parking_lot::RwLock::new(deleted_ids)),
};
info!(
"Loaded Vamana index: {} vectors in {:?}",
num_vectors,
start.elapsed()
);
Ok(index)
}
pub fn index_file_exists(path: &Path) -> bool {
if !path.exists() {
return false;
}
if let Ok(file) = File::open(path) {
let mut header_bytes = [0u8; HEADER_SIZE];
let mut reader = BufReader::new(file);
if reader.read_exact(&mut header_bytes).is_ok() {
return VamanaHeader::from_bytes(&header_bytes).is_ok();
}
}
false
}
pub fn verify_index_file(path: &Path) -> Result<bool> {
let file = File::open(path)?;
let mmap = unsafe { Mmap::map(&file)? };
if mmap.len() < HEADER_SIZE {
return Ok(false);
}
let header = VamanaHeader::from_bytes(&mmap[..HEADER_SIZE])?;
let stored_checksum = header.checksum;
let computed_checksum = compute_checksum(&mmap[HEADER_SIZE..]);
Ok(stored_checksum == computed_checksum)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_save_and_load() {
let temp_dir = tempdir().unwrap();
let index_path = temp_dir.path().join("test.vamana");
let config = VamanaConfig {
dimension: 4,
max_degree: 8,
use_mmap: false,
..Default::default()
};
let mut index = VamanaIndex::new(config).unwrap();
let vectors = vec![
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],
vec![0.0, 0.0, 0.0, 1.0],
vec![0.5, 0.5, 0.0, 0.0],
];
index.build(vectors.clone()).unwrap();
index.save_to_file(&index_path).unwrap();
assert!(index_path.exists());
assert!(VamanaIndex::verify_index_file(&index_path).unwrap());
let loaded = VamanaIndex::load_from_file(&index_path).unwrap();
assert_eq!(loaded.len(), 5);
let query = vec![1.0, 0.0, 0.0, 0.0];
let results = loaded.search(&query, 3).unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].0, 0); }
#[test]
fn test_checksum_detects_corruption() {
let temp_dir = tempdir().unwrap();
let index_path = temp_dir.path().join("corrupt.vamana");
let config = VamanaConfig {
dimension: 4,
use_mmap: false,
..Default::default()
};
let mut index = VamanaIndex::new(config).unwrap();
index
.build(vec![vec![1.0, 0.0, 0.0, 0.0], vec![0.0, 1.0, 0.0, 0.0]])
.unwrap();
index.save_to_file(&index_path).unwrap();
let file = OpenOptions::new()
.read(true)
.write(true)
.open(&index_path)
.unwrap();
let mut mmap = unsafe { MmapMut::map_mut(&file).unwrap() };
mmap[HEADER_SIZE + 10] ^= 0xFF; mmap.flush().unwrap();
assert!(!VamanaIndex::verify_index_file(&index_path).unwrap());
}
#[test]
fn test_header_serialization() {
let header = VamanaHeader::new(
1000,
384,
32,
42,
DistanceMetric::NormalizedDotProduct,
5,
100,
);
let bytes = header.to_bytes();
let restored = VamanaHeader::from_bytes(&bytes).unwrap();
let num_vectors = restored.num_vectors;
let dimension = restored.dimension;
let max_degree = restored.max_degree;
let medoid = restored.medoid;
let deleted_count = restored.deleted_count;
let incremental_inserts = restored.incremental_inserts;
assert_eq!(num_vectors, 1000);
assert_eq!(dimension, 384);
assert_eq!(max_degree, 32);
assert_eq!(medoid, 42);
assert_eq!(deleted_count, 5);
assert_eq!(incremental_inserts, 100);
}
}