Skip to main content

zvec_rust/
config.rs

1use std::ffi::CStr;
2use std::path::{Path, PathBuf};
3
4use crate::error::{check_error, to_cstring, Error, ErrorCode, Result};
5use crate::types::LogLevel;
6
7/// Configuration builder for initializing the zvec library.
8///
9/// This struct collects configuration values without touching the C library,
10/// making it safe to use even before the library is initialized.
11///
12/// # Example
13/// ```no_run
14/// use zvec_rust::ConfigBuilder;
15///
16/// let config = ConfigBuilder::new()
17///     .memory_limit(1024 * 1024 * 1024)
18///     .num_threads(4)
19///     .enable_console_log(true)
20///     .build();
21/// ```
22pub struct ConfigBuilder {
23    /// Memory limit in bytes (0 = use library default).
24    pub memory_limit: u64,
25    /// Number of threads for query and optimize (0 = use library default).
26    pub num_threads: u32,
27    /// Whether to enable console logging at Info level.
28    pub enable_console_log: bool,
29    /// FTS brute-force-by-keys ratio (None = use library default).
30    pub fts_brute_force_by_keys_ratio: Option<f32>,
31    /// Jieba dict directory for the FTS `jieba` tokenizer
32    /// (None = auto-discover and register the process-wide default).
33    pub jieba_dict_dir: Option<String>,
34}
35
36impl ConfigBuilder {
37    /// Creates a new builder with default values.
38    pub fn new() -> Self {
39        ConfigBuilder {
40            memory_limit: 0,
41            num_threads: 0,
42            enable_console_log: false,
43            fts_brute_force_by_keys_ratio: None,
44            jieba_dict_dir: None,
45        }
46    }
47
48    /// Sets the memory limit in bytes.
49    pub fn memory_limit(mut self, bytes: u64) -> Self {
50        self.memory_limit = bytes;
51        self
52    }
53
54    /// Sets the number of threads for both query and optimize.
55    pub fn num_threads(mut self, count: u32) -> Self {
56        self.num_threads = count;
57        self
58    }
59
60    /// Enables or disables console logging at Info level.
61    pub fn enable_console_log(mut self, enable: bool) -> Self {
62        self.enable_console_log = enable;
63        self
64    }
65
66    /// Sets the FTS brute-force-by-keys ratio.
67    pub fn fts_brute_force_by_keys_ratio(mut self, ratio: f32) -> Self {
68        self.fts_brute_force_by_keys_ratio = Some(ratio);
69        self
70    }
71
72    /// Sets the jieba dict directory for the FTS `jieba` tokenizer.
73    ///
74    /// The directory must contain `jieba.dict.utf8` and `hmm_model.utf8`
75    /// (shipped with the zvec prebuilt packages under `data/jieba_dict`).
76    /// When unset, [`initialize`] auto-discovers the dict directory that
77    /// ships with the zvec library and registers it as the process-wide
78    /// default via [`set_default_jieba_dict_dir`].
79    pub fn jieba_dict_dir(mut self, dir: impl Into<String>) -> Self {
80        self.jieba_dict_dir = Some(dir.into());
81        self
82    }
83
84    /// Finalizes the builder configuration.
85    ///
86    /// This is a no-op that returns `self` for API consistency. The builder
87    /// is a plain-data struct — no C resources are allocated here.
88    /// To apply the configuration, pass the result to [`initialize`].
89    pub fn build(self) -> Self {
90        self
91    }
92}
93
94impl Default for ConfigBuilder {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100/// Low-level configuration handle wrapping the C API config.
101pub(crate) struct ConfigData {
102    pub(crate) handle: *mut zvec_rust_sys::zvec_config_data_t,
103}
104
105impl ConfigData {
106    pub(crate) fn new() -> Result<Self> {
107        let handle = unsafe { zvec_rust_sys::zvec_config_data_create() };
108        if handle.is_null() {
109            return Err(Error {
110                code: ErrorCode::InternalError,
111                message: "failed to create config data".into(),
112            });
113        }
114        Ok(ConfigData { handle })
115    }
116
117    pub(crate) fn set_memory_limit(&mut self, bytes: u64) -> Result<()> {
118        check_error(unsafe { zvec_rust_sys::zvec_config_data_set_memory_limit(self.handle, bytes) })
119    }
120
121    pub(crate) fn set_query_thread_count(&mut self, count: u32) -> Result<()> {
122        check_error(unsafe {
123            zvec_rust_sys::zvec_config_data_set_query_thread_count(self.handle, count)
124        })
125    }
126
127    pub(crate) fn set_optimize_thread_count(&mut self, count: u32) -> Result<()> {
128        check_error(unsafe {
129            zvec_rust_sys::zvec_config_data_set_optimize_thread_count(self.handle, count)
130        })
131    }
132
133    pub(crate) fn set_fts_brute_force_by_keys_ratio(&mut self, ratio: f32) -> Result<()> {
134        check_error(unsafe {
135            zvec_rust_sys::zvec_config_data_set_fts_brute_force_by_keys_ratio(self.handle, ratio)
136        })
137    }
138
139    pub(crate) fn set_jieba_dict_dir(&mut self, dir: &str) -> Result<()> {
140        let c_dir = to_cstring(dir)?;
141        check_error(unsafe {
142            zvec_rust_sys::zvec_config_data_set_jieba_dict_dir(self.handle, c_dir.as_ptr())
143        })
144    }
145
146    pub(crate) fn set_console_log(&mut self, level: LogLevel) -> Result<()> {
147        let log_config = unsafe { zvec_rust_sys::zvec_config_log_create_console(level as u32) };
148        if log_config.is_null() {
149            return Err(Error {
150                code: ErrorCode::InternalError,
151                message: "failed to create console log config".into(),
152            });
153        }
154        // Ownership of log_config transfers to config_data on success.
155        // On failure, we must free it manually to avoid a leak.
156        let result = check_error(unsafe {
157            zvec_rust_sys::zvec_config_data_set_log_config(self.handle, log_config)
158        });
159        if result.is_err() {
160            unsafe { zvec_rust_sys::zvec_config_log_destroy(log_config) };
161        }
162        result
163    }
164}
165
166impl Drop for ConfigData {
167    fn drop(&mut self) {
168        if !self.handle.is_null() {
169            // Safety: handle was created by zvec_config_data_create
170            unsafe { zvec_rust_sys::zvec_config_data_destroy(self.handle) };
171        }
172    }
173}
174
175/// Initializes the zvec library with optional configuration.
176///
177/// Pass `None` to use default configuration, or provide a [`ConfigBuilder`]
178/// to customize memory limits, thread counts, and logging.
179///
180/// # Examples
181///
182/// ```no_run
183/// use zvec_rust::*;
184///
185/// // Default initialization
186/// initialize(None)?;
187///
188/// // With builder
189/// let config = ConfigBuilder::new()
190///     .memory_limit(1024 * 1024 * 1024)
191///     .num_threads(4)
192///     .build();
193/// initialize(Some(&config))?;
194/// # Ok::<(), zvec_rust::Error>(())
195/// ```
196pub fn initialize(config: Option<&ConfigBuilder>) -> Result<()> {
197    match config {
198        None => {
199            auto_register_default_jieba_dict_dir();
200            check_error(unsafe { zvec_rust_sys::zvec_initialize(std::ptr::null()) })
201        }
202        Some(builder) => {
203            let mut cfg = ConfigData::new()?;
204            if builder.memory_limit > 0 {
205                cfg.set_memory_limit(builder.memory_limit)?;
206            }
207            if builder.num_threads > 0 {
208                cfg.set_query_thread_count(builder.num_threads)?;
209                cfg.set_optimize_thread_count(builder.num_threads)?;
210            }
211            if builder.enable_console_log {
212                cfg.set_console_log(LogLevel::Info)?;
213            }
214            if let Some(ratio) = builder.fts_brute_force_by_keys_ratio {
215                cfg.set_fts_brute_force_by_keys_ratio(ratio)?;
216            }
217            if let Some(ref dir) = builder.jieba_dict_dir {
218                cfg.set_jieba_dict_dir(dir)?;
219            } else {
220                auto_register_default_jieba_dict_dir();
221            }
222            check_error(unsafe { zvec_rust_sys::zvec_initialize(cfg.handle as *const _) })
223        }
224    }
225}
226
227/// Sets the process-wide default jieba dict directory.
228///
229/// The directory must contain `jieba.dict.utf8` and `hmm_model.utf8` (as
230/// shipped with the zvec prebuilt packages under `data/jieba_dict`). The FTS
231/// `jieba` tokenizer uses this default when neither the per-field
232/// `jieba_dict_dir` extra param nor the `ZVEC_JIEBA_DICT_DIR` environment
233/// variable is set. Passing an empty string clears the default.
234///
235/// Thread-safe; last writer wins. A subsequent [`initialize`] with a
236/// non-empty [`ConfigBuilder::jieba_dict_dir`] overrides this default.
237pub fn set_default_jieba_dict_dir(dir: &str) -> Result<()> {
238    let c_dir = to_cstring(dir)?;
239    unsafe { zvec_rust_sys::zvec_set_default_jieba_dict_dir(c_dir.as_ptr()) };
240    Ok(())
241}
242
243/// Returns the process-wide default jieba dict directory, or `""` if unset.
244pub fn get_default_jieba_dict_dir() -> String {
245    unsafe {
246        let ptr = zvec_rust_sys::zvec_get_default_jieba_dict_dir();
247        if ptr.is_null() {
248            return String::new();
249        }
250        CStr::from_ptr(ptr).to_string_lossy().into_owned()
251    }
252}
253
254/// Registers an auto-discovered jieba dict directory as the process-wide
255/// default, unless one is already registered.
256///
257/// Called automatically by [`initialize`] so the `jieba` FTS tokenizer works
258/// out of the box with the bundled prebuilt libraries.
259fn auto_register_default_jieba_dict_dir() {
260    if !get_default_jieba_dict_dir().is_empty() {
261        return;
262    }
263    if let Some(dir) = discover_jieba_dict_dir() {
264        let dir = dir.to_string_lossy().into_owned();
265        let _ = set_default_jieba_dict_dir(&dir);
266    }
267}
268
269/// Searches known locations for a directory containing `jieba.dict.utf8`
270/// and `hmm_model.utf8`, returning the first hit.
271fn discover_jieba_dict_dir() -> Option<PathBuf> {
272    let mut candidates: Vec<PathBuf> = Vec::new();
273
274    // 1. Path recorded at build time by zvec-rust-sys (prebuilt download
275    //    cache, sibling/submodule checkout, or ZVEC_LIB_DIR).
276    if let Some(build_time_dir) = zvec_rust_sys::build_time_jieba_dict_dir() {
277        candidates.push(PathBuf::from(build_time_dir));
278    }
279
280    // 2. Relative to the loaded libzvec_c_api: prebuilt packages ship the
281    //    dict at `<lib_dir>/data/jieba_dict`, native SDK install prefixes at
282    //    `<prefix>/data/jieba_dict` next to `<prefix>/lib`.
283    if let Some(lib_dir) = loaded_library_dir() {
284        candidates.push(lib_dir.join("data").join("jieba_dict"));
285        candidates.push(lib_dir.join("..").join("data").join("jieba_dict"));
286    }
287
288    candidates.into_iter().find(|dir| is_jieba_dict_dir(dir))
289}
290
291fn is_jieba_dict_dir(dir: &Path) -> bool {
292    dir.join("jieba.dict.utf8").is_file() && dir.join("hmm_model.utf8").is_file()
293}
294
295/// Directory of the loaded `libzvec_c_api` shared library, if determinable.
296#[cfg(unix)]
297fn loaded_library_dir() -> Option<PathBuf> {
298    #[repr(C)]
299    struct DlInfo {
300        dli_fname: *const std::ffi::c_char,
301        dli_fbase: *mut std::ffi::c_void,
302        dli_sname: *const std::ffi::c_char,
303        dli_saddr: *mut std::ffi::c_void,
304    }
305    extern "C" {
306        fn dladdr(addr: *const std::ffi::c_void, info: *mut DlInfo) -> std::ffi::c_int;
307    }
308
309    // Probe with a symbol that was definitely called already: with lazy
310    // binding the GOT slot only holds the resolved in-library address after
311    // the first call; an uncalled symbol could still point at the PLT stub
312    // inside the executable.
313    let symbol =
314        zvec_rust_sys::zvec_get_default_jieba_dict_dir as *const () as *const std::ffi::c_void;
315
316    let mut info = DlInfo {
317        dli_fname: std::ptr::null(),
318        dli_fbase: std::ptr::null_mut(),
319        dli_sname: std::ptr::null(),
320        dli_saddr: std::ptr::null_mut(),
321    };
322    let path = unsafe {
323        if dladdr(symbol, &mut info) == 0 || info.dli_fname.is_null() {
324            return None;
325        }
326        PathBuf::from(CStr::from_ptr(info.dli_fname).to_string_lossy().as_ref())
327    };
328    let resolved = std::fs::canonicalize(&path).unwrap_or(path);
329    resolved.parent().map(PathBuf::from)
330}
331
332/// Directory of the loaded `zvec_c_api.dll`, if determinable.
333#[cfg(windows)]
334fn loaded_library_dir() -> Option<PathBuf> {
335    use std::os::windows::ffi::OsStringExt;
336
337    type HModule = *mut std::ffi::c_void;
338    const FROM_ADDRESS: u32 = 0x0000_0004;
339    const UNCHANGED_REFCOUNT: u32 = 0x0000_0002;
340
341    #[link(name = "kernel32")]
342    extern "system" {
343        fn GetModuleHandleExW(
344            flags: u32,
345            module_name: *mut std::ffi::c_void,
346            module: *mut HModule,
347        ) -> i32;
348        fn GetModuleFileNameW(module: HModule, filename: *mut u16, size: u32) -> u32;
349    }
350
351    let symbol =
352        zvec_rust_sys::zvec_get_default_jieba_dict_dir as *const () as *mut std::ffi::c_void;
353    unsafe {
354        let mut module: HModule = std::ptr::null_mut();
355        if GetModuleHandleExW(FROM_ADDRESS | UNCHANGED_REFCOUNT, symbol, &mut module) == 0
356            || module.is_null()
357        {
358            return None;
359        }
360        let mut size = 260u32; // MAX_PATH
361        loop {
362            let mut buf = vec![0u16; size as usize];
363            let len = GetModuleFileNameW(module, buf.as_mut_ptr(), size);
364            if len == 0 {
365                return None;
366            }
367            if len < size {
368                buf.truncate(len as usize);
369                return PathBuf::from(std::ffi::OsString::from_wide(&buf))
370                    .parent()
371                    .map(PathBuf::from);
372            }
373            size = size.checked_mul(2)?; // buffer too small; grow and retry
374        }
375    }
376}
377
378#[cfg(not(any(unix, windows)))]
379fn loaded_library_dir() -> Option<PathBuf> {
380    None
381}
382
383/// Shuts down the zvec library and releases all resources.
384#[doc(hidden)]
385pub fn shutdown() -> Result<()> {
386    check_error(unsafe { zvec_rust_sys::zvec_shutdown() })
387}
388
389/// Returns `true` if the library has been initialized.
390pub fn is_initialized() -> bool {
391    unsafe { zvec_rust_sys::zvec_is_initialized() }
392}
393
394/// Returns the library version string.
395pub fn version() -> String {
396    unsafe {
397        let ptr = zvec_rust_sys::zvec_get_version();
398        if ptr.is_null() {
399            return String::new();
400        }
401        CStr::from_ptr(ptr).to_string_lossy().into_owned()
402    }
403}
404
405/// Checks if the current library version meets the minimum requirements.
406pub fn check_version(major: i32, minor: i32, patch: i32) -> bool {
407    unsafe { zvec_rust_sys::zvec_check_version(major, minor, patch) }
408}
409
410/// Returns the major version number.
411pub fn version_major() -> i32 {
412    unsafe { zvec_rust_sys::zvec_get_version_major() }
413}
414
415/// Returns the minor version number.
416pub fn version_minor() -> i32 {
417    unsafe { zvec_rust_sys::zvec_get_version_minor() }
418}
419
420/// Returns the patch version number.
421pub fn version_patch() -> i32 {
422    unsafe { zvec_rust_sys::zvec_get_version_patch() }
423}
424
425/// Returns the I/O backend type code used for DiskAnn disk reads.
426///
427/// Compare against the `ZVEC_IO_BACKEND_TYPE_*` constants in [`crate::sys`]:
428/// Linux selects io_uring, then libaio, then synchronous pread; macOS ARM64
429/// uses pread; Windows uses overlapped I/O. Use [`io_backend_type_name`] for
430/// the matching name.
431pub fn io_backend_type() -> u32 {
432    unsafe { zvec_rust_sys::zvec_get_io_backend_type() }
433}
434
435/// Returns the name of an I/O backend type code.
436///
437/// One of `"io_uring"`, `"libaio"`, `"pread"`, `"windows_overlapped"`, or
438/// `"unknown"` for an unrecognized code.
439pub fn io_backend_type_name(backend_type: u32) -> String {
440    unsafe {
441        let ptr = zvec_rust_sys::zvec_get_io_backend_type_name(backend_type);
442        if ptr.is_null() {
443            return String::new();
444        }
445        CStr::from_ptr(ptr).to_string_lossy().into_owned()
446    }
447}
448
449/// Returns a human-readable description of the active I/O backend.
450///
451/// On Linux the pread description also explains why io_uring and libaio were
452/// unavailable. The returned string is a copy of a thread-local buffer owned
453/// by the zvec library.
454pub fn io_backend_description() -> String {
455    unsafe {
456        let ptr = zvec_rust_sys::zvec_get_io_backend_description();
457        if ptr.is_null() {
458            return String::new();
459        }
460        CStr::from_ptr(ptr).to_string_lossy().into_owned()
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn config_builder_defaults() {
470        let builder = ConfigBuilder::new();
471        assert_eq!(builder.memory_limit, 0);
472        assert_eq!(builder.num_threads, 0);
473        assert!(!builder.enable_console_log);
474        assert!(builder.jieba_dict_dir.is_none());
475    }
476
477    #[test]
478    fn config_builder_chaining() {
479        let builder = ConfigBuilder::new()
480            .memory_limit(1024)
481            .num_threads(4)
482            .enable_console_log(true)
483            .build();
484        assert_eq!(builder.memory_limit, 1024);
485        assert_eq!(builder.num_threads, 4);
486        assert!(builder.enable_console_log);
487    }
488
489    #[test]
490    fn config_builder_default_trait() {
491        let builder = ConfigBuilder::default();
492        assert_eq!(builder.memory_limit, 0);
493        assert_eq!(builder.num_threads, 0);
494        assert!(!builder.enable_console_log);
495    }
496
497    #[test]
498    fn config_builder_memory_limit_setter() {
499        let builder = ConfigBuilder::new().memory_limit(2048);
500        assert_eq!(builder.memory_limit, 2048);
501    }
502
503    #[test]
504    fn config_builder_num_threads_setter() {
505        let builder = ConfigBuilder::new().num_threads(8);
506        assert_eq!(builder.num_threads, 8);
507    }
508
509    #[test]
510    fn config_builder_enable_console_log_setter() {
511        let builder = ConfigBuilder::new().enable_console_log(true);
512        assert!(builder.enable_console_log);
513    }
514
515    #[test]
516    fn config_builder_jieba_dict_dir_setter() {
517        let builder = ConfigBuilder::new().jieba_dict_dir("/tmp/jieba_dict");
518        assert_eq!(builder.jieba_dict_dir.as_deref(), Some("/tmp/jieba_dict"));
519    }
520
521    #[test]
522    fn default_jieba_dict_dir_roundtrip() {
523        let previous = get_default_jieba_dict_dir();
524        set_default_jieba_dict_dir("/tmp/zvec_rust_jieba_roundtrip").unwrap();
525        assert_eq!(
526            get_default_jieba_dict_dir(),
527            "/tmp/zvec_rust_jieba_roundtrip"
528        );
529        // Empty string clears; restore the previous value afterwards so the
530        // process-wide default is left as found.
531        set_default_jieba_dict_dir("").unwrap();
532        assert_eq!(get_default_jieba_dict_dir(), "");
533        set_default_jieba_dict_dir(&previous).unwrap();
534        assert_eq!(get_default_jieba_dict_dir(), previous);
535    }
536
537    #[test]
538    fn discovered_jieba_dict_dir_contains_dict_files() {
539        // Discovery is environment-dependent (build-time path or dylib
540        // location); when it finds something, it must be a valid dict dir.
541        if let Some(dir) = discover_jieba_dict_dir() {
542            assert!(is_jieba_dict_dir(&dir), "invalid dict dir: {:?}", dir);
543        }
544    }
545
546    #[test]
547    fn config_builder_build_returns_self() {
548        let builder = ConfigBuilder::new()
549            .memory_limit(4096)
550            .num_threads(2)
551            .enable_console_log(true)
552            .build();
553        assert_eq!(builder.memory_limit, 4096);
554        assert_eq!(builder.num_threads, 2);
555        assert!(builder.enable_console_log);
556    }
557
558    #[test]
559    fn config_builder_overwrite_values() {
560        let builder = ConfigBuilder::new()
561            .memory_limit(1024)
562            .memory_limit(2048)
563            .num_threads(4)
564            .num_threads(8)
565            .enable_console_log(false)
566            .enable_console_log(true);
567        assert_eq!(builder.memory_limit, 2048);
568        assert_eq!(builder.num_threads, 8);
569        assert!(builder.enable_console_log);
570    }
571
572    #[test]
573    fn config_builder_zero_values() {
574        let builder = ConfigBuilder::new().memory_limit(0).num_threads(0);
575        assert_eq!(builder.memory_limit, 0);
576        assert_eq!(builder.num_threads, 0);
577    }
578
579    #[test]
580    fn config_builder_large_memory_limit() {
581        let builder = ConfigBuilder::new().memory_limit(u64::MAX);
582        assert_eq!(builder.memory_limit, u64::MAX);
583    }
584
585    #[test]
586    fn config_builder_fts_ratio() {
587        let builder = ConfigBuilder::new();
588        assert_eq!(builder.fts_brute_force_by_keys_ratio, None);
589
590        let builder = ConfigBuilder::new().fts_brute_force_by_keys_ratio(0.5);
591        assert_eq!(builder.fts_brute_force_by_keys_ratio, Some(0.5));
592    }
593
594    #[test]
595    fn loaded_library_dir_resolves_zvec_library() {
596        // dladdr / GetModuleHandleEx based discovery of libzvec_c_api.
597        if let Some(dir) = loaded_library_dir() {
598            let dir_str = dir.to_string_lossy();
599            assert!(
600                dir_str.contains("zvec"),
601                "unexpected dylib dir: {}",
602                dir_str
603            );
604        }
605    }
606
607    #[test]
608    fn io_backend_type_name_maps_every_code() {
609        use zvec_rust_sys::{
610            ZVEC_IO_BACKEND_TYPE_IO_URING, ZVEC_IO_BACKEND_TYPE_LIBAIO, ZVEC_IO_BACKEND_TYPE_PREAD,
611            ZVEC_IO_BACKEND_TYPE_WINDOWS_OVERLAPPED,
612        };
613
614        assert_eq!(io_backend_type_name(ZVEC_IO_BACKEND_TYPE_PREAD), "pread");
615        assert_eq!(io_backend_type_name(ZVEC_IO_BACKEND_TYPE_LIBAIO), "libaio");
616        assert_eq!(
617            io_backend_type_name(ZVEC_IO_BACKEND_TYPE_IO_URING),
618            "io_uring"
619        );
620        assert_eq!(
621            io_backend_type_name(ZVEC_IO_BACKEND_TYPE_WINDOWS_OVERLAPPED),
622            "windows_overlapped"
623        );
624        assert_eq!(io_backend_type_name(u32::MAX), "unknown");
625    }
626
627    #[test]
628    fn io_backend_introspection_reports_the_active_backend() {
629        let backend = io_backend_type();
630        let name = io_backend_type_name(backend);
631        assert!(
632            ["pread", "libaio", "io_uring", "windows_overlapped"].contains(&name.as_str()),
633            "unexpected io backend {} (code {})",
634            name,
635            backend
636        );
637        assert!(!io_backend_description().is_empty());
638    }
639}