diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index be466bd7..32fa165e 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -16,9 +16,19 @@ cmake_minimum_required(VERSION 3.21) project(svs_c_api VERSION 0.4.0 LANGUAGES CXX C) set(TARGET_NAME svs_c_api) +include(GNUInstallDirs) + +set(SVS_C_API_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/include") +set(SVS_C_API_VERSION_HEADER "${SVS_C_API_GENERATED_INCLUDE_DIR}/svs/c/svs_c_version.h") +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/include/svs/c/svs_c_version.h.in" + "${SVS_C_API_VERSION_HEADER}" +) + set(SVS_C_API_HEADERS - include/svs/c_api/svs_c_config.h - include/svs/c_api/svs_c.h + include/svs/c/svs_c_config.h + include/svs/c/svs_c.h + ${SVS_C_API_VERSION_HEADER} ) set(SVS_C_API_SOURCES @@ -42,9 +52,12 @@ add_library(${TARGET_NAME} SHARED ${SVS_C_API_SOURCES} ) -target_include_directories(${TARGET_NAME} PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/include - ${CMAKE_CURRENT_SOURCE_DIR}/src +target_include_directories(${TARGET_NAME} + PUBLIC + $ + $ + $ + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ) find_package(OpenMP REQUIRED) @@ -170,8 +183,6 @@ else() endif() # Installing -include(GNUInstallDirs) - set(SVS_C_API_EXPORT_NAME ${TARGET_NAME}) set(VERSION_CONFIG "${CMAKE_CURRENT_BINARY_DIR}/${SVS_C_API_EXPORT_NAME}ConfigVersion.cmake") set(SVS_C_API_CONFIG_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/svs_c_api) @@ -181,11 +192,11 @@ install(TARGETS ${TARGET_NAME} EXPORT ${SVS_C_API_EXPORT_NAME} COMPONENT ${SVS_C_API_COMPONENT_NAME} LIBRARY DESTINATION lib - PUBLIC_HEADER DESTINATION include/svs/c_api + PUBLIC_HEADER DESTINATION include/svs/c INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) -install(DIRECTORY include/svs/c_api +install(DIRECTORY include/svs/c COMPONENT ${SVS_C_API_COMPONENT_NAME} DESTINATION include/svs FILES_MATCHING PATTERN "*.h" diff --git a/bindings/c/README.md b/bindings/c/README.md new file mode 100644 index 00000000..42317317 --- /dev/null +++ b/bindings/c/README.md @@ -0,0 +1,199 @@ + + +# SVS C API + +A C ABI binding for [Scalable Vector Search](../..) that enables integration with +C applications and any language with C FFI support. + +The API is built around a small set of opaque handles and a builder pattern: +configure an *algorithm*, optional *storage* and *thread pool*, hand them to an +*index builder*, then use the resulting *index* to run TopK searches (with +optional ID filtering), save/load the index, and — for dynamic indices — add or +delete points at runtime. + +For the design rationale, naming conventions, and full API reference see +[docs/C_API_Design.md](docs/C_API_Design.md). + +## Public Headers + +```c +#include "svs/c/svs_c.h" // Main C API +``` + +## Building and Consuming + +The C API is built as a shared library target `svs_c_api` and installs a CMake +package `svs_c_api` with the imported target `svs::svs_c_api`. + +### Build from source + +Configure and build from the top of the ScalableVectorSearch tree; the C API is +picked up as a subdirectory under `bindings/c`: + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --target svs_c_api +cmake --install build +``` + +### Consume from a downstream CMake project + +```cmake +find_package(svs_c_api REQUIRED) +target_link_libraries(my_app PRIVATE svs::svs_c_api) +``` + +The library only exposes a C ABI, so downstream code can be plain C99+ (or +C++20+); the C++20 requirement is a private build-time detail of the library +itself. + +## Language Requirements + +- C consumers: **C99** or later +- C++ consumers: **C++20** or later + +`svs_c.h` enforces this at include time via `#error` if the compiler standard +is below the required version. + +## Quick Start + +```c +#include "svs/c/svs_c.h" +#include +#include + +int main(void) { + // 1. Create error handle for diagnostics + svs_error_h err = svs_error_create(); + + // 2. Create Vamana algorithm configuration + svs_algorithm_h algo = svs_algorithm_create_vamana( + 64, // graph_degree + 128, // build_window_size + 128, // default search_window_size + err + ); + if (!algo) { + fprintf(stderr, "Algorithm creation failed: %s\n", + svs_error_get_message(err)); + svs_error_free(err); + return 1; + } + + // 3. Create index builder + size_t dimensions = 128; + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, dimensions, algo, err + ); + + // 4. Optional: configure storage (default is Simple FP32) + svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, err); + svs_index_builder_set_storage(builder, storage, err); + + // 5. Optional: configure thread pool + svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, /*num_threads=*/8, err + ); + + // 6. Prepare data + size_t num_vectors = 10000; + float* data = (float*)malloc(num_vectors * dimensions * sizeof(float)); + // ... fill data with vectors ... + + // 7. Build index + svs_index_h index = svs_index_build(builder, data, num_vectors, err); + if (!index) { + fprintf(stderr, "Index build failed: %s\n", svs_error_get_message(err)); + goto cleanup; + } + + // 8. Prepare queries + size_t num_queries = 10; + float* queries = (float*)malloc(num_queries * dimensions * sizeof(float)); + // ... fill queries ... + + // 9. Perform search (library-owned result buffers; reused across calls) + size_t k = 5; + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + + if (!svs_index_search_topk( + index, queries, num_queries, k, &results, + /*search_params=*/NULL, + /*id_filter=*/NULL, + err + )) { + fprintf(stderr, "Search failed: %s\n", svs_error_get_message(err)); + goto cleanup; + } + + // 10. Process results + for (size_t q = 0; q < results.num_queries; ++q) { + const size_t* ids; + const float* dists; + size_t count; + svs_search_results_row(&results, q, &ids, &dists, &count); + printf("Query %zu:\n", q); + for (size_t j = 0; j < count; ++j) { + printf(" Index: %zu, Distance: %f\n", ids[j], dists[j]); + } + } + + // 11. Optional: introspect memory usage + svs_memory_breakdown_t breakdown = SVS_INIT_MEMORY_BREAKDOWN(); + if (svs_index_get_memory_breakdown(index, &breakdown, err)) { + printf("Memory: graph=%zu data=%zu metadata=%zu bytes\n", + breakdown.graph_bytes, + breakdown.data_bytes, + breakdown.metadata_bytes); + } + + // 12. Optional: persist the index for later reuse + svs_index_save(index, "/tmp/my_index", err); + +cleanup: + svs_search_results_free(&results); + if (index) svs_index_free(index); + if (builder) svs_index_builder_free(builder); + if (storage) svs_storage_free(storage); + if (algo) svs_algorithm_free(algo); + svs_error_free(err); + + free(data); + free(queries); + + return 0; +} +``` + +## Samples + +Runnable sample applications live in [samples/](samples/): + +- [`simple.c`](samples/simple.c) – minimal static index build + search with a + custom thread pool +- [`dynamic.c`](samples/dynamic.c) – dynamic index with add / delete / + consolidate +- [`save_load.c`](samples/save_load.c) – persisting and reloading indices from + disk + +Additional integration examples: [`examples/c/`](../../examples/c/). + +## Further Reading + +- [docs/C_API_Design.md](docs/C_API_Design.md) – design goals, architecture, core + components, error-handling strategy, naming conventions, and complete API + reference. diff --git a/bindings/c/SVS_C_API_Design.md b/bindings/c/SVS_C_API_Design.md deleted file mode 100644 index 5c5bcbfe..00000000 --- a/bindings/c/SVS_C_API_Design.md +++ /dev/null @@ -1,673 +0,0 @@ - - -# SVS C API Design Proposal - -## Overview - -This document describes the design proposal for the Scalable Vector Search (SVS) C API. The API provides a C interface to SVS's vector similarity search capabilities, enabling integration with C applications and other languages that support C FFI (Foreign Function Interface). - -### Design Goals - -The SVS C API is designed with the following principles: - -1. **Simplicity** - Provide a minimal, intuitive set of operations to create and use vector search indices -2. **Flexibility** - Allow fine-grained control over: - - Index building parameters (graph degree, window sizes, etc.) - - Memory allocation strategies (simple, hugepage, custom) - - Thread pool configuration (native, OpenMP, custom) - - Vector storage formats (simple, compressed, quantized) - - Search parameters and filters - - Logging system -3. **Safety** - Comprehensive error handling with detailed error messages -4. **Portability** - Standard C interface that works across platforms and languages - -## Architecture Overview - -The API is built around a builder pattern with the following core abstractions: - -``` -┌─────────────────┐ -│ Index Builder │ Configure index parameters -│ - Algorithm │ -│ - Storage │ -│ - Threadpool │ -└────────┬────────┘ - │ build() - ↓ -┌─────────────────┐ -│ Index │ Perform searches -│ - search() │ with optional search params -└─────────────────┘ - │ - ├─ Search Params (optional) - └─ Search Results -``` - -## Core Components - -### 1. Index - -The main search structure providing vector similarity search operations. - -**Current Capabilities:** -- **TopK Search** - Find the k nearest neighbors for query vectors -- Configurable search parameters (window size, etc.) -- Multiple distance metrics (Euclidean, Cosine, Inner Product) - -**Requirements:** -- Built from a non-empty dataset using Index Builder -- Immutable after creation - -**Future Extensions:** -- Range search (all neighbors within distance threshold) -- Filtered search (predicate-based filtering) -- Dynamic updates (add/remove vectors) - -### 2. Index Builder - -Configures and creates index instances using the builder pattern. - -**Required Parameters:** -- Algorithm configuration handle -- Vector dimensions -- Distance metric (Euclidean, Cosine, Inner Product) - -**Optional Configuration:** -- Storage format (default: Simple FP32) -- Thread pool kind and size (default: native with hardware concurrency) -- Custom thread pool interface (for advanced use cases) - -### 3. Algorithm Configuration - -Defines the search algorithm and its parameters. - -**Current Support:** -- **Vamana** - Graph-based approximate nearest neighbor search - - Graph degree (connectivity) - - Build window size (construction search budget) - - Default search window size - - Alpha parameter (pruning threshold) - - Search history mode - -**Future Support:** -- **Flat** - Exhaustive brute-force search -- **IVF** - Inverted file with clustering - -### 4. Storage Configuration - -Defines how vectors are stored in memory, supporting various compression schemes. - -| Storage Type | Configuration Options | Description | -|--------------|----------------------|-------------| -| **Simple** | FP32, FP16, INT8, UINT8, INT4, UINT4 | Uncompressed storage | -| **SQ** | INT8, UINT8 | Scalar quantization | -| **LVQ** | Primary: INT4/UINT4/INT8/UINT8
Residual: VOID/INT4/UINT4/INT8/UINT8 | Locally-adaptive vector quantization | -| **LeanVec** | Dimensions
Primary: data type
Secondary: data type | LeanVec dimensionality reduced storage | - -**Example:** -```c -// Simple FP32 storage (default) -svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, err); - -// LVQ with 8-bit primary and 4-bit residual -svs_storage_h storage = svs_storage_create_lvq( - SVS_DATA_TYPE_UINT8, SVS_DATA_TYPE_UINT4, err -); - -// LeanVec with 128 dimensions -svs_storage_h storage = svs_storage_create_leanvec( - 128, SVS_DATA_TYPE_FLOAT16, SVS_DATA_TYPE_INT8, err -); - -// Scalar quantization -svs_storage_h storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, err); -``` - -### 5. Thread Pool Configuration - -Controls parallelization strategy for index operations. - -| Type | Configuration | Use Case | -|------|---------------|----------| -| **Native** | Thread count | Default SVS thread pool (recommended) | -| **OpenMP** | Uses OMP_NUM_THREADS | Integration with OpenMP applications | -| **Single Thread** | No parallelization | Debugging or minimal overhead | -| **Custom** | User-defined interface | Custom scheduling/work-stealing | - -**Custom Interface:** -```c -struct svs_threadpool_interface_ops { - size_t (*size)(void* self); - void (*parallel_for)( - void* self, - void (*func)(void* svs_param, size_t i), - void* svs_param, // SVS state - size_t n // Number of tasks - ); -}; - -struct svs_threadpool_interface { - struct svs_threadpool_interface_ops ops; - void* self; // User-defined state -}; -``` - -### 6. Search Parameters - -Configures runtime search behavior (algorithm-specific). - -**Vamana Search Parameters:** -- **Search window size** - Controls search accuracy vs. speed tradeoff - - Larger values: more accurate but slower - - Smaller values: faster but less accurate - - Typically 50-200 for good recall - -**Usage:** -```c -// Use custom search parameters -svs_search_params_h params = svs_search_params_create_vamana(100, err); -svs_search_results_t results = svs_index_search( - index, queries, num_queries, k, params, err -); -svs_search_params_free(params); - -// Or use defaults from algorithm configuration -svs_search_results_t results = svs_index_search( - index, queries, num_queries, k, NULL, err -); -``` - -## Error Handling Strategy - -The API uses a dual approach for error reporting: return codes and optional detailed error information. - -### Return Values - -- Functions returning handles return `NULL` on failure -- Functions returning booleans return `false` on failure -- All functions accept an optional `svs_error_h` parameter for detailed diagnostics - -### Detailed Error Information - -For comprehensive error diagnostics, create an error handle and pass it to API calls: - -```c -// Create error handle -svs_error_h err = svs_error_create(); - -// Use in API calls (last parameter, can be NULL) -svs_algorithm_h algo = svs_algorithm_create_vamana( - 64, // graph_degree - 128, // build_window_size - 128, // search_window_size - err // optional error handle (can be NULL) -); - -if (algo == NULL) { - // Check error status - if (!svs_error_ok(err)) { - // Query error details - svs_error_code_t code = svs_error_get_code(err); - const char* msg = svs_error_get_message(err); - fprintf(stderr, "Error [%d]: %s\n", code, msg); - } -} - -// Error handle can be reused across multiple calls -svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, err); - -// Free error handle when done -svs_error_free(err); -``` - -### Error Codes - -```c -enum svs_error_code { - SVS_OK = 0, // Success - SVS_ERROR_GENERIC = 1, // Generic/unspecified error - SVS_ERROR_INVALID_ARGUMENT = 2, // Invalid function parameter - SVS_ERROR_OUT_OF_MEMORY = 3, // Memory allocation failed - SVS_ERROR_NOT_IMPLEMENTED = 5, // Feature not yet available - SVS_ERROR_UNSUPPORTED_HW = 6, // Hardware doesn't support required features - SVS_ERROR_RUNTIME = 7, // Runtime error during operation - SVS_ERROR_UNKNOWN = 1000 // Unknown/unexpected error -}; -``` - -### Best Practices - -1. **Always check return values** - Test for `NULL` or `false` before using results -2. **Use error handles during development** - Provides detailed diagnostics and error messages -3. **Reuse error handles** - Single handle can be reused across multiple API calls -4. **Free all resources** - Always call appropriate `_free()` functions to prevent leaks -5. **Pass NULL for optional parameters** - Error handle and search params can be `NULL` if not needed -6. **Check `svs_error_ok()`** - Use this helper to check if operation succeeded - -## Naming Conventions - -Consistent naming improves API discoverability and reduces cognitive load. - -### Prefixes - -- `svs_` - All functions and types -- `SVS_` - Macros and constants - -### Type Suffixes - -| Suffix | Meaning | Example | -|--------|---------|----------| -| `_t` | Value type (enum, struct) | `svs_metric_t`, `svs_error_code_t` | -| `_h` | Handle (opaque pointer) | `svs_index_h`, `svs_algorithm_h` | -| `_i` | Interface structure | `svs_allocator_i`, `svs_threadpool_i` | - -### Function Naming Pattern - -``` -svs_[_]_ -``` - -**Examples:** - -| Function | Breakdown | Description | -|----------|-----------|-------------| -| `svs_index_search()` | `svs` + `index` + `search` | Generic index search | -| `svs_algo_vamana_set_alpha()` | `svs` + `algo` + `vamana` + `set_alpha` | Set Vamana-specific parameter | -| `svs_storage_create_lvq()` | `svs` + `storage` + `create` + `lvq` | Create LVQ storage configuration | -| `svs_factory_set_threadpool()` | `svs` + `factory` + `set` + `threadpool` | Configure builder thread pool | - -### Examples by Category - -```c -// Handles (opaque pointers) -typedef struct svs_index* svs_index_h; -typedef struct svs_algorithm* svs_algorithm_h; -typedef struct svs_storage* svs_storage_h; - -// Value types -typedef enum svs_metric svs_metric_t; -typedef enum svs_error_code svs_error_code_t; - -// Interface structures -typedef struct svs_allocator_interface svs_allocator_i; -typedef struct svs_threadpool_interface svs_threadpool_i; -``` - -## API Reference - -### Type Definitions - -```c -// Opaque handles (suffix: _h) -typedef struct svs_error_desc* svs_error_h; -typedef struct svs_index* svs_index_h; -typedef struct svs_index_builder* svs_index_builder_h; -typedef struct svs_algorithm* svs_algorithm_h; -typedef struct svs_storage* svs_storage_h; -typedef struct svs_search_params* svs_search_params_h; - -// Fully defined types (suffix: _t) -typedef enum svs_error_code svs_error_code_t; -typedef enum svs_distance_metric svs_distance_metric_t; -typedef enum svs_algorithm_type svs_algorithm_type_t; -typedef enum svs_data_type svs_data_type_t; -typedef enum svs_storage_kind svs_storage_kind_t; -typedef enum svs_threadpool_kind svs_threadpool_kind_t; - -// Interface pointers -typedef struct svs_threadpool_interface* svs_threadpool_i; -typedef struct svs_search_results* svs_search_results_t; -``` - -### Error Handling API - -```c -// Create and manage error handles -svs_error_h svs_error_create(void); -void svs_error_free(svs_error_h err); - -// Query error information -bool svs_error_ok(svs_error_h err); -svs_error_code_t svs_error_get_code(svs_error_h err); -const char* svs_error_get_message(svs_error_h err); -``` - -### Algorithm API - -Create and configure search algorithms. - -```c -// Vamana graph-based approximate nearest neighbor search -svs_algorithm_h svs_algorithm_create_vamana( - size_t graph_degree, // Graph connectivity (e.g., 64) - size_t build_window_size, // Construction search window (e.g., 128) - size_t search_window_size, // Default query search window (e.g., 128) - svs_error_h out_err // optional, can be NULL -); - -// Cleanup -void svs_algorithm_free(svs_algorithm_h algorithm); - -// Get/Set Vamana parameters -bool svs_algorithm_vamana_get_alpha( - svs_algorithm_h algorithm, - float* out_alpha, - svs_error_h out_err -); - -bool svs_algorithm_vamana_set_alpha( - svs_algorithm_h algorithm, - float alpha, // Pruning parameter (typically 1.0 - 1.4) - svs_error_h out_err -); - -bool svs_algorithm_vamana_get_graph_degree( - svs_algorithm_h algorithm, - size_t* out_graph_degree, - svs_error_h out_err -); - -bool svs_algorithm_vamana_set_graph_degree( - svs_algorithm_h algorithm, - size_t graph_degree, - svs_error_h out_err -); - -bool svs_algorithm_vamana_get_build_window_size( - svs_algorithm_h algorithm, - size_t* out_build_window_size, - svs_error_h out_err -); - -bool svs_algorithm_vamana_set_build_window_size( - svs_algorithm_h algorithm, - size_t build_window_size, - svs_error_h out_err -); - -bool svs_algorithm_vamana_get_use_search_history( - svs_algorithm_h algorithm, - bool* out_use_full_search_history, - svs_error_h out_err -); - -bool svs_algorithm_vamana_set_use_search_history( - svs_algorithm_h algorithm, - bool use_full_search_history, - svs_error_h out_err -); -``` - -### Storage API - -Configure vector storage format and compression. - -```c -// Simple uncompressed storage -svs_storage_h svs_storage_create_simple( - svs_data_type_t data_type, // SVS_DATA_TYPE_FLOAT32, FLOAT16, INT8, etc. - svs_error_h out_err // optional, can be NULL -); - -// Scalar quantization -svs_storage_h svs_storage_create_sq( - svs_data_type_t data_type, // SVS_DATA_TYPE_INT8, SVS_DATA_TYPE_UINT8 - svs_error_h out_err -); - -// Locally-adaptive Vector Quantization (LVQ) -svs_storage_h svs_storage_create_lvq( - svs_data_type_t primary, // Primary quantization type - svs_data_type_t residual, // Residual type (or SVS_DATA_TYPE_VOID) - svs_error_h out_err -); - -// LeanVec two-level hierarchical storage -svs_storage_h svs_storage_create_leanvec( - size_t leanvec_dims, // Primary dimensions (usually much smaller) - svs_data_type_t primary, // Primary storage type - svs_data_type_t secondary, // Secondary/residual storage type - svs_error_h out_err -); - -// Cleanup -void svs_storage_free(svs_storage_h storage); -``` - - -### Search Parameters API - -Configure runtime search behavior. - -```c -// Create Vamana search parameters -svs_search_params_h svs_search_params_create_vamana( - size_t search_window_size, // Search window size (e.g., 100) - svs_error_h out_err // optional, can be NULL -); - -// Cleanup -void svs_search_params_free(svs_search_params_h params); -``` - -### Index Builder API - -Configure and build index instances. - -```c -// Create index builder with required parameters -svs_index_builder_h svs_index_builder_create( - svs_distance_metric_t metric, // Distance metric - size_t dimension, // Vector dimensionality - svs_algorithm_h algorithm, // Algorithm configuration - svs_error_h out_err // optional, can be NULL -); - -// Configure storage (optional, default: Simple FP32) -bool svs_index_builder_set_storage( - svs_index_builder_h builder, - svs_storage_h storage, // Storage configuration - svs_error_h out_err -); - -// Configure thread pool (optional, default: native) -bool svs_index_builder_set_threadpool( - svs_index_builder_h builder, - svs_threadpool_kind_t kind, // Thread pool type - size_t num_threads, // Number of threads (for native) - svs_error_h out_err -); - -// Configure custom thread pool (advanced) -bool svs_index_builder_set_threadpool_custom( - svs_index_builder_h builder, - svs_threadpool_interface_t pool, // Custom thread pool interface - svs_error_h out_err -); - -// Cleanup -void svs_index_builder_free(svs_index_builder_h builder); -``` - - -### Index API - -Build and query vector search indices. - -```c -// Build index from vector data -svs_index_h svs_index_build( - svs_index_builder_h builder, - const float* data, // Vector data [num_vectors × dimensions] - size_t num_vectors, - svs_error_h out_err // optional, can be NULL -); - -// Cleanup -void svs_index_free(svs_index_h index); -``` - -### Search Results - -```c -// Search results structure -struct svs_search_results { - size_t num_queries; // Number of query vectors - size_t* results_per_query; // Number of results per query - size_t* indices; // Indices of the nearest neighbors - float* distances; // Distances to the nearest neighbors -}; - -typedef struct svs_search_results* svs_search_results_t; - -// Access pattern: -// For query i, neighbor j (where k is the number of neighbors): -// index = results->indices[i * k + j] -// distance = results->distances[i * k + j] -``` - -### Search Operations - -```c -// Top-K nearest neighbor search -svs_search_results_t svs_index_search( - svs_index_h index, - const float* queries, // Query vectors [num_queries × dimensions] - size_t num_queries, - size_t k, // Number of neighbors to return - svs_search_params_h search_params, // optional, can be NULL for defaults - svs_error_h out_err // optional, can be NULL -); - -// Cleanup search results -void svs_search_results_free(svs_search_results_t results); -``` - -## Complete Usage Example - -```c -#include "svs/c_api/svs_c.h" -#include -#include - -int main() { - // 1. Create error handle for diagnostics - svs_error_h err = svs_error_create(); - - // 2. Create Vamana algorithm configuration - svs_algorithm_h algo = svs_algorithm_create_vamana( - 64, // graph_degree - 128, // build_window_size - 128, // default search_window_size - err - ); - if (!algo || !svs_error_ok(err)) { - fprintf(stderr, "Algorithm creation failed: %s\n", - svs_error_get_message(err)); - svs_error_free(err); - return 1; - } - - // 3. Create index builder - size_t dimensions = 128; - svs_index_builder_h builder = svs_index_builder_create( - SVS_DISTANCE_METRIC_EUCLIDEAN, - dimensions, - algo, - err - ); - - // 4. Optional: Configure storage (default is FP32) - svs_storage_h storage = svs_storage_create_simple( - SVS_DATA_TYPE_FLOAT32, err - ); - svs_index_builder_set_storage(builder, storage, err); - - // 5. Optional: Configure thread pool - svs_index_builder_set_threadpool( - builder, - SVS_THREADPOOL_KIND_NATIVE, - 8, // num_threads - err - ); - - // 6. Prepare data - size_t num_vectors = 10000; - float* data = (float*)malloc(num_vectors * dimensions * sizeof(float)); - // ... fill data with vectors ... - - // 7. Build index - svs_index_h index = svs_index_build(builder, data, num_vectors, err); - if (!index || !svs_error_ok(err)) { - fprintf(stderr, "Index build failed: %s\n", - svs_error_get_message(err)); - goto cleanup; - } - - // 8. Prepare queries - size_t num_queries = 10; - float* queries = (float*)malloc(num_queries * dimensions * sizeof(float)); - // ... fill queries ... - - // 9. Perform search with default parameters - size_t k = 5; - svs_search_results_t results = svs_index_search( - index, queries, num_queries, k, NULL, err - ); - - // Or with custom search parameters: - // svs_search_params_h params = svs_search_params_create_vamana(100, err); - // svs_search_results_t results = svs_index_search( - // index, queries, num_queries, k, params, err - // ); - // svs_search_params_free(params); - - // 10. Process results - if (results && svs_error_ok(err)) { - for (size_t i = 0; i < results->num_queries; i++) { - printf("Query %zu:\n", i); - for (size_t j = 0; j < k; j++) { - size_t idx = i * k + j; - printf(" Index: %zu, Distance: %f\n", - results->indices[idx], results->distances[idx]); - } - } - svs_search_results_free(results); - } - - // 11. Cleanup -cleanup: - if (index) svs_index_free(index); - if (builder) svs_index_builder_free(builder); - if (storage) svs_storage_free(storage); - if (algo) svs_algorithm_free(algo); - svs_error_free(err); - - free(data); - free(queries); - - return 0; -} -``` - -## Next Steps - -- See [ERROR_HANDLING.md](c/ERROR_HANDLING.md) for comprehensive error handling guide -- See [examples/c/](../examples/c/) for additional usage examples -- See [bindings/c/samples/](c/samples/) for complete sample applications - -``` diff --git a/bindings/c/docs/C_API_Design.md b/bindings/c/docs/C_API_Design.md new file mode 100644 index 00000000..b3e7bbb7 --- /dev/null +++ b/bindings/c/docs/C_API_Design.md @@ -0,0 +1,879 @@ + + +# SVS C API Design + +> Looking for build/consume instructions or a quick-start example? See +> [../README.md](../README.md). This document focuses on the design rationale, +> conventions, and the full API reference. + +## Table of Contents + +- [Overview](#overview) + - [Design Goals](#design-goals) +- [Architecture Overview](#architecture-overview) +- [Error Handling Strategy](#error-handling-strategy) + - [Return Values](#return-values) + - [Detailed Error Information](#detailed-error-information) + - [Error Codes](#error-codes) + - [Best Practices](#best-practices) +- [Naming Conventions](#naming-conventions) + - [Prefixes](#prefixes) + - [Type Suffixes](#type-suffixes) + - [Function Naming Pattern](#function-naming-pattern) + - [Examples by Category](#examples-by-category) +- [Core Components](#core-components) + - [1. Index](#1-index) + - [2. Index Builder](#2-index-builder) + - [3. Algorithm Configuration](#3-algorithm-configuration) + - [4. Storage Configuration](#4-storage-configuration) + - [5. Thread Pool Configuration](#5-thread-pool-configuration) + - [6. Search Parameters](#6-search-parameters) + - [7. ID Filter (optional)](#7-id-filter-optional) +- [API Reference](#api-reference) + - [Public Headers](#public-headers) + - [Type Definitions](#type-definitions) + - [Version Information](#version-information) + - [Error Handling API](#error-handling-api) + - [Algorithm API](#algorithm-api) + - [Storage API](#storage-api) + - [Search Parameters API](#search-parameters-api) + - [Index Builder API](#index-builder-api) + - [Index API](#index-api) + - [Dynamic Index Operations](#dynamic-index-operations) + - [Index Introspection](#index-introspection) + - [Search Results](#search-results) + - [Search Operations](#search-operations) +- [Next Steps](#next-steps) + +## Overview + +This document describes the design proposal for the Scalable Vector Search (SVS) C API. The API provides a C interface to SVS's vector similarity search capabilities, enabling integration with C applications and other languages that support C FFI (Foreign Function Interface). + +### Design Goals + +The SVS C API is designed with the following principles: + +1. **Simplicity** - Provide a minimal, intuitive set of operations to create and use vector search indices +2. **Flexibility** - Allow fine-grained control over: + - Index building parameters (graph degree, window sizes, etc.) + - Memory allocation strategies (simple, hugepage, custom) + - Thread pool configuration (native, OpenMP, custom) + - Vector storage formats (simple, compressed, quantized) + - Search parameters and filters + - Logging system +3. **Safety** - Comprehensive error handling with detailed error messages +4. **Portability** - Standard C interface that works across platforms and languages + +## Architecture Overview + +The API is built around a builder pattern with the following core abstractions: + +``` +┌─────────────────┐ +│ Index Builder │ Configure index parameters +│ - Algorithm │ +│ - Storage │ +│ - Threadpool │ +└────────┬────────┘ + │ build() + ↓ +┌─────────────────┐ +│ Index │ Perform searches +│ - search() │ with optional search params +└─────────────────┘ + │ + ├─ Search Params (optional) + └─ Search Results +``` + +## Error Handling Strategy + +The API uses a dual approach for error reporting: return codes and optional detailed error information. + +### Return Values + +- Functions returning handles return `NULL` on failure +- Functions returning booleans return `false` on failure +- All functions accept an optional `svs_error_h` parameter for detailed diagnostics + +### Detailed Error Information + +For comprehensive error diagnostics, create an error handle and pass it to API calls: + +```c +// Create error handle +svs_error_h err = svs_error_create(); + +// Use in API calls (last parameter, can be NULL) +svs_algorithm_h algo = svs_algorithm_create_vamana( + 64, // graph_degree + 128, // build_window_size + 128, // search_window_size + err // optional error handle (can be NULL) +); + +if (algo == NULL) { + // Check error status + if (!svs_error_ok(err)) { + // Query error details + svs_error_code_t code = svs_error_get_code(err); + const char* msg = svs_error_get_message(err); + fprintf(stderr, "Error [%d]: %s\n", code, msg); + } +} + +// Error handle can be reused across multiple calls +svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, err); + +// Free error handle when done +svs_error_free(err); +``` + +### Error Codes + +```c +enum svs_error_code { + SVS_OK = 0, // Success + SVS_ERROR_GENERIC = 1, // Generic/unspecified error + SVS_ERROR_INVALID_ARGUMENT = 2, // Invalid function parameter + SVS_ERROR_OUT_OF_MEMORY = 3, // Memory allocation failed + SVS_ERROR_NOT_IMPLEMENTED = 5, // Feature not yet available + SVS_ERROR_UNSUPPORTED_HW = 6, // Hardware doesn't support required features + SVS_ERROR_RUNTIME = 7, // Runtime error during operation + SVS_ERROR_INVALID_OPERATION = 8, // Operation not valid in the current state + SVS_ERROR_UNKNOWN = 1000 // Unknown/unexpected error +}; +``` + +User-provided callbacks (custom thread pool, ID filter) can report failures back to +the library by calling `svs_error_set()` on the `out_err` handle they receive. + +### Best Practices + +1. **Always check return values** - Test for `NULL` or `false` before using results +2. **Use error handles during development** - Provides detailed diagnostics and error messages +3. **Reuse error handles** - Single handle can be reused across multiple API calls +4. **Free all resources** - Always call appropriate `_free()` functions to prevent leaks +5. **Pass NULL for optional parameters** - Error handle and search params can be `NULL` if not needed +6. **Check `svs_error_ok()`** - Use this helper to check if operation succeeded + +## Naming Conventions + +Consistent naming improves API discoverability and reduces cognitive load. + +### Prefixes + +- `svs_` - All functions and types +- `SVS_` - Macros and constants + +### Type Suffixes + +| Suffix | Meaning | Example | +|--------|---------|----------| +| `_t` | Value type (enum, struct) | `svs_distance_metric_t`, `svs_error_code_t` | +| `_h` | Handle (opaque pointer) | `svs_index_h`, `svs_algorithm_h` | +| `_i` | Interface pointer type | `svs_threadpool_i`, `svs_id_filter_i` | + +### Function Naming Pattern + +``` +svs_[_]_ +``` + +**Examples:** + +| Function | Breakdown | Description | +|----------|-----------|-------------| +| `svs_index_search_topk()` | `svs` + `index` + `search_topk` | TopK index search (with optional ID filter) | +| `svs_algorithm_vamana_set_alpha()` | `svs` + `algorithm` + `vamana` + `set_alpha` | Set Vamana-specific parameter | +| `svs_storage_create_lvq()` | `svs` + `storage` + `create` + `lvq` | Create LVQ storage configuration | +| `svs_index_builder_set_threadpool()` | `svs` + `index_builder` + `set_threadpool` | Configure builder thread pool | + +### Examples by Category + +```c +// Handles (opaque pointers) +typedef struct svs_index* svs_index_h; +typedef struct svs_algorithm* svs_algorithm_h; +typedef struct svs_storage* svs_storage_h; + +// Value types +typedef enum svs_distance_metric svs_distance_metric_t; +typedef enum svs_error_code svs_error_code_t; + +// Interface pointer types +typedef struct svs_threadpool_interface* svs_threadpool_i; +typedef struct svs_id_filter_interface* svs_id_filter_i; +``` + +## Core Components + +### 1. Index + +The main search structure providing vector similarity search operations. + +**Current Capabilities:** +- **TopK Search** - Find the k nearest neighbors for query vectors +- **Filtered TopK Search** - Optional caller-provided ID filter applied during topk search +- Configurable search parameters (window size, etc.) +- Multiple distance metrics (Euclidean, Cosine, Dot Product) +- **Persistence** - Save an index to disk and reload it later +- **Introspection** - Query total memory usage and per-component breakdown +- **Dynamic Updates** *(dynamic index only)* - Add / delete points, consolidate, compact + +**Requirements:** +- Built from a non-empty dataset using Index Builder, or loaded from disk +- A static index is immutable after creation; a dynamic index additionally supports + add/delete/consolidate/compact operations + +**Future Extensions:** +- Range search (all neighbors within distance threshold) +- Additional algorithms (Flat, IVF) + +### 2. Index Builder + +Configures and creates index instances using the builder pattern. + +**Required Parameters:** +- Algorithm configuration handle +- Vector dimensions +- Distance metric (Euclidean, Cosine, Dot Product) + +**Optional Configuration:** +- Storage format (default: Simple FP32) +- Thread pool kind and size (default: native with hardware concurrency) +- Custom thread pool interface (for advanced use cases) + +### 3. Algorithm Configuration + +Defines the search algorithm and its parameters. + +**Current Support:** +- **Vamana** - Graph-based approximate nearest neighbor search + - Graph degree (connectivity) + - Build window size (construction search budget) + - Default search window size + - Alpha parameter (pruning threshold) + - Search history mode + +**Future Support:** +- **Flat** - Exhaustive brute-force search +- **IVF** - Inverted file with clustering + +### 4. Storage Configuration + +Defines how vectors are stored in memory, supporting various compression schemes. + +| Storage Type | Configuration Options | Description | +|--------------|----------------------|-------------| +| **Simple** | FP32, FP16, INT8, UINT8, INT4, UINT4 | Uncompressed storage | +| **SQ** | INT8, UINT8 | Scalar quantization | +| **LVQ** | Primary: INT4/UINT4/INT8/UINT8
Residual: VOID/INT4/UINT4/INT8/UINT8 | Locally-adaptive vector quantization | +| **LeanVec** | Dimensions
Primary: data type
Secondary: data type | LeanVec dimensionality reduced storage | + +**Example:** +```c +// Simple FP32 storage (default) +svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, err); + +// LVQ with 8-bit primary and 4-bit residual +svs_storage_h storage = svs_storage_create_lvq( + SVS_DATA_TYPE_UINT8, SVS_DATA_TYPE_UINT4, err +); + +// LeanVec with 128 dimensions +svs_storage_h storage = svs_storage_create_leanvec( + 128, SVS_DATA_TYPE_FLOAT16, SVS_DATA_TYPE_INT8, err +); + +// Scalar quantization +svs_storage_h storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, err); +``` + +### 5. Thread Pool Configuration + +Controls parallelization strategy for index operations. + +| Type | Configuration | Use Case | +|------|---------------|----------| +| **Native** | Thread count | Default SVS thread pool (recommended) | +| **OpenMP** | Uses OMP_NUM_THREADS | Integration with OpenMP applications | +| **Single Thread** | No parallelization | Debugging or minimal overhead | +| **Custom** | User-defined interface | Custom scheduling/work-stealing | + +**Custom Interface:** + +The custom thread pool interface is split into a versioned operations table and an +instance structure that carries an opaque `self` pointer. Ops tables must always be +initialised through the provided `SVS_INIT_THREADPOOL_OPS()` macro so that the +`version` and `struct_size` fields are populated correctly for forward compatibility. + +```c +struct svs_threadpool_interface_ops { + uint32_t version; // Set by SVS_INIT_THREADPOOL_OPS + size_t struct_size; // Set by SVS_INIT_THREADPOOL_OPS + size_t (*size)(void* self); + bool (*parallel_for)( + void* self, + void (*func)(void* svs_param, size_t i), + void* svs_param, // SVS state + size_t n, // Number of tasks + svs_error_h out_err // Set via svs_error_set() to abort execution + ); +}; + +struct svs_threadpool_interface { + struct svs_threadpool_interface_ops* ops; + void* self; // User-defined state +}; + +// Handy typedef used by the API surface +typedef struct svs_threadpool_interface* svs_threadpool_i; + +// Initialisation macros +static svs_threadpool_ops_t my_ops = + SVS_INIT_THREADPOOL_OPS(my_size_func, my_parallel_for_func); +static svs_threadpool_t my_pool = SVS_MAKE_INTERFACE(NULL, my_ops); +``` + +### 6. Search Parameters + +Configures runtime search behavior (algorithm-specific). + +**Vamana Search Parameters:** +- **Search window size** - Controls search accuracy vs. speed tradeoff + - Larger values: more accurate but slower + - Smaller values: faster but less accurate + - Typically 50-200 for good recall + +**Usage:** +```c +svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + +// Use custom search parameters +svs_search_params_h params = svs_search_params_create_vamana(100, err); +svs_index_search_topk( + index, queries, num_queries, k, &results, params, /*id_filter=*/NULL, err +); +svs_search_params_free(params); + +// Or use defaults from algorithm configuration (search_params = NULL) +svs_index_search_topk( + index, queries, num_queries, k, &results, NULL, NULL, err +); + +svs_search_results_free(&results); +``` + +### 7. ID Filter (optional) + +A caller-supplied ID filter can be passed to `svs_index_search_topk()` to restrict +results to a subset of vector IDs. Like the thread pool, the filter is a versioned +ops table plus an opaque `self` pointer. + +```c +struct svs_id_filter_interface_ops { + uint32_t version; + size_t struct_size; + bool (*is_member)(void* self, size_t id); + float (*filter_rate)(void* self); // Optional selectivity hint, or NULL / 0.0 +}; + +struct svs_id_filter_interface { + struct svs_id_filter_interface_ops* ops; + void* self; +}; +typedef struct svs_id_filter_interface* svs_id_filter_i; + +static svs_id_filter_ops_t my_filter_ops = + SVS_INIT_ID_FILTER_OPS(my_is_member, my_filter_rate); +static svs_id_filter_t my_filter = SVS_MAKE_INTERFACE(user_state, my_filter_ops); +``` + +Providing a non-zero `filter_rate` lets the search account for the expected +selectivity; if the observed hit rate ends up lower than the reported estimate the +function returns an empty result set for that query. + +## API Reference + +### Public Headers + +```c +#include "svs/c/svs_c.h" // Main C API +#include "svs/c/svs_c_config.h" // API configuration macroses +#include "svs/c/svs_c_version.h" // SVS_C_API_VERSION[_MAJOR|_MINOR|_PATCH|_STRING] +``` + +`svs_c_version.h` is generated by CMake from `svs_c_version.h.in` and defines both +the encoded `SVS_C_API_VERSION` integer (used by the `SVS_INIT_*` macros) and +human-readable component macros. + +### Type Definitions + +```c +// Opaque handles (suffix: _h) +typedef struct svs_error_desc* svs_error_h; +typedef struct svs_index* svs_index_h; +typedef struct svs_index_builder* svs_index_builder_h; +typedef struct svs_algorithm* svs_algorithm_h; +typedef struct svs_storage* svs_storage_h; +typedef struct svs_search_params* svs_search_params_h; + +// Fully defined enum types (suffix: _t) +typedef enum svs_error_code svs_error_code_t; +typedef enum svs_distance_metric svs_distance_metric_t; +typedef enum svs_algorithm_type svs_algorithm_type_t; +typedef enum svs_data_type svs_data_type_t; +typedef enum svs_storage_kind svs_storage_kind_t; +typedef enum svs_threadpool_kind svs_threadpool_kind_t; + +// Custom-interface value + pointer types +typedef struct svs_threadpool_interface_ops svs_threadpool_ops_t; +typedef struct svs_threadpool_interface svs_threadpool_t; +typedef struct svs_threadpool_interface* svs_threadpool_i; + +typedef struct svs_id_filter_interface_ops svs_id_filter_ops_t; +typedef struct svs_id_filter_interface svs_id_filter_t; +typedef struct svs_id_filter_interface* svs_id_filter_i; + +// Result / introspection value types (defined in header) +typedef struct svs_search_results svs_search_results_t; +typedef struct svs_memory_breakdown svs_memory_breakdown_t; + +// Distance metric enum values +// SVS_DISTANCE_METRIC_EUCLIDEAN, SVS_DISTANCE_METRIC_COSINE, +// SVS_DISTANCE_METRIC_DOT_PRODUCT +``` + +### Version Information + +```c +uint32_t svs_get_version(void); // (major<<16)|(minor<<8)|patch +const char* svs_get_version_string(void); // "major.minor.patch" +``` + +### Error Handling API + +```c +svs_error_h svs_error_create(void); +void svs_error_free(svs_error_h err); + +// Set an error code + message. Intended for user callbacks (custom threadpool, +// ID filter) so they can propagate failures back to the library. +bool svs_error_set(svs_error_h err, svs_error_code_t code, const char* message); + +bool svs_error_ok(svs_error_h err); +svs_error_code_t svs_error_get_code(svs_error_h err); +const char* svs_error_get_message(svs_error_h err); +``` + +### Algorithm API + +Create and configure search algorithms. + +```c +// Vamana graph-based approximate nearest neighbor search +svs_algorithm_h svs_algorithm_create_vamana( + size_t graph_degree, // Graph connectivity (e.g., 64) + size_t build_window_size, // Construction search window (e.g., 128) + size_t search_window_size, // Default query search window (e.g., 128) + svs_error_h out_err // optional, can be NULL +); + +// Cleanup +void svs_algorithm_free(svs_algorithm_h algorithm); + +// Introspection +bool svs_algorithm_get_type( + svs_algorithm_h algorithm, + svs_algorithm_type_t* out_type, + svs_error_h out_err +); + +// Get/Set Vamana parameters +bool svs_algorithm_vamana_get_alpha( + svs_algorithm_h algorithm, + float* out_alpha, + svs_error_h out_err +); + +bool svs_algorithm_vamana_set_alpha( + svs_algorithm_h algorithm, + float alpha, // Pruning parameter (typically 1.0 - 1.4) + svs_error_h out_err +); + +bool svs_algorithm_vamana_get_graph_degree( + svs_algorithm_h algorithm, + size_t* out_graph_degree, + svs_error_h out_err +); + +bool svs_algorithm_vamana_set_graph_degree( + svs_algorithm_h algorithm, + size_t graph_degree, + svs_error_h out_err +); + +bool svs_algorithm_vamana_get_build_window_size( + svs_algorithm_h algorithm, + size_t* out_build_window_size, + svs_error_h out_err +); + +bool svs_algorithm_vamana_set_build_window_size( + svs_algorithm_h algorithm, + size_t build_window_size, + svs_error_h out_err +); + +bool svs_algorithm_vamana_get_use_search_history( + svs_algorithm_h algorithm, + bool* out_use_full_search_history, + svs_error_h out_err +); + +bool svs_algorithm_vamana_set_use_search_history( + svs_algorithm_h algorithm, + bool use_full_search_history, + svs_error_h out_err +); +``` + +### Storage API + +Configure vector storage format and compression. + +```c +// Simple uncompressed storage +svs_storage_h svs_storage_create_simple( + svs_data_type_t data_type, // SVS_DATA_TYPE_FLOAT32, FLOAT16, INT8, etc. + svs_error_h out_err // optional, can be NULL +); + +// Scalar quantization +svs_storage_h svs_storage_create_sq( + svs_data_type_t data_type, // SVS_DATA_TYPE_INT8, SVS_DATA_TYPE_UINT8 + svs_error_h out_err +); + +// Locally-adaptive Vector Quantization (LVQ) +svs_storage_h svs_storage_create_lvq( + svs_data_type_t primary, // Primary quantization type + svs_data_type_t residual, // Residual type (or SVS_DATA_TYPE_VOID) + svs_error_h out_err +); + +// LeanVec two-level hierarchical storage +svs_storage_h svs_storage_create_leanvec( + size_t leanvec_dims, // Primary dimensions (usually much smaller) + svs_data_type_t primary, // Primary storage type + svs_data_type_t secondary, // Secondary/residual storage type + svs_error_h out_err +); + +// Introspection +bool svs_storage_get_kind( + svs_storage_h storage, + svs_storage_kind_t* out_kind, + svs_error_h out_err +); + +// Cleanup +void svs_storage_free(svs_storage_h storage); +``` + +> LVQ and LeanVec require a build that includes the compression backend and, in +> some cases, specific x86 ISA support. When they are unavailable the create +> functions return `NULL` and populate `out_err` with `SVS_ERROR_NOT_IMPLEMENTED` +> or `SVS_ERROR_UNSUPPORTED_HW`. + +### Search Parameters API + +Configure runtime search behavior. + +```c +// Create Vamana search parameters +svs_search_params_h svs_search_params_create_vamana( + size_t search_window_size, // Search window size (e.g., 100) + svs_error_h out_err // optional, can be NULL +); + +// Cleanup +void svs_search_params_free(svs_search_params_h params); +``` + +### Index Builder API + +Configure and build index instances. + +```c +// Create index builder with required parameters +svs_index_builder_h svs_index_builder_create( + svs_distance_metric_t metric, // Distance metric + size_t dimension, // Vector dimensionality + svs_algorithm_h algorithm, // Algorithm configuration + svs_error_h out_err // optional, can be NULL +); + +// Configure storage (optional, default: Simple FP32) +bool svs_index_builder_set_storage( + svs_index_builder_h builder, + svs_storage_h storage, + svs_error_h out_err +); + +// Configure thread pool (optional, default: native) +bool svs_index_builder_set_threadpool( + svs_index_builder_h builder, + svs_threadpool_kind_t kind, + size_t num_threads, + svs_error_h out_err +); + +// Configure custom thread pool (advanced) +bool svs_index_builder_set_threadpool_custom( + svs_index_builder_h builder, + svs_threadpool_i pool, // Pointer to svs_threadpool_interface + svs_error_h out_err +); + +// Cleanup +void svs_index_builder_free(svs_index_builder_h builder); +``` + +### Index API + +Build, persist and manage vector search indices. + +```c +// Build a static index from vector data +svs_index_h svs_index_build( + svs_index_builder_h builder, + const float* data, // [num_vectors * dimensions] + size_t num_vectors, + svs_error_h out_err // optional, can be NULL +); + +// Build a dynamic index. Passing ids = NULL auto-generates IDs 0..num_vectors-1; +// blocksize_bytes = 0 selects an implementation-defined default. +svs_index_h svs_index_build_dynamic( + svs_index_builder_h builder, + const float* data, + const size_t* ids /*=NULL*/, + size_t num_vectors, + size_t blocksize_bytes /*=0*/, + svs_error_h out_err /*=NULL*/ +); + +// Load a previously saved static index. The builder supplies configuration +// (storage, threadpool, ...). +svs_index_h svs_index_load( + svs_index_builder_h builder, + const char* directory, + svs_error_h out_err /*=NULL*/ +); + +// Load a previously saved dynamic index. +svs_index_h svs_index_load_dynamic( + svs_index_builder_h builder, + const char* directory, + size_t blocksize_bytes /*=0*/, + svs_error_h out_err /*=NULL*/ +); + +// Persist an index (static or dynamic) to disk. +bool svs_index_save(svs_index_h index, const char* directory, svs_error_h out_err); + +// Cleanup +void svs_index_free(svs_index_h index); +``` + +### Dynamic Index Operations + +Available only for indices created via `svs_index_build_dynamic()` / +`svs_index_load_dynamic()`. + +```c +bool svs_index_dynamic_add_points( + svs_index_h index, + const float* new_points, + const size_t* ids, + size_t num_vectors, + size_t* out_added_count /*=NULL*/, + svs_error_h out_err /*=NULL*/ +); + +bool svs_index_dynamic_delete_points( + svs_index_h index, + const size_t* ids, + size_t num_ids, + size_t* out_deleted_count /*=NULL*/, + svs_error_h out_err /*=NULL*/ +); + +bool svs_index_dynamic_has_id( + svs_index_h index, size_t id, bool* out_has_id, svs_error_h out_err /*=NULL*/ +); + +// Reclaim space and consolidate deletes. +bool svs_index_dynamic_consolidate(svs_index_h index, svs_error_h out_err /*=NULL*/); +bool svs_index_dynamic_compact( + svs_index_h index, size_t batchsize /*=0*/, svs_error_h out_err /*=NULL*/ +); +``` + +### Index Introspection + +```c +bool svs_index_get_num_threads(svs_index_h index, size_t* out_num_threads, + svs_error_h out_err); + +// Only supported for indices built with SVS_THREADPOOL_KIND_NATIVE / _OMP. +// Returns false with SVS_ERROR_INVALID_OPERATION for _CUSTOM / _SINGLE_THREAD. +bool svs_index_set_num_threads(svs_index_h index, size_t num_threads, + svs_error_h out_err); + +// Compute distance from a stored vector (by id) to a query. +bool svs_index_get_distance(svs_index_h index, size_t id, const float* query, + float* out_distance, svs_error_h out_err); + +// Reconstruct stored vectors for a set of ids into a caller-provided buffer of +// size num_ids * data_dim. +bool svs_index_reconstruct(svs_index_h index, const size_t* ids, size_t num_ids, + float* out_data, size_t data_dim, svs_error_h out_err); + +// Total memory (graph + data + metadata) used by the index. +bool svs_index_get_memory_usage(svs_index_h index, size_t* out_bytes, + svs_error_h out_err); + +// Per-component memory breakdown. +struct svs_memory_breakdown { + uint32_t version; + size_t struct_size; + size_t graph_bytes; + size_t data_bytes; + size_t metadata_bytes; +}; + +#define SVS_INIT_MEMORY_BREAKDOWN() /* zero-inits + version + struct_size */ + +bool svs_index_get_memory_breakdown( + svs_index_h index, + svs_memory_breakdown_t* out_breakdown, + svs_error_h out_err +); +``` + +### Search Results + +Search results are returned in a caller-provided CSR-layout struct. The struct is +versioned and forward-compatible: the library only writes fields covered by the +caller-supplied `struct_size`, so binaries built against an older header keep +working against newer libraries. + +```c +struct svs_search_results { + uint32_t version; + size_t struct_size; + + size_t num_queries; // Number of populated rows + size_t total_results; // == offsets[num_queries] + size_t* offsets; // Length num_queries + 1 (row starts) + size_t* indices; // Length total_results + float* distances; // Length total_results + + size_t offsets_capacity; // Allocation size of offsets + size_t results_capacity; // Allocation size of indices / distances + bool owns_buffers; // If true, the library allocated / will free them +}; + +// Row-oriented access (O(1)): +// For query q neighbor j: +// size_t begin = results.offsets[q]; +// size_t idx = results.indices[begin + j]; +// float distance = results.distances[begin + j]; +// +// For fixed top-k searches offsets[q] == q * k, so results.indices[q*k + j] is +// equivalent. + +// Convenience row accessor +static inline void svs_search_results_row( + const struct svs_search_results* results, + size_t q, + const size_t** out_ids, + const float** out_distances, + size_t* out_count +); + +// Initialisation macros +#define SVS_INIT_SEARCH_RESULTS() /* library-owned buffers, allocated on demand */ +#define SVS_INIT_SEARCH_RESULTS_WITH_BUFFERS( \ + p_offsets, p_indices, p_distances, p_offsets_cap, p_results_cap) \ + /* caller-owned buffers; call fails without reallocating if capacity too low */ + +// Release library-owned buffers and reset the struct. Safe on NULL, +// zero-initialised, and caller-owned-buffer instances (in which case only the +// descriptor is reset). Safe to call multiple times. +void svs_search_results_free(svs_search_results_t* results); +``` + +**Buffer reuse:** passing the same `svs_search_results_t` object to consecutive +searches lets the library reuse existing library-owned buffers whenever capacity +is sufficient; steady-state batches of the same shape allocate only on the first +call. + +### Search Operations + +```c +// TopK search with optional ID filter and search parameters. +// out_results should be initialised with SVS_INIT_SEARCH_RESULTS() (or +// SVS_INIT_SEARCH_RESULTS_WITH_BUFFERS()) before the first call. +bool svs_index_search_topk( + svs_index_h index, + const float* queries, // [num_queries * dimensions] + size_t num_queries, + size_t k, + svs_search_results_t* out_results, + svs_search_params_h search_params /*=NULL*/, + svs_id_filter_i id_filter /*=NULL*/, + svs_error_h out_err /*=NULL*/ +); + +// Deprecated shim retained for source compatibility; equivalent to +// svs_index_search_topk() with id_filter = NULL. +SVS_DEPRECATED("Use svs_index_search_topk() instead") +static inline bool svs_index_search( + svs_index_h index, + const float* queries, + size_t num_queries, + size_t k, + svs_search_results_t* out_results, + svs_search_params_h search_params /*=NULL*/, + svs_error_h out_err /*=NULL*/ +); +``` + +## Next Steps + +- See the top-level [../README.md](../README.md) for a quick start, build/consume + instructions, and a complete end-to-end usage example. +- See [../samples/](../samples/) for runnable sample applications: + - `simple.c` – minimal static index build + search with a custom thread pool + - `dynamic.c` – dynamic index with add / delete / consolidate + - `save_load.c` – persisting and reloading indices from disk +- See [examples/c/](../../../examples/c/) for additional usage examples diff --git a/bindings/c/include/svs/c_api/svs_c.h b/bindings/c/include/svs/c/svs_c.h similarity index 50% rename from bindings/c/include/svs/c_api/svs_c.h rename to bindings/c/include/svs/c/svs_c.h index dccccb97..ed6b5218 100644 --- a/bindings/c/include/svs/c_api/svs_c.h +++ b/bindings/c/include/svs/c/svs_c.h @@ -16,14 +16,36 @@ #pragma once -#include "svs_c_config.h" +#include "svs/c/svs_c_config.h" + +#include +#include +#include + +// SVS C API requires a C99 or later compiler, or a C++20 or later compiler. If the +// compiler does not meet these requirements, a compilation error will be generated with a +// clear message indicating the required standard version. +#if defined(__cplusplus) +#if __cplusplus < 202002L +#error \ + "svs_c.h requires C++20 or later (designated initializers in SVS_INIT_* / SVS_MAKE_INTERFACE)." +#endif +#elif defined(__STDC_VERSION__) +#if __STDC_VERSION__ < 199901L +#error \ + "svs_c.h requires C99 or later (designated initializers in SVS_INIT_* / SVS_MAKE_INTERFACE)." +#endif +#else +#error "svs_c.h requires C99 or later, or C++20 or later." +#endif #ifdef __cplusplus extern "C" { #endif -#include -#include +/// @brief Error codes returned by the API and stored in svs_error_h handles. +/// @remarks Values are stable across API versions: existing codes are never renumbered; +/// new codes are only appended. Do not assume the set is contiguous. enum svs_error_code { SVS_OK = 0, SVS_ERROR_GENERIC = 1, @@ -36,28 +58,57 @@ enum svs_error_code { SVS_ERROR_UNKNOWN = 1000 }; +typedef struct svs_error_desc* svs_error_h; + +/// @brief Distance metric used to compare vectors. enum svs_distance_metric { SVS_DISTANCE_METRIC_EUCLIDEAN = 0, SVS_DISTANCE_METRIC_COSINE = 1, SVS_DISTANCE_METRIC_DOT_PRODUCT = 2 }; +/// @brief Index search algorithm kind. enum svs_algorithm_type { SVS_ALGORITHM_TYPE_VAMANA = 0, SVS_ALGORITHM_TYPE_FLAT = 1, SVS_ALGORITHM_TYPE_IVF = 2, }; +/// @brief Element data type for vectors and quantization configurations. +/// @remarks Each value encodes its category, sub-kind, and bit-width so it can be +/// classified directly with bitwise operations: +/// - bits 0-7 element width in bits: @c (dt & 0xFF) +/// - bits 8-11 category: @c ((dt >> 8) & 0xF) — 0 = none, 1 = unsigned integer, +/// 2 = signed integer, 3 = floating-point +/// - bits 12-15 variant: @c ((dt >> 12) & 0xF) — 0 for the default, distinguishes +/// types sharing the same category and width (e.g. 1 marks bfloat16 +/// apart from float16) +/// So a value is @c ((variant << 12) | (category << 8) | bits). Values are stable across +/// API versions: existing values are never changed; new types are added with a fresh +/// encoding. enum svs_data_type { - SVS_DATA_TYPE_VOID = 0, - SVS_DATA_TYPE_FLOAT32 = 32, - SVS_DATA_TYPE_FLOAT16 = 16, - SVS_DATA_TYPE_INT8 = 8, - SVS_DATA_TYPE_UINT8 = SVS_DATA_TYPE_INT8 - 1, - SVS_DATA_TYPE_INT4 = 4, - SVS_DATA_TYPE_UINT4 = SVS_DATA_TYPE_INT4 - 1 + SVS_DATA_TYPE_NONE = 0, ///< Unspecified / absent type (e.g. "no residual" in LVQ) + SVS_DATA_TYPE_VOID = SVS_DATA_TYPE_NONE, ///< Alias of SVS_DATA_TYPE_NONE + + SVS_DATA_TYPE_UINT4 = (0 << 12) | (1 << 8) | 4, + SVS_DATA_TYPE_UINT8 = (0 << 12) | (1 << 8) | 8, + SVS_DATA_TYPE_UINT16 = (0 << 12) | (1 << 8) | 16, + SVS_DATA_TYPE_UINT32 = (0 << 12) | (1 << 8) | 32, + SVS_DATA_TYPE_UINT64 = (0 << 12) | (1 << 8) | 64, + + SVS_DATA_TYPE_INT4 = (0 << 12) | (2 << 8) | 4, + SVS_DATA_TYPE_INT8 = (0 << 12) | (2 << 8) | 8, + SVS_DATA_TYPE_INT16 = (0 << 12) | (2 << 8) | 16, + SVS_DATA_TYPE_INT32 = (0 << 12) | (2 << 8) | 32, + SVS_DATA_TYPE_INT64 = (0 << 12) | (2 << 8) | 64, + + SVS_DATA_TYPE_FLOAT16 = (0 << 12) | (3 << 8) | 16, + SVS_DATA_TYPE_BFLOAT16 = (1 << 12) | (3 << 8) | 16, + SVS_DATA_TYPE_FLOAT32 = (0 << 12) | (3 << 8) | 32, + SVS_DATA_TYPE_FLOAT64 = (0 << 12) | (3 << 8) | 64 }; +/// @brief Vector storage / compression scheme. enum svs_storage_kind { SVS_STORAGE_KIND_SIMPLE = 0, SVS_STORAGE_KIND_LEANVEC = 1, @@ -65,6 +116,7 @@ enum svs_storage_kind { SVS_STORAGE_KIND_SQ = 3 }; +/// @brief Thread pool implementation used for parallel operations. enum svs_threadpool_kind { SVS_THREADPOOL_KIND_NATIVE = 0, SVS_THREADPOOL_KIND_OMP = 1, @@ -72,54 +124,293 @@ enum svs_threadpool_kind { SVS_THREADPOOL_KIND_CUSTOM = 3 }; +/// @brief Operations table for a custom thread pool interface +/// @remarks The user must ensure that the thread pool implementation is thread-safe and +/// that the provided function pointers remain valid for the lifetime of the thread pool +/// interface. +/// +/// @var svs_threadpool_interface_ops::version +/// Version of the thread pool interface. +/// @var svs_threadpool_interface_ops::struct_size +/// Size of the structure, used for versioning and compatibility checks. +/// @var svs_threadpool_interface_ops::size +/// Function pointer to retrieve the number of threads in the thread pool. +/// @param self Pointer to the thread pool instance. +/// @return Number of threads in the thread pool. +/// +/// @var svs_threadpool_interface_ops::parallel_for +/// Function pointer to execute a function in parallel across the thread pool. +/// The user is responsible for ensuring that @p func and @p svs_param remain valid +/// for the duration of the parallel execution. The implementation must call @p func +/// exactly once for each index in [0, n). If @p func signals failure via @p out_err, +/// the parallel execution should be aborted. +/// @param self Pointer to the thread pool instance. +/// @param func Function pointer to execute per iteration. Takes a user data pointer +/// (@p svs_param) and a zero-based iteration index (@p i). +/// @param svs_param Pointer to user-defined data passed to each @p func invocation. +/// @param n Number of iterations to execute in parallel. +/// @param out_err Handle to capture any error that occurs during execution. User code may +/// call svs_error_set() to set the error code and message if an error occurs. +/// @return @c true if all iterations completed successfully, @c false otherwise. // clang-format off struct svs_threadpool_interface_ops { + uint32_t version; + size_t struct_size; size_t (*size)(void* self); - void (*parallel_for)( + bool (*parallel_for)( void* self, - void (*func)(void* svs_param, size_t n), + void (*func)(void* svs_param, size_t i), void* svs_param, - size_t n + size_t n, + svs_error_h out_err ); }; // clang-format on +/// @brief Macro to create a user-defined thread pool interface operations structure +/// @param size_func Function pointer to retrieve the number of threads in the thread pool +/// @param parallel_for_func Function pointer to execute a function in parallel across the +/// thread pool +#define SVS_INIT_THREADPOOL_OPS(size_func, parallel_for_func) \ + { \ + .version = SVS_C_API_VERSION, \ + .struct_size = sizeof(struct svs_threadpool_interface_ops), .size = &size_func, \ + .parallel_for = ¶llel_for_func \ + } + +/// @brief Structure representing a custom thread pool interface +/// @var svs_threadpool_interface_ops::ops +/// Function pointers for the thread pool operations. +/// @var svs_threadpool_interface_ops::self +/// Pointer to the user-defined thread pool instance. This pointer is passed to the +/// function pointers in @p ops when they are called. struct svs_threadpool_interface { - struct svs_threadpool_interface_ops ops; + struct svs_threadpool_interface_ops* ops; void* self; }; +/// @brief Operations table for a custom ID filter interface +/// @remarks The user must ensure that the ID filter implementation is thread-safe and +/// that the provided function pointers remain valid for the lifetime of the ID filter +/// interface. +/// @var svs_id_filter_interface_ops::version +/// Version of the ID filter interface. +/// @var svs_id_filter_interface_ops::struct_size +/// Size of the structure, used for versioning and compatibility checks. +/// @var svs_id_filter_interface_ops::is_member +/// Function pointer to check if a given ID is a member of the filter. +/// @var svs_id_filter_interface_ops::filter_rate +/// Optional function pointer to get the estimated selectivity of the filter, i.e., the +/// fraction of IDs that are expected to pass the filter. A value of 0.01 indicates that +/// 1% of IDs are expected to pass, while a value of 1.0 indicates that all IDs are +/// expected to pass. If the filter does not provide an estimate, it should be set to NULL +/// or return 0.0. struct svs_id_filter_interface_ops { + uint32_t version; + size_t struct_size; bool (*is_member)(void* self, size_t id); + float (*filter_rate)(void* self); }; +/// @brief Macro to create a user-defined ID filter interface operations structure +/// @param is_member_func Function pointer to check if a given ID is a member of the filter +/// @param filter_rate_func Optional function pointer to get the estimated selectivity of +/// the filter +#define SVS_INIT_ID_FILTER_OPS(is_member_func, filter_rate_func) \ + { \ + .version = SVS_C_API_VERSION, \ + .struct_size = sizeof(struct svs_id_filter_interface_ops), \ + .is_member = &is_member_func, .filter_rate = &filter_rate_func \ + } + +/// @brief Structure representing a custom ID filter interface +/// @var svs_id_filter_interface::ops +/// Function pointers for the ID filter operations. +/// @var svs_id_filter_interface::self +/// Pointer to the user-defined ID filter instance. This pointer is passed to the +/// function pointers in @p ops when they are called. struct svs_id_filter_interface { - struct svs_id_filter_interface_ops ops; + struct svs_id_filter_interface_ops* ops; void* self; - // filter_rate provides the estimated selectivity of the filter, i.e., the fraction of - // IDs that are expected to pass the filter. A value of 0.01 indicates that 1% of IDs - // are expected to pass, while a value of 1.0 indicates that all IDs are expected to - // pass. If the filter does not provide an estimate, it should be set to 0.0. - float filter_rate; }; -/// @brief Structure to hold search results +/// @brief Macro to create a user-defined interface implementation structure +/// @param user_ptr Pointer to the user-defined object +/// @param vtable Function pointers for the interface operations +/// @return A fully initialized interface implementation structure +#define SVS_MAKE_INTERFACE(user_ptr, vtable) \ + { .ops = &vtable, .self = (void*)(user_ptr) } + +/// @brief Structure to hold search results in a compressed sparse row (CSR) layout. +/// +/// Row @p q (results for query @p q) occupies half-open range +/// [@p offsets[q], @p offsets[q+1]) in @p indices and @p distances. This supports +/// variadic per-query result counts (filtered search, range search) while keeping +/// the data flat and cache-friendly. For fixed top-k searches @p offsets[q] equals +/// @p q * k, so the classical @p indices[q*k+j] / @p distances[q*k+j] access pattern +/// remains valid. +/// +/// Ownership: when @p owns_buffers is true the library allocated @p offsets, +/// @p indices, @p distances and svs_search_results_free() will release them. +/// When false, the caller is responsible for the storage; svs_search_results_free() +/// only resets the descriptor. On zero-initialized objects the free call is a no-op. +/// +/// Buffer reuse: passing the same object to consecutive search calls lets the +/// library reuse existing library-owned buffers whenever capacity is sufficient; +/// steady-state batches of equal shape allocate only on the first call. +/// +/// Forward-compatibility contract: On any write to this OUT struct, the library +/// only touches fields covered by the caller-supplied @p struct_size. A caller +/// compiled against an older header will never observe writes beyond its known +/// fields. Any optional field added in a future API version is written only when +/// the caller opts in by supplying a large-enough @p struct_size (e.g. via the +/// SVS_INIT_SEARCH_RESULTS() macro from the newer header). +/// +/// @var svs_search_results::version +/// API version at which the struct was initialized (SVS_C_API_VERSION). +/// @var svs_search_results::struct_size +/// Size of this structure, used for versioning and forward compatibility. +/// @var svs_search_results::num_queries +/// Number of populated rows (queries) in this result set. +/// @var svs_search_results::total_results +/// Total number of populated results; equals offsets[num_queries]. +/// @var svs_search_results::offsets +/// Row start offsets, length @p num_queries + 1. Monotonically non-decreasing. +/// @var svs_search_results::indices +/// Neighbor IDs, length @p total_results. +/// @var svs_search_results::distances +/// Neighbor distances, length @p total_results. +/// @var svs_search_results::offsets_capacity +/// Number of elements allocated in @p offsets. +/// @var svs_search_results::results_capacity +/// Number of elements allocated in @p indices and @p distances. +/// @var svs_search_results::owns_buffers +/// True if the library owns @p offsets, @p indices, and @p distances and must +/// free them; false if the buffers are caller-provided. struct svs_search_results { - size_t num_queries; /// Number of query vectors - size_t* results_per_query; /// Number of results per query - size_t* indices; /// Indices of the nearest neighbors - float* distances; /// Distances to the nearest neighbors + uint32_t version; + size_t struct_size; + + size_t num_queries; + size_t total_results; + size_t* offsets; + size_t* indices; + float* distances; + + size_t offsets_capacity; + size_t results_capacity; + bool owns_buffers; }; -/// @brief Structure to hold memory breakdown for an index +/// @brief Macro to initialize a svs_search_results structure with default values +#define SVS_INIT_SEARCH_RESULTS() \ + { \ + .version = SVS_C_API_VERSION, .struct_size = sizeof(struct svs_search_results), \ + .num_queries = 0, .total_results = 0, .offsets = NULL, .indices = NULL, \ + .distances = NULL, .offsets_capacity = 0, .results_capacity = 0, \ + .owns_buffers = false \ + } + +/// @brief Initialize a svs_search_results structure with caller-provided buffers. +/// +/// Ownership stays with the caller (owns_buffers = false); svs_search_results_free() +/// will not release the buffers. The library reuses these buffers on search calls +/// as long as their capacities are sufficient; otherwise the call fails without +/// reallocating caller-owned storage. +/// +/// @param p_offsets Pointer to caller-owned offsets buffer (size_t[p_offsets_cap]). +/// Must hold at least @p num_queries + 1 elements at call time. +/// @param p_indices Pointer to caller-owned indices buffer (size_t[p_results_cap]). +/// @param p_distances Pointer to caller-owned distances buffer (float[p_results_cap]). +/// @param p_offsets_cap Number of elements allocated in @p p_offsets. +/// @param p_results_cap Number of elements allocated in @p p_indices and @p p_distances. +/// @example +/// size_t offsets[NQ + 1]; +/// size_t indices[NQ * K]; +/// float distances[NQ * K]; +/// svs_search_results_t results = SVS_INIT_SEARCH_RESULTS_WITH_BUFFERS( +/// offsets, indices, distances, NQ + 1, NQ * K +/// ); +#define SVS_INIT_SEARCH_RESULTS_WITH_BUFFERS( \ + p_offsets, p_indices, p_distances, p_offsets_cap, p_results_cap \ +) \ + { \ + .version = SVS_C_API_VERSION, .struct_size = sizeof(struct svs_search_results), \ + .num_queries = 0, .total_results = 0, .offsets = (p_offsets), \ + .indices = (p_indices), .distances = (p_distances), \ + .offsets_capacity = (p_offsets_cap), .results_capacity = (p_results_cap), \ + .owns_buffers = false \ + } + +/// @brief Convenience accessor for one query's row (O(1)). +/// @param results Pointer to a populated search results structure. +/// @param q Zero-based query index; must be < @p results->num_queries. +/// @param out_ids Optional out pointer to the first neighbor ID for query @p q. +/// @param out_distances Optional out pointer to the first neighbor distance for +/// query @p q. +/// @param out_count Optional out pointer to the number of results for query @p q. +static inline void svs_search_results_row( + const struct svs_search_results* results, + size_t q, + const size_t** out_ids, + const float** out_distances, + size_t* out_count +) { + if (q >= results->num_queries) { + if (out_ids) { + *out_ids = NULL; + } + if (out_distances) { + *out_distances = NULL; + } + if (out_count) { + *out_count = 0; + } + return; + } + size_t begin = results->offsets[q]; + size_t end = results->offsets[q + 1]; + if (out_ids) { + *out_ids = results->indices + begin; + } + if (out_distances) { + *out_distances = results->distances + begin; + } + if (out_count) { + *out_count = end - begin; + } + return; +} + +/// @brief Structure to hold memory breakdown for an index. +/// +/// Forward-compatibility contract: On any write to this OUT struct, the library +/// only touches fields covered by the caller-supplied @p struct_size. A caller +/// compiled against an older header will never observe writes beyond its known +/// fields. Any optional field added in a future API version is written only when +/// the caller opts in by supplying a large-enough @p struct_size (e.g. via the +/// SVS_INIT_MEMORY_BREAKDOWN() macro from the newer header). struct svs_memory_breakdown { + uint32_t version; /// Version of the memory breakdown structure + size_t struct_size; /// Size of the structure, used for versioning size_t graph_bytes; /// Allocated bytes for the graph structure size_t data_bytes; /// Allocated bytes for the data vectors size_t metadata_bytes; /// Allocated bytes for metadata (entry points, status, etc.) }; +/// @brief Macro to initialize a svs_memory_breakdown structure with default values +#define SVS_INIT_MEMORY_BREAKDOWN() \ + { \ + .version = SVS_C_API_VERSION, .struct_size = sizeof(struct svs_memory_breakdown), \ + .graph_bytes = 0, .data_bytes = 0, .metadata_bytes = 0 \ + } + // Handle typedefs; "_h" suffix indicates a handle to an opaque struct -typedef struct svs_error_desc* svs_error_h; +/// +/// @remarks Thread-safety: unless a specific function documents otherwise, handles +/// (svs_error_h, svs_index_h, svs_index_builder_h, svs_algorithm_h, svs_storage_h, +/// svs_search_params_h) are not internally synchronized. Do not operate on the same +/// handle from multiple threads concurrently without external synchronization. typedef struct svs_index* svs_index_h; typedef struct svs_index_builder* svs_index_builder_h; typedef struct svs_algorithm* svs_algorithm_h; @@ -131,17 +422,45 @@ typedef enum svs_error_code svs_error_code_t; typedef enum svs_distance_metric svs_distance_metric_t; typedef enum svs_algorithm_type svs_algorithm_type_t; typedef enum svs_data_type svs_data_type_t; +typedef enum svs_storage_kind svs_storage_kind_t; typedef enum svs_threadpool_kind svs_threadpool_kind_t; +typedef struct svs_threadpool_interface_ops svs_threadpool_ops_t; +typedef struct svs_threadpool_interface svs_threadpool_t; typedef struct svs_threadpool_interface* svs_threadpool_i; + +typedef struct svs_id_filter_interface_ops svs_id_filter_ops_t; +typedef struct svs_id_filter_interface svs_id_filter_t; typedef struct svs_id_filter_interface* svs_id_filter_i; -typedef struct svs_search_results* svs_search_results_t; + +typedef struct svs_search_results svs_search_results_t; typedef struct svs_memory_breakdown svs_memory_breakdown_t; +/// @brief Get SVS version information +/// @return An integer representing the version of the SVS library, encoded as (major << 16) +/// | (minor << 8) | patch +SVS_API uint32_t svs_get_version(); + +/// @brief Get SVS version string +/// @return A string representing the version of the SVS library in "major.minor.patch" +/// format +SVS_API const char* svs_get_version_string(); + /// @brief Create an error handle -/// @return A handle to the created error object +/// @return A handle to the created error object or NULL if creation failed (e.g., due to +/// memory allocation failure) +/// @remarks If this returns NULL, the value may still be passed as the optional @c out_err +/// argument to other API functions; error details simply will not be captured. SVS_API svs_error_h svs_error_create(); +/// @brief Set an error code and message in the error handle +/// @param err The error handle to set +/// @param code The error code to set +/// @param message A string describing the error +/// @return true if the error was set successfully, false if failed (e.g., if the error +/// handle is NULL) +SVS_API bool svs_error_set(svs_error_h err, svs_error_code_t code, const char* message); + /// @brief Check if the error handle indicates success /// @param err The error handle to check /// @return true if no error occurred, false otherwise @@ -155,6 +474,7 @@ SVS_API svs_error_code_t svs_error_get_code(svs_error_h err); /// @brief Get the error message from the error handle /// @param err The error handle /// @return A string describing the error +/// @remarks The returned string is valid until the error handle is freed or modified. SVS_API const char* svs_error_get_message(svs_error_h err); /// @brief Free the error handle @@ -174,6 +494,15 @@ SVS_API svs_algorithm_h svs_algorithm_create_vamana( svs_error_h out_err /*=NULL*/ ); +/// @brief Get algorithm type from an algorithm handle +/// @param algorithm The algorithm handle +/// @param out_type Pointer to store the retrieved algorithm type +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +SVS_API bool svs_algorithm_get_type( + svs_algorithm_h algorithm, svs_algorithm_type_t* out_type, svs_error_h out_err /*=NULL*/ +); + /// @brief Free the algorithm configuration handle /// @param algorithm The algorithm handle to free SVS_API void svs_algorithm_free(svs_algorithm_h algorithm); @@ -301,6 +630,15 @@ SVS_API svs_storage_h svs_storage_create_sq( svs_data_type_t data_type, svs_error_h out_err /*=NULL*/ ); +/// @brief Get the kind of storage configuration +/// @param storage The storage handle +/// @param out_kind Pointer to store the retrieved storage kind +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +SVS_API bool svs_storage_get_kind( + svs_storage_h storage, svs_storage_kind_t* out_kind, svs_error_h out_err /*=NULL*/ +); + /// @brief Free the storage handle /// @param storage The storage handle to free SVS_API void svs_storage_free(svs_storage_h storage); @@ -351,6 +689,9 @@ SVS_API bool svs_index_builder_set_threadpool( /// @param pool The custom thread pool interface /// @param out_err An optional error handle to capture errors /// @return true on success, false on failure +/// @remarks The builder copies @p pool (the interface struct and its ops table) by value +/// before returning, so the caller may free or modify @p pool and its ops table once this +/// call returns. SVS_API bool svs_index_builder_set_threadpool_custom( svs_index_builder_h builder, svs_threadpool_i pool, svs_error_h out_err /*=NULL*/ ); @@ -361,6 +702,9 @@ SVS_API bool svs_index_builder_set_threadpool_custom( /// @param num_vectors The number of vectors in the data /// @param out_err An optional error handle to capture errors /// @return A handle to the built index +/// @remarks @p data is a row-major array of @p num_vectors * dimension floats (dimension +/// as passed to svs_index_builder_create). The data is copied into the index's internal +/// storage, so the caller may free or modify @p data once this call returns. SVS_API svs_index_h svs_index_build( svs_index_builder_h builder, const float* data, @@ -378,6 +722,8 @@ SVS_API svs_index_h svs_index_build( /// default) /// @param out_err An optional error handle to capture errors /// @return A handle to the built dynamic index +/// @remarks Both @p data and @p ids are copied into the index's internal storage; the +/// caller may free or modify them once this call returns. SVS_API svs_index_h svs_index_build_dynamic( svs_index_builder_h builder, const float* data, @@ -413,60 +759,75 @@ SVS_API svs_index_h svs_index_load_dynamic( /// @param index The index handle to free SVS_API void svs_index_free(svs_index_h index); -/// @brief Search the index with the provided queries +/// @brief TopK search the index with the provided queries and an optional ID filter +/// @details Performs a TopK search on the index with the provided queries and an optional +/// ID filter. The ID filter allows for filtering the search results based on specific IDs, +/// enabling more targeted searches. If the ID filter is NULL, the search will return the +/// top K results. If ID filter is provided, only the results that pass the filter will be +/// returned. Results are written into @p out_results in CSR layout (see +/// svs_search_results); library-owned buffers are reused across calls when their +/// capacity suffices. If ID filter is provided with `filter_rate > 0.0` then the +/// function will account for the actual filter hit rate during the search. If the +/// actual observed filter hit rate is less than the provided `filter_rate` value, the +/// function returns an empty result set. +/// @note After use, release library-owned buffers with svs_search_results_free(). /// @param index The index handle /// @param queries Pointer to the query data (float array) /// @param num_queries The number of query vectors /// @param k The number of nearest neighbors to retrieve per query +/// @param out_results Pointer to a caller-provided results structure (typically +/// initialized with SVS_INIT_SEARCH_RESULTS()). See svs_search_results for +/// ownership and buffer-reuse semantics. /// @param search_params The search parameters handle (can be NULL for defaults) +/// @param id_filter The ID filter interface (can be NULL for no filtering) /// @param out_err An optional error handle to capture errors -/// @return A pointer to the search results structure -/// @deprecated Use svs_index_search_topK() instead, which additionally supports an -/// optional ID filter. This function is equivalent to calling svs_index_search_topK() -/// with a NULL id_filter. -SVS_DEPRECATED("Use svs_index_search_topK() instead") -SVS_API svs_search_results_t svs_index_search( +/// @return true on success, false on failure +SVS_API bool svs_index_search_topk( svs_index_h index, const float* queries, size_t num_queries, size_t k, + svs_search_results_t* out_results, svs_search_params_h search_params /*=NULL*/, + svs_id_filter_i id_filter /*=NULL*/, svs_error_h out_err /*=NULL*/ ); -/// @brief TopK search the index with the provided queries and an optional ID filter -/// @details Performs a TopK search on the index with the provided queries and an optional -/// ID filter. The ID filter allows for filtering the search results based on specific IDs, -/// enabling more targeted searches. If the ID filter is NULL, the search will return the -/// top K results. If ID filter is provided, only the results that pass the filter will be -/// returned. The function returns a pointer to the search results structure, which contains -/// the indices and distances of the nearest neighbors for each query. If ID filter is -/// provided with `filter_rate > 0.0` then the function will account for the actual filter -/// hit rate during the search. If the actual observed filter hit rate is less than the -/// provided `filter_rate` value, the function returns an empty result set. -/// @note The search results structure must be freed using svs_search_results_free() to -/// avoid memory leaks. +/// @brief Release library-owned buffers held by a search results structure and +/// reset it to the SVS_INIT_SEARCH_RESULTS() state. +/// @param results Pointer to the results structure. Safe on NULL, on +/// zero-initialized objects, and on caller-owned buffers (in which case only the +/// descriptor is reset). Safe to call multiple times. +SVS_API void svs_search_results_free(svs_search_results_t* results); + +/// @brief Search the index with the provided queries /// @param index The index handle /// @param queries Pointer to the query data (float array) /// @param num_queries The number of query vectors /// @param k The number of nearest neighbors to retrieve per query +/// @param out_results Pointer to a caller-provided results structure (typically +/// initialized with SVS_INIT_SEARCH_RESULTS()). See svs_search_results for +/// ownership and buffer-reuse semantics. /// @param search_params The search parameters handle (can be NULL for defaults) -/// @param id_filter The ID filter interface (can be NULL for no filtering) /// @param out_err An optional error handle to capture errors -/// @return A pointer to the search results structure -SVS_API svs_search_results_t svs_index_search_topK( +/// @return true on success, false on failure +/// @deprecated Use svs_index_search_topk() instead, which additionally supports an +/// optional ID filter. This function is equivalent to calling svs_index_search_topk() +/// with a NULL id_filter. +SVS_DEPRECATED("Use svs_index_search_topk() instead") +static inline bool svs_index_search( svs_index_h index, const float* queries, size_t num_queries, size_t k, + svs_search_results_t* out_results, svs_search_params_h search_params /*=NULL*/, - svs_id_filter_i id_filter /*=NULL*/, svs_error_h out_err /*=NULL*/ -); - -/// @brief Free the search results structure -/// @param results The search results structure to release -SVS_API void svs_search_results_free(svs_search_results_t results); +) { + return svs_index_search_topk( + index, queries, num_queries, k, out_results, search_params, NULL, out_err + ); +} /// @brief Save the index to disk /// @param index The index handle @@ -481,13 +842,16 @@ svs_index_save(svs_index_h index, const char* directory, svs_error_h out_err /*= /// @param new_points Pointer to the new vector data (float array) /// @param ids Pointer to the new vector IDs (size_t array) /// @param num_vectors The number of new vectors to add +/// @param out_added_count Optional pointer to store the number of successfully added +/// vectors /// @param out_err An optional error handle to capture errors -/// @return number of points successfully added, or (size_t)-1 on failure -SVS_API size_t svs_index_dynamic_add_points( +/// @return true on success, false on failure +SVS_API bool svs_index_dynamic_add_points( svs_index_h index, const float* new_points, const size_t* ids, size_t num_vectors, + size_t* out_added_count /*=NULL*/, svs_error_h out_err /*=NULL*/ ); @@ -495,11 +859,18 @@ SVS_API size_t svs_index_dynamic_add_points( /// @param index The dynamic index handle /// @param ids Pointer to the vector IDs to delete (size_t array) /// @param num_ids The number of vector IDs to delete +/// @param out_deleted_count Optional pointer to store the number of successfully deleted +/// vectors /// @param out_err An optional error handle to capture errors -/// @return number of points successfully deleted, or (size_t)-1 on failure -SVS_API size_t svs_index_dynamic_delete_points( - svs_index_h index, const size_t* ids, size_t num_ids, svs_error_h out_err /*=NULL*/ +/// @return true on success, false on failure +SVS_API bool svs_index_dynamic_delete_points( + svs_index_h index, + const size_t* ids, + size_t num_ids, + size_t* out_deleted_count /*=NULL*/, + svs_error_h out_err /*=NULL*/ ); + /// @brief Check if a dynamic index has a specific ID /// @param index The dynamic index handle /// @param id The vector ID to check for @@ -564,6 +935,9 @@ SVS_API bool svs_index_dynamic_compact( /// @param out_num_threads Pointer to store the retrieved number of threads /// @param out_err An optional error handle to capture errors /// @return true on success, false on failure +/// @error On failure, if out_err is provided, it will contain: +/// - SVS_ERROR_INVALID_ARGUMENT if index or out_num_threads is NULL +/// - SVS_ERROR_RUNTIME for other runtime failures SVS_API bool svs_index_get_num_threads( svs_index_h index, size_t* out_num_threads, svs_error_h out_err /*=NULL*/ ); diff --git a/bindings/c/include/svs/c_api/svs_c_config.h b/bindings/c/include/svs/c/svs_c_config.h similarity index 97% rename from bindings/c/include/svs/c_api/svs_c_config.h rename to bindings/c/include/svs/c/svs_c_config.h index 14de7efb..4106d53a 100644 --- a/bindings/c/include/svs/c_api/svs_c_config.h +++ b/bindings/c/include/svs/c/svs_c_config.h @@ -16,6 +16,8 @@ #pragma once +#include "svs/c/svs_c_version.h" + // All symbols shall be internal unless marked as SVS_API #if defined _WIN32 || defined __CYGWIN__ #define SVS_HELPER_DLL_IMPORT __declspec(dllimport) diff --git a/bindings/c/include/svs/c/svs_c_version.h.in b/bindings/c/include/svs/c/svs_c_version.h.in new file mode 100644 index 00000000..a6898ba8 --- /dev/null +++ b/bindings/c/include/svs/c/svs_c_version.h.in @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This file is generated by CMake from svs_c_version.h.in. Do not edit directly. + +#pragma once + +#define SVS_C_API_VERSION_MAJOR @svs_c_api_VERSION_MAJOR@ +#define SVS_C_API_VERSION_MINOR @svs_c_api_VERSION_MINOR@ +#define SVS_C_API_VERSION_PATCH @svs_c_api_VERSION_PATCH@ +#define SVS_C_API_VERSION_STRING "@svs_c_api_VERSION@" + +#define SVS_C_API_VERSION \ + ((SVS_C_API_VERSION_MAJOR << 16) | (SVS_C_API_VERSION_MINOR << 8) | \ + SVS_C_API_VERSION_PATCH) + +#define SVS_GET_VERSION_MAJOR(version) ((version >> 16) & 0xFF) +#define SVS_GET_VERSION_MINOR(version) ((version >> 8) & 0xFF) +#define SVS_GET_VERSION_PATCH(version) (version & 0xFF) diff --git a/bindings/c/samples/dynamic.c b/bindings/c/samples/dynamic.c index 960e45bd..9f74cc43 100644 --- a/bindings/c/samples/dynamic.c +++ b/bindings/c/samples/dynamic.c @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include #include #include @@ -36,21 +36,19 @@ void generate_random_data(float* data, size_t count, size_t dim) { size_t sequential_tp_size(void* self) { return 1; } -void sequential_tp_parallel_for( - void* self, void (*func)(void*, size_t), void* svs_param, size_t n +bool sequential_tp_parallel_for( + void* self, void (*func)(void*, size_t), void* svs_param, size_t n, svs_error_h out_err ) { for (size_t i = 0; i < n; ++i) { func(svs_param, i); } + return true; } -static struct svs_threadpool_interface sequential_threadpool = { - { - &sequential_tp_size, - &sequential_tp_parallel_for, - }, - NULL, -}; +static svs_threadpool_ops_t sequential_tp_ops = + SVS_INIT_THREADPOOL_OPS(sequential_tp_size, sequential_tp_parallel_for); + +static svs_threadpool_t sequential_threadpool = SVS_MAKE_INTERFACE(NULL, sequential_tp_ops); int main() { int ret = 0; @@ -65,7 +63,7 @@ int main() { svs_index_builder_h builder = NULL; svs_index_h index = NULL; svs_search_params_h search_params = NULL; - svs_search_results_t results = NULL; + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); // Allocate random data data = (float*)malloc(NUM_VECTORS * DIMENSION * sizeof(float)); @@ -194,21 +192,22 @@ int main() { // Add more points to the index printf("Adding %d more vectors to the index...\n", TAILING_VECTORS); - size_t num_added = svs_index_dynamic_add_points( - index, - data + INITIAL_VECTORS * DIMENSION, - ids + INITIAL_VECTORS, - TAILING_VECTORS, - error - ); - if (num_added == (size_t)-1) { + size_t num_added = 0; + if (!svs_index_dynamic_add_points( + index, + data + INITIAL_VECTORS * DIMENSION, + ids + INITIAL_VECTORS, + TAILING_VECTORS, + &num_added, + error + )) { fprintf( stderr, "Failed to add points to index: %s\n", svs_error_get_message(error) ); ret = 1; goto cleanup; } - printf("Points added successfully!\n"); + printf("Added %zu points successfully!\n", num_added); // Search params search_params = svs_search_params_create_vamana(100, error); @@ -222,10 +221,16 @@ int main() { // Search printf("Searching %d queries for top-%d neighbors...\n", NUM_QUERIES, K); - results = svs_index_search_topK( - index, queries, NUM_QUERIES, K, search_params, NULL /* id_filter */, error - ); - if (!results) { + if (!svs_index_search_topk( + index, + queries, + NUM_QUERIES, + K, + &results, + search_params, + NULL /* id_filter */, + error + )) { fprintf(stderr, "Failed to search index: %s\n", svs_error_get_message(error)); ret = 1; goto cleanup; @@ -233,21 +238,17 @@ int main() { printf("Search completed successfully!\n"); // Print results - size_t offset = 0; - for (size_t q = 0; q < results->num_queries; q++) { + for (size_t q = 0; q < results.num_queries; q++) { + const size_t* ids; + const float* dists; + size_t count; + svs_search_results_row(&results, q, &ids, &dists, &count); printf("Query %zu results:\n", q); - for (size_t i = 0; i < results->results_per_query[q]; i++) { - printf( - " [%zu] id=%zu, distance=%.4f\n", - i, - results->indices[offset + i], - results->distances[offset + i] - ); + for (size_t i = 0; i < count; i++) { + printf(" [%zu] id=%zu, distance=%.4f\n", i, ids[i], dists[i]); } - offset += results->results_per_query[q]; } - svs_search_results_free(results); - results = NULL; + svs_search_results_free(&results); // Delete some points printf( @@ -255,10 +256,14 @@ int main() { DELETE_VECTORS_BEGIN, DELETE_VECTORS_END - 1 ); - size_t num_deleted = svs_index_dynamic_delete_points( - index, ids + DELETE_VECTORS_BEGIN, DELETE_VECTORS_END - DELETE_VECTORS_BEGIN, error - ); - if (num_deleted == (size_t)-1) { + size_t num_deleted = 0; + if (!svs_index_dynamic_delete_points( + index, + ids + DELETE_VECTORS_BEGIN, + DELETE_VECTORS_END - DELETE_VECTORS_BEGIN, + &num_deleted, + error + )) { fprintf( stderr, "Failed to delete points from index: %s\n", svs_error_get_message(error) ); @@ -269,10 +274,16 @@ int main() { // Search again after deletion printf("Searching again after deletion...\n"); - results = svs_index_search_topK( - index, queries, NUM_QUERIES, K, search_params, NULL /* id_filter */, error - ); - if (!results) { + if (!svs_index_search_topk( + index, + queries, + NUM_QUERIES, + K, + &results, + search_params, + NULL /* id_filter */, + error + )) { fprintf( stderr, "Failed to search index after deletion: %s\n", @@ -285,16 +296,17 @@ int main() { // Validate that deleted points are not returned in search results printf("Validating results after deletion...\n"); - offset = 0; - for (size_t q = 0; q < results->num_queries; q++) { - for (size_t i = 0; i < results->results_per_query[q]; i++) { - size_t id = results->indices[offset + i]; + for (size_t q = 0; q < results.num_queries; q++) { + const size_t* ids; + size_t count; + svs_search_results_row(&results, q, &ids, NULL, &count); + for (size_t i = 0; i < count; i++) { + size_t id = ids[i]; if (id >= DELETE_VECTORS_BEGIN && id < DELETE_VECTORS_END) { fprintf(stderr, "Error: Deleted id %zu returned in search results!\n", id); ret = 1; } } - offset += results->results_per_query[q]; } // Check if specific IDs exist in the index @@ -406,7 +418,7 @@ int main() { cleanup: // Cleanup - svs_search_results_free(results); + svs_search_results_free(&results); svs_search_params_free(search_params); svs_index_free(index); svs_index_builder_free(builder); diff --git a/bindings/c/samples/save_load.c b/bindings/c/samples/save_load.c index 2315020a..97cc5995 100644 --- a/bindings/c/samples/save_load.c +++ b/bindings/c/samples/save_load.c @@ -19,7 +19,7 @@ // required for mkdtemp #define _GNU_SOURCE -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include #include #include @@ -66,10 +66,10 @@ int main() { svs_storage_h storage = NULL; svs_index_builder_h builder = NULL; svs_index_h index = NULL; - svs_search_results_t results = NULL; + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); char tmp_dir_template[] = "svs_index_XXXXXX"; char* tmp_dir = NULL; - svs_search_results_t loaded_results = NULL; + svs_search_results_t loaded_results = SVS_INIT_SEARCH_RESULTS(); // Allocate random data data = (float*)malloc(NUM_VECTORS * DIMENSION * sizeof(float)); @@ -183,16 +183,16 @@ int main() { // Search printf("Searching %d queries for top-%d neighbors...\n", NUM_QUERIES, K); - results = svs_index_search_topK( - index, - queries, - NUM_QUERIES, - K, - NULL /* search_params */, - NULL /* id_filter */, - error - ); - if (!results) { + if (!svs_index_search_topk( + index, + queries, + NUM_QUERIES, + K, + &results, + NULL /* search_params */, + NULL /* id_filter */, + error + )) { fprintf(stderr, "Failed to search index: %s\n", svs_error_get_message(error)); ret = 1; goto cleanup; @@ -232,16 +232,16 @@ int main() { printf( "Searching loaded index for %d queries for top-%d neighbors...\n", NUM_QUERIES, K ); - loaded_results = svs_index_search_topK( - index, - queries, - NUM_QUERIES, - K, - NULL /* search_params */, - NULL /* id_filter */, - error - ); - if (!loaded_results) { + if (!svs_index_search_topk( + index, + queries, + NUM_QUERIES, + K, + &loaded_results, + NULL /* search_params */, + NULL /* id_filter */, + error + )) { fprintf( stderr, "Failed to search loaded index: %s\n", svs_error_get_message(error) ); @@ -251,7 +251,7 @@ int main() { printf("Search on loaded index completed successfully!\n"); // Compare results - if (results->num_queries != loaded_results->num_queries) { + if (results.num_queries != loaded_results.num_queries) { fprintf( stderr, "Mismatch in number of queries between original and loaded results\n" ); @@ -260,15 +260,17 @@ int main() { } size_t offset = 0; - for (size_t q = 0; q < results->num_queries; q++) { - if (results->results_per_query[q] != loaded_results->results_per_query[q]) { + for (size_t q = 0; q < results.num_queries; q++) { + size_t count = results.offsets[q + 1] - results.offsets[q]; + size_t loaded_count = loaded_results.offsets[q + 1] - loaded_results.offsets[q]; + if (count != loaded_count) { fprintf(stderr, "Mismatch in number of results for query %zu\n", q); ret = 1; goto cleanup; } printf("Query %zu results:\n", q); - for (size_t i = 0; i < results->results_per_query[q]; i++) { - if (results->indices[offset + i] != loaded_results->indices[offset + i]) { + for (size_t i = 0; i < count; i++) { + if (results.indices[offset + i] != loaded_results.indices[offset + i]) { fprintf( stderr, "Mismatch in neighbor indices for query %zu, result %zu\n", q, i ); @@ -278,12 +280,12 @@ int main() { printf( " [%zu] id=%zu, distance=%.4f, diff=%.4f\n", i, - results->indices[offset + i], - results->distances[offset + i], - results->distances[offset + i] - loaded_results->distances[offset + i] + results.indices[offset + i], + results.distances[offset + i], + results.distances[offset + i] - loaded_results.distances[offset + i] ); } - offset += results->results_per_query[q]; + offset += count; } printf("Done!\n"); @@ -294,8 +296,8 @@ int main() { // remove the temporary directory and its contents remove_directory_recursive(tmp_dir); } - svs_search_results_free(results); - svs_search_results_free(loaded_results); + svs_search_results_free(&results); + svs_search_results_free(&loaded_results); svs_index_free(index); svs_index_builder_free(builder); svs_storage_free(storage); diff --git a/bindings/c/samples/simple.c b/bindings/c/samples/simple.c index 8b79dc9b..6d00e496 100644 --- a/bindings/c/samples/simple.c +++ b/bindings/c/samples/simple.c @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include #include #include @@ -32,21 +32,19 @@ void generate_random_data(float* data, size_t count, size_t dim) { size_t sequential_tp_size(void* self) { return 1; } -void sequential_tp_parallel_for( - void* self, void (*func)(void*, size_t), void* svs_param, size_t n +bool sequential_tp_parallel_for( + void* self, void (*func)(void*, size_t), void* svs_param, size_t n, svs_error_h out_err ) { for (size_t i = 0; i < n; ++i) { func(svs_param, i); } + return true; } -static struct svs_threadpool_interface sequential_threadpool = { - { - &sequential_tp_size, - &sequential_tp_parallel_for, - }, - NULL, -}; +static svs_threadpool_ops_t sequential_tp_ops = + SVS_INIT_THREADPOOL_OPS(sequential_tp_size, sequential_tp_parallel_for); + +static svs_threadpool_t sequential_threadpool = SVS_MAKE_INTERFACE(NULL, sequential_tp_ops); int main() { int ret = 0; @@ -59,7 +57,7 @@ int main() { svs_storage_h storage = NULL; svs_index_builder_h builder = NULL; svs_index_h index = NULL; - svs_search_results_t results = NULL; + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); // Allocate random data data = (float*)malloc(NUM_VECTORS * DIMENSION * sizeof(float)); @@ -190,10 +188,17 @@ int main() { // Search printf("Searching %d queries for top-%d neighbors...\n", NUM_QUERIES, K); - results = svs_index_search_topK( - index, queries, NUM_QUERIES, K, search_params, NULL /* id_filter */, error - ); - if (!results) { + + if (!svs_index_search_topk( + index, + queries, + NUM_QUERIES, + K, + &results, + search_params, + NULL /* id_filter */, + error + )) { fprintf(stderr, "Failed to search index: %s\n", svs_error_get_message(error)); ret = 1; goto cleanup; @@ -201,25 +206,22 @@ int main() { printf("Search completed successfully!\n"); // Print results - size_t offset = 0; - for (size_t q = 0; q < results->num_queries; q++) { + for (size_t q = 0; q < results.num_queries; q++) { + const size_t* ids; + const float* dists; + size_t count; + svs_search_results_row(&results, q, &ids, &dists, &count); printf("Query %zu results:\n", q); - for (size_t i = 0; i < results->results_per_query[q]; i++) { - printf( - " [%zu] id=%zu, distance=%.4f\n", - i, - results->indices[offset + i], - results->distances[offset + i] - ); + for (size_t i = 0; i < count; i++) { + printf(" [%zu] id=%zu, distance=%.4f\n", i, ids[i], dists[i]); } - offset += results->results_per_query[q]; } printf("Done!\n"); cleanup: // Cleanup - svs_search_results_free(results); + svs_search_results_free(&results); svs_index_free(index); svs_index_builder_free(builder); svs_storage_free(storage); diff --git a/bindings/c/src/algorithm.hpp b/bindings/c/src/algorithm.hpp index 127a7902..3ca449f2 100644 --- a/bindings/c/src/algorithm.hpp +++ b/bindings/c/src/algorithm.hpp @@ -15,7 +15,7 @@ */ #pragma once -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" // #include // #include diff --git a/bindings/c/src/data_builder/leanvec.hpp b/bindings/c/src/data_builder/leanvec.hpp index 13ba58a5..e74fbd94 100644 --- a/bindings/c/src/data_builder/leanvec.hpp +++ b/bindings/c/src/data_builder/leanvec.hpp @@ -17,7 +17,7 @@ #ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include "storage.hpp" #include "types_support.hpp" diff --git a/bindings/c/src/data_builder/lvq.hpp b/bindings/c/src/data_builder/lvq.hpp index d7d5912a..c5002b6c 100644 --- a/bindings/c/src/data_builder/lvq.hpp +++ b/bindings/c/src/data_builder/lvq.hpp @@ -17,7 +17,7 @@ #ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include "storage.hpp" #include "types_support.hpp" diff --git a/bindings/c/src/data_builder/simple.hpp b/bindings/c/src/data_builder/simple.hpp index 1f016241..e7e8a71c 100644 --- a/bindings/c/src/data_builder/simple.hpp +++ b/bindings/c/src/data_builder/simple.hpp @@ -15,7 +15,7 @@ */ #pragma once -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include "allocator.hpp" #include "storage.hpp" diff --git a/bindings/c/src/data_builder/sq.hpp b/bindings/c/src/data_builder/sq.hpp index f2fd9559..c4dee7a3 100644 --- a/bindings/c/src/data_builder/sq.hpp +++ b/bindings/c/src/data_builder/sq.hpp @@ -15,7 +15,7 @@ */ #pragma once -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include "allocator.hpp" #include "storage.hpp" diff --git a/bindings/c/src/error.cpp b/bindings/c/src/error.cpp index f3d845f7..97719082 100644 --- a/bindings/c/src/error.cpp +++ b/bindings/c/src/error.cpp @@ -13,16 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include "error.hpp" #include -extern "C" svs_error_h svs_error_create() { return new svs_error_desc{SVS_OK, "Success"}; } -extern "C" bool svs_error_ok(svs_error_h err) { return err->code == SVS_OK; } -extern "C" svs_error_code_t svs_error_get_code(svs_error_h err) { return err->code; } +extern "C" svs_error_h svs_error_create() { return new svs_error_desc{}; } +extern "C" bool svs_error_set(svs_error_h err, svs_error_code_t code, const char* message) { + if (!err) { + return false; + } + try { + err->message = message != nullptr ? message : ""; + } catch (const std::exception& e) { return false; } + err->code = code; + return true; +} +extern "C" bool svs_error_ok(svs_error_h err) { return err && err->code == SVS_OK; } +extern "C" svs_error_code_t svs_error_get_code(svs_error_h err) { + return err ? err->code : SVS_ERROR_INVALID_ARGUMENT; +} extern "C" const char* svs_error_get_message(svs_error_h err) { - return err->message.c_str(); + return err ? err->message.c_str() : "Invalid error handle"; } extern "C" void svs_error_free(svs_error_h err) { delete err; } diff --git a/bindings/c/src/error.hpp b/bindings/c/src/error.hpp index 1c183dd3..a26ce0ef 100644 --- a/bindings/c/src/error.hpp +++ b/bindings/c/src/error.hpp @@ -15,7 +15,7 @@ */ #pragma once -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include #include @@ -24,8 +24,8 @@ // C API error structure struct svs_error_desc { - svs_error_code_t code; - std::string message; + svs_error_code_t code = SVS_OK; + std::string message = "Success"; }; #define SET_ERROR(err, c, msg) \ diff --git a/bindings/c/src/filtered_search.hpp b/bindings/c/src/filtered_search.hpp index b75f4e69..e5d76e8c 100644 --- a/bindings/c/src/filtered_search.hpp +++ b/bindings/c/src/filtered_search.hpp @@ -15,7 +15,7 @@ */ #pragma once -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include "types_support.hpp" diff --git a/bindings/c/src/index.hpp b/bindings/c/src/index.hpp index 59ae0184..060a2449 100644 --- a/bindings/c/src/index.hpp +++ b/bindings/c/src/index.hpp @@ -15,7 +15,7 @@ */ #pragma once -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include "algorithm.hpp" #include "filtered_search.hpp" diff --git a/bindings/c/src/index_builder.hpp b/bindings/c/src/index_builder.hpp index 4b221397..e86e7d26 100644 --- a/bindings/c/src/index_builder.hpp +++ b/bindings/c/src/index_builder.hpp @@ -15,7 +15,7 @@ */ #pragma once -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include "algorithm.hpp" #include "dispatcher_dynamic_vamana.hpp" diff --git a/bindings/c/src/storage.hpp b/bindings/c/src/storage.hpp index b35927ca..8c0a5689 100644 --- a/bindings/c/src/storage.hpp +++ b/bindings/c/src/storage.hpp @@ -15,7 +15,7 @@ */ #pragma once -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include "allocator.hpp" #include "error.hpp" diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 40baf635..4cb7931a 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include "algorithm.hpp" #include "error.hpp" @@ -24,6 +24,7 @@ #include "threadpool.hpp" #include "types_support.hpp" +#include #include #include #include @@ -55,6 +56,10 @@ struct svs_storage { std::shared_ptr impl; }; +extern "C" uint32_t svs_get_version() { return SVS_C_API_VERSION; } + +extern "C" const char* svs_get_version_string() { return SVS_C_API_VERSION_STRING; } + extern "C" svs_algorithm_h svs_algorithm_create_vamana( size_t graph_degree, size_t build_window_size, @@ -78,6 +83,21 @@ extern "C" svs_algorithm_h svs_algorithm_create_vamana( ); } +extern "C" bool svs_algorithm_get_type( + svs_algorithm_h algorithm, svs_algorithm_type_t* out_type, svs_error_h out_err +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + EXPECT_ARG_NOT_NULL(algorithm); + EXPECT_ARG_NOT_NULL(out_type); + *out_type = algorithm->impl->type; + return true; + }, + out_err + ); +} + extern "C" void svs_algorithm_free(svs_algorithm_h algorithm) { delete algorithm; } #define EXPECT_VAMANA(algorithm) \ @@ -340,6 +360,21 @@ svs_storage_create_sq(svs_data_type_t data_type, svs_error_h out_err) { ); } +extern "C" SVS_API bool svs_storage_get_kind( + svs_storage_h storage, svs_storage_kind_t* out_kind, svs_error_h out_err +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + EXPECT_ARG_NOT_NULL(storage); + EXPECT_ARG_NOT_NULL(out_kind); + *out_kind = storage->impl->kind; + return true; + }, + out_err + ); +} + extern "C" void svs_storage_free(svs_storage_h storage) { delete storage; } extern "C" svs_index_builder_h svs_index_builder_create( @@ -557,26 +592,57 @@ svs_index_load(svs_index_builder_h builder, const char* directory, svs_error_h o extern "C" void svs_index_free(svs_index_h index) { delete index; } -extern "C" svs_search_results_t svs_index_search( - svs_index_h index, - const float* queries, - size_t num_queries, - size_t k, - svs_search_params_h search_params, - svs_error_h out_err +namespace { + +// Ensures out_results has at least `min_capacity` results and +// `min_offsets_capacity` offset entries; grows library-owned buffers as needed. +// For caller-owned buffers, insufficient capacity is a hard error. +inline void ensure_search_results_capacity( + svs_search_results_t* r, size_t min_offsets_capacity, size_t min_results_capacity ) { - // Deprecated: delegate to svs_index_search_topK without an ID filter to avoid - // duplicating the search and result-marshalling logic. - return svs_index_search_topK( - index, queries, num_queries, k, search_params, nullptr, out_err - ); + if (r->owns_buffers) { + if (r->offsets_capacity < min_offsets_capacity) { + delete[] r->offsets; + r->offsets = nullptr; + r->offsets_capacity = 0; + r->offsets = new size_t[min_offsets_capacity]; + r->offsets_capacity = min_offsets_capacity; + } + if (r->results_capacity < min_results_capacity) { + delete[] r->indices; + delete[] r->distances; + r->indices = nullptr; + r->distances = nullptr; + r->results_capacity = 0; + r->indices = new size_t[min_results_capacity]; + r->distances = new float[min_results_capacity]; + r->results_capacity = min_results_capacity; + } + } else if (r->offsets == nullptr && r->indices == nullptr && r->distances == nullptr) { + // Descriptor was zero-initialized: take ownership and allocate. + r->offsets = new size_t[min_offsets_capacity]; + r->offsets_capacity = min_offsets_capacity; + r->indices = new size_t[min_results_capacity]; + r->distances = new float[min_results_capacity]; + r->results_capacity = min_results_capacity; + r->owns_buffers = true; + } else { + INVALID_ARGUMENT_IF( + r->offsets_capacity < min_offsets_capacity || + r->results_capacity < min_results_capacity, + "Caller-provided svs_search_results buffers are too small" + ); + } } -extern "C" svs_search_results_t svs_index_search_topK( +} // namespace + +extern "C" bool svs_index_search_topk( svs_index_h index, const float* queries, size_t num_queries, size_t k, + svs_search_results_t* out_results, svs_search_params_h search_params, svs_id_filter_i id_filter, svs_error_h out_err @@ -588,9 +654,17 @@ extern "C" svs_search_results_t svs_index_search_topK( EXPECT_ARG_NOT_NULL(queries); EXPECT_ARG_GT_THAN(num_queries, 0); EXPECT_ARG_GT_THAN(k, 0); + EXPECT_ARG_NOT_NULL(out_results); auto& index_ptr = index->impl; INVALID_ARGUMENT_IF(index_ptr == nullptr, "Invalid index handle"); - + INVALID_ARGUMENT_IF( + out_results->version > svs_get_version(), + "Incompatible svs_search_results_t version" + ); + INVALID_ARGUMENT_IF( + out_results->struct_size != sizeof(svs_search_results_t), + "Incompatible svs_search_results_t struct_size" + ); auto queries_view = svs::data::ConstSimpleDataView( queries, num_queries, index_ptr->dimensions() ); @@ -604,36 +678,43 @@ extern "C" svs_search_results_t svs_index_search_topK( id_filter == nullptr ? nullptr : &id_filter_adapter ); - svs_search_results_t results = - new svs_search_results{0, nullptr, nullptr, nullptr}; - - results->num_queries = num_queries; - results->results_per_query = new size_t[num_queries]; - results->indices = new size_t[num_queries * k]; - results->distances = new float[num_queries * k]; + ensure_search_results_capacity(out_results, num_queries + 1, num_queries * k); + out_results->num_queries = num_queries; + out_results->total_results = num_queries * k; for (size_t i = 0; i < num_queries; ++i) { - results->results_per_query[i] = k; + out_results->offsets[i] = i * k; for (size_t j = 0; j < k; ++j) { - results->indices[i * k + j] = search_results.index(i, j); - results->distances[i * k + j] = search_results.distance(i, j); + out_results->indices[i * k + j] = search_results.index(i, j); + out_results->distances[i * k + j] = search_results.distance(i, j); } } + out_results->offsets[num_queries] = num_queries * k; - return results; + return true; }, out_err ); } -extern "C" void svs_search_results_free(svs_search_results_t results) { +extern "C" void svs_search_results_free(svs_search_results_t* results) { if (results == nullptr) { return; } - delete[] results->results_per_query; + results->num_queries = 0; + results->total_results = 0; + if (!results->owns_buffers) { + return; // caller-owned: leave pointers/capacities intact. + } + delete[] results->offsets; delete[] results->indices; delete[] results->distances; - delete results; + results->offsets = nullptr; + results->indices = nullptr; + results->distances = nullptr; + results->offsets_capacity = 0; + results->results_capacity = 0; + results->owns_buffers = false; } extern "C" bool @@ -650,11 +731,12 @@ svs_index_save(svs_index_h index, const char* directory, svs_error_h out_err) { ); } -extern "C" size_t svs_index_dynamic_add_points( +extern "C" bool svs_index_dynamic_add_points( svs_index_h index, const float* new_points, const size_t* ids, size_t num_vectors, + size_t* out_added_count, svs_error_h out_err ) { using namespace svs::c_runtime; @@ -671,15 +753,23 @@ extern "C" size_t svs_index_dynamic_add_points( auto src_data = svs::data::ConstSimpleDataView( new_points, num_vectors, dynamic_index_ptr->dimensions() ); - return dynamic_index_ptr->add_points(src_data, std::span(ids, num_vectors)); + auto added_count = + dynamic_index_ptr->add_points(src_data, std::span(ids, num_vectors)); + if (out_added_count) { + *out_added_count = added_count; + } + return true; }, - out_err, - static_cast(-1) + out_err ); } -extern "C" size_t svs_index_dynamic_delete_points( - svs_index_h index, const size_t* ids, size_t num_ids, svs_error_h out_err +extern "C" bool svs_index_dynamic_delete_points( + svs_index_h index, + const size_t* ids, + size_t num_ids, + size_t* out_deleted_count, + svs_error_h out_err ) { using namespace svs::c_runtime; return wrap_exceptions( @@ -691,10 +781,13 @@ extern "C" size_t svs_index_dynamic_delete_points( INVALID_ARGUMENT_IF( dynamic_index_ptr == nullptr, "Index does not support dynamic updates" ); - return dynamic_index_ptr->delete_points(std::span(ids, num_ids)); + auto deleted_count = dynamic_index_ptr->delete_points(std::span(ids, num_ids)); + if (out_deleted_count) { + *out_deleted_count = deleted_count; + } + return true; }, - out_err, - static_cast(-1) + out_err ); } @@ -869,6 +962,14 @@ extern "C" bool svs_index_get_memory_breakdown( [&]() { EXPECT_ARG_NOT_NULL(index); EXPECT_ARG_NOT_NULL(out_breakdown); + INVALID_ARGUMENT_IF( + out_breakdown->version > svs_get_version(), + "Incompatible svs_memory_breakdown_t version" + ); + INVALID_ARGUMENT_IF( + out_breakdown->struct_size > sizeof(svs_memory_breakdown_t), + "Incompatible svs_memory_breakdown_t struct_size" + ); auto& index_ptr = index->impl; INVALID_ARGUMENT_IF(index_ptr == nullptr, "Invalid index handle"); auto breakdown = index_ptr->get_memory_breakdown(); diff --git a/bindings/c/src/threadpool.hpp b/bindings/c/src/threadpool.hpp index 9c529ec2..1fcd9d67 100644 --- a/bindings/c/src/threadpool.hpp +++ b/bindings/c/src/threadpool.hpp @@ -15,7 +15,7 @@ */ #pragma once -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include "error.hpp" #include "types_support.hpp" @@ -29,45 +29,71 @@ namespace svs::c_runtime { class ThreadPoolBuilder { struct CustomThreadPool { - static svs_threadpool_i validate(svs_threadpool_i impl) { + static void validate(svs_threadpool_i impl) { if (impl == nullptr) { throw std::invalid_argument("Custom threadpool pointer cannot be null."); } - if (impl->ops.size == nullptr || impl->ops.parallel_for == nullptr) { + if (impl->ops == nullptr) { + throw std::invalid_argument("Custom threadpool is not initialized."); + } + if (impl->ops->size == nullptr || impl->ops->parallel_for == nullptr) { throw std::invalid_argument( "Custom threadpool interface has null function pointers." ); } - return impl; } - CustomThreadPool(svs_threadpool_i impl) - : impl{validate(impl)} {} + // Holds a value copy of the user's ops table; only `self` is referenced and + // must outlive the pool. + CustomThreadPool(const svs_threadpool_interface_ops& ops, void* self) + : ops_{ops} + , self_{self} {} - size_t size() const { - assert(impl != nullptr); - return impl->ops.size(impl->self); - } + size_t size() const { return ops_.size(self_); } void parallel_for(std::function f, size_t n) const { - assert(impl != nullptr); - impl->ops.parallel_for( - impl->self, - [](void* svs_param, size_t i) { - auto& func = *static_cast*>(svs_param); - func(i); - }, - &f, - n + std::vector exceptions(n); + auto svs_param = std::make_pair(&f, &exceptions); + svs_error_desc impl_error{ + SVS_ERROR_UNKNOWN, "Unknown error in custom threadpool parallel_for"}; + if (!ops_.parallel_for( + self_, + [](void* svs_param, size_t i) { + auto& [func, exceptions] = *static_cast*, + std::vector*>*>(svs_param); + try { + (*func)(i); + } catch (...) { (*exceptions)[i] = std::current_exception(); } + }, + &svs_param, + n, + &impl_error + )) { + throw std::runtime_error( + "Custom threadpool parallel_for failed: (" + + std::to_string(impl_error.code) + ") " + impl_error.message + ); + } + auto it = std::find_if( + exceptions.begin(), + exceptions.end(), + [](const std::exception_ptr& e) { return static_cast(e); } ); + if (it != exceptions.end()) { + std::rethrow_exception(*it); + } } - svs_threadpool_i impl; + svs_threadpool_interface_ops ops_; + void* self_; }; svs_threadpool_kind kind; size_t num_threads; - svs_threadpool_i user_threadpool; + // Owned copy of the user's threadpool vtable; `user_self_` is referenced only. + svs_threadpool_interface_ops user_ops_{}; + void* user_self_ = nullptr; public: ThreadPoolBuilder() @@ -75,8 +101,7 @@ class ThreadPoolBuilder { ThreadPoolBuilder(svs_threadpool_kind kind, size_t num_threads) : kind(kind) - , num_threads(kind == SVS_THREADPOOL_KIND_SINGLE_THREAD ? 1 : num_threads) - , user_threadpool(nullptr) { + , num_threads(kind == SVS_THREADPOOL_KIND_SINGLE_THREAD ? 1 : num_threads) { if (kind == SVS_THREADPOOL_KIND_CUSTOM) { throw std::invalid_argument( "SVS_THREADPOOL_KIND_CUSTOM cannot be built automatically." @@ -86,19 +111,23 @@ class ThreadPoolBuilder { ThreadPoolBuilder(svs_threadpool_i pool) : kind(SVS_THREADPOOL_KIND_CUSTOM) - , num_threads(0) - , user_threadpool(CustomThreadPool::validate(pool)) {} + , num_threads(0) { + CustomThreadPool::validate(pool); + // Copy the vtable so the caller may free/modify `pool` and its ops table on + // return; `self` is referenced and must outlive the builder and its indices. + user_ops_ = *pool->ops; + user_self_ = pool->self; + } static size_t default_threads_num() { return std::max(size_t{1}, size_t{std::thread::hardware_concurrency()}); } svs_threadpool_kind get_kind() const { return kind; } - svs_threadpool_i get_user_threadpool() const { return user_threadpool; } size_t get_threads_num() const { if (kind == SVS_THREADPOOL_KIND_CUSTOM) { - return user_threadpool->ops.size(user_threadpool->self); + return user_ops_.size(user_self_); } return num_threads; } @@ -128,8 +157,7 @@ class ThreadPoolBuilder { case SVS_THREADPOOL_KIND_SINGLE_THREAD: return ThreadPoolHandle(SequentialThreadPool()); case SVS_THREADPOOL_KIND_CUSTOM: - assert(user_threadpool != nullptr); - return ThreadPoolHandle(CustomThreadPool{this->user_threadpool}); + return ThreadPoolHandle(CustomThreadPool{user_ops_, user_self_}); default: throw std::invalid_argument("Unknown svs_threadpool_kind value."); } diff --git a/bindings/c/src/types_support.hpp b/bindings/c/src/types_support.hpp index 5b261324..591d10a7 100644 --- a/bindings/c/src/types_support.hpp +++ b/bindings/c/src/types_support.hpp @@ -15,7 +15,7 @@ */ #pragma once -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include #include @@ -68,33 +68,37 @@ struct IDFilterInterface { struct IDFilterAdapter : public IDFilterInterface { const svs_id_filter_i c_filter; + float filter_rate_value; // Caching the filter rate value IDFilterAdapter(const svs_id_filter_i filter) - : c_filter(filter) { + : c_filter(filter) + , filter_rate_value(0.0f) { if (c_filter != nullptr) { - const auto rate = c_filter->filter_rate; - if (rate < 0.0f || rate > 1.0f) { + if (c_filter->ops == nullptr) { + throw std::invalid_argument("Custom ID filter is not initialized."); + } + if (c_filter->ops->is_member == nullptr) { throw std::invalid_argument( - "Filter rate must be between 0.0 and 1.0, inclusive." + "Custom ID filter is missing the is_member function." ); } + if (c_filter->ops->filter_rate != nullptr) { + filter_rate_value = c_filter->ops->filter_rate(c_filter->self); + if (filter_rate_value < 0.0f || filter_rate_value > 1.0f) { + throw std::invalid_argument( + "Filter rate must be between 0.0 and 1.0, inclusive." + ); + } + } } } bool is_member(size_t id) const override { - if (c_filter == nullptr || c_filter->ops.is_member == nullptr) { - return true; // If no filter is provided, consider all IDs as valid - } - return c_filter->ops.is_member(c_filter->self, id); + // If no filter is provided, consider all IDs as valid + return c_filter != nullptr ? c_filter->ops->is_member(c_filter->self, id) : true; } - float filter_rate() const override { - // If no filter is provided or the filter rate is NaN, return 0.0 - if (c_filter == nullptr || std::isnan(c_filter->filter_rate)) { - return 0.0f; // If no filter is provided, return 0.0 - } - return c_filter->filter_rate; - } + float filter_rate() const override { return filter_rate_value; } }; } // namespace c_runtime diff --git a/bindings/c/tests/CMakeLists.txt b/bindings/c/tests/CMakeLists.txt index a8ca24db..3b199579 100644 --- a/bindings/c/tests/CMakeLists.txt +++ b/bindings/c/tests/CMakeLists.txt @@ -75,11 +75,6 @@ set_target_properties(${TARGET_NAME} PROPERTIES CXX_EXTENSIONS OFF ) -# Include directories -target_include_directories(${TARGET_NAME} PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/../include -) - # Add test to CTest include(CTest) enable_testing() diff --git a/bindings/c/tests/c_api_algorithm.cpp b/bindings/c/tests/c_api_algorithm.cpp index e72f044a..7490d5a5 100644 --- a/bindings/c/tests/c_api_algorithm.cpp +++ b/bindings/c/tests/c_api_algorithm.cpp @@ -15,7 +15,7 @@ */ // C API -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" // catch2 #include "catch2/catch_test_macros.hpp" diff --git a/bindings/c/tests/c_api_dynamic_index.cpp b/bindings/c/tests/c_api_dynamic_index.cpp index bc1a9565..2d356940 100644 --- a/bindings/c/tests/c_api_dynamic_index.cpp +++ b/bindings/c/tests/c_api_dynamic_index.cpp @@ -15,7 +15,7 @@ */ // C API -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" // catch2 #include "catch2/catch_test_macros.hpp" @@ -121,9 +121,10 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { new_ids[i] = NUM_VECTORS + i; } - size_t added_count = svs_index_dynamic_add_points( - index, new_data.data(), new_ids.data(), num_new_points, error - ); + size_t added_count = 0; + CATCH_REQUIRE(svs_index_dynamic_add_points( + index, new_data.data(), new_ids.data(), num_new_points, &added_count, error + )); CATCH_REQUIRE(added_count == num_new_points); CATCH_REQUIRE(svs_error_ok(error)); @@ -148,8 +149,10 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { size_t ids_to_delete[] = {0, 5, 10}; size_t num_to_delete = 3; - size_t deleted_count = - svs_index_dynamic_delete_points(index, ids_to_delete, num_to_delete, error); + size_t deleted_count = 0; + CATCH_REQUIRE(svs_index_dynamic_delete_points( + index, ids_to_delete, num_to_delete, &deleted_count, error + )); CATCH_REQUIRE(deleted_count == num_to_delete); CATCH_REQUIRE(svs_error_ok(error)); @@ -179,15 +182,17 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { // Delete some points size_t ids_to_delete[] = {0, 1}; - svs_index_dynamic_delete_points(index, ids_to_delete, 2, error); + svs_index_dynamic_delete_points(index, ids_to_delete, 2, nullptr, error); CATCH_REQUIRE(svs_error_ok(error)); // Add new points with the deleted IDs std::vector new_data; generate_test_data(new_data, 2, DIMENSION); - size_t added_count = - svs_index_dynamic_add_points(index, new_data.data(), ids_to_delete, 2, error); + size_t added_count = 0; + CATCH_REQUIRE(svs_index_dynamic_add_points( + index, new_data.data(), ids_to_delete, 2, &added_count, error + )); CATCH_REQUIRE(added_count == 2); CATCH_REQUIRE(svs_error_ok(error)); @@ -214,10 +219,12 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { std::vector new_ids = {NUM_VECTORS, NUM_VECTORS + 1}; generate_test_data(new_data, 2, DIMENSION); - svs_index_dynamic_add_points(index, new_data.data(), new_ids.data(), 2, error); + svs_index_dynamic_add_points( + index, new_data.data(), new_ids.data(), 2, nullptr, error + ); size_t ids_to_delete[] = {0, 1}; - svs_index_dynamic_delete_points(index, ids_to_delete, 2, error); + svs_index_dynamic_delete_points(index, ids_to_delete, 2, nullptr, error); // Consolidate the index bool success = svs_index_dynamic_consolidate(index, error); @@ -240,7 +247,7 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { // Delete some points size_t ids_to_delete[] = {0, 1, 2}; - svs_index_dynamic_delete_points(index, ids_to_delete, 3, error); + svs_index_dynamic_delete_points(index, ids_to_delete, 3, nullptr, error); CATCH_REQUIRE(svs_error_ok(error)); // Consolidate the index @@ -266,30 +273,33 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { std::vector new_data; std::vector new_ids = {NUM_VECTORS, NUM_VECTORS + 1, NUM_VECTORS + 2}; generate_test_data(new_data, 3, DIMENSION); - svs_index_dynamic_add_points(index, new_data.data(), new_ids.data(), 3, error); + svs_index_dynamic_add_points( + index, new_data.data(), new_ids.data(), 3, nullptr, error + ); // Delete some points size_t ids_to_delete[] = {0, 1}; - svs_index_dynamic_delete_points(index, ids_to_delete, 2, error); + svs_index_dynamic_delete_points(index, ids_to_delete, 2, nullptr, error); // Perform search std::vector queries; generate_test_data(queries, 2, DIMENSION); - svs_search_results_t results = - svs_index_search_topK(index, queries.data(), 2, K, nullptr, nullptr, error); - CATCH_REQUIRE(results != nullptr); + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, queries.data(), 2, K, &results, nullptr, nullptr, error + )); CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(results->num_queries == 2); + CATCH_REQUIRE(results.num_queries == 2); // Verify deleted IDs don't appear in results - for (size_t i = 0; i < results->num_queries * K; ++i) { - size_t result_id = results->indices[i]; + for (size_t i = 0; i < results.num_queries * K; ++i) { + size_t result_id = results.indices[i]; CATCH_REQUIRE(result_id != 0); CATCH_REQUIRE(result_id != 1); } - svs_search_results_free(results); + svs_search_results_free(&results); svs_index_free(index); } @@ -301,15 +311,19 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { // Try to delete non-existing ID size_t non_existing_id = NUM_VECTORS + 1000; - size_t deleted_count = - svs_index_dynamic_delete_points(index, &non_existing_id, 1, error); + size_t deleted_count = 0; + CATCH_REQUIRE(svs_index_dynamic_delete_points( + index, &non_existing_id, 1, &deleted_count, error + )); // Should return 0 for non-existing ID and no error CATCH_REQUIRE(svs_error_ok(error)); CATCH_REQUIRE(deleted_count == 0); // Try to delete mix of existing and non-existing IDs size_t ids_to_delete[] = {0, non_existing_id}; - deleted_count = svs_index_dynamic_delete_points(index, ids_to_delete, 2, error); + CATCH_REQUIRE( + svs_index_dynamic_delete_points(index, ids_to_delete, 2, &deleted_count, error) + ); // Should return 1 for the existing ID and no error CATCH_REQUIRE(svs_error_ok(error)); CATCH_REQUIRE(deleted_count == 1); @@ -343,19 +357,19 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { std::vector queries; generate_test_data(queries, 2, DIMENSION); - svs_search_results_t results = svs_index_search_topK( - loaded_index, queries.data(), 2, K, nullptr, nullptr, error - ); - CATCH_REQUIRE(results != nullptr); + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + loaded_index, queries.data(), 2, K, &results, nullptr, nullptr, error + )); CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(results->num_queries == 2); + CATCH_REQUIRE(results.num_queries == 2); - svs_search_results_free(results); + svs_search_results_free(&results); svs_index_free(loaded_index); svs_index_free(index); } - CATCH_SECTION("Memory Accounting Functions") { + CATCH_SECTION("Dynamic Index Memory Accounting") { // Build dynamic index svs_index_h index = svs_index_build_dynamic( builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error @@ -371,7 +385,7 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { CATCH_REQUIRE(memory_usage > 0); // Test get_memory_breakdown - svs_memory_breakdown_t breakdown; + svs_memory_breakdown_t breakdown = SVS_INIT_MEMORY_BREAKDOWN(); success = svs_index_get_memory_breakdown(index, &breakdown, error); CATCH_REQUIRE(success); CATCH_REQUIRE(svs_error_ok(error)); @@ -403,6 +417,10 @@ bool filter_below_threshold(void* self, size_t id) { return id < *static_cast(self); } +// Estimated selectivity callbacks (conservative estimates below true rates). +float filter_rate_odd(void* /*self*/) { return 0.4f; } +float filter_rate_low(void* /*self*/) { return 0.05f; } + } // namespace CATCH_TEST_CASE( @@ -451,37 +469,43 @@ CATCH_TEST_CASE( // ~50% of the IDs pass the filter. Provide a conservative filter_rate estimate // (below the true selectivity) so the search is not short-circuited. - svs_id_filter_interface id_filter{}; - id_filter.ops.is_member = &filter_is_odd; - id_filter.self = nullptr; - id_filter.filter_rate = 0.4f; - - svs_search_results_t results = svs_index_search_topK( - index, queries.data(), NUM_QUERIES, K, search_params, &id_filter, error - ); - CATCH_REQUIRE(results != nullptr); + svs_id_filter_interface_ops odd_ops = + SVS_INIT_ID_FILTER_OPS(filter_is_odd, filter_rate_odd); + svs_id_filter_interface id_filter = SVS_MAKE_INTERFACE(nullptr, odd_ops); + + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, + queries.data(), + NUM_QUERIES, + K, + &results, + search_params, + &id_filter, + error + )); CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + CATCH_REQUIRE(results.num_queries == NUM_QUERIES); for (size_t q = 0; q < NUM_QUERIES; ++q) { - CATCH_REQUIRE(results->results_per_query[q] == K); + CATCH_REQUIRE(results.offsets[q + 1] - results.offsets[q] == K); for (size_t j = 0; j < K; ++j) { - size_t idx = results->indices[q * K + j]; + size_t idx = results.indices[q * K + j]; // Every neighbor must be a valid, in-range odd ID. CATCH_REQUIRE(idx != static_cast(-1)); CATCH_REQUIRE(idx < NUM_VECTORS); CATCH_REQUIRE((idx % 2) == 1); // Distances must be finite and non-decreasing. - CATCH_REQUIRE(std::isfinite(results->distances[q * K + j])); + CATCH_REQUIRE(std::isfinite(results.distances[q * K + j])); if (j > 0) { CATCH_REQUIRE( - results->distances[q * K + j] >= results->distances[q * K + j - 1] + results.distances[q * K + j] >= results.distances[q * K + j - 1] ); } } } - svs_search_results_free(results); + svs_search_results_free(&results); svs_search_params_free(search_params); svs_index_free(index); svs_index_builder_free(builder); @@ -518,28 +542,34 @@ CATCH_TEST_CASE( // filter_rate (below the true selectivity) so the search keeps iterating instead // of giving up early. size_t max_valid_id = NUM_VECTORS / 10; - svs_id_filter_interface id_filter{}; - id_filter.ops.is_member = &filter_below_threshold; - id_filter.self = &max_valid_id; - id_filter.filter_rate = 0.05f; - - svs_search_results_t results = svs_index_search_topK( - index, queries.data(), NUM_QUERIES, K, search_params, &id_filter, error - ); - CATCH_REQUIRE(results != nullptr); + svs_id_filter_interface_ops low_ops = + SVS_INIT_ID_FILTER_OPS(filter_below_threshold, filter_rate_low); + svs_id_filter_interface id_filter = SVS_MAKE_INTERFACE(&max_valid_id, low_ops); + + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, + queries.data(), + NUM_QUERIES, + K, + &results, + search_params, + &id_filter, + error + )); CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + CATCH_REQUIRE(results.num_queries == NUM_QUERIES); size_t total_found = 0; for (size_t q = 0; q < NUM_QUERIES; ++q) { - CATCH_REQUIRE(results->results_per_query[q] == K); + CATCH_REQUIRE(results.offsets[q + 1] - results.offsets[q] == K); for (size_t j = 0; j < K; ++j) { - size_t idx = results->indices[q * K + j]; + size_t idx = results.indices[q * K + j]; // Padding (unspecified) entries are allowed for a restrictive filter, but // any specified neighbor must pass the filter predicate. if (idx != static_cast(-1)) { CATCH_REQUIRE(idx < max_valid_id); - CATCH_REQUIRE(std::isfinite(results->distances[q * K + j])); + CATCH_REQUIRE(std::isfinite(results.distances[q * K + j])); ++total_found; } } @@ -548,7 +578,7 @@ CATCH_TEST_CASE( // return at least some valid neighbors. CATCH_REQUIRE(total_found > 0); - svs_search_results_free(results); + svs_search_results_free(&results); svs_search_params_free(search_params); svs_index_free(index); svs_index_builder_free(builder); diff --git a/bindings/c/tests/c_api_error.cpp b/bindings/c/tests/c_api_error.cpp index e62c8888..1a2ff8d6 100644 --- a/bindings/c/tests/c_api_error.cpp +++ b/bindings/c/tests/c_api_error.cpp @@ -15,7 +15,7 @@ */ // C API -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" // catch2 #include "catch2/catch_test_macros.hpp" diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp index d9ab2446..0c91e402 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -15,7 +15,7 @@ */ // C API -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" // catch2 #include "catch2/catch_test_macros.hpp" @@ -71,35 +71,35 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") CATCH_REQUIRE(svs_error_ok(error)); // Perform search - svs_search_results_t results = svs_index_search_topK( - index, queries.data(), NUM_QUERIES, K, search_params, nullptr, error - ); - CATCH_REQUIRE(results != nullptr); + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, queries.data(), NUM_QUERIES, K, &results, search_params, nullptr, error + )); CATCH_REQUIRE(svs_error_ok(error)); // Validate results structure - CATCH_REQUIRE(results->num_queries == NUM_QUERIES); - CATCH_REQUIRE(results->results_per_query != nullptr); - CATCH_REQUIRE(results->indices != nullptr); - CATCH_REQUIRE(results->distances != nullptr); + CATCH_REQUIRE(results.num_queries == NUM_QUERIES); + CATCH_REQUIRE(results.offsets != nullptr); + CATCH_REQUIRE(results.indices != nullptr); + CATCH_REQUIRE(results.distances != nullptr); // Check that each query returned K results for (size_t i = 0; i < NUM_QUERIES; ++i) { - CATCH_REQUIRE(results->results_per_query[i] == K); + CATCH_REQUIRE(results.offsets[i + 1] - results.offsets[i] == K); } // Check that indices are within valid range for (size_t i = 0; i < NUM_QUERIES * K; ++i) { - CATCH_REQUIRE(results->indices[i] < NUM_VECTORS); + CATCH_REQUIRE(results.indices[i] < NUM_VECTORS); } // Check that distances are non-negative for (size_t i = 0; i < NUM_QUERIES * K; ++i) { - CATCH_REQUIRE(results->distances[i] >= 0.0f); + CATCH_REQUIRE(results.distances[i] >= 0.0f); } // Cleanup - svs_search_results_free(results); + svs_search_results_free(&results); svs_search_params_free(search_params); svs_index_free(index); svs_index_builder_free(builder); @@ -127,14 +127,14 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") CATCH_REQUIRE(index != nullptr); // Search without explicit search parameters (uses defaults) - svs_search_results_t results = svs_index_search_topK( - index, queries.data(), NUM_QUERIES, K, nullptr, nullptr, error - ); - CATCH_REQUIRE(results != nullptr); + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, queries.data(), NUM_QUERIES, K, &results, nullptr, nullptr, error + )); CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + CATCH_REQUIRE(results.num_queries == NUM_QUERIES); - svs_search_results_free(results); + svs_search_results_free(&results); svs_index_free(index); svs_index_builder_free(builder); svs_algorithm_free(algorithm); @@ -167,13 +167,13 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); CATCH_REQUIRE(index != nullptr); - svs_search_results_t results = svs_index_search_topK( - index, queries.data(), NUM_QUERIES, K, nullptr, nullptr, error - ); - CATCH_REQUIRE(results != nullptr); - CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, queries.data(), NUM_QUERIES, K, &results, nullptr, nullptr, error + )); + CATCH_REQUIRE(results.num_queries == NUM_QUERIES); - svs_search_results_free(results); + svs_search_results_free(&results); svs_index_free(index); svs_storage_free(storage); svs_index_builder_free(builder); @@ -212,18 +212,18 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") CATCH_REQUIRE(index != nullptr); CATCH_REQUIRE(svs_error_ok(error)); - svs_search_results_t results = svs_index_search_topK( - index, queries.data(), NUM_QUERIES, K, nullptr, nullptr, error - ); - CATCH_REQUIRE(results != nullptr); + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, queries.data(), NUM_QUERIES, K, &results, nullptr, nullptr, error + )); CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + CATCH_REQUIRE(results.num_queries == NUM_QUERIES); for (size_t i = 0; i < NUM_QUERIES; ++i) { - CATCH_REQUIRE(results->results_per_query[i] == K); + CATCH_REQUIRE(results.offsets[i + 1] - results.offsets[i] == K); } - svs_search_results_free(results); + svs_search_results_free(&results); svs_index_free(index); svs_index_builder_free(builder); svs_algorithm_free(algorithm); @@ -264,8 +264,10 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") ); // Set custom threadpool - struct svs_threadpool_interface custom_pool = { - {sequential_tp_size, sequential_tp_parallel_for}, nullptr}; + struct svs_threadpool_interface_ops custom_ops = + SVS_INIT_THREADPOOL_OPS(sequential_tp_size, sequential_tp_parallel_for); + struct svs_threadpool_interface custom_pool = + SVS_MAKE_INTERFACE(nullptr, custom_ops); bool success = svs_index_builder_set_threadpool_custom(builder, &custom_pool, error); CATCH_REQUIRE(success); @@ -276,13 +278,13 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") CATCH_REQUIRE(svs_error_ok(error)); // Verify index works with custom threadpool - svs_search_results_t results = svs_index_search_topK( - index, queries.data(), NUM_QUERIES, K, nullptr, nullptr, error - ); - CATCH_REQUIRE(results != nullptr); + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, queries.data(), NUM_QUERIES, K, &results, nullptr, nullptr, error + )); CATCH_REQUIRE(svs_error_ok(error)); - svs_search_results_free(results); + svs_search_results_free(&results); svs_index_free(index); svs_index_builder_free(builder); svs_algorithm_free(algorithm); @@ -306,21 +308,22 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") CATCH_REQUIRE(index != nullptr); // Intentionally exercise the deprecated API to ensure the wrapper still delegates - // correctly to svs_index_search_topK. + // correctly to svs_index_search_topk. + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif - svs_search_results_t results = - svs_index_search(index, queries.data(), NUM_QUERIES, K, nullptr, error); + CATCH_REQUIRE(svs_index_search( + index, queries.data(), NUM_QUERIES, K, &results, nullptr, error + )); #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop #endif - CATCH_REQUIRE(results != nullptr); CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + CATCH_REQUIRE(results.num_queries == NUM_QUERIES); - svs_search_results_free(results); + svs_search_results_free(&results); svs_index_free(index); svs_index_builder_free(builder); svs_algorithm_free(algorithm); @@ -424,17 +427,17 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") size_t k_values[] = {1, 5, 10, 20}; for (size_t i = 0; i < sizeof(k_values) / sizeof(k_values[0]); ++i) { size_t k = k_values[i]; - svs_search_results_t results = svs_index_search_topK( - index, queries.data(), NUM_QUERIES, k, nullptr, nullptr, error - ); - CATCH_REQUIRE(results != nullptr); - CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, queries.data(), NUM_QUERIES, k, &results, nullptr, nullptr, error + )); + CATCH_REQUIRE(results.num_queries == NUM_QUERIES); for (size_t q = 0; q < NUM_QUERIES; ++q) { - CATCH_REQUIRE(results->results_per_query[q] == k); + CATCH_REQUIRE(results.offsets[q + 1] - results.offsets[q] == k); } - svs_search_results_free(results); + svs_search_results_free(&results); } svs_index_free(index); @@ -461,13 +464,13 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") // Perform multiple searches for (size_t i = 0; i < 3; ++i) { - svs_search_results_t results = svs_index_search_topK( - index, queries.data(), NUM_QUERIES, K, nullptr, nullptr, error - ); - CATCH_REQUIRE(results != nullptr); + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, queries.data(), NUM_QUERIES, K, &results, nullptr, nullptr, error + )); CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(results->num_queries == NUM_QUERIES); - svs_search_results_free(results); + CATCH_REQUIRE(results.num_queries == NUM_QUERIES); + svs_search_results_free(&results); } svs_index_free(index); @@ -521,15 +524,15 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") std::vector queries; generate_test_data(queries, 2, DIMENSION); - svs_search_results_t results = svs_index_search_topK( - loaded_index, queries.data(), 2, K, nullptr, nullptr, error - ); - CATCH_REQUIRE(results != nullptr); + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + loaded_index, queries.data(), 2, K, &results, nullptr, nullptr, error + )); CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(results->num_queries == 2); + CATCH_REQUIRE(results.num_queries == 2); // Cleanup - svs_search_results_free(results); + svs_search_results_free(&results); svs_index_free(loaded_index); svs_index_free(index); svs_index_builder_free(builder); @@ -636,8 +639,10 @@ CATCH_TEST_CASE("C API Threadpool Management", "[c_api][index][threadpool]") { ); // Set custom threadpool - struct svs_threadpool_interface custom_pool = { - {sequential_tp_size, sequential_tp_parallel_for}, nullptr}; + struct svs_threadpool_interface_ops custom_ops = + SVS_INIT_THREADPOOL_OPS(sequential_tp_size, sequential_tp_parallel_for); + struct svs_threadpool_interface custom_pool = + SVS_MAKE_INTERFACE(nullptr, custom_ops); bool success = svs_index_builder_set_threadpool_custom(builder, &custom_pool, error); CATCH_REQUIRE(success); @@ -765,6 +770,14 @@ CATCH_TEST_CASE("C API Threadpool Management", "[c_api][index][threadpool]") { svs_algorithm_free(algorithm); svs_error_free(error); } +} + +CATCH_TEST_CASE("C API Index Memory Management", "[c_api][index][memory]") { + const size_t NUM_VECTORS = 100; + const size_t DIMENSION = 32; + + std::vector data; + generate_test_data(data, NUM_VECTORS, DIMENSION); CATCH_SECTION("Memory Accounting Functions") { svs_error_h error = svs_error_create(); @@ -799,7 +812,7 @@ CATCH_TEST_CASE("C API Threadpool Management", "[c_api][index][threadpool]") { CATCH_REQUIRE(memory_usage > 0); // Test get_memory_breakdown - svs_memory_breakdown_t breakdown; + svs_memory_breakdown_t breakdown = SVS_INIT_MEMORY_BREAKDOWN(); success = svs_index_get_memory_breakdown(index, &breakdown, error); CATCH_REQUIRE(success); CATCH_REQUIRE(svs_error_ok(error)); @@ -852,6 +865,10 @@ bool filter_below_threshold(void* self, size_t id) { return id < *static_cast(self); } +// Estimated selectivity callbacks (conservative estimates below true rates). +float filter_rate_odd(void* /*self*/) { return 0.4f; } +float filter_rate_low(void* /*self*/) { return 0.05f; } + } // namespace CATCH_TEST_CASE("C API Filtered Search topK", "[c_api][index][search][filter]") { @@ -891,37 +908,43 @@ CATCH_TEST_CASE("C API Filtered Search topK", "[c_api][index][search][filter]") // ~50% of the IDs pass the filter. Provide a conservative filter_rate estimate // (below the true selectivity) so the search is not short-circuited. - svs_id_filter_interface id_filter{}; - id_filter.ops.is_member = &filter_is_odd; - id_filter.self = nullptr; - id_filter.filter_rate = 0.4f; - - svs_search_results_t results = svs_index_search_topK( - index, queries.data(), NUM_QUERIES, K, search_params, &id_filter, error - ); - CATCH_REQUIRE(results != nullptr); + svs_id_filter_interface_ops odd_ops = + SVS_INIT_ID_FILTER_OPS(filter_is_odd, filter_rate_odd); + svs_id_filter_interface id_filter = SVS_MAKE_INTERFACE(nullptr, odd_ops); + + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, + queries.data(), + NUM_QUERIES, + K, + &results, + search_params, + &id_filter, + error + )); CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + CATCH_REQUIRE(results.num_queries == NUM_QUERIES); for (size_t q = 0; q < NUM_QUERIES; ++q) { - CATCH_REQUIRE(results->results_per_query[q] == K); + CATCH_REQUIRE(results.offsets[q + 1] - results.offsets[q] == K); for (size_t j = 0; j < K; ++j) { - size_t idx = results->indices[q * K + j]; + size_t idx = results.indices[q * K + j]; // Every neighbor must be a valid, in-range odd ID. CATCH_REQUIRE(idx != static_cast(-1)); CATCH_REQUIRE(idx < NUM_VECTORS); CATCH_REQUIRE((idx % 2) == 1); // Distances must be finite and non-decreasing. - CATCH_REQUIRE(std::isfinite(results->distances[q * K + j])); + CATCH_REQUIRE(std::isfinite(results.distances[q * K + j])); if (j > 0) { CATCH_REQUIRE( - results->distances[q * K + j] >= results->distances[q * K + j - 1] + results.distances[q * K + j] >= results.distances[q * K + j - 1] ); } } } - svs_search_results_free(results); + svs_search_results_free(&results); svs_search_params_free(search_params); svs_index_free(index); svs_index_builder_free(builder); @@ -956,28 +979,34 @@ CATCH_TEST_CASE("C API Filtered Search topK", "[c_api][index][search][filter]") // filter_rate (below the true selectivity) so the search keeps iterating instead // of giving up early. size_t max_valid_id = NUM_VECTORS / 10; - svs_id_filter_interface id_filter{}; - id_filter.ops.is_member = &filter_below_threshold; - id_filter.self = &max_valid_id; - id_filter.filter_rate = 0.05f; - - svs_search_results_t results = svs_index_search_topK( - index, queries.data(), NUM_QUERIES, K, search_params, &id_filter, error - ); - CATCH_REQUIRE(results != nullptr); + svs_id_filter_interface_ops low_ops = + SVS_INIT_ID_FILTER_OPS(filter_below_threshold, filter_rate_low); + svs_id_filter_interface id_filter = SVS_MAKE_INTERFACE(&max_valid_id, low_ops); + + svs_search_results_t results = SVS_INIT_SEARCH_RESULTS(); + CATCH_REQUIRE(svs_index_search_topk( + index, + queries.data(), + NUM_QUERIES, + K, + &results, + search_params, + &id_filter, + error + )); CATCH_REQUIRE(svs_error_ok(error)); - CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + CATCH_REQUIRE(results.num_queries == NUM_QUERIES); size_t total_found = 0; for (size_t q = 0; q < NUM_QUERIES; ++q) { - CATCH_REQUIRE(results->results_per_query[q] == K); + CATCH_REQUIRE(results.offsets[q + 1] - results.offsets[q] == K); for (size_t j = 0; j < K; ++j) { - size_t idx = results->indices[q * K + j]; + size_t idx = results.indices[q * K + j]; // Padding (unspecified) entries are allowed for a restrictive filter, but // any specified neighbor must pass the filter predicate. if (idx != static_cast(-1)) { CATCH_REQUIRE(idx < max_valid_id); - CATCH_REQUIRE(std::isfinite(results->distances[q * K + j])); + CATCH_REQUIRE(std::isfinite(results.distances[q * K + j])); ++total_found; } } @@ -986,7 +1015,7 @@ CATCH_TEST_CASE("C API Filtered Search topK", "[c_api][index][search][filter]") // return at least some valid neighbors. CATCH_REQUIRE(total_found > 0); - svs_search_results_free(results); + svs_search_results_free(&results); svs_search_params_free(search_params); svs_index_free(index); svs_index_builder_free(builder); diff --git a/bindings/c/tests/c_api_index_builder.cpp b/bindings/c/tests/c_api_index_builder.cpp index 3d77ae67..435026e5 100644 --- a/bindings/c/tests/c_api_index_builder.cpp +++ b/bindings/c/tests/c_api_index_builder.cpp @@ -15,7 +15,7 @@ */ // C API -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" // catch2 #include "catch2/catch_test_macros.hpp" @@ -142,8 +142,10 @@ CATCH_TEST_CASE("C API Index Builder", "[c_api][index_builder]") { svs_index_builder_create(SVS_DISTANCE_METRIC_EUCLIDEAN, 128, algorithm, error); CATCH_REQUIRE(builder != nullptr); - struct svs_threadpool_interface custom_pool = { - {sequential_tp_size, sequential_tp_parallel_for}, nullptr}; + struct svs_threadpool_interface_ops custom_ops = + SVS_INIT_THREADPOOL_OPS(sequential_tp_size, sequential_tp_parallel_for); + struct svs_threadpool_interface custom_pool = + SVS_MAKE_INTERFACE(nullptr, custom_ops); bool success = svs_index_builder_set_threadpool_custom(builder, &custom_pool, error); diff --git a/bindings/c/tests/c_api_search_params.cpp b/bindings/c/tests/c_api_search_params.cpp index a94111fd..1d3daa17 100644 --- a/bindings/c/tests/c_api_search_params.cpp +++ b/bindings/c/tests/c_api_search_params.cpp @@ -15,7 +15,7 @@ */ // C API -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" // catch2 #include "catch2/catch_test_macros.hpp" diff --git a/bindings/c/tests/c_api_storage.cpp b/bindings/c/tests/c_api_storage.cpp index da7aae6c..b965aab4 100644 --- a/bindings/c/tests/c_api_storage.cpp +++ b/bindings/c/tests/c_api_storage.cpp @@ -15,7 +15,7 @@ */ // C API -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" // catch2 #include "catch2/catch_test_macros.hpp" diff --git a/bindings/c/tests/c_api_test_utils.h b/bindings/c/tests/c_api_test_utils.h index 3920d039..003d6907 100644 --- a/bindings/c/tests/c_api_test_utils.h +++ b/bindings/c/tests/c_api_test_utils.h @@ -15,7 +15,7 @@ #pragma once // C API -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" // Standard library #include @@ -90,12 +90,17 @@ generate_test_data(std::vector& data, size_t num_vectors, size_t dimensio // Sequential threadpool for testing inline size_t sequential_tp_size(void* /*self*/) { return 1; } -inline void sequential_tp_parallel_for( - void* /*self*/, void (*func)(void*, size_t), void* svs_param, size_t n +inline bool sequential_tp_parallel_for( + void* /*self*/, + void (*func)(void*, size_t), + void* svs_param, + size_t n, + svs_error_h /*out_err*/ ) { for (size_t i = 0; i < n; ++i) { func(svs_param, i); } + return true; } // Helper to calculate Euclidean distance diff --git a/bindings/c/tests/consumer/main.c b/bindings/c/tests/consumer/main.c index 6f6cc0b3..07b41ebe 100644 --- a/bindings/c/tests/consumer/main.c +++ b/bindings/c/tests/consumer/main.c @@ -24,7 +24,7 @@ * capability a downstream integration has to branch on today. */ -#include "svs/c_api/svs_c.h" +#include "svs/c/svs_c.h" #include #include