omendb/
lib.rs

1//! C FFI bindings for `OmenDB`
2//!
3//! Provides a C-compatible API for embedding `OmenDB` in other languages.
4//!
5//! # Safety
6//!
7//! All functions that take raw pointers are marked `unsafe` because:
8//! - The caller must ensure pointer validity
9//! - The caller must ensure proper memory management
10//!
11//! # Example (C)
12//! ```c
13//! #include "omendb.h"
14//!
15//! omendb_db_t* db = omendb_open("./vectors", 384, NULL);
16//! if (!db) {
17//!     printf("Error: %s\n", omendb_last_error());
18//!     return 1;
19//! }
20//!
21//! // Insert vectors
22//! const char* items = "[{\"id\":\"doc1\",\"vector\":[0.1,...],\"metadata\":{}}]";
23//! omendb_set(db, items);
24//!
25//! // Search
26//! float query[384] = {0.1, ...};
27//! char* results = NULL;
28//! omendb_search(db, query, 384, 10, NULL, &results);
29//! printf("Results: %s\n", results);
30//! omendb_free_string(results);
31//!
32//! omendb_close(db);
33//! ```
34
35use std::cell::RefCell;
36use std::collections::HashMap;
37use std::ffi::{CStr, CString};
38use std::os::raw::c_char;
39use std::path::Path;
40use std::ptr;
41
42use omendb::vector::{MetadataFilter, Vector, VectorStore, VectorStoreOptions};
43use serde_json::{json, Value as JsonValue};
44
45thread_local! {
46    static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
47}
48
49fn set_last_error(err: String) {
50    LAST_ERROR.with(|e| {
51        *e.borrow_mut() = CString::new(err).ok();
52    });
53}
54
55fn clear_last_error() {
56    LAST_ERROR.with(|e| *e.borrow_mut() = None);
57}
58
59/// Opaque database handle
60pub struct OmenDB {
61    store: VectorStore,
62    dimensions: usize,
63    /// Cache: internal index -> string ID (rebuilt when needed)
64    index_to_id: HashMap<usize, String>,
65}
66
67impl OmenDB {
68    /// Rebuild the index->ID cache from the store's id_to_index map
69    fn rebuild_cache(&mut self) {
70        self.index_to_id = self
71            .store
72            .id_to_index
73            .iter()
74            .map(|(id, &idx)| (idx, id.clone()))
75            .collect();
76    }
77
78    /// Get string ID for an internal index
79    fn get_id(&self, idx: usize) -> String {
80        self.index_to_id
81            .get(&idx)
82            .cloned()
83            .unwrap_or_else(|| idx.to_string())
84    }
85}
86
87/// Open a database at the given path
88///
89/// # Arguments
90/// * `path` - Path to database directory (UTF-8)
91/// * `dimensions` - Vector dimensionality
92/// * `config_json` - Optional JSON config string (NULL for defaults)
93///
94/// Config JSON format:
95/// ```json
96/// {
97///   "m": 16,                    // Number of neighbors per node (default: 16)
98///   "ef_construction": 100,     // Build quality (default: 100)
99///   "ef_search": 100            // Search quality (default: 100)
100/// }
101/// ```
102///
103/// # Returns
104/// Database handle on success, NULL on failure (check `omendb_last_error`)
105///
106/// # Safety
107/// - `path` must be a valid, null-terminated UTF-8 string
108/// - `config_json` must be NULL or a valid, null-terminated UTF-8 string
109#[no_mangle]
110pub unsafe extern "C" fn omendb_open(
111    path: *const c_char,
112    dimensions: usize,
113    config_json: *const c_char,
114) -> *mut OmenDB {
115    clear_last_error();
116
117    if path.is_null() {
118        set_last_error("Null path pointer".to_string());
119        return ptr::null_mut();
120    }
121
122    let path = match CStr::from_ptr(path).to_str() {
123        Ok(s) => s,
124        Err(e) => {
125            set_last_error(format!("Invalid path: {e}"));
126            return ptr::null_mut();
127        }
128    };
129
130    // Parse config if provided
131    let config: Option<JsonValue> = if config_json.is_null() {
132        None
133    } else {
134        let config_str = match CStr::from_ptr(config_json).to_str() {
135            Ok(s) => s,
136            Err(e) => {
137                set_last_error(format!("Invalid config string: {e}"));
138                return ptr::null_mut();
139            }
140        };
141        match serde_json::from_str(config_str) {
142            Ok(v) => Some(v),
143            Err(e) => {
144                set_last_error(format!("Invalid config JSON: {e}"));
145                return ptr::null_mut();
146            }
147        }
148    };
149
150    // Build store with optional config
151    let result = if let Some(cfg) = config {
152        let mut options = VectorStoreOptions::new().dimensions(dimensions);
153
154        if let Some(m) = cfg.get("m").and_then(JsonValue::as_u64) {
155            options = options.m(m as usize);
156        }
157        if let Some(ef_c) = cfg.get("ef_construction").and_then(JsonValue::as_u64) {
158            options = options.ef_construction(ef_c as usize);
159        }
160        if let Some(ef_s) = cfg.get("ef_search").and_then(JsonValue::as_u64) {
161            options = options.ef_search(ef_s as usize);
162        }
163
164        options.open(Path::new(path))
165    } else {
166        VectorStore::open_with_dimensions(Path::new(path), dimensions)
167    };
168
169    match result {
170        Ok(store) => {
171            // Build initial index->ID cache
172            let index_to_id: HashMap<usize, String> = store
173                .id_to_index
174                .iter()
175                .map(|(id, &idx)| (idx, id.clone()))
176                .collect();
177            Box::into_raw(Box::new(OmenDB {
178                store,
179                dimensions,
180                index_to_id,
181            }))
182        }
183        Err(e) => {
184            set_last_error(format!("Failed to open database: {e}"));
185            ptr::null_mut()
186        }
187    }
188}
189
190/// Close database and free resources
191///
192/// # Safety
193/// - `db` must be NULL or a valid pointer returned by `omendb_open`
194/// - After calling this function, `db` is invalid and must not be used
195#[no_mangle]
196pub unsafe extern "C" fn omendb_close(db: *mut OmenDB) {
197    if !db.is_null() {
198        drop(Box::from_raw(db));
199    }
200}
201
202/// Insert or replace vectors
203///
204/// # Arguments
205/// * `db` - Database handle
206/// * `items_json` - JSON array: `[{"id": "...", "vector": [...], "metadata": {...}}, ...]`
207///
208/// # Returns
209/// Number of vectors inserted, or -1 on error
210///
211/// # Safety
212/// - `db` must be a valid pointer returned by `omendb_open`
213/// - `items_json` must be a valid, null-terminated UTF-8 string
214#[no_mangle]
215pub unsafe extern "C" fn omendb_set(db: *mut OmenDB, items_json: *const c_char) -> i64 {
216    clear_last_error();
217
218    let Some(db) = db.as_mut() else {
219        set_last_error("Null database handle".to_string());
220        return -1;
221    };
222
223    if items_json.is_null() {
224        set_last_error("Null items_json pointer".to_string());
225        return -1;
226    }
227
228    let items_str = match CStr::from_ptr(items_json).to_str() {
229        Ok(s) => s,
230        Err(e) => {
231            set_last_error(format!("Invalid JSON string: {e}"));
232            return -1;
233        }
234    };
235
236    let items: Vec<JsonValue> = match serde_json::from_str(items_str) {
237        Ok(v) => v,
238        Err(e) => {
239            set_last_error(format!("JSON parse error: {e}"));
240            return -1;
241        }
242    };
243
244    let mut count = 0i64;
245    for item in items {
246        let id = if let Some(s) = item.get("id").and_then(|v| v.as_str()) {
247            s.to_string()
248        } else {
249            set_last_error("Item missing 'id' field".to_string());
250            return -1;
251        };
252
253        let vector_data: Vec<f32> = if let Some(arr) = item.get("vector").and_then(|v| v.as_array())
254        {
255            arr.iter()
256                .filter_map(|v| v.as_f64().map(|f| f as f32))
257                .collect()
258        } else {
259            set_last_error("Item missing 'vector' field".to_string());
260            return -1;
261        };
262
263        let metadata = item.get("metadata").cloned().unwrap_or(json!({}));
264
265        let vector = Vector::new(vector_data);
266        if let Err(e) = db.store.set(id, vector, metadata) {
267            // Rebuild cache even on partial failure to stay in sync
268            db.rebuild_cache();
269            set_last_error(format!("Set failed after {count} items: {e}"));
270            return -1;
271        }
272        count += 1;
273    }
274
275    // Rebuild cache after insertions
276    db.rebuild_cache();
277    count
278}
279
280/// Get vectors by ID
281///
282/// # Arguments
283/// * `db` - Database handle
284/// * `ids_json` - JSON array of IDs: `["id1", "id2", ...]`
285/// * `result` - Output pointer for result JSON (caller must free with `omendb_free_string`)
286///
287/// # Returns
288/// 0 on success, -1 on error
289///
290/// # Safety
291/// - `db` must be a valid pointer returned by `omendb_open`
292/// - `ids_json` must be a valid, null-terminated UTF-8 string
293/// - `result` must be a valid pointer to a `*mut c_char`
294#[no_mangle]
295pub unsafe extern "C" fn omendb_get(
296    db: *mut OmenDB,
297    ids_json: *const c_char,
298    result: *mut *mut c_char,
299) -> i32 {
300    clear_last_error();
301
302    let Some(db) = db.as_ref() else {
303        set_last_error("Null database handle".to_string());
304        return -1;
305    };
306
307    if ids_json.is_null() {
308        set_last_error("Null ids_json pointer".to_string());
309        return -1;
310    }
311
312    let ids_str = match CStr::from_ptr(ids_json).to_str() {
313        Ok(s) => s,
314        Err(e) => {
315            set_last_error(format!("Invalid JSON string: {e}"));
316            return -1;
317        }
318    };
319
320    let ids: Vec<String> = match serde_json::from_str(ids_str) {
321        Ok(v) => v,
322        Err(e) => {
323            set_last_error(format!("JSON parse error: {e}"));
324            return -1;
325        }
326    };
327
328    let mut results = Vec::new();
329    for id in ids {
330        if let Some((vector, metadata)) = db.store.get(&id) {
331            results.push(json!({
332                "id": id,
333                "vector": vector.data,
334                "metadata": metadata
335            }));
336        }
337    }
338
339    let json_str = match serde_json::to_string(&results) {
340        Ok(s) => s,
341        Err(e) => {
342            set_last_error(format!("JSON serialize error: {e}"));
343            return -1;
344        }
345    };
346
347    if result.is_null() {
348        set_last_error("Output pointer is NULL".to_string());
349        return -1;
350    }
351
352    match CString::new(json_str) {
353        Ok(cstr) => {
354            *result = cstr.into_raw();
355            0
356        }
357        Err(e) => {
358            set_last_error(format!("CString error: {e}"));
359            -1
360        }
361    }
362}
363
364/// Delete vectors by ID
365///
366/// # Returns
367/// Number of vectors deleted, or -1 on error
368///
369/// # Safety
370/// - `db` must be a valid pointer returned by `omendb_open`
371/// - `ids_json` must be a valid, null-terminated UTF-8 string
372#[no_mangle]
373pub unsafe extern "C" fn omendb_delete(db: *mut OmenDB, ids_json: *const c_char) -> i64 {
374    clear_last_error();
375
376    let Some(db) = db.as_mut() else {
377        set_last_error("Null database handle".to_string());
378        return -1;
379    };
380
381    if ids_json.is_null() {
382        set_last_error("Null ids_json pointer".to_string());
383        return -1;
384    }
385
386    let ids_str = match CStr::from_ptr(ids_json).to_str() {
387        Ok(s) => s,
388        Err(e) => {
389            set_last_error(format!("Invalid JSON string: {e}"));
390            return -1;
391        }
392    };
393
394    let ids: Vec<String> = match serde_json::from_str(ids_str) {
395        Ok(v) => v,
396        Err(e) => {
397            set_last_error(format!("JSON parse error: {e}"));
398            return -1;
399        }
400    };
401
402    match db.store.delete_batch(&ids) {
403        Ok(count) => {
404            db.rebuild_cache();
405            i64::try_from(count).unwrap_or(i64::MAX)
406        }
407        Err(e) => {
408            set_last_error(format!("Delete failed: {e}"));
409            -1
410        }
411    }
412}
413
414/// Search for similar vectors
415///
416/// # Arguments
417/// * `db` - Database handle
418/// * `query` - Query vector (float array)
419/// * `query_len` - Length of query vector
420/// * `k` - Number of results to return
421/// * `filter_json` - Optional filter JSON (NULL for no filter)
422/// * `result` - Output pointer for result JSON (caller must free with `omendb_free_string`)
423///
424/// # Returns
425/// 0 on success, -1 on error
426///
427/// # Safety
428/// - `db` must be a valid pointer returned by `omendb_open`
429/// - `query` must point to at least `query_len` valid f32 values
430/// - `filter_json` must be NULL or a valid, null-terminated UTF-8 string
431/// - `result` must be a valid pointer to a `*mut c_char`
432#[no_mangle]
433pub unsafe extern "C" fn omendb_search(
434    db: *mut OmenDB,
435    query: *const f32,
436    query_len: usize,
437    k: usize,
438    filter_json: *const c_char,
439    result: *mut *mut c_char,
440) -> i32 {
441    clear_last_error();
442
443    let Some(db) = db.as_mut() else {
444        set_last_error("Null database handle".to_string());
445        return -1;
446    };
447
448    if query.is_null() {
449        set_last_error("Null query pointer".to_string());
450        return -1;
451    }
452
453    if query_len != db.dimensions {
454        set_last_error(format!(
455            "Query dimension mismatch: expected {}, got {query_len}",
456            db.dimensions
457        ));
458        return -1;
459    }
460
461    let query_vec: Vec<f32> = std::slice::from_raw_parts(query, query_len).to_vec();
462    let query = Vector::new(query_vec);
463
464    // Parse filter if provided
465    let filter: Option<MetadataFilter> = if filter_json.is_null() {
466        None
467    } else {
468        let filter_str = match CStr::from_ptr(filter_json).to_str() {
469            Ok(s) => s,
470            Err(e) => {
471                set_last_error(format!("Invalid filter string: {e}"));
472                return -1;
473            }
474        };
475        match serde_json::from_str::<JsonValue>(filter_str) {
476            Ok(v) => match MetadataFilter::from_json(&v) {
477                Ok(f) => Some(f),
478                Err(e) => {
479                    set_last_error(format!("Invalid filter format: {e}"));
480                    return -1;
481                }
482            },
483            Err(e) => {
484                set_last_error(format!("Invalid filter JSON: {e}"));
485                return -1;
486            }
487        }
488    };
489
490    // Search using the store's search method (returns index, distance, metadata)
491    let results = match db.store.search(&query, k, filter.as_ref()) {
492        Ok(r) => r,
493        Err(e) => {
494            set_last_error(format!("Search failed: {e}"));
495            return -1;
496        }
497    };
498
499    // Convert results to JSON, mapping internal indices to string IDs
500    let json_results: Vec<JsonValue> = results
501        .into_iter()
502        .map(|(idx, distance, metadata)| {
503            json!({
504                "id": db.get_id(idx),
505                "distance": distance,
506                "metadata": metadata
507            })
508        })
509        .collect();
510
511    let json_str = match serde_json::to_string(&json_results) {
512        Ok(s) => s,
513        Err(e) => {
514            set_last_error(format!("JSON serialize error: {e}"));
515            return -1;
516        }
517    };
518
519    if result.is_null() {
520        set_last_error("Output pointer is NULL".to_string());
521        return -1;
522    }
523
524    match CString::new(json_str) {
525        Ok(cstr) => {
526            *result = cstr.into_raw();
527            0
528        }
529        Err(e) => {
530            set_last_error(format!("CString error: {e}"));
531            -1
532        }
533    }
534}
535
536/// Get number of vectors in database
537///
538/// # Safety
539/// - `db` must be NULL or a valid pointer returned by `omendb_open`
540#[no_mangle]
541pub unsafe extern "C" fn omendb_count(db: *const OmenDB) -> i64 {
542    clear_last_error();
543    match db.as_ref() {
544        Some(db) => i64::try_from(db.store.len()).unwrap_or(i64::MAX),
545        None => {
546            set_last_error("Null database handle".to_string());
547            -1
548        }
549    }
550}
551
552/// Save database to disk
553///
554/// # Safety
555/// - `db` must be a valid pointer returned by `omendb_open`
556#[no_mangle]
557pub unsafe extern "C" fn omendb_save(db: *mut OmenDB) -> i32 {
558    clear_last_error();
559
560    let Some(db) = db.as_mut() else {
561        set_last_error("Null database handle".to_string());
562        return -1;
563    };
564
565    match db.store.flush() {
566        Ok(()) => 0,
567        Err(e) => {
568            set_last_error(format!("Save failed: {e}"));
569            -1
570        }
571    }
572}
573
574/// Get last error message
575///
576/// # Returns
577/// Error message string (valid until next FFI call), or NULL if no error
578#[no_mangle]
579pub extern "C" fn omendb_last_error() -> *const c_char {
580    LAST_ERROR.with(|e| match &*e.borrow() {
581        Some(cstr) => cstr.as_ptr(),
582        None => ptr::null(),
583    })
584}
585
586/// Free a string returned by `OmenDB`
587///
588/// # Safety
589/// - `s` must be NULL or a valid pointer returned by an `OmenDB` function
590/// - After calling this function, `s` is invalid and must not be used
591#[no_mangle]
592pub unsafe extern "C" fn omendb_free_string(s: *mut c_char) {
593    if !s.is_null() {
594        drop(CString::from_raw(s));
595    }
596}
597
598/// Get `OmenDB` version
599#[no_mangle]
600pub extern "C" fn omendb_version() -> *const c_char {
601    // Use compile-time version from Cargo.toml with null terminator
602    concat!(env!("CARGO_PKG_VERSION"), "\0")
603        .as_ptr()
604        .cast::<c_char>()
605}
606
607// ============================================================================
608// Hybrid Search FFI
609// ============================================================================
610
611/// Enable text search for hybrid search
612///
613/// # Returns
614/// 0 on success, -1 on error
615///
616/// # Safety
617/// - `db` must be a valid pointer returned by `omendb_open`
618#[no_mangle]
619pub unsafe extern "C" fn omendb_enable_text_search(db: *mut OmenDB) -> i32 {
620    clear_last_error();
621
622    let Some(db) = db.as_mut() else {
623        set_last_error("Null database handle".to_string());
624        return -1;
625    };
626
627    match db.store.enable_text_search() {
628        Ok(()) => 0,
629        Err(e) => {
630            set_last_error(format!("Failed to enable text search: {e}"));
631            -1
632        }
633    }
634}
635
636/// Check if text search is enabled
637///
638/// # Returns
639/// 1 if enabled, 0 if not, -1 on error
640///
641/// # Safety
642/// - `db` must be a valid pointer returned by `omendb_open`
643#[no_mangle]
644pub unsafe extern "C" fn omendb_has_text_search(db: *const OmenDB) -> i32 {
645    clear_last_error();
646    let Some(db) = db.as_ref() else {
647        set_last_error("Null database handle".to_string());
648        return -1;
649    };
650    i32::from(db.store.has_text_search())
651}
652
653/// Set vectors with text for hybrid search
654///
655/// # Arguments
656/// * `db` - Database handle
657/// * `items_json` - JSON array: `[{"id": "...", "vector": [...], "text": "...", "metadata": {...}}, ...]`
658///
659/// # Returns
660/// Number of vectors inserted, or -1 on error
661///
662/// # Safety
663/// - `db` must be a valid pointer returned by `omendb_open`
664/// - `items_json` must be a valid, null-terminated UTF-8 string
665#[no_mangle]
666pub unsafe extern "C" fn omendb_set_with_text(db: *mut OmenDB, items_json: *const c_char) -> i64 {
667    clear_last_error();
668
669    let Some(db) = db.as_mut() else {
670        set_last_error("Null database handle".to_string());
671        return -1;
672    };
673
674    if !db.store.has_text_search() {
675        set_last_error(
676            "Text search not enabled. Call omendb_enable_text_search first.".to_string(),
677        );
678        return -1;
679    }
680
681    if items_json.is_null() {
682        set_last_error("Null items_json pointer".to_string());
683        return -1;
684    }
685
686    let items_str = match CStr::from_ptr(items_json).to_str() {
687        Ok(s) => s,
688        Err(e) => {
689            set_last_error(format!("Invalid JSON string: {e}"));
690            return -1;
691        }
692    };
693
694    let items: Vec<JsonValue> = match serde_json::from_str(items_str) {
695        Ok(v) => v,
696        Err(e) => {
697            set_last_error(format!("JSON parse error: {e}"));
698            return -1;
699        }
700    };
701
702    let mut count = 0i64;
703    for item in items {
704        let id = if let Some(s) = item.get("id").and_then(|v| v.as_str()) {
705            s.to_string()
706        } else {
707            set_last_error("Item missing 'id' field".to_string());
708            return -1;
709        };
710
711        let vector_data: Vec<f32> = if let Some(arr) = item.get("vector").and_then(|v| v.as_array())
712        {
713            arr.iter()
714                .filter_map(|v| v.as_f64().map(|f| f as f32))
715                .collect()
716        } else {
717            set_last_error("Item missing 'vector' field".to_string());
718            return -1;
719        };
720
721        let text = if let Some(s) = item.get("text").and_then(|v| v.as_str()) {
722            s
723        } else {
724            set_last_error("Item missing 'text' field".to_string());
725            return -1;
726        };
727
728        let metadata = item.get("metadata").cloned().unwrap_or(json!({}));
729
730        let vector = Vector::new(vector_data);
731        if let Err(e) = db.store.set_with_text(id, vector, text, metadata) {
732            // Rebuild cache even on partial failure to stay in sync
733            db.rebuild_cache();
734            set_last_error(format!("Set with text failed after {count} items: {e}"));
735            return -1;
736        }
737        count += 1;
738    }
739
740    // Rebuild cache after insertions
741    db.rebuild_cache();
742    count
743}
744
745/// Text-only search (BM25)
746///
747/// # Arguments
748/// * `db` - Database handle
749/// * `query` - Text query string
750/// * `k` - Number of results
751/// * `result` - Output pointer for result JSON
752///
753/// # Returns
754/// 0 on success, -1 on error
755///
756/// # Safety
757/// - All pointer arguments must be valid
758#[no_mangle]
759pub unsafe extern "C" fn omendb_text_search(
760    db: *mut OmenDB,
761    query: *const c_char,
762    k: usize,
763    result: *mut *mut c_char,
764) -> i32 {
765    clear_last_error();
766
767    let Some(db) = db.as_ref() else {
768        set_last_error("Null database handle".to_string());
769        return -1;
770    };
771
772    if query.is_null() {
773        set_last_error("Null query pointer".to_string());
774        return -1;
775    }
776
777    let query_str = match CStr::from_ptr(query).to_str() {
778        Ok(s) => s,
779        Err(e) => {
780            set_last_error(format!("Invalid query string: {e}"));
781            return -1;
782        }
783    };
784
785    let search_results = match db.store.text_search(query_str, k) {
786        Ok(r) => r,
787        Err(e) => {
788            set_last_error(format!("Text search failed: {e}"));
789            return -1;
790        }
791    };
792
793    let json_results: Vec<JsonValue> = search_results
794        .into_iter()
795        .map(|(id, score)| json!({"id": id, "score": score}))
796        .collect();
797
798    let json_str = match serde_json::to_string(&json_results) {
799        Ok(s) => s,
800        Err(e) => {
801            set_last_error(format!("JSON serialize error: {e}"));
802            return -1;
803        }
804    };
805
806    if result.is_null() {
807        set_last_error("Output pointer is NULL".to_string());
808        return -1;
809    }
810
811    match CString::new(json_str) {
812        Ok(cstr) => {
813            *result = cstr.into_raw();
814            0
815        }
816        Err(e) => {
817            set_last_error(format!("CString error: {e}"));
818            -1
819        }
820    }
821}
822
823/// Hybrid search combining vector and text
824///
825/// # Arguments
826/// * `db` - Database handle
827/// * `query_vector` - Query vector (float array)
828/// * `query_len` - Length of query vector
829/// * `query_text` - Text query string
830/// * `k` - Number of results
831/// * `alpha` - Weight for vector vs text (0.0=text only, 1.0=vector only, <0 for default 0.5)
832/// * `rrf_k` - RRF constant (0 for default 60)
833/// * `filter_json` - Optional filter JSON string (NULL for no filter)
834/// * `result` - Output pointer for result JSON
835///
836/// # Returns
837/// 0 on success, -1 on error
838///
839/// Result JSON format: `[{"id": "...", "score": 0.5, "metadata": {...}}, ...]`
840///
841/// # Safety
842/// - All pointer arguments must be valid (except filter_json which can be NULL)
843#[no_mangle]
844pub unsafe extern "C" fn omendb_hybrid_search(
845    db: *mut OmenDB,
846    query_vector: *const f32,
847    query_len: usize,
848    query_text: *const c_char,
849    k: usize,
850    alpha: f32,
851    rrf_k: usize,
852    filter_json: *const c_char,
853    result: *mut *mut c_char,
854) -> i32 {
855    clear_last_error();
856
857    let Some(db) = db.as_mut() else {
858        set_last_error("Null database handle".to_string());
859        return -1;
860    };
861
862    if query_vector.is_null() {
863        set_last_error("Null query_vector pointer".to_string());
864        return -1;
865    }
866
867    if query_text.is_null() {
868        set_last_error("Null query_text pointer".to_string());
869        return -1;
870    }
871
872    if query_len != db.dimensions {
873        set_last_error(format!(
874            "Query dimension mismatch: expected {}, got {query_len}",
875            db.dimensions
876        ));
877        return -1;
878    }
879
880    let query_vec: Vec<f32> = std::slice::from_raw_parts(query_vector, query_len).to_vec();
881    let vector = Vector::new(query_vec);
882
883    let text_str = match CStr::from_ptr(query_text).to_str() {
884        Ok(s) => s,
885        Err(e) => {
886            set_last_error(format!("Invalid text query: {e}"));
887            return -1;
888        }
889    };
890
891    // Use None for default (0.5), otherwise use provided alpha
892    let alpha_opt = if alpha < 0.0 { None } else { Some(alpha) };
893    let rrf_k_opt = if rrf_k == 0 { None } else { Some(rrf_k) };
894
895    // Parse optional filter
896    let filter = if filter_json.is_null() {
897        None
898    } else {
899        let filter_str = match CStr::from_ptr(filter_json).to_str() {
900            Ok(s) => s,
901            Err(e) => {
902                set_last_error(format!("Invalid filter string: {e}"));
903                return -1;
904            }
905        };
906        match serde_json::from_str::<JsonValue>(filter_str) {
907            Ok(v) => match MetadataFilter::from_json(&v) {
908                Ok(f) => Some(f),
909                Err(e) => {
910                    set_last_error(format!("Invalid filter format: {e}"));
911                    return -1;
912                }
913            },
914            Err(e) => {
915                set_last_error(format!("Invalid filter JSON: {e}"));
916                return -1;
917            }
918        }
919    };
920
921    let search_results = if let Some(f) = filter {
922        match db
923            .store
924            .hybrid_search_with_filter_rrf_k(&vector, text_str, k, &f, alpha_opt, rrf_k_opt)
925        {
926            Ok(r) => r,
927            Err(e) => {
928                set_last_error(format!("Hybrid search failed: {e}"));
929                return -1;
930            }
931        }
932    } else {
933        match db
934            .store
935            .hybrid_search_with_rrf_k(&vector, text_str, k, alpha_opt, rrf_k_opt)
936        {
937            Ok(r) => r,
938            Err(e) => {
939                set_last_error(format!("Hybrid search failed: {e}"));
940                return -1;
941            }
942        }
943    };
944
945    let json_results: Vec<JsonValue> = search_results
946        .into_iter()
947        .map(|(id, score, metadata)| json!({"id": id, "score": score, "metadata": metadata}))
948        .collect();
949
950    let json_str = match serde_json::to_string(&json_results) {
951        Ok(s) => s,
952        Err(e) => {
953            set_last_error(format!("JSON serialize error: {e}"));
954            return -1;
955        }
956    };
957
958    if result.is_null() {
959        set_last_error("Output pointer is NULL".to_string());
960        return -1;
961    }
962
963    match CString::new(json_str) {
964        Ok(cstr) => {
965            *result = cstr.into_raw();
966            0
967        }
968        Err(e) => {
969            set_last_error(format!("CString error: {e}"));
970            -1
971        }
972    }
973}
974
975/// Flush pending changes (commits text index)
976///
977/// # Safety
978/// - `db` must be a valid pointer returned by `omendb_open`
979#[no_mangle]
980pub unsafe extern "C" fn omendb_flush(db: *mut OmenDB) -> i32 {
981    clear_last_error();
982
983    let Some(db) = db.as_mut() else {
984        set_last_error("Null database handle".to_string());
985        return -1;
986    };
987
988    match db.store.flush() {
989        Ok(()) => 0,
990        Err(e) => {
991            set_last_error(format!("Flush failed: {e}"));
992            -1
993        }
994    }
995}