zvec-rust 0.7.0

Safe Rust bindings for the zvec vector database
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
//! Multi-query and sub-query types for combined vector searches.
//!
//! A [`MultiQuery`] aggregates multiple [`SubQuery`] instances and reranks the
//! results using either RRF (Reciprocal Rank Fusion) or weighted strategies.

use std::os::raw::c_void;

use crate::error::{check_error, to_cstring, Error, ErrorCode, Result};
use crate::query::Fts;
use crate::query::{
    DiskannQueryParams, FlatQueryParams, FtsQueryParams, HnswQueryParams, IvfQueryParams,
    IvfRabitqQueryParams,
};

/// A multi-query operation combining multiple [`SubQuery`] objects.
///
/// Use [`MultiQuery::new`] to construct, then [`MultiQuery::add_sub_query`] to
/// attach individual sub-queries. Configure top-k, filters, and rerank strategy
/// before passing to [`crate::Collection::multi_query`].
pub struct MultiQuery {
    pub(crate) handle: *mut zvec_rust_sys::zvec_multi_query_t,
}

impl MultiQuery {
    /// Creates a new multi-query.
    pub fn new() -> Result<Self> {
        let handle = unsafe { zvec_rust_sys::zvec_multi_query_create() };
        if handle.is_null() {
            return Err(Error {
                code: ErrorCode::InternalError,
                message: "failed to create multi-query".into(),
            });
        }
        Ok(MultiQuery { handle })
    }

    /// Returns the raw FFI handle.
    ///
    /// # Safety
    /// The caller must not use the handle after the `MultiQuery` is dropped.
    pub unsafe fn as_raw(&self) -> *mut zvec_rust_sys::zvec_multi_query_t {
        self.handle
    }

    /// Adds a sub-query to this multi-query.
    ///
    /// The sub-query is copied internally; the caller retains ownership.
    pub fn add_sub_query(&mut self, sub: &SubQuery) -> Result<()> {
        check_error(unsafe {
            zvec_rust_sys::zvec_multi_query_add_sub_query(self.handle, sub.handle)
        })
    }

    /// Returns the number of sub-queries currently registered.
    pub fn sub_query_count(&self) -> usize {
        unsafe { zvec_rust_sys::zvec_multi_query_get_sub_query_count(self.handle) }
    }

    /// Sets the top-k parameter for the merged result.
    pub fn set_topk(&mut self, topk: i32) -> Result<()> {
        check_error(unsafe { zvec_rust_sys::zvec_multi_query_set_topk(self.handle, topk) })
    }

    /// Returns the configured top-k value.
    pub fn topk(&self) -> i32 {
        unsafe { zvec_rust_sys::zvec_multi_query_get_topk(self.handle) }
    }

    /// Sets the filter expression applied to the final merged result.
    pub fn set_filter(&mut self, filter: &str) -> Result<()> {
        let c_filter = to_cstring(filter)?;
        check_error(unsafe {
            zvec_rust_sys::zvec_multi_query_set_filter(self.handle, c_filter.as_ptr())
        })
    }

    /// Sets whether vector data is returned in the result documents.
    pub fn set_include_vector(&mut self, include: bool) -> Result<()> {
        check_error(unsafe {
            zvec_rust_sys::zvec_multi_query_set_include_vector(self.handle, include)
        })
    }

    /// Returns whether vector data is included in the result documents.
    pub fn include_vector(&self) -> bool {
        unsafe { zvec_rust_sys::zvec_multi_query_get_include_vector(self.handle) }
    }

    /// Sets the output field whitelist.
    pub fn set_output_fields(&mut self, fields: &[&str]) -> Result<()> {
        if fields.is_empty() {
            return Ok(());
        }
        let c_fields: Vec<_> = fields
            .iter()
            .map(|f| to_cstring(f))
            .collect::<Result<Vec<_>>>()?;
        let c_ptrs: Vec<_> = c_fields.iter().map(|s| s.as_ptr()).collect();
        check_error(unsafe {
            zvec_rust_sys::zvec_multi_query_set_output_fields(
                self.handle,
                c_ptrs.as_ptr(),
                c_ptrs.len(),
            )
        })
    }

    /// Configures Reciprocal Rank Fusion (RRF) as the rerank strategy.
    pub fn set_rerank_rrf(&mut self, rank_constant: i32) -> Result<()> {
        check_error(unsafe {
            zvec_rust_sys::zvec_multi_query_set_rerank_rrf(self.handle, rank_constant)
        })
    }

    /// Configures a weighted rerank strategy with the given per-sub-query weights.
    pub fn set_rerank_weighted(&mut self, weights: &[f64]) -> Result<()> {
        if weights.is_empty() {
            return Err(Error {
                code: ErrorCode::InvalidArgument,
                message: "weights cannot be empty".into(),
            });
        }
        check_error(unsafe {
            zvec_rust_sys::zvec_multi_query_set_rerank_weighted(
                self.handle,
                weights.as_ptr(),
                weights.len(),
            )
        })
    }
}

