use std::io::Cursor;
use byteorder::{LittleEndian, ReadBytesExt};
use diskann::{ANNError, ANNResult};
use thiserror::Error;
use super::{GraphLayoutVersion, GraphMetadata};
pub struct GraphHeader {
metadata: GraphMetadata,
block_size: u64,
layout_version: GraphLayoutVersion,
}
#[derive(Error, Debug, PartialEq)]
pub enum GraphHeaderError {
#[error("Overflow occurred during max_degree calculation.")]
MaxDegreeOverflow,
#[error("Unsupported graph layout version {0} for max_degree calculation.")]
MaxDegreeUnsupportedLayoutVersion(GraphLayoutVersion),
}
impl From<GraphHeaderError> for ANNError {
#[track_caller]
fn from(value: GraphHeaderError) -> Self {
ANNError::log_index_error(value)
}
}
impl GraphHeader {
pub const CURRENT_LAYOUT_VERSION: GraphLayoutVersion = GraphLayoutVersion::new(1, 0);
pub fn new(
metadata: GraphMetadata,
block_size: u64,
layout_version: GraphLayoutVersion,
) -> Self {
Self {
metadata,
block_size,
layout_version,
}
}
pub fn to_bytes(&self) -> ANNResult<Vec<u8>> {
let mut buffer = vec![];
buffer.extend_from_slice(self.metadata.to_bytes()?.as_ref());
buffer.extend_from_slice(self.block_size.to_le_bytes().as_ref());
buffer.extend_from_slice(self.layout_version.to_bytes().as_ref());
Ok(buffer)
}
#[inline]
pub fn get_size() -> usize {
GraphMetadata::get_size()
+ std::mem::size_of::<u64>()
+ std::mem::size_of::<GraphLayoutVersion>()
}
pub fn metadata(&self) -> &GraphMetadata {
&self.metadata
}
pub fn block_size(&self) -> u64 {
self.block_size
}
pub fn layout_version(&self) -> &GraphLayoutVersion {
&self.layout_version
}
pub fn max_degree<DataType>(&self) -> Result<usize, GraphHeaderError> {
let supported_versions = [GraphLayoutVersion::new(0, 0), GraphLayoutVersion::new(1, 0)];
if supported_versions.contains(&self.layout_version) {
let vector_len = std::mem::size_of::<DataType>() * self.metadata.dims;
let max_degree = (self.metadata.node_len as usize)
.checked_sub(vector_len)
.and_then(|len| len.checked_sub(self.metadata.associated_data_length))
.and_then(|len| len.checked_div(std::mem::size_of::<u32>()))
.and_then(|len| len.checked_sub(1));
match max_degree {
Some(degree) => Ok(degree),
None => Err(GraphHeaderError::MaxDegreeOverflow),
}
} else {
Err(GraphHeaderError::MaxDegreeUnsupportedLayoutVersion(
self.layout_version.clone(),
))
}
}
}
impl<'a> TryFrom<&'a [u8]> for GraphHeader {
type Error = ANNError;
fn try_from(value: &'a [u8]) -> ANNResult<Self> {
if value.len() < Self::get_size() {
Err(ANNError::log_parse_slice_error(
"&[u8]".to_string(),
"GraphHeader".to_string(),
"The given bytes are not long enough to create a valid graph header.".to_string(),
))
} else {
let metadata_len = GraphMetadata::get_size();
let metadata = GraphMetadata::try_from(&value[0..metadata_len])?;
let block_size = Cursor::new(&value[metadata_len..]).read_u64::<LittleEndian>()?;
let layout_version = GraphLayoutVersion::try_from(&value[metadata_len + 8..])?;
Ok(Self::new(metadata, block_size, layout_version))
}
}
}
#[cfg(test)]
mod tests {
use diskann::ANNErrorKind;
use rstest::rstest;
use super::*;
use crate::data_model::{GraphHeader, GraphLayoutVersion, GraphMetadata};
#[test]
fn test_graph_header_to_bytes_and_try_from() {
let layout_version = GraphLayoutVersion::new(1, 0);
let block_size = 128;
let num_pts = 1000;
let dims = 32;
let medoid = 500;
let node_len = 64;
let num_nodes_per_sector = 4;
let vamana_frozen_num = 20;
let vamana_frozen_loc = 50;
let disk_index_file_size = 1024;
let data_size = 256;
let metadata = GraphMetadata::new(
num_pts,
dims,
medoid,
node_len,
num_nodes_per_sector,
vamana_frozen_num,
vamana_frozen_loc,
disk_index_file_size,
data_size,
);
let header = GraphHeader::new(metadata.clone(), block_size, layout_version.clone());
let bytes = header.to_bytes().unwrap();
assert_eq!(bytes.len(), GraphHeader::get_size());
let deserialized_header = GraphHeader::try_from(bytes.as_slice()).unwrap();
assert_eq!(metadata.num_pts, deserialized_header.metadata.num_pts);
assert_eq!(metadata.dims, deserialized_header.metadata.dims);
assert_eq!(metadata.medoid, deserialized_header.metadata.medoid);
assert_eq!(metadata.node_len, deserialized_header.metadata.node_len);
assert_eq!(
metadata.num_nodes_per_block,
deserialized_header.metadata.num_nodes_per_block
);
assert_eq!(
metadata.vamana_frozen_num,
deserialized_header.metadata.vamana_frozen_num
);
assert_eq!(
metadata.vamana_frozen_loc,
deserialized_header.metadata.vamana_frozen_loc
);
assert_eq!(
metadata.disk_index_file_size,
deserialized_header.metadata.disk_index_file_size
);
assert_eq!(
metadata.associated_data_length,
deserialized_header.metadata.associated_data_length
);
assert_eq!(block_size, deserialized_header.block_size);
assert_eq!(layout_version, deserialized_header.layout_version);
}
#[test]
fn test_graph_header_try_from_invalid_bytes() {
let invalid_bytes = vec![1; GraphHeader::get_size() - 1];
let result = GraphHeader::try_from(&invalid_bytes[..]);
assert!(result.is_err());
}
#[rstest]
#[case(384, 1008, 0, 59, GraphLayoutVersion::new(0, 0))]
#[case(384, 1008, 0, 59, GraphLayoutVersion::new(1, 0))]
#[case(3072, 6384, 0, 59, GraphLayoutVersion::new(0, 0))]
#[case(3072, 6384, 0, 59, GraphLayoutVersion::new(1, 0))]
#[case(384, 1008, 0, 59, GraphHeader::CURRENT_LAYOUT_VERSION)]
fn test_graph_header_max_degree(
#[case] dims: usize,
#[case] node_len: u64,
#[case] data_size: usize,
#[case] expected_max_degree: usize,
#[case] layout_version: GraphLayoutVersion,
) {
let num_pts = 1000;
let medoid = 500;
let num_nodes_per_sector = 4;
let vamana_frozen_num = 20;
let vamana_frozen_loc = 50;
let disk_index_file_size = 1024;
let metadata = GraphMetadata::new(
num_pts,
dims,
medoid,
node_len,
num_nodes_per_sector,
vamana_frozen_num,
vamana_frozen_loc,
disk_index_file_size,
data_size,
);
let block_size = 128;
let header = GraphHeader::new(metadata, block_size, layout_version);
let max_degree = header.max_degree::<diskann_vector::Half>();
assert!(max_degree.is_ok());
assert_eq!(max_degree.unwrap(), expected_max_degree);
}
#[rstest]
#[case(1, 1)]
#[case(2, 0)]
fn test_graph_header_max_degree_unsupported_layout_version(
#[case] major_version: u32,
#[case] minor_version: u32,
) {
let num_pts = 1000;
let dims = 32;
let medoid = 500;
let node_len = 64;
let num_nodes_per_sector = 4;
let vamana_frozen_num = 20;
let vamana_frozen_loc = 50;
let disk_index_file_size = 1024;
let data_size = 256;
let metadata = GraphMetadata::new(
num_pts,
dims,
medoid,
node_len,
num_nodes_per_sector,
vamana_frozen_num,
vamana_frozen_loc,
disk_index_file_size,
data_size,
);
let layout_version = GraphLayoutVersion::new(major_version, minor_version);
let block_size = 128;
let header = GraphHeader::new(metadata, block_size, layout_version.clone());
let max_degree = header.max_degree::<diskann_vector::Half>();
assert!(max_degree.is_err());
assert_eq!(
max_degree.unwrap_err(),
GraphHeaderError::MaxDegreeUnsupportedLayoutVersion(layout_version.clone())
);
}
#[test]
fn test_graph_header_max_degree_overflow() {
let dims = 384;
let node_len = 384;
let data_size = 0;
let num_pts = 1000;
let medoid = 500;
let num_nodes_per_sector = 4;
let vamana_frozen_num = 20;
let vamana_frozen_loc = 50;
let disk_index_file_size = 1024;
let metadata = GraphMetadata::new(
num_pts,
dims,
medoid,
node_len,
num_nodes_per_sector,
vamana_frozen_num,
vamana_frozen_loc,
disk_index_file_size,
data_size,
);
let layout_version = GraphLayoutVersion::new(1, 0);
let block_size = 128;
let header = GraphHeader::new(metadata, block_size, layout_version);
let max_degree = header.max_degree::<diskann_vector::Half>();
assert!(max_degree.is_err());
assert_eq!(max_degree.unwrap_err(), GraphHeaderError::MaxDegreeOverflow);
}
#[test]
fn test_graph_header_error_conversion() {
let error = GraphHeaderError::MaxDegreeOverflow;
let ann_error: ANNError = error.into();
assert_eq!(ann_error.kind(), ANNErrorKind::IndexError);
let layout_version = GraphLayoutVersion::new(1, 0);
let error = GraphHeaderError::MaxDegreeUnsupportedLayoutVersion(layout_version);
let ann_error: ANNError = error.into();
assert_eq!(ann_error.kind(), ANNErrorKind::IndexError);
}
}