Skip to main content

zvec_rust/
collection.rs

1use std::ffi::CStr;
2use std::ptr;
3
4use crate::doc::Doc;
5use crate::error::{check_error, to_cstring, Error, ErrorCode, Result};
6use crate::multi_query::MultiQuery;
7use crate::query::SearchQuery;
8use crate::schema::{CollectionSchema, FieldSchema, IndexParams};
9
10/// Options for creating or opening a collection.
11pub struct CollectionOptions {
12    pub(crate) handle: *mut zvec_rust_sys::zvec_collection_options_t,
13}
14
15impl CollectionOptions {
16    /// Creates new collection options with default values.
17    pub fn new() -> Result<Self> {
18        let handle = unsafe { zvec_rust_sys::zvec_collection_options_create() };
19        if handle.is_null() {
20            return Err(Error {
21                code: ErrorCode::InternalError,
22                message: "failed to create collection options".into(),
23            });
24        }
25        Ok(CollectionOptions { handle })
26    }
27
28    /// Sets whether to enable memory mapping.
29    pub fn set_enable_mmap(&mut self, enable: bool) -> Result<()> {
30        check_error(unsafe {
31            zvec_rust_sys::zvec_collection_options_set_enable_mmap(self.handle, enable)
32        })
33    }
34
35    /// Returns whether memory mapping is enabled.
36    pub fn enable_mmap(&self) -> bool {
37        unsafe { zvec_rust_sys::zvec_collection_options_get_enable_mmap(self.handle) }
38    }
39
40    /// Sets the maximum buffer size in bytes.
41    pub fn set_max_buffer_size(&mut self, size: u64) -> Result<()> {
42        check_error(unsafe {
43            zvec_rust_sys::zvec_collection_options_set_max_buffer_size(self.handle, size as usize)
44        })
45    }
46
47    /// Returns the maximum buffer size in bytes.
48    pub fn max_buffer_size(&self) -> u64 {
49        unsafe { zvec_rust_sys::zvec_collection_options_get_max_buffer_size(self.handle) as u64 }
50    }
51
52    /// Sets whether the collection is read-only.
53    pub fn set_read_only(&mut self, read_only: bool) -> Result<()> {
54        check_error(unsafe {
55            zvec_rust_sys::zvec_collection_options_set_read_only(self.handle, read_only)
56        })
57    }
58
59    /// Returns whether the collection is read-only.
60    pub fn read_only(&self) -> bool {
61        unsafe { zvec_rust_sys::zvec_collection_options_get_read_only(self.handle) }
62    }
63}
64
65impl Drop for CollectionOptions {
66    fn drop(&mut self) {
67        if !self.handle.is_null() {
68            unsafe { zvec_rust_sys::zvec_collection_options_destroy(self.handle) };
69        }
70    }
71}
72
73/// Statistics about a single index in a collection.
74#[derive(Debug, Clone)]
75pub struct IndexStat {
76    pub name: String,
77    pub completeness: f32,
78}
79
80/// Statistics about a collection.
81#[derive(Debug, Clone)]
82pub struct CollectionStats {
83    pub doc_count: u64,
84    pub indexes: Vec<IndexStat>,
85}
86
87/// Per-document result of a write operation.
88#[derive(Debug, Clone)]
89pub struct DocWriteResult {
90    pub success: bool,
91    pub code: ErrorCode,
92    pub message: String,
93}
94
95impl DocWriteResult {
96    /// Returns `true` if this individual write succeeded.
97    pub fn is_success(&self) -> bool {
98        self.success
99    }
100}
101
102/// Result of a write operation (insert/update/upsert/delete).
103#[derive(Debug, Clone)]
104pub struct WriteResult {
105    pub success_count: u64,
106    pub error_count: u64,
107    pub results: Vec<DocWriteResult>,
108}
109
110/// A zvec collection for storing and querying vector data.
111///
112/// Collections are the primary data container in zvec. They hold documents
113/// with typed fields and support vector similarity search.
114///
115/// The collection is automatically closed when dropped.
116pub struct Collection {
117    handle: *mut zvec_rust_sys::zvec_collection_t,
118}
119
120impl Collection {
121    /// Returns the raw FFI handle.
122    ///
123    /// # Safety
124    /// The caller must not use the handle after the `Collection` is dropped.
125    pub unsafe fn as_raw(&self) -> *mut zvec_rust_sys::zvec_collection_t {
126        self.handle
127    }
128
129    /// Creates a `Collection` from a raw FFI handle.
130    ///
131    /// # Safety
132    /// The caller must ensure the handle is valid and was created by the zvec C API.
133    /// The `Collection` takes ownership and will call `zvec_collection_close` on drop.
134    pub unsafe fn from_raw(handle: *mut zvec_rust_sys::zvec_collection_t) -> Self {
135        Collection { handle }
136    }
137
138    /// Creates a new collection and opens it.
139    pub fn create_and_open(
140        path: &str,
141        schema: &CollectionSchema,
142        options: Option<&CollectionOptions>,
143    ) -> Result<Self> {
144        let c_path = to_cstring(path)?;
145        let c_options = options.map(|o| o.handle as *const _).unwrap_or(ptr::null());
146
147        let mut handle: *mut zvec_rust_sys::zvec_collection_t = ptr::null_mut();
148        check_error(unsafe {
149            zvec_rust_sys::zvec_collection_create_and_open(
150                c_path.as_ptr(),
151                schema.handle,
152                c_options,
153                &mut handle,
154            )
155        })?;
156
157        Ok(Collection { handle })
158    }
159
160    /// Opens an existing collection.
161    pub fn open(path: &str, options: Option<&CollectionOptions>) -> Result<Self> {
162        let c_path = to_cstring(path)?;
163        let c_options = options.map(|o| o.handle as *const _).unwrap_or(ptr::null());
164
165        let mut handle: *mut zvec_rust_sys::zvec_collection_t = ptr::null_mut();
166        check_error(unsafe {
167            zvec_rust_sys::zvec_collection_open(c_path.as_ptr(), c_options, &mut handle)
168        })?;
169
170        Ok(Collection { handle })
171    }
172
173    /// Flushes collection data to disk.
174    pub fn flush(&self) -> Result<()> {
175        check_error(unsafe { zvec_rust_sys::zvec_collection_flush(self.handle) })
176    }
177
178    /// Returns the collection schema.
179    pub fn schema(&self) -> Result<CollectionSchema> {
180        let mut schema_handle: *mut zvec_rust_sys::zvec_collection_schema_t = ptr::null_mut();
181        check_error(unsafe {
182            zvec_rust_sys::zvec_collection_get_schema(self.handle, &mut schema_handle)
183        })?;
184        Ok(CollectionSchema::from_owned(schema_handle))
185    }
186
187    /// Returns collection statistics.
188    pub fn stats(&self) -> Result<CollectionStats> {
189        let mut stats_handle: *mut zvec_rust_sys::zvec_collection_stats_t = ptr::null_mut();
190        check_error(unsafe {
191            zvec_rust_sys::zvec_collection_get_stats(self.handle, &mut stats_handle)
192        })?;
193
194        let doc_count = unsafe { zvec_rust_sys::zvec_collection_stats_get_doc_count(stats_handle) };
195        let index_count =
196            unsafe { zvec_rust_sys::zvec_collection_stats_get_index_count(stats_handle) };
197
198        let mut indexes = Vec::with_capacity(index_count);
199
200        for i in 0..index_count {
201            let name_ptr =
202                unsafe { zvec_rust_sys::zvec_collection_stats_get_index_name(stats_handle, i) };
203            let name = if name_ptr.is_null() {
204                String::new()
205            } else {
206                unsafe { CStr::from_ptr(name_ptr).to_string_lossy().into_owned() }
207            };
208
209            let completeness = unsafe {
210                zvec_rust_sys::zvec_collection_stats_get_index_completeness(stats_handle, i)
211            };
212            indexes.push(IndexStat { name, completeness });
213        }
214
215        unsafe { zvec_rust_sys::zvec_collection_stats_destroy(stats_handle) };
216
217        Ok(CollectionStats { doc_count, indexes })
218    }
219
220    // =========================================================================
221    // DML Operations
222    // =========================================================================
223
224    /// Inserts documents into the collection.
225    pub fn insert(&self, docs: &[&Doc]) -> Result<WriteResult> {
226        let ptrs: Vec<*const zvec_rust_sys::zvec_doc_t> =
227            docs.iter().map(|d| d.handle as *const _).collect();
228        let mut results: *mut zvec_rust_sys::zvec_write_result_t = ptr::null_mut();
229        let mut result_count: usize = 0;
230
231        check_error(unsafe {
232            zvec_rust_sys::zvec_collection_insert_with_results(
233                self.handle,
234                ptrs.as_ptr(),
235                ptrs.len(),
236                &mut results,
237                &mut result_count,
238            )
239        })?;
240
241        Ok(collect_write_results(results, result_count))
242    }
243
244    /// Updates documents in the collection.
245    pub fn update(&self, docs: &[&Doc]) -> Result<WriteResult> {
246        let ptrs: Vec<*const zvec_rust_sys::zvec_doc_t> =
247            docs.iter().map(|d| d.handle as *const _).collect();
248        let mut results: *mut zvec_rust_sys::zvec_write_result_t = ptr::null_mut();
249        let mut result_count: usize = 0;
250
251        check_error(unsafe {
252            zvec_rust_sys::zvec_collection_update_with_results(
253                self.handle,
254                ptrs.as_ptr(),
255                ptrs.len(),
256                &mut results,
257                &mut result_count,
258            )
259        })?;
260
261        Ok(collect_write_results(results, result_count))
262    }
263
264    /// Inserts or updates documents (upsert).
265    pub fn upsert(&self, docs: &[&Doc]) -> Result<WriteResult> {
266        let ptrs: Vec<*const zvec_rust_sys::zvec_doc_t> =
267            docs.iter().map(|d| d.handle as *const _).collect();
268        let mut results: *mut zvec_rust_sys::zvec_write_result_t = ptr::null_mut();
269        let mut result_count: usize = 0;
270
271        check_error(unsafe {
272            zvec_rust_sys::zvec_collection_upsert_with_results(
273                self.handle,
274                ptrs.as_ptr(),
275                ptrs.len(),
276                &mut results,
277                &mut result_count,
278            )
279        })?;
280
281        Ok(collect_write_results(results, result_count))
282    }
283
284    /// Deletes documents by primary keys.
285    pub fn delete(&self, pks: &[&str]) -> Result<WriteResult> {
286        let c_pks: Vec<_> = pks
287            .iter()
288            .map(|pk| to_cstring(pk))
289            .collect::<Result<Vec<_>>>()?;
290        let c_ptrs: Vec<_> = c_pks.iter().map(|pk| pk.as_ptr()).collect();
291        let mut results: *mut zvec_rust_sys::zvec_write_result_t = ptr::null_mut();
292        let mut result_count: usize = 0;
293
294        check_error(unsafe {
295            zvec_rust_sys::zvec_collection_delete_with_results(
296                self.handle,
297                c_ptrs.as_ptr(),
298                c_ptrs.len(),
299                &mut results,
300                &mut result_count,
301            )
302        })?;
303
304        Ok(collect_write_results(results, result_count))
305    }
306
307    /// Deletes documents matching a filter expression.
308    pub fn delete_by_filter(&self, filter: &str) -> Result<()> {
309        let c_filter = to_cstring(filter)?;
310        check_error(unsafe {
311            zvec_rust_sys::zvec_collection_delete_by_filter(self.handle, c_filter.as_ptr())
312        })
313    }
314
315    // =========================================================================
316    // DQL Operations
317    // =========================================================================
318
319    /// Performs a vector similarity search.
320    pub fn query(&self, query: &SearchQuery) -> Result<Vec<Doc>> {
321        let mut results: *mut *mut zvec_rust_sys::zvec_doc_t = ptr::null_mut();
322        let mut result_count: usize = 0;
323
324        check_error(unsafe {
325            zvec_rust_sys::zvec_collection_query(
326                self.handle,
327                query.handle,
328                &mut results,
329                &mut result_count,
330            )
331        })?;
332
333        let docs = unsafe { collect_docs(results, result_count) };
334        Ok(docs)
335    }
336
337    /// Performs a multi-query that combines several sub-queries with a rerank
338    /// strategy (RRF or weighted).
339    pub fn multi_query(&self, query: &MultiQuery) -> Result<Vec<Doc>> {
340        let mut results: *mut *mut zvec_rust_sys::zvec_doc_t = ptr::null_mut();
341        let mut result_count: usize = 0;
342
343        check_error(unsafe {
344            zvec_rust_sys::zvec_collection_multi_query(
345                self.handle,
346                query.handle,
347                &mut results,
348                &mut result_count,
349            )
350        })?;
351
352        let docs = unsafe { collect_docs(results, result_count) };
353        Ok(docs)
354    }
355
356    /// Fetches documents by primary keys, returning all fields including vectors.
357    pub fn fetch(&self, pks: &[&str]) -> Result<Vec<Doc>> {
358        self.fetch_with_options(pks, None, true)
359    }
360
361    /// Fetches documents by primary keys with control over which fields to return.
362    pub fn fetch_with_options(
363        &self,
364        pks: &[&str],
365        output_fields: Option<&[&str]>,
366        include_vector: bool,
367    ) -> Result<Vec<Doc>> {
368        let c_pks: Vec<_> = pks
369            .iter()
370            .map(|pk| to_cstring(pk))
371            .collect::<Result<Vec<_>>>()?;
372        let c_pk_ptrs: Vec<_> = c_pks.iter().map(|pk| pk.as_ptr()).collect();
373
374        let c_fields = output_fields
375            .map(|fields| {
376                fields
377                    .iter()
378                    .map(|f| to_cstring(f))
379                    .collect::<Result<Vec<_>>>()
380            })
381            .transpose()?;
382        let c_field_ptrs: Option<Vec<_>> = c_fields
383            .as_ref()
384            .map(|f| f.iter().map(|s| s.as_ptr()).collect());
385        let (fields_ptr, fields_count) = match &c_field_ptrs {
386            Some(ptrs) => (ptrs.as_ptr(), ptrs.len()),
387            None => (ptr::null(), 0),
388        };
389
390        let mut documents: *mut *mut zvec_rust_sys::zvec_doc_t = ptr::null_mut();
391        let mut found_count: usize = 0;
392
393        check_error(unsafe {
394            zvec_rust_sys::zvec_collection_fetch(
395                self.handle,
396                c_pk_ptrs.as_ptr(),
397                c_pk_ptrs.len(),
398                fields_ptr,
399                fields_count,
400                include_vector,
401                &mut documents,
402                &mut found_count,
403            )
404        })?;
405
406        let docs = unsafe { collect_docs(documents, found_count) };
407        Ok(docs)
408    }
409
410    // =========================================================================
411    // Index Management
412    // =========================================================================
413
414    /// Creates an index on a field.
415    pub fn create_index(&self, field_name: &str, params: &IndexParams) -> Result<()> {
416        let c_name = to_cstring(field_name)?;
417        check_error(unsafe {
418            zvec_rust_sys::zvec_collection_create_index(self.handle, c_name.as_ptr(), params.handle)
419        })
420    }
421
422    /// Drops an index from a field.
423    pub fn drop_index(&self, field_name: &str) -> Result<()> {
424        let c_name = to_cstring(field_name)?;
425        check_error(unsafe {
426            zvec_rust_sys::zvec_collection_drop_index(self.handle, c_name.as_ptr())
427        })
428    }
429
430    /// Optimizes the collection (rebuild indexes, merge segments, etc.).
431    pub fn optimize(&self) -> Result<()> {
432        check_error(unsafe { zvec_rust_sys::zvec_collection_optimize(self.handle) })
433    }
434
435    // =========================================================================
436    // DDL Operations
437    // =========================================================================
438
439    /// Adds a column to the collection.
440    pub fn add_column(&self, field_schema: &FieldSchema, default_expr: Option<&str>) -> Result<()> {
441        let c_expr_owned = default_expr.map(to_cstring).transpose()?;
442        let c_expr_ptr = c_expr_owned
443            .as_ref()
444            .map(|s| s.as_ptr())
445            .unwrap_or(ptr::null());
446        check_error(unsafe {
447            zvec_rust_sys::zvec_collection_add_column(self.handle, field_schema.handle, c_expr_ptr)
448        })
449    }
450
451    /// Drops a column from the collection.
452    pub fn drop_column(&self, name: &str) -> Result<()> {
453        let c_name = to_cstring(name)?;
454        check_error(unsafe {
455            zvec_rust_sys::zvec_collection_drop_column(self.handle, c_name.as_ptr())
456        })
457    }
458
459    /// Closes the collection explicitly.
460    pub fn close(self) -> Result<()> {
461        // Drop will handle the close
462        drop(self);
463        Ok(())
464    }
465}
466
467impl Drop for Collection {
468    fn drop(&mut self) {
469        if !self.handle.is_null() {
470            // Safety: handle was created by zvec_collection_create_and_open or zvec_collection_open
471            let rc = unsafe { zvec_rust_sys::zvec_collection_close(self.handle) };
472            if rc != zvec_rust_sys::ZVEC_OK {
473                eprintln!(
474                    "zvec warning: failed to close collection (error code {})",
475                    rc
476                );
477            }
478        }
479    }
480}
481
482// Safety: The zvec C-API documents that collection operations are internally
483// thread-safe. All mutable state is protected by internal locks in the C library.
484// See: https://github.com/alibaba/zvec — C-API thread-safety guarantees.
485unsafe impl Send for Collection {}
486unsafe impl Sync for Collection {}
487
488/// Parses a C array of `zvec_write_result_t` into a `WriteResult`.
489fn collect_write_results(
490    results: *mut zvec_rust_sys::zvec_write_result_t,
491    count: usize,
492) -> WriteResult {
493    let mut doc_results = Vec::with_capacity(count);
494    let mut success_count: u64 = 0;
495    let mut error_count: u64 = 0;
496
497    if !results.is_null() && count > 0 {
498        for i in 0..count {
499            let wr = unsafe { &*results.add(i) };
500            let is_ok = wr.code == zvec_rust_sys::ZVEC_OK;
501            let message = if wr.message.is_null() {
502                String::new()
503            } else {
504                unsafe { CStr::from_ptr(wr.message).to_string_lossy().into_owned() }
505            };
506            if is_ok {
507                success_count += 1;
508            } else {
509                error_count += 1;
510            }
511            doc_results.push(DocWriteResult {
512                success: is_ok,
513                code: ErrorCode::from(wr.code),
514                message,
515            });
516        }
517        unsafe { zvec_rust_sys::zvec_write_results_free(results, count) };
518    }
519
520    WriteResult {
521        success_count,
522        error_count,
523        results: doc_results,
524    }
525}
526
527/// Collects document pointers from a C array into a Vec<Doc>.
528///
529/// # Safety
530/// `results` must point to a valid array of `count` document pointers
531/// allocated by the zvec C library.
532unsafe fn collect_docs(results: *mut *mut zvec_rust_sys::zvec_doc_t, count: usize) -> Vec<Doc> {
533    if results.is_null() || count == 0 {
534        return Vec::new();
535    }
536
537    let mut docs = Vec::with_capacity(count);
538    for i in 0..count {
539        let doc_ptr = *results.add(i);
540        if !doc_ptr.is_null() {
541            // Take ownership: Rust will call zvec_doc_destroy on drop
542            docs.push(Doc::from_raw(doc_ptr));
543        }
544    }
545
546    // Free only the pointer array itself (not the individual docs, which are now
547    // owned by the Doc wrappers above and will be freed via their Drop impls).
548    zvec_rust_sys::zvec_free(results as *mut std::os::raw::c_void);
549
550    docs
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556
557    #[test]
558    fn collect_docs_handles_null_pointer() {
559        let docs = unsafe { collect_docs(ptr::null_mut(), 0) };
560        assert!(docs.is_empty());
561    }
562
563    #[test]
564    fn collect_docs_handles_zero_count() {
565        // Even with a non-null pointer, zero count should return empty
566        let mut fake: *mut zvec_rust_sys::zvec_doc_t = ptr::null_mut();
567        let docs = unsafe { collect_docs(&mut fake as *mut _, 0) };
568        assert!(docs.is_empty());
569    }
570}