@@ -158,6 +158,7 @@ class C_DatabaseConfig(ctypes.Structure):
158158
159159
160160class C_StorageStats (ctypes .Structure ):
161+ """Storage statistics returned by toondb_stats."""
161162 _fields_ = [
162163 ("memtable_size_bytes" , ctypes .c_uint64 ),
163164 ("wal_size_bytes" , ctypes .c_uint64 ),
@@ -167,6 +168,15 @@ class C_StorageStats(ctypes.Structure):
167168 ]
168169
169170
171+ class C_SearchResult (ctypes .Structure ):
172+ """Search result from toondb_collection_search."""
173+ _fields_ = [
174+ ("id_ptr" , ctypes .c_char_p ),
175+ ("score" , ctypes .c_float ),
176+ ("metadata_ptr" , ctypes .c_char_p ),
177+ ]
178+
179+
170180class _FFI :
171181 """FFI bindings to the native library."""
172182
@@ -324,43 +334,82 @@ def _setup_bindings(cls):
324334 ]
325335 lib .toondb_get_table_index_policy .restype = ctypes .c_uint8
326336
337+ # Graph Overlay API
338+ # toondb_graph_add_node(ptr, ns, id, type, props) -> c_int
339+ try :
340+ lib .toondb_graph_add_node .argtypes = [
341+ ctypes .c_void_p , ctypes .c_char_p , ctypes .c_char_p , ctypes .c_char_p , ctypes .c_char_p
342+ ]
343+ lib .toondb_graph_add_node .restype = ctypes .c_int
344+
345+ lib .toondb_graph_add_edge .argtypes = [
346+ ctypes .c_void_p , ctypes .c_char_p , ctypes .c_char_p , ctypes .c_char_p , ctypes .c_char_p , ctypes .c_char_p
347+ ]
348+ lib .toondb_graph_add_edge .restype = ctypes .c_int
349+
350+ lib .toondb_graph_traverse .argtypes = [
351+ ctypes .c_void_p , ctypes .c_char_p , ctypes .c_char_p , ctypes .c_size_t , ctypes .c_int , ctypes .POINTER (ctypes .c_size_t )
352+ ]
353+ lib .toondb_graph_traverse .restype = ctypes .c_void_p # Returns *char (json string)
354+ except (AttributeError , OSError ):
355+ pass
356+
327357 # Temporal Graph API
328- # NOTE: These FFI bindings are not yet available in the native library
329- # They are defined here for future compatibility but will cause dlsym errors if used
330- # Commenting out until the native library exports these symbols
331-
332- # class C_TemporalEdge(ctypes.Structure):
333- # _fields_ = [
334- # ("from_id", ctypes.c_char_p),
335- # ("edge_type", ctypes.c_char_p),
336- # ("to_id", ctypes.c_char_p),
337- # ("valid_from", ctypes.c_uint64),
338- # ("valid_until", ctypes.c_uint64),
339- # ("properties_json", ctypes.c_char_p),
340- # ]
341-
342- # # toondb_add_temporal_edge(ptr, namespace, edge) -> c_int
343- # lib.toondb_add_temporal_edge.argtypes = [
344- # ctypes.c_void_p, # ptr
345- # ctypes.c_char_p, # namespace
346- # C_TemporalEdge # edge struct by value
347- # ]
348- # lib.toondb_add_temporal_edge.restype = ctypes.c_int
349-
350- # # toondb_query_temporal_graph(ptr, namespace, node_id, mode, timestamp, edge_type) -> *c_char
351- # lib.toondb_query_temporal_graph.argtypes = [
352- # ctypes.c_void_p, # ptr
353- # ctypes.c_char_p, # namespace
354- # ctypes.c_char_p, # node_id
355- # ctypes.c_int, # mode (0=CURRENT, 1=POINT_IN_TIME, 2=RANGE)
356- # ctypes.c_uint64, # timestamp
357- # ctypes.c_char_p # edge_type (optional, can be NULL)
358- # ]
359- # lib.toondb_query_temporal_graph.restype = ctypes.c_char_p
360-
361- # # toondb_free_string(ptr) - Free strings returned by query_temporal_graph
362- # lib.toondb_free_string.argtypes = [ctypes.c_char_p]
363- # lib.toondb_free_string.restype = None
358+ try :
359+ # toondb_query_temporal_graph(ptr, ns, node, mode, ts, start, end, type, out_len)
360+ lib .toondb_query_temporal_graph .argtypes = [
361+ ctypes .c_void_p ,
362+ ctypes .c_char_p ,
363+ ctypes .c_char_p ,
364+ ctypes .c_uint8 , # mode u8
365+ ctypes .c_uint64 , # timestamp
366+ ctypes .c_uint64 , # start_time
367+ ctypes .c_uint64 , # end_time
368+ ctypes .c_char_p , # edge_type
369+ ctypes .POINTER (ctypes .c_size_t ) # out_len
370+ ]
371+ lib .toondb_query_temporal_graph .restype = ctypes .c_void_p # Returns *char
372+
373+ # toondb_free_string(ptr)
374+ lib .toondb_free_string .argtypes = [ctypes .c_void_p ]
375+ lib .toondb_free_string .restype = None
376+ except (AttributeError , OSError ):
377+ pass
378+
379+ # Collection Search API (Native Rust vector search)
380+ # Optional: Only available in newer native library versions
381+ try :
382+ lib .toondb_collection_search .argtypes = [
383+ ctypes .c_void_p , # ptr
384+ ctypes .c_char_p , # namespace
385+ ctypes .c_char_p , # collection
386+ ctypes .POINTER (ctypes .c_float ), # query_ptr
387+ ctypes .c_size_t , # query_len
388+ ctypes .c_size_t , # k
389+ ctypes .POINTER (C_SearchResult ), # results_out
390+ ]
391+ lib .toondb_collection_search .restype = ctypes .c_int
392+
393+ # Keyword Search API (Native Rust text search)
394+ # toondb_collection_keyword_search(ptr, namespace, collection, query_ptr, k, results_out) -> c_int
395+ lib .toondb_collection_keyword_search .argtypes = [
396+ ctypes .c_void_p , # ptr
397+ ctypes .c_char_p , # namespace
398+ ctypes .c_char_p , # collection
399+ ctypes .c_char_p , # query_ptr (string)
400+ ctypes .c_size_t , # k
401+ ctypes .POINTER (C_SearchResult ), # results_out
402+ ]
403+ lib .toondb_collection_keyword_search .restype = ctypes .c_int
404+
405+ lib .toondb_search_result_free .argtypes = [
406+ ctypes .POINTER (C_SearchResult ),
407+ ctypes .c_size_t ,
408+ ]
409+ lib .toondb_search_result_free .restype = None
410+ except (AttributeError , OSError ):
411+ # Symbol not available in this library version
412+ pass
364413
365414
366415class Transaction :
@@ -1950,18 +1999,21 @@ def query_temporal_graph(
19501999 import time
19512000 timestamp = int (time .time () * 1000 )
19522001
1953- # Convert mode string to int
1954- mode_map = {"CURRENT " : 0 , "POINT_IN_TIME " : 1 , "RANGE " : 2 }
2002+ # Convert mode string to int (Must match ffi.rs: 0=POINT_IN_TIME, 1=RANGE, 2=CURRENT)
2003+ mode_map = {"POINT_IN_TIME " : 0 , "RANGE " : 1 , "CURRENT " : 2 }
19552004 mode_int = mode_map .get (mode , 0 )
19562005
19572006 # Call FFI function
1958- result_ptr = _FFI . lib .toondb_query_temporal_graph (
1959- self ._ptr ,
2007+ result_ptr = self . _lib .toondb_query_temporal_graph (
2008+ self ._handle ,
19602009 namespace .encode ("utf-8" ),
19612010 node_id .encode ("utf-8" ),
19622011 mode_int ,
1963- timestamp or 0 ,
1964- edge_type .encode ("utf-8" ) if edge_type else None
2012+ ctypes .c_uint64 (timestamp or 0 ),
2013+ ctypes .c_uint64 (0 ), # start_time
2014+ ctypes .c_uint64 (0 ), # end_time
2015+ edge_type .encode ("utf-8" ) if edge_type else None ,
2016+ ctypes .byref (ctypes .c_size_t ()) # Add missing out_len arg from FFI signature!
19652017 )
19662018
19672019 if result_ptr is None :
@@ -1975,7 +2027,8 @@ def query_temporal_graph(
19752027 return edges
19762028 finally :
19772029 # Free the C string
1978- _FFI .lib .toondb_free_string (result_ptr )
2030+ if result_ptr :
2031+ self ._lib .toondb_free_string (result_ptr )
19792032
19802033 # =========================================================================
19812034 # Graph Overlay Operations (FFI) - Nodes, Edges, Traversal
@@ -2009,8 +2062,8 @@ def add_node(
20092062 import json
20102063 props_json = json .dumps (properties or {}).encode ("utf-8" )
20112064
2012- result = _FFI . lib .toondb_graph_add_node (
2013- self ._ptr ,
2065+ result = self . _lib .toondb_graph_add_node (
2066+ self ._handle ,
20142067 namespace .encode ("utf-8" ),
20152068 node_id .encode ("utf-8" ),
20162069 node_type .encode ("utf-8" ),
@@ -2051,8 +2104,8 @@ def add_edge(
20512104 import json
20522105 props_json = json .dumps (properties or {}).encode ("utf-8" )
20532106
2054- result = _FFI . lib .toondb_graph_add_edge (
2055- self ._ptr ,
2107+ result = self . _lib .toondb_graph_add_edge (
2108+ self ._handle ,
20562109 namespace .encode ("utf-8" ),
20572110 from_id .encode ("utf-8" ),
20582111 edge_type .encode ("utf-8" ),
@@ -2098,8 +2151,8 @@ def traverse(
20982151 order_int = 0 if order .lower () == "bfs" else 1
20992152 out_len = ctypes .c_size_t ()
21002153
2101- result_ptr = _FFI . lib .toondb_graph_traverse (
2102- self ._ptr ,
2154+ result_ptr = self . _lib .toondb_graph_traverse (
2155+ self ._handle ,
21032156 namespace .encode ("utf-8" ),
21042157 start_node .encode ("utf-8" ),
21052158 max_depth ,
@@ -2115,8 +2168,135 @@ def traverse(
21152168 data = json .loads (json_str )
21162169 return data .get ("nodes" , []), data .get ("edges" , [])
21172170 finally :
2118- _FFI .lib .toondb_free_string (result_ptr )
2171+ if result_ptr :
2172+ self ._lib .toondb_free_string (result_ptr )
2173+
2174+ # =========================================================================
2175+ # Collection Search FFI (Native Rust performance)
2176+ # =========================================================================
2177+
2178+ def ffi_collection_search (
2179+ self ,
2180+ namespace : str ,
2181+ collection : str ,
2182+ query_vector : List [float ],
2183+ k : int = 10
2184+ ) -> List [Dict ]:
2185+ """
2186+ Native vector search using Rust FFI.
2187+
2188+ This is 40x faster than Python brute-force search.
2189+ Returns list of {id, score, metadata} dicts.
2190+ """
2191+ self ._check_open ()
2192+
2193+ import numpy as np
2194+
2195+ # Prepare query vector
2196+ query_array = np .array (query_vector , dtype = np .float32 )
2197+ query_ptr = query_array .ctypes .data_as (ctypes .POINTER (ctypes .c_float ))
2198+
2199+ # Allocate results array
2200+ results = (C_SearchResult * k )()
2201+
2202+ # Call FFI
2203+ try :
2204+ num_results = self ._lib .toondb_collection_search (
2205+ self ._handle ,
2206+ namespace .encode ("utf-8" ),
2207+ collection .encode ("utf-8" ),
2208+ query_ptr ,
2209+ len (query_vector ),
2210+ k ,
2211+ results
2212+ )
2213+
2214+ if num_results < 0 :
2215+ return []
2216+
2217+ # Parse results
2218+ output = []
2219+ for i in range (num_results ):
2220+ result = results [i ]
2221+ doc_id = result .id_ptr .decode ("utf-8" ) if result .id_ptr else None
2222+ metadata_str = result .metadata_ptr .decode ("utf-8" ) if result .metadata_ptr else "{}"
2223+
2224+ try :
2225+ import json
2226+ metadata = json .loads (metadata_str )
2227+ except :
2228+ metadata = {}
2229+
2230+ output .append ({
2231+ "id" : doc_id ,
2232+ "score" : result .score ,
2233+ "metadata" : metadata ,
2234+ })
2235+
2236+ # Free results
2237+ self ._lib .toondb_search_result_free (results , num_results )
2238+
2239+ return output
2240+ except (AttributeError , OSError ) as e :
2241+ # FFI not available, return empty (caller should fallback)
2242+ return None
21192243
2244+ def ffi_collection_keyword_search (
2245+ self ,
2246+ namespace : str ,
2247+ collection : str ,
2248+ query_text : str ,
2249+ k : int = 10
2250+ ) -> List [Dict ]:
2251+ """
2252+ Native keyword search using Rust FFI.
2253+ """
2254+ self ._check_open ()
2255+
2256+ # Allocate results array
2257+ results = (C_SearchResult * k )()
2258+
2259+ # Call FFI
2260+ try :
2261+ num_results = self ._lib .toondb_collection_keyword_search (
2262+ self ._handle ,
2263+ namespace .encode ("utf-8" ),
2264+ collection .encode ("utf-8" ),
2265+ query_text .encode ("utf-8" ),
2266+ k ,
2267+ results
2268+ )
2269+
2270+ if num_results < 0 :
2271+ return []
2272+
2273+ # Parse results
2274+ output = []
2275+ for i in range (num_results ):
2276+ result = results [i ]
2277+ doc_id = result .id_ptr .decode ("utf-8" ) if result .id_ptr else None
2278+ metadata_str = result .metadata_ptr .decode ("utf-8" ) if result .metadata_ptr else "{}"
2279+
2280+ try :
2281+ import json
2282+ metadata = json .loads (metadata_str )
2283+ except :
2284+ metadata = {}
2285+
2286+ output .append ({
2287+ "id" : doc_id ,
2288+ "score" : result .score ,
2289+ "metadata" : metadata ,
2290+ })
2291+
2292+ # Free results
2293+ self ._lib .toondb_search_result_free (results , num_results )
2294+
2295+ return output
2296+ except (AttributeError , OSError ) as e :
2297+ # FFI not available, return empty (caller should fallback)
2298+ return None
2299+
21202300 # =========================================================================
21212301 # Semantic Cache Operations (FFI)
21222302 # =========================================================================
@@ -2163,7 +2343,7 @@ def cache_put(
21632343 emb_ptr = emb_array .ctypes .data_as (ctypes .POINTER (ctypes .c_float ))
21642344
21652345 result = _FFI .lib .toondb_cache_put (
2166- self ._ptr ,
2346+ self ._handle ,
21672347 cache_name .encode ("utf-8" ),
21682348 key .encode ("utf-8" ),
21692349 value .encode ("utf-8" ),
@@ -2235,7 +2415,7 @@ def cache_get(
22352415 out_len = ctypes .c_size_t ()
22362416
22372417 result_ptr = _FFI .lib .toondb_cache_get (
2238- self ._ptr ,
2418+ self ._handle ,
22392419 cache_name .encode ("utf-8" ),
22402420 emb_ptr ,
22412421 len (query_embedding ),
0 commit comments