pub mod error;
mod ffi;
use std::os::raw::c_void;
use ffi::dump_index;
use crate::error::Result;
use crate::ffi::{
build_index, create_index, free_index, from_c_error, from_c_f32_vector, from_c_i64_vector,
knn_search_index, to_c_string,
};
pub struct VsagIndex {
ptr: *const c_void,
}
unsafe impl Send for VsagIndex {}
impl VsagIndex {
pub fn new(index_type: &str, params: &str) -> Result<Self> {
let index_type_c = to_c_string(index_type);
let parameters_c = to_c_string(params);
unsafe {
let out_index_ptr = &mut std::ptr::null();
let err = create_index(index_type_c.as_ptr(), parameters_c.as_ptr(), out_index_ptr);
if !err.is_null() {
Err(from_c_error(err))
} else {
Ok(VsagIndex {
ptr: *out_index_ptr,
})
}
}
}
pub fn build(
&self,
num_vectors: usize,
dim: usize,
ids: &[i64],
vectors: &[f32],
) -> Result<Vec<i64>> {
unsafe {
let out_failed_ids: *mut *const i64 = &mut std::ptr::null();
let out_num_failed: *mut usize = &mut 0;
let err = build_index(
self.ptr,
num_vectors,
dim,
ids.as_ptr(),
vectors.as_ptr(),
out_failed_ids,
out_num_failed,
);
if !err.is_null() {
Err(from_c_error(err))
} else {
Ok(from_c_i64_vector(*out_failed_ids, *out_num_failed))
}
}
}
pub fn knn_search(
&self,
query_vector: &[f32],
k: usize,
search_params: &str,
) -> Result<KnnSearchOutput> {
let search_params = to_c_string(search_params);
unsafe {
let out_ids: *mut *const i64 = &mut std::ptr::null();
let out_distances: *mut *const f32 = &mut std::ptr::null();
let out_num_results: *mut usize = &mut 0;
let err = knn_search_index(
self.ptr,
query_vector.len(),
query_vector.as_ptr(),
k,
search_params.as_ptr(),
out_ids,
out_distances,
out_num_results,
);
if !err.is_null() {
Err(from_c_error(err))
} else {
Ok(KnnSearchOutput {
ids: from_c_i64_vector(*out_ids, *out_num_results),
distances: from_c_f32_vector(*out_distances, *out_num_results),
})
}
}
}
pub fn dump(self, path: &str) -> Result<()> {
let path = to_c_string(path);
unsafe {
let err = dump_index(self.ptr, path.as_ptr());
if !err.is_null() {
Err(from_c_error(err))
} else {
Ok(())
}
}
}
pub fn load(path: &str, index_type: &str, params: &str) -> Result<Self> {
let path = to_c_string(path);
let index_type = to_c_string(index_type);
let params = to_c_string(params);
unsafe {
let out_index_ptr: *mut *const c_void = &mut std::ptr::null();
let err = ffi::load_index(
path.as_ptr(),
index_type.as_ptr(),
params.as_ptr(),
out_index_ptr,
);
if !err.is_null() {
Err(from_c_error(err))
} else {
Ok(VsagIndex {
ptr: *out_index_ptr,
})
}
}
}
}
impl Drop for VsagIndex {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe {
free_index(self.ptr);
}
}
}
}
pub struct KnnSearchOutput {
pub ids: Vec<i64>,
pub distances: Vec<f32>,
}
#[cfg(test)]
mod tests {
use simsimd::SpatialSimilarity;
use super::*;
#[test]
fn test_create_build_search_index_hnsw_l2() {
let index_type = "hnsw";
let con_params = r#"{
"dtype": "float32",
"metric_type": "l2",
"dim": 128,
"hnsw": {
"max_degree": 16,
"ef_construction": 100
}
}"#;
let search_params = r#"{
"hnsw": {
"ef_search": 100
}
}"#;
let index = VsagIndex::new(index_type, con_params).unwrap();
let num_vectors: usize = 1000;
let dim: usize = 128;
let ids: Vec<i64> = (0..num_vectors as i64).collect();
let vectors = (0..num_vectors)
.map(|_| {
(0..dim)
.map(|_| rand::random::<f32>())
.collect::<Vec<f32>>()
})
.collect::<Vec<_>>();
let vectors_for_index: Vec<f32> = vectors.iter().flat_map(|v| v.iter().copied()).collect();
let failed_ids = index
.build(num_vectors, dim, &ids, &vectors_for_index)
.unwrap();
assert_eq!(failed_ids.len(), 0);
let query_vector: Vec<f32> = (0..dim).map(|_| rand::random()).collect();
let k = 10;
let output = index.knn_search(&query_vector, k, search_params).unwrap();
assert_eq!(output.ids.len(), k.min(num_vectors));
assert_eq!(output.distances.len(), k.min(num_vectors));
let mut distances = vectors
.iter()
.zip(ids.iter())
.map(|(v, id)| {
let d: f32 = f32::l2sq(&query_vector, &v).unwrap() as _;
(d, *id)
})
.collect::<Vec<_>>();
distances.sort_by(|(a, _), (b, _)| a.total_cmp(b));
distances.truncate(k.min(num_vectors));
let dir = tempdir::TempDir::new("test_create_build_search_index_l2_").unwrap();
let path = dir.path().join("index");
index.dump(path.to_str().unwrap()).unwrap();
let index = VsagIndex::load(path.to_str().unwrap(), index_type, con_params).unwrap();
let output2 = index.knn_search(&query_vector, k, search_params).unwrap();
assert_eq!(output.ids, output2.ids);
assert_eq!(output.distances, output2.distances);
}
#[test]
fn test_create_build_search_index_cos() {
let index_type = "hnsw";
let con_params = r#"{
"dtype": "float32",
"metric_type": "cosine",
"dim": 128,
"hnsw": {
"max_degree": 16,
"ef_construction": 100
}
}"#;
let search_params = r#"{
"hnsw": {
"ef_search": 100
}
}"#;
let index = VsagIndex::new(index_type, con_params).unwrap();
let num_vectors: usize = 1000;
let dim: usize = 128;
let ids: Vec<i64> = (0..num_vectors as i64).collect();
let vectors = (0..num_vectors)
.map(|_| {
(0..dim)
.map(|_| rand::random::<f32>())
.collect::<Vec<f32>>()
})
.collect::<Vec<_>>();
let vectors_for_index: Vec<f32> = vectors.iter().flat_map(|v| v.iter().copied()).collect();
let failed_ids = index
.build(num_vectors, dim, &ids, &vectors_for_index)
.unwrap();
assert_eq!(failed_ids.len(), 0);
let query_vector: Vec<f32> = (0..dim).map(|_| rand::random()).collect();
let k = 10;
let output = index.knn_search(&query_vector, k, search_params).unwrap();
assert_eq!(output.ids.len(), k.min(num_vectors));
assert_eq!(output.distances.len(), k.min(num_vectors));
let mut distances = vectors
.iter()
.zip(ids.iter())
.map(|(v, id)| {
let d: f32 = <f32 as SpatialSimilarity>::cos(&query_vector, &v).unwrap() as _;
(d, *id)
})
.collect::<Vec<_>>();
distances.sort_by(|(a, _), (b, _)| a.total_cmp(b));
distances.truncate(k.min(num_vectors));
let dir = tempdir::TempDir::new("test_create_build_search_index_cos").unwrap();
let path = dir.path().join("index");
index.dump(path.to_str().unwrap()).unwrap();
let index = VsagIndex::load(path.to_str().unwrap(), index_type, con_params).unwrap();
let output2 = index.knn_search(&query_vector, k, search_params).unwrap();
assert_eq!(output.ids, output2.ids);
assert_eq!(output.distances, output2.distances);
}
}