impl Drop for MultiQuery {
    fn drop(&mut self) {
        if !self.handle.is_null() {
            unsafe { zvec_rust_sys::zvec_multi_query_destroy(self.handle) };
        }
    }
}

// Safety: MultiQuery owns its handle exclusively.
unsafe impl Send for MultiQuery {}

/// A sub-query inside a [`MultiQuery`].
///
/// Each sub-query targets a single vector or sparse-vector field.
pub struct SubQuery {
    pub(crate) handle: *mut zvec_rust_sys::zvec_sub_query_t,
}

impl SubQuery {
    /// Creates a new sub-query.
    pub fn new() -> Result<Self> {
        let handle = unsafe { zvec_rust_sys::zvec_sub_query_create() };
        if handle.is_null() {
            return Err(Error {
                code: ErrorCode::InternalError,
                message: "failed to create sub-query".into(),
            });
        }
        Ok(SubQuery { handle })
    }

    /// Returns the raw FFI handle.
    ///
    /// # Safety
    /// The caller must not use the handle after the `SubQuery` is dropped.
    pub unsafe fn as_raw(&self) -> *mut zvec_rust_sys::zvec_sub_query_t {
        self.handle
    }

    /// Sets the number of candidates to retrieve before reranking.
    pub fn set_num_candidates(&mut self, n: i32) -> Result<()> {
        check_error(unsafe { zvec_rust_sys::zvec_sub_query_set_num_candidates(self.handle, n) })
    }

    /// Returns the number of candidates.
    pub fn num_candidates(&self) -> i32 {
        unsafe { zvec_rust_sys::zvec_sub_query_get_num_candidates(self.handle) }
    }

    /// Sets the target field name.
    pub fn set_field_name(&mut self, name: &str) -> Result<()> {
        let c_name = to_cstring(name)?;
        check_error(unsafe {
            zvec_rust_sys::zvec_sub_query_set_field_name(self.handle, c_name.as_ptr())
        })
    }

    /// Sets the dense query vector (f32).
    pub fn set_query_vector(&mut self, data: &[f32]) -> Result<()> {
        if data.is_empty() {
            return Err(Error {
                code: ErrorCode::InvalidArgument,
                message: "query vector cannot be empty".into(),
            });
        }
        let bytes = std::mem::size_of_val(data);
        check_error(unsafe {
            zvec_rust_sys::zvec_sub_query_set_query_vector(
                self.handle,
                data.as_ptr() as *const c_void,
                bytes,
            )
        })
    }

    /// Sets the sparse vector (indices + values, equal length).
    pub fn set_sparse_vector(&mut self, indices: &[u32], values: &[f32]) -> Result<()> {
        if indices.len() != values.len() {
            return Err(Error {
                code: ErrorCode::InvalidArgument,
                message: "indices and values must have the same length".into(),
            });
        }
        if indices.is_empty() {
            return Err(Error {
                code: ErrorCode::InvalidArgument,
                message: "sparse vector cannot be empty".into(),
            });
        }
        check_error(unsafe {
            zvec_rust_sys::zvec_sub_query_set_sparse_vector(
                self.handle,
                indices.as_ptr(),
                values.as_ptr(),
                indices.len(),
            )
        })
    }

    /// Sets only the sparse-vector indices.
    pub fn set_sparse_indices(&mut self, indices: &[u32]) -> Result<()> {
        check_error(unsafe {
            zvec_rust_sys::zvec_sub_query_set_sparse_indices(
                self.handle,
                indices.as_ptr(),
                indices.len(),
            )
        })
    }

    /// Sets only the sparse-vector values.
    pub fn set_sparse_values(&mut self, values: &[f32]) -> Result<()> {
        check_error(unsafe {
            zvec_rust_sys::zvec_sub_query_set_sparse_values(
                self.handle,
                values.as_ptr(),
                values.len(),
            )
        })
    }

    /// Sets HNSW query parameters (takes ownership on success).
    pub fn set_hnsw_params(&mut self, mut params: HnswQueryParams) -> Result<()> {
        check_error(unsafe {
            zvec_rust_sys::zvec_sub_query_set_hnsw_params(self.handle, params.handle)
        })?;
        params.handle = std::ptr::null_mut();
        Ok(())
    }

    /// Sets IVF query parameters (takes ownership on success).
    pub fn set_ivf_params(&mut self, mut params: IvfQueryParams) -> Result<()> {
        check_error(unsafe {
            zvec_rust_sys::zvec_sub_query_set_ivf_params(self.handle, params.handle)
        })?;
        params.handle = std::ptr::null_mut();
        Ok(())
    }

