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