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#[cfg(test)]
426mod tests {
427    use super::*;
428
429    #[test]
430    fn config_builder_defaults() {
431        let builder = ConfigBuilder::new();
432        assert_eq!(builder.memory_limit, 0);
433        assert_eq!(builder.num_threads, 0);
434        assert!(!builder.enable_console_log);
435        assert!(builder.jieba_dict_dir.is_none());
436    }
437
438    #[test]
439    fn config_builder_chaining() {
440        let builder = ConfigBuilder::new()
441            .memory_limit(1024)
442            .num_threads(4)
443            .enable_console_log(true)
444            .build();
445        assert_eq!(builder.memory_limit, 1024);
446        assert_eq!(builder.num_threads, 4);
447        assert!(builder.enable_console_log);
448    }
449
450    #[test]
451    fn config_builder_default_trait() {
452        let builder = ConfigBuilder::default();
453        assert_eq!(builder.memory_limit, 0);
454        assert_eq!(builder.num_threads, 0);
455        assert!(!builder.enable_console_log);
456    }
457
458    #[test]
459    fn config_builder_memory_limit_setter() {
460        let builder = ConfigBuilder::new().memory_limit(2048);
461        assert_eq!(builder.memory_limit, 2048);
462    }
463
464    #[test]
465    fn config_builder_num_threads_setter() {
466        let builder = ConfigBuilder::new().num_threads(8);
467        assert_eq!(builder.num_threads, 8);
468    }
469
470    #[test]
471    fn config_builder_enable_console_log_setter() {
472        let builder = ConfigBuilder::new().enable_console_log(true);
473        assert!(builder.enable_console_log);
474    }
475
476    #[test]
477    fn config_builder_jieba_dict_dir_setter() {
478        let builder = ConfigBuilder::new().jieba_dict_dir("/tmp/jieba_dict");
479        assert_eq!(builder.jieba_dict_dir.as_deref(), Some("/tmp/jieba_dict"));
480    }
481
482    #[test]
483    fn default_jieba_dict_dir_roundtrip() {
484        let previous = get_default_jieba_dict_dir();
485        set_default_jieba_dict_dir("/tmp/zvec_rust_jieba_roundtrip").unwrap();
486        assert_eq!(
487            get_default_jieba_dict_dir(),
488            "/tmp/zvec_rust_jieba_roundtrip"
489        );
490        // Empty string clears; restore the previous value afterwards so the
491        // process-wide default is left as found.
492        set_default_jieba_dict_dir("").unwrap();
493        assert_eq!(get_default_jieba_dict_dir(), "");
494        set_default_jieba_dict_dir(&previous).unwrap();
495        assert_eq!(get_default_jieba_dict_dir(), previous);
496    }
497
498    #[test]
499    fn discovered_jieba_dict_dir_contains_dict_files() {
500        // Discovery is environment-dependent (build-time path or dylib
501        // location); when it finds something, it must be a valid dict dir.
502        if let Some(dir) = discover_jieba_dict_dir() {
503            assert!(is_jieba_dict_dir(&dir), "invalid dict dir: {:?}", dir);
504        }
505    }
506
507    #[test]
508    fn config_builder_build_returns_self() {
509        let builder = ConfigBuilder::new()
510            .memory_limit(4096)
511            .num_threads(2)
512            .enable_console_log(true)
513            .build();
514        assert_eq!(builder.memory_limit, 4096);
515        assert_eq!(builder.num_threads, 2);
516        assert!(builder.enable_console_log);
517    }
518
519    #[test]
520    fn config_builder_overwrite_values() {
521        let builder = ConfigBuilder::new()
522            .memory_limit(1024)
523            .memory_limit(2048)
524            .num_threads(4)
525            .num_threads(8)
526            .enable_console_log(false)
527            .enable_console_log(true);
528        assert_eq!(builder.memory_limit, 2048);
529        assert_eq!(builder.num_threads, 8);
530        assert!(builder.enable_console_log);
531    }
532
533    #[test]
534    fn config_builder_zero_values() {
535        let builder = ConfigBuilder::new().memory_limit(0).num_threads(0);
536        assert_eq!(builder.memory_limit, 0);
537        assert_eq!(builder.num_threads, 0);
538    }
539
540    #[test]
541    fn config_builder_large_memory_limit() {
542        let builder = ConfigBuilder::new().memory_limit(u64::MAX);
543        assert_eq!(builder.memory_limit, u64::MAX);
544    }
545
546    #[test]
547    fn config_builder_fts_ratio() {
548        let builder = ConfigBuilder::new();
549        assert_eq!(builder.fts_brute_force_by_keys_ratio, None);
550
551        let builder = ConfigBuilder::new().fts_brute_force_by_keys_ratio(0.5);
552        assert_eq!(builder.fts_brute_force_by_keys_ratio, Some(0.5));
553    }
554
555    #[test]
556    fn loaded_library_dir_resolves_zvec_library() {
557        // dladdr / GetModuleHandleEx based discovery of libzvec_c_api.
558        if let Some(dir) = loaded_library_dir() {
559            let dir_str = dir.to_string_lossy();
560            assert!(
561                dir_str.contains("zvec"),
562                "unexpected dylib dir: {}",
563                dir_str
564            );
565        }
566    }
567}