    /// Sets IVF RaBitQ query parameters (takes ownership on success).
    pub fn set_ivf_rabitq_params(&mut self, mut params: IvfRabitqQueryParams) -> Result<()> {
        check_error(unsafe {
            zvec_rust_sys::zvec_sub_query_set_ivf_rabitq_params(self.handle, params.handle)
        })?;
        params.handle = std::ptr::null_mut();
        Ok(())
    }

    /// Sets Flat query parameters (takes ownership on success).
    pub fn set_flat_params(&mut self, mut params: FlatQueryParams) -> Result<()> {
        check_error(unsafe {
            zvec_rust_sys::zvec_sub_query_set_flat_params(self.handle, params.handle)
        })?;
        params.handle = std::ptr::null_mut();
        Ok(())
    }

    /// Sets DiskANN query parameters (takes ownership on success).
    pub fn set_diskann_params(&mut self, mut params: DiskannQueryParams) -> Result<()> {
        check_error(unsafe {
            zvec_rust_sys::zvec_sub_query_set_diskann_params(self.handle, params.handle)
        })?;
        params.handle = std::ptr::null_mut();
        Ok(())
    }

    /// Sets FTS query parameters (takes ownership on success).
    pub fn set_fts_params(&mut self, mut params: FtsQueryParams) -> Result<()> {
        check_error(unsafe {
            zvec_rust_sys::zvec_sub_query_set_fts_params(self.handle, params.handle)
        })?;
        params.handle = std::ptr::null_mut();
        Ok(())
    }

    /// Sets FTS payload (payload is copied, caller retains ownership).
    pub fn set_fts(&mut self, fts: &Fts) -> Result<()> {
        check_error(unsafe { zvec_rust_sys::zvec_sub_query_set_fts(self.handle, fts.handle) })
    }
}

impl Drop for SubQuery {
    fn drop(&mut self) {
        if !self.handle.is_null() {
            unsafe { zvec_rust_sys::zvec_sub_query_destroy(self.handle) };
        }
    }
}

// Safety: SubQuery owns its handle exclusively.
unsafe impl Send for SubQuery {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn create_and_drop_multi_query() {
        let mq = MultiQuery::new().expect("create multi-query");
        assert_eq!(mq.sub_query_count(), 0);
    }

    #[test]
    fn create_and_drop_sub_query() {
        let _sq = SubQuery::new().expect("create sub-query");
    }

    #[test]
    fn multi_query_basic_setters() {
        let mut mq = MultiQuery::new().expect("create multi-query");
        mq.set_topk(20).expect("set topk");
        assert_eq!(mq.topk(), 20);
        mq.set_include_vector(true).expect("set include_vector");
        assert!(mq.include_vector());
    }

    #[test]
    fn add_sub_query_increments_count() {
        let mut mq = MultiQuery::new().expect("create multi-query");
        let mut sq = SubQuery::new().expect("create sub-query");
        sq.set_field_name("vec").expect("set field name");
        sq.set_query_vector(&[0.1, 0.2, 0.3, 0.4])
            .expect("set query vector");
        sq.set_num_candidates(50).expect("set num candidates");
        mq.add_sub_query(&sq).expect("add sub-query");
        assert_eq!(mq.sub_query_count(), 1);
    }

    #[test]
    fn rerank_weighted_rejects_empty() {
        let mut mq = MultiQuery::new().expect("create multi-query");
        let err = mq.set_rerank_weighted(&[]).unwrap_err();
        assert_eq!(err.code, ErrorCode::InvalidArgument);
    }

    #[test]
    fn sub_query_set_fts() {
        let mut sub = SubQuery::new().expect("create sub-query");
        sub.set_field_name("content").expect("set field name");

        let mut fts = Fts::new().expect("create fts payload");
        fts.set_match_string("hello world")
            .expect("set match string");

        sub.set_fts(&fts).expect("set fts payload");
    }

    #[test]
    fn sub_query_set_fts_params() {
        let mut sub = SubQuery::new().expect("create sub-query");
        sub.set_field_name("content").expect("set field name");

        let params = FtsQueryParams::new(Some("AND")).expect("create fts params");
        sub.set_fts_params(params).expect("set fts params");
    }

    #[test]
    fn sub_query_set_ivf_rabitq_params() {
        let mut sub = SubQuery::new().expect("create sub-query");
        sub.set_field_name("embedding").expect("set field name");
        sub.set_query_vector(&[0.1, 0.2, 0.3, 0.4])
            .expect("set query vector");

        let params = IvfRabitqQueryParams::new(16, 0.0, false, false);
        sub.set_ivf_rabitq_params(params)
            .expect("set ivf rabitq params");
    }

    #[test]
    fn sub_query_set_diskann_params() {
        let mut sub = SubQuery::new().expect("create sub-query");
        sub.set_field_name("embedding").expect("set field name");
        sub.set_query_vector(&[0.1, 0.2, 0.3, 0.4])
            .expect("set query vector");

        let params = DiskannQueryParams::new(200);
        sub.set_diskann_params(params).expect("set diskann params");
    }
}