use std::ffi::CString;
use std::os::raw::c_int;
use std::path::Path;
use std::ptr;
use crate::doc::{Doc, DocList, DocMap, WriteResults};
use crate::error::{check_error, Result};
use crate::ffi;
use crate::query::{GroupByVectorQuery, GroupResults, VectorQuery};
use crate::schema::{CollectionSchema, FieldSchema};
use crate::types::{IndexType, MetricType, QuantizeType};
pub struct CollectionStats {
ptr: *mut ffi::zvec_collection_stats_t,
}
impl CollectionStats {
pub fn doc_count(&self) -> u64 {
unsafe { ffi::zvec_collection_stats_get_doc_count(self.ptr) }
}
pub fn index_count(&self) -> usize {
unsafe { ffi::zvec_collection_stats_get_index_count(self.ptr) }
}
pub fn index_name(&self, index: usize) -> Option<&str> {
unsafe {
let p = ffi::zvec_collection_stats_get_index_name(self.ptr, index);
if p.is_null() {
None
} else {
std::ffi::CStr::from_ptr(p).to_str().ok()
}
}
}
pub fn index_completeness(&self, index: usize) -> f32 {
unsafe { ffi::zvec_collection_stats_get_index_completeness(self.ptr, index) }
}
}
impl Drop for CollectionStats {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { ffi::zvec_collection_stats_destroy(self.ptr) };
}
}
}
pub struct Collection {
ptr: *mut ffi::zvec_collection_t,
path: Option<String>,
}
impl Collection {
pub fn create_and_open<P: AsRef<Path>>(path: P, schema: CollectionSchema) -> Result<Self> {
let path_str = path.as_ref().to_string_lossy().into_owned();
let path_c = CString::new(path_str.as_str())
.map_err(|e| crate::error::Error::InvalidArgument(e.to_string()))?;
let mut handle: *mut ffi::zvec_collection_t = ptr::null_mut();
let code = unsafe {
ffi::zvec_collection_create_and_open(
path_c.as_ptr(),
schema.ptr,
ptr::null(),
&mut handle,
)
};
std::mem::forget(schema);
check_error(code as c_int)?;
if handle.is_null() {
return Err(crate::error::Error::InternalError(
"Failed to create collection: null pointer".into(),
));
}
Ok(Self {
ptr: handle,
path: Some(path_str),
})
}
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let path_str = path.as_ref().to_string_lossy().into_owned();
let path_c = CString::new(path_str.as_str())
.map_err(|e| crate::error::Error::InvalidArgument(e.to_string()))?;
let mut handle: *mut ffi::zvec_collection_t = ptr::null_mut();
let code = unsafe { ffi::zvec_collection_open(path_c.as_ptr(), ptr::null(), &mut handle) };
check_error(code as c_int)?;
if handle.is_null() {
return Err(crate::error::Error::InternalError(
"Failed to open collection: null pointer".into(),
));
}
Ok(Self {
ptr: handle,
path: Some(path_str),
})
}
pub fn path(&self) -> Result<String> {
Ok(self.path.clone().unwrap_or_default())
}
pub fn insert(&self, docs: &[Doc]) -> Result<WriteResults> {
let doc_ptrs: Vec<*const ffi::zvec_doc_t> = docs
.iter()
.map(|d| d.ptr as *const ffi::zvec_doc_t)
.collect();
let mut results: *mut ffi::zvec_write_result_t = ptr::null_mut();
let mut result_count: usize = 0;
let code = unsafe {
ffi::zvec_collection_insert_with_results(
self.ptr,
doc_ptrs.as_ptr() as *mut *const ffi::zvec_doc_t,
doc_ptrs.len(),
&mut results,
&mut result_count,
)
};
check_error(code as c_int)?;
Ok(WriteResults::from_raw(results, result_count))
}
pub fn insert_counted(&self, docs: &[Doc]) -> Result<(usize, usize)> {
let doc_ptrs: Vec<*const ffi::zvec_doc_t> = docs
.iter()
.map(|d| d.ptr as *const ffi::zvec_doc_t)
.collect();
let mut success: usize = 0;
let mut errors: usize = 0;
let code = unsafe {
ffi::zvec_collection_insert(
self.ptr,
doc_ptrs.as_ptr() as *mut *const ffi::zvec_doc_t,
doc_ptrs.len(),
&mut success,
&mut errors,
)
};
check_error(code as c_int)?;
Ok((success, errors))
}
pub fn upsert_one(&self, doc: Doc) -> Result<WriteResults> {
self.upsert(&[doc])
}
pub fn insert_one(&self, doc: Doc) -> Result<WriteResults> {
self.insert(&[doc])
}
pub fn upsert(&self, docs: &[Doc]) -> Result<WriteResults> {
let doc_ptrs: Vec<*const ffi::zvec_doc_t> = docs
.iter()
.map(|d| d.ptr as *const ffi::zvec_doc_t)
.collect();
let mut results: *mut ffi::zvec_write_result_t = ptr::null_mut();
let mut result_count: usize = 0;
let code = unsafe {
ffi::zvec_collection_upsert_with_results(
self.ptr,
doc_ptrs.as_ptr() as *mut *const ffi::zvec_doc_t,
doc_ptrs.len(),
&mut results,
&mut result_count,
)
};
check_error(code as c_int)?;
Ok(WriteResults::from_raw(results, result_count))
}
pub fn update(&self, docs: &[Doc]) -> Result<WriteResults> {
let doc_ptrs: Vec<*const ffi::zvec_doc_t> = docs
.iter()
.map(|d| d.ptr as *const ffi::zvec_doc_t)
.collect();
let mut results: *mut ffi::zvec_write_result_t = ptr::null_mut();
let mut result_count: usize = 0;
let code = unsafe {
ffi::zvec_collection_update_with_results(
self.ptr,
doc_ptrs.as_ptr() as *mut *const ffi::zvec_doc_t,
doc_ptrs.len(),
&mut results,
&mut result_count,
)
};
check_error(code as c_int)?;
Ok(WriteResults::from_raw(results, result_count))
}
pub fn update_one(&self, doc: Doc) -> Result<WriteResults> {
self.update(&[doc])
}
pub fn delete(&self, pks: &[&str]) -> Result<WriteResults> {
let pk_cstrings: Vec<CString> = pks
.iter()
.map(|pk| CString::new(*pk).expect("primary key contains NUL byte"))
.collect();
let pk_ptrs: Vec<*const std::os::raw::c_char> =
pk_cstrings.iter().map(|pk| pk.as_ptr()).collect();
let mut results: *mut ffi::zvec_write_result_t = ptr::null_mut();
let mut result_count: usize = 0;
let code = unsafe {
ffi::zvec_collection_delete_with_results(
self.ptr,
pk_ptrs.as_ptr(),
pk_ptrs.len(),
&mut results,
&mut result_count,
)
};
check_error(code as c_int)?;
Ok(WriteResults::from_raw(results, result_count))
}
pub fn delete_one(&self, pk: &str) -> Result<WriteResults> {
self.delete(&[pk])
}
pub fn delete_by_filter(&self, filter: &str) -> Result<()> {
let filter_c = CString::new(filter).expect("filter expression contains NUL byte");
let code = unsafe { ffi::zvec_collection_delete_by_filter(self.ptr, filter_c.as_ptr()) };
check_error(code as c_int)
}
pub fn query(&self, query: VectorQuery) -> Result<DocList> {
let mut docs: *mut *mut ffi::zvec_doc_t = ptr::null_mut();
let mut count: usize = 0;
let code =
unsafe { ffi::zvec_collection_query(self.ptr, query.ptr, &mut docs, &mut count) };
check_error(code as c_int)?;
Ok(DocList::from_raw(docs, count))
}
pub fn group_by_query(&self, query: GroupByVectorQuery) -> Result<GroupResults> {
let mut out: *mut ffi::zvecgb_group_results_t = ptr::null_mut();
let code = unsafe {
ffi::zvecgb_collection_group_by_query(
self.ptr as *mut std::os::raw::c_void,
query.ptr,
&mut out,
)
};
check_error(code as c_int)?;
Ok(GroupResults::from_ptr(out))
}
pub fn multi_query(&self, query: &crate::MultiQuery) -> Result<DocList> {
let mut docs: *mut *mut ffi::zvec_doc_t = ptr::null_mut();
let mut count: usize = 0;
let code = unsafe {
ffi::zvec_collection_multi_query(self.ptr, query.as_ptr(), &mut docs, &mut count)
};
check_error(code as c_int)?;
Ok(DocList::from_raw(docs, count))
}
pub fn fetch(&self, pks: &[&str]) -> Result<DocMap> {
let pk_cstrings: Vec<CString> = pks
.iter()
.map(|pk| CString::new(*pk).expect("primary key contains NUL byte"))
.collect();
let pk_ptrs: Vec<*const std::os::raw::c_char> =
pk_cstrings.iter().map(|pk| pk.as_ptr()).collect();
let mut docs: *mut *mut ffi::zvec_doc_t = ptr::null_mut();
let mut found: usize = 0;
let code = unsafe {
ffi::zvec_collection_fetch(
self.ptr,
pk_ptrs.as_ptr(),
pk_ptrs.len(),
ptr::null(),
0,
true,
&mut docs,
&mut found,
)
};
check_error(code as c_int)?;
Ok(DocMap::from_raw(docs, found))
}
pub fn create_index(&self, column_name: &str, params: IndexParams) -> Result<()> {
let column_c = CString::new(column_name).expect("column name contains NUL byte");
let code =
unsafe { ffi::zvec_collection_create_index(self.ptr, column_c.as_ptr(), params.ptr) };
check_error(code as c_int)
}
pub fn drop_index(&self, column_name: &str) -> Result<()> {
let column_c = CString::new(column_name).expect("column name contains NUL byte");
let code = unsafe { ffi::zvec_collection_drop_index(self.ptr, column_c.as_ptr()) };
check_error(code as c_int)
}
pub fn optimize(&self) -> Result<()> {
let code = unsafe { ffi::zvec_collection_optimize(self.ptr) };
check_error(code as c_int)
}
pub fn flush(&self) -> Result<()> {
let code = unsafe { ffi::zvec_collection_flush(self.ptr) };
check_error(code as c_int)
}
pub fn stats(&self) -> Result<CollectionStats> {
let mut stats_ptr: *mut ffi::zvec_collection_stats_t = ptr::null_mut();
let code = unsafe { ffi::zvec_collection_get_stats(self.ptr, &mut stats_ptr) };
check_error(code as c_int)?;
if stats_ptr.is_null() {
return Err(crate::error::Error::InternalError(
"Failed to get collection stats: null pointer".into(),
));
}
Ok(CollectionStats { ptr: stats_ptr })
}
pub fn schema(&self) -> Result<CollectionSchema> {
let mut schema_ptr: *mut ffi::zvec_collection_schema_t = ptr::null_mut();
let code = unsafe { ffi::zvec_collection_get_schema(self.ptr, &mut schema_ptr) };
check_error(code as c_int)?;
if schema_ptr.is_null() {
return Err(crate::error::Error::InternalError(
"Failed to get collection schema: null pointer".into(),
));
}
Ok(CollectionSchema::from_ptr(schema_ptr))
}
pub fn add_column(&self, column_schema: FieldSchema, expression: Option<&str>) -> Result<()> {
let expr_c = expression.map(|e| CString::new(e).expect("expression contains NUL byte"));
let expr_ptr = expr_c.as_ref().map(|e| e.as_ptr()).unwrap_or(ptr::null());
let code =
unsafe { ffi::zvec_collection_add_column(self.ptr, column_schema.ptr, expr_ptr) };
check_error(code as c_int)
}
pub fn drop_column(&self, column_name: &str) -> Result<()> {
let column_c = CString::new(column_name).expect("column name contains NUL byte");
let code = unsafe { ffi::zvec_collection_drop_column(self.ptr, column_c.as_ptr()) };
check_error(code as c_int)
}
pub fn alter_column(
&self,
column_name: &str,
rename: Option<&str>,
new_column_schema: Option<FieldSchema>,
) -> Result<()> {
let rename_c = rename.map(|r| CString::new(r).expect("rename contains NUL byte"));
let rename_ptr = rename_c.as_ref().map(|r| r.as_ptr()).unwrap_or(ptr::null());
let new_schema_ptr = new_column_schema
.as_ref()
.map(|s| s.ptr as *const ffi::zvec_field_schema_t)
.unwrap_or(ptr::null());
let column_c = CString::new(column_name).expect("column name contains NUL byte");
let code = unsafe {
ffi::zvec_collection_alter_column(
self.ptr,
column_c.as_ptr(),
rename_ptr,
new_schema_ptr,
)
};
check_error(code as c_int)
}
pub fn destroy(self) -> Result<()> {
let code = unsafe { ffi::zvec_collection_destroy(self.ptr) };
std::mem::forget(self);
check_error(code as c_int)
}
}
impl Drop for Collection {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { ffi::zvec_collection_close(self.ptr) };
}
}
}
pub struct IndexParams {
pub(crate) ptr: *mut ffi::zvec_index_params_t,
}
impl IndexParams {
pub fn hnsw(m: i32, ef_construction: i32, metric: MetricType, quantize: QuantizeType) -> Self {
Self::build(crate::types::IndexType::Hnsw, |ptr| {
let _ = check_error(unsafe {
ffi::zvec_index_params_set_hnsw_params(ptr, m, ef_construction)
} as c_int);
let _ =
check_error(
unsafe { ffi::zvec_index_params_set_metric_type(ptr, metric.into()) } as c_int,
);
let _ = check_error(unsafe {
ffi::zvec_index_params_set_quantize_type(ptr, quantize.into())
} as c_int);
})
}
pub fn ivf(
n_list: i32,
n_iters: i32,
use_soar: bool,
metric: MetricType,
quantize: QuantizeType,
) -> Self {
Self::build(crate::types::IndexType::Ivf, |ptr| {
let _ = check_error(unsafe {
ffi::zvec_index_params_set_ivf_params(ptr, n_list, n_iters, use_soar)
} as c_int);
let _ =
check_error(
unsafe { ffi::zvec_index_params_set_metric_type(ptr, metric.into()) } as c_int,
);
let _ = check_error(unsafe {
ffi::zvec_index_params_set_quantize_type(ptr, quantize.into())
} as c_int);
})
}
pub fn flat(metric: MetricType, quantize: QuantizeType) -> Self {
Self::build(crate::types::IndexType::Flat, |ptr| {
let _ =
check_error(
unsafe { ffi::zvec_index_params_set_metric_type(ptr, metric.into()) } as c_int,
);
let _ = check_error(unsafe {
ffi::zvec_index_params_set_quantize_type(ptr, quantize.into())
} as c_int);
})
}
pub fn invert(enable_range_optimization: bool) -> Self {
Self::build(crate::types::IndexType::Invert, |ptr| {
let _ = check_error(unsafe {
ffi::zvec_index_params_set_invert_params(ptr, enable_range_optimization, false)
} as c_int);
})
}
pub fn fts(
tokenizer_name: &str,
filters: Option<&[&str]>,
extra_params: Option<&str>,
) -> Result<Self> {
let ptr = unsafe { ffi::zvec_index_params_create(crate::types::IndexType::Fts.into()) };
if ptr.is_null() {
return Err(crate::error::Error::InternalError(
"zvec_index_params_create returned null".into(),
));
}
let tokenizer_c = CString::new(tokenizer_name)
.map_err(|e| crate::error::Error::InvalidArgument(e.to_string()))?;
let extra_c = match extra_params {
Some(s) => Some(
CString::new(s).map_err(|e| crate::error::Error::InvalidArgument(e.to_string()))?,
),
None => None,
};
let extra_ptr = extra_c
.as_ref()
.map(|c| c.as_ptr())
.unwrap_or(std::ptr::null());
let filters_array: Option<Vec<CString>> = filters.map(|fs| {
fs.iter()
.map(|f| CString::new(*f).expect("filter name contains NUL byte"))
.collect()
});
let (filters_ptr, _filters_owned) = match &filters_array {
Some(arr) if !arr.is_empty() => {
let sa = unsafe { ffi::zvec_string_array_create(arr.len()) };
if sa.is_null() {
return Err(crate::error::Error::InternalError(
"zvec_string_array_create returned null".into(),
));
}
for (i, cstr) in arr.iter().enumerate() {
unsafe { ffi::zvec_string_array_add(sa, i, cstr.as_ptr()) };
}
(sa, true)
}
_ => (
std::ptr::null::<ffi::zvec_string_array_t>() as *mut ffi::zvec_string_array_t,
false,
),
};
let code = unsafe {
ffi::zvec_index_params_set_fts_params(
ptr,
tokenizer_c.as_ptr(),
filters_ptr as *const ffi::zvec_string_array_t,
extra_ptr,
)
};
let result = check_error(code as c_int);
if !filters_ptr.is_null() {
unsafe { ffi::zvec_string_array_destroy(filters_ptr as *mut ffi::zvec_string_array_t) };
}
result?;
Ok(Self { ptr })
}
fn build<F: FnOnce(*mut ffi::zvec_index_params_t)>(
index_type: IndexType,
configure: F,
) -> Self {
let ptr = unsafe { ffi::zvec_index_params_create(index_type.into()) };
configure(ptr);
Self { ptr }
}
pub fn index_type(&self) -> IndexType {
unsafe { ffi::zvec_index_params_get_type(self.ptr).into() }
}
}
impl Drop for IndexParams {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { ffi::zvec_index_params_destroy(self.ptr) };
}
}
}
pub struct CollectionOptions {
ptr: *mut ffi::zvec_collection_options_t,
}
impl CollectionOptions {
pub fn new() -> Self {
let ptr = unsafe { ffi::zvec_collection_options_create() };
Self { ptr }
}
pub fn read_only(self, read_only: bool) -> Self {
let _ =
check_error(
unsafe { ffi::zvec_collection_options_set_read_only(self.ptr, read_only) } as c_int,
);
self
}
pub fn enable_mmap(self, enable: bool) -> Self {
let _ =
check_error(
unsafe { ffi::zvec_collection_options_set_enable_mmap(self.ptr, enable) } as c_int,
);
self
}
pub fn max_buffer_size(self, size: usize) -> Self {
let _ =
check_error(
unsafe { ffi::zvec_collection_options_set_max_buffer_size(self.ptr, size) }
as c_int,
);
self
}
}
impl Default for CollectionOptions {
fn default() -> Self {
Self::new()
}
}
impl Drop for CollectionOptions {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { ffi::zvec_collection_options_destroy(self.ptr) };
}
}
}
unsafe impl Send for Collection {}
unsafe impl Sync for Collection {}