diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt
index 7a7c50ec..440c4a36 100644
--- a/bindings/c/CMakeLists.txt
+++ b/bindings/c/CMakeLists.txt
@@ -37,6 +37,7 @@ set(SVS_C_API_SOURCES
src/filtered_search.hpp
src/index.hpp
src/index_builder.hpp
+ src/leanvec_training_data.hpp
src/storage.hpp
src/threadpool.hpp
src/types_support.hpp
diff --git a/bindings/c/docs/C_API_Design.md b/bindings/c/docs/C_API_Design.md
index d40391db..523c973a 100644
--- a/bindings/c/docs/C_API_Design.md
+++ b/bindings/c/docs/C_API_Design.md
@@ -289,7 +289,7 @@ Defines how vectors are stored in memory, supporting various compression schemes
| **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 |
+| **LeanVec** | Dimensions
Primary: data type
Secondary: data type
Optional pre-trained matrices | LeanVec dimensionality reduced storage; matrices computed via PCA at build time, or trained up front (in-distribution or out-of-distribution) |
**Example:**
```c
@@ -301,11 +301,24 @@ svs_storage_h storage = svs_storage_create_lvq(
SVS_DATA_TYPE_UINT8, SVS_DATA_TYPE_UINT4, err
);
-// LeanVec with 128 dimensions
+// LeanVec with 128 dimensions; reduction matrices are computed from the
+// dataset (PCA) at build time
svs_storage_h storage = svs_storage_create_leanvec(
128, SVS_DATA_TYPE_FLOAT16, SVS_DATA_TYPE_INT8, err
);
+// LeanVec from matrices trained up front. Passing training queries learns
+// out-of-distribution (OOD) matrices; passing none learns PCA matrices.
+// `leanvec_dims` comes from the training data.
+svs_leanvec_training_data_h td = svs_leanvec_training_data_build(
+ dim, num_vectors, x, num_queries, x_q, 128, err
+);
+svs_storage_h storage = svs_storage_create_leanvec_trained(
+ td, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, err
+);
+// The storage holds its own reference to the matrices.
+svs_leanvec_training_data_free(td);
+
// Scalar quantization
svs_storage_h storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, err);
```
diff --git a/bindings/c/include/svs/c/svs_c.h b/bindings/c/include/svs/c/svs_c.h
index 8656622e..dad9378e 100644
--- a/bindings/c/include/svs/c/svs_c.h
+++ b/bindings/c/include/svs/c/svs_c.h
@@ -416,6 +416,7 @@ 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;
+typedef struct svs_leanvec_training_data* svs_leanvec_training_data_h;
// Fully defined types; "_t" suffix indicates a fully defined struct
typedef enum svs_error_code svs_error_code_t;
@@ -643,6 +644,47 @@ SVS_API bool svs_storage_get_kind(
/// @param storage The storage handle to free
SVS_API void svs_storage_free(svs_storage_h storage);
+/// @brief Train LeanVec dimensionality-reduction matrices from a data sample
+/// @param dim The dimensionality of the data (and training queries)
+/// @param num_vectors The number of data vectors in x
+/// @param x Pointer to the data vectors [num_vectors x dim] (float array)
+/// @param num_queries The number of training queries in x_q (0 for in-distribution)
+/// @param x_q Pointer to the training queries [num_queries x dim], or NULL. When
+/// provided, matrices are trained out-of-distribution (OOD) using these queries;
+/// when num_queries is 0 or x_q is NULL, in-distribution (PCA) matrices are computed.
+/// @param leanvec_dims The reduced number of LeanVec dimensions
+/// @param out_err An optional error handle to capture errors
+/// @return A handle to the trained LeanVec matrices
+SVS_API svs_leanvec_training_data_h svs_leanvec_training_data_build(
+ size_t dim,
+ size_t num_vectors,
+ const float* x,
+ size_t num_queries,
+ const float* x_q /*=NULL*/,
+ size_t leanvec_dims,
+ svs_error_h out_err /*=NULL*/
+);
+
+/// @brief Free the LeanVec training data handle
+/// @param training_data The training data handle to free
+SVS_API void svs_leanvec_training_data_free(svs_leanvec_training_data_h training_data);
+
+/// @brief Create a LeanVec storage configuration from pre-trained matrices
+/// @param training_data The trained LeanVec matrices to use when reducing the data,
+/// instead of computing PCA matrices at build time. The number of LeanVec dimensions
+/// is taken from the training data. The storage retains a reference to the trained
+/// matrices, so the training data handle may be freed once this call returns.
+/// @param primary The data type of the primary quantization
+/// @param secondary The data type of the secondary quantization
+/// @param out_err An optional error handle to capture errors
+/// @return A handle to the created LeanVec storage
+SVS_API svs_storage_h svs_storage_create_leanvec_trained(
+ svs_leanvec_training_data_h training_data,
+ svs_data_type_t primary,
+ svs_data_type_t secondary,
+ svs_error_h out_err /*=NULL*/
+);
+
/// @brief Create an index builder configuration
/// @param metric The distance metric to use
/// @param dimension The dimensionality of the vectors
diff --git a/bindings/c/src/data_builder/leanvec.hpp b/bindings/c/src/data_builder/leanvec.hpp
index e74fbd94..ecc6f34d 100644
--- a/bindings/c/src/data_builder/leanvec.hpp
+++ b/bindings/c/src/data_builder/leanvec.hpp
@@ -40,17 +40,25 @@
#endif // SVS_LEANVEC_HEADER
#include
+#include
#include
+#include
namespace svs {
template >
class LeanVecDataBuilder {
size_t leanvec_dims_;
+ // Pre-trained (e.g. out-of-distribution) matrices; empty for PCA reduction.
+ std::optional> matrices_;
public:
- LeanVecDataBuilder(size_t leanvec_dims)
- : leanvec_dims_(leanvec_dims) {}
+ LeanVecDataBuilder(
+ size_t leanvec_dims,
+ std::optional> matrices = std::nullopt
+ )
+ : leanvec_dims_(leanvec_dims)
+ , matrices_(std::move(matrices)) {}
using data_type = svs::leanvec::LeanDataset<
svs::leanvec::UsingLVQ,
@@ -67,7 +75,7 @@ class LeanVecDataBuilder {
const allocator_type& allocator = {}
) {
return data_type::reduce(
- view, std::nullopt, pool, 0, svs::lib::MaybeStatic{leanvec_dims_}, allocator
+ view, matrices_, pool, 0, svs::lib::MaybeStatic{leanvec_dims_}, allocator
);
}
@@ -95,7 +103,12 @@ struct lib::
static To convert(From from) {
auto leanvec = static_cast(from);
- return To{leanvec->lenavec_dims};
+ // `leanvec_dims` is taken from the training data at storage construction,
+ // so it is authoritative in both cases.
+ if (leanvec->training_data) {
+ return To{leanvec->leanvec_dims, leanvec->training_data->matrices()};
+ }
+ return To{leanvec->leanvec_dims};
}
};
diff --git a/bindings/c/src/leanvec_training_data.hpp b/bindings/c/src/leanvec_training_data.hpp
new file mode 100644
index 00000000..cb3f9cfd
--- /dev/null
+++ b/bindings/c/src/leanvec_training_data.hpp
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC
+
+#include "svs/c/svs_c.h"
+
+#include
+#include
+#include
+#include
+
+#ifdef SVS_LEANVEC_HEADER
+#include SVS_LEANVEC_HEADER
+#else
+#include
+#endif
+
+#include
+
+namespace svs::c_runtime {
+
+// Holds LeanVec dimensionality-reduction matrices trained from a data sample.
+// Mirrors the runtime bindings' LeanVecTrainingData: matrices are computed once
+// and later handed to LeanVecDataBuilder to reduce the dataset. When training
+// queries are supplied the matrices are learned out-of-distribution (OOD),
+// otherwise in-distribution (PCA) matrices are used for both data and queries.
+class LeanVecTrainingData {
+ public:
+ using matrices_type = svs::leanvec::LeanVecMatrices;
+
+ LeanVecTrainingData(
+ svs::data::ConstSimpleDataView data,
+ svs::data::ConstSimpleDataView queries,
+ size_t leanvec_dims,
+ svs::threads::ThreadPoolHandle& pool
+ )
+ : leanvec_dims_{leanvec_dims}
+ , matrices_{
+ queries.size() == 0 ? compute_pca(data, leanvec_dims, pool)
+ : compute_ood(data, queries, leanvec_dims, pool)} {}
+
+ size_t leanvec_dims() const { return leanvec_dims_; }
+ const matrices_type& matrices() const { return matrices_; }
+
+ private:
+ size_t leanvec_dims_;
+ matrices_type matrices_;
+
+ static matrices_type compute_pca(
+ svs::data::ConstSimpleDataView data,
+ size_t leanvec_dims,
+ svs::threads::ThreadPoolHandle& pool
+ ) {
+ auto means = svs::utils::compute_medioid(data, pool);
+ auto matrix = svs::leanvec::compute_leanvec_matrix(
+ data, means, pool, svs::lib::MaybeStatic{leanvec_dims}
+ );
+ // A copy is used for the query matrix: in PCA mode data and query
+ // transforms are identical, and passing the same object twice trips
+ // use-after-move warnings and DenseArray double-free issues.
+ auto query_matrix = matrix;
+ return matrices_type{std::move(matrix), std::move(query_matrix)};
+ }
+
+ static matrices_type compute_ood(
+ svs::data::ConstSimpleDataView data,
+ svs::data::ConstSimpleDataView queries,
+ size_t leanvec_dims,
+ svs::threads::ThreadPoolHandle& pool
+ ) {
+ return svs::leanvec::compute_leanvec_matrices_ood(
+ data, queries, pool, svs::lib::MaybeStatic{leanvec_dims}
+ );
+ }
+};
+
+} // namespace svs::c_runtime
+
+#endif // SVS_RUNTIME_ENABLE_LVQ_LEANVEC
diff --git a/bindings/c/src/storage.hpp b/bindings/c/src/storage.hpp
index 8c0a5689..1205db9d 100644
--- a/bindings/c/src/storage.hpp
+++ b/bindings/c/src/storage.hpp
@@ -25,10 +25,13 @@
#include
#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC
+#include "leanvec_training_data.hpp"
+
#include
#endif
#include
+#include
#include
namespace svs {
@@ -56,13 +59,20 @@ struct StorageSimple : public Storage {
};
struct StorageLeanVec : public Storage {
- size_t lenavec_dims;
+ size_t leanvec_dims;
size_t primary_bits;
size_t secondary_bits;
+#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC
+ // Pre-trained reduction matrices; when set, they are used instead of PCA
+ // matrices computed at build time (enables out-of-distribution LeanVec).
+ // Fixed at construction: `leanvec_dims` is taken from the training data, so
+ // the two can never disagree.
+ std::shared_ptr training_data;
+#endif
- StorageLeanVec(size_t lenavec_dims, svs_data_type_t primary, svs_data_type_t secondary)
+ StorageLeanVec(size_t leanvec_dims, svs_data_type_t primary, svs_data_type_t secondary)
: Storage{SVS_STORAGE_KIND_LEANVEC}
- , lenavec_dims(lenavec_dims)
+ , leanvec_dims(leanvec_dims)
, primary_bits(to_bits_number(primary))
, secondary_bits(to_bits_number(secondary)) {
#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC
@@ -78,6 +88,19 @@ struct StorageLeanVec : public Storage {
#endif
}
+#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC
+ // Construct from pre-trained matrices. `leanvec_dims` is the single value
+ // carried by the training data, so no reconciliation is needed.
+ StorageLeanVec(
+ std::shared_ptr training_data,
+ svs_data_type_t primary,
+ svs_data_type_t secondary
+ )
+ : StorageLeanVec(training_data->leanvec_dims(), primary, secondary) {
+ this->training_data = std::move(training_data);
+ }
+#endif
+
static size_t to_bits_number(svs_data_type_t data_type) {
switch (data_type) {
case SVS_DATA_TYPE_INT4:
diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp
index 1bc392c4..520a42e1 100644
--- a/bindings/c/src/svs_c.cpp
+++ b/bindings/c/src/svs_c.cpp
@@ -24,6 +24,10 @@
#include "threadpool.hpp"
#include "types_support.hpp"
+#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC
+#include "leanvec_training_data.hpp"
+#endif
+
#include
#include
#include
@@ -56,6 +60,12 @@ struct svs_storage {
std::shared_ptr impl;
};
+struct svs_leanvec_training_data {
+#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC
+ std::shared_ptr impl;
+#endif
+};
+
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; }
@@ -280,6 +290,27 @@ svs_storage_create_simple(svs_data_type_t data_type, svs_error_h out_err) {
);
}
+namespace {
+// Shared by both LeanVec storage constructors so their accepted types stay in sync.
+void validate_leanvec_data_types(svs_data_type_t primary, svs_data_type_t secondary) {
+ NOT_IMPLEMENTED_IF(
+ (primary == SVS_DATA_TYPE_FLOAT32 || primary == SVS_DATA_TYPE_FLOAT16 ||
+ secondary == SVS_DATA_TYPE_FLOAT32 || secondary == SVS_DATA_TYPE_FLOAT16),
+ "Unsupported simple data types for LeanVec primary and secondary"
+ );
+ INVALID_ARGUMENT_IF(
+ (primary != SVS_DATA_TYPE_INT4 && primary != SVS_DATA_TYPE_UINT4 &&
+ primary != SVS_DATA_TYPE_INT8 && primary != SVS_DATA_TYPE_UINT8),
+ "Unsupported data type for LeanVec primary storage"
+ );
+ INVALID_ARGUMENT_IF(
+ (secondary != SVS_DATA_TYPE_INT4 && secondary != SVS_DATA_TYPE_UINT4 &&
+ secondary != SVS_DATA_TYPE_INT8 && secondary != SVS_DATA_TYPE_UINT8),
+ "Unsupported data type for LeanVec secondary storage"
+ );
+}
+} // namespace
+
extern "C" svs_storage_h svs_storage_create_leanvec(
size_t leanvec_dims,
svs_data_type_t primary,
@@ -290,27 +321,49 @@ extern "C" svs_storage_h svs_storage_create_leanvec(
return wrap_exceptions(
[&]() {
EXPECT_ARG_GT_THAN(leanvec_dims, 0);
- NOT_IMPLEMENTED_IF(
- (primary == SVS_DATA_TYPE_FLOAT32 || primary == SVS_DATA_TYPE_FLOAT16 ||
- secondary == SVS_DATA_TYPE_FLOAT32 || secondary == SVS_DATA_TYPE_FLOAT16),
- "Unsupported simple data types for LeanVec primary and secondary"
- );
- INVALID_ARGUMENT_IF(
- (primary != SVS_DATA_TYPE_INT4 && primary != SVS_DATA_TYPE_UINT4 &&
- primary != SVS_DATA_TYPE_INT8 && primary != SVS_DATA_TYPE_UINT8),
- "Unsupported data type for LeanVec primary storage"
- );
+ validate_leanvec_data_types(primary, secondary);
+
+ auto storage =
+ std::make_shared(leanvec_dims, primary, secondary);
+ auto result = new svs_storage;
+ result->impl = storage;
+ return result;
+ },
+ out_err
+ );
+}
+
+extern "C" svs_storage_h svs_storage_create_leanvec_trained(
+ svs_leanvec_training_data_h training_data,
+ svs_data_type_t primary,
+ svs_data_type_t secondary,
+ svs_error_h out_err
+) {
+ using namespace svs::c_runtime;
+ return wrap_exceptions(
+ [&]() -> svs_storage_h {
+#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC
+ EXPECT_ARG_NOT_NULL(training_data);
INVALID_ARGUMENT_IF(
- (secondary != SVS_DATA_TYPE_INT4 && secondary != SVS_DATA_TYPE_UINT4 &&
- secondary != SVS_DATA_TYPE_INT8 && secondary != SVS_DATA_TYPE_UINT8),
- "Unsupported data type for LeanVec secondary storage"
+ (training_data->impl == nullptr), "training_data holds no trained matrices"
);
+ validate_leanvec_data_types(primary, secondary);
+ // The storage shares ownership of the trained matrices, so the caller
+ // may free the training data handle as soon as this returns.
auto storage =
- std::make_shared(leanvec_dims, primary, secondary);
+ std::make_shared(training_data->impl, primary, secondary);
auto result = new svs_storage;
result->impl = storage;
return result;
+#else
+ (void)training_data;
+ (void)primary;
+ (void)secondary;
+ throw svs::c_runtime::not_implemented(
+ "LeanVec storage is not implemented in this build"
+ );
+#endif
},
out_err
);
@@ -377,6 +430,63 @@ extern "C" SVS_API bool svs_storage_get_kind(
extern "C" void svs_storage_free(svs_storage_h storage) { delete storage; }
+extern "C" svs_leanvec_training_data_h svs_leanvec_training_data_build(
+ size_t dim,
+ size_t num_vectors,
+ const float* x,
+ size_t num_queries,
+ const float* x_q,
+ size_t leanvec_dims,
+ svs_error_h out_err
+) {
+ using namespace svs::c_runtime;
+ return wrap_exceptions(
+ [&]() -> svs_leanvec_training_data_h {
+#ifdef SVS_RUNTIME_ENABLE_LVQ_LEANVEC
+ EXPECT_ARG_GT_THAN(dim, 0);
+ EXPECT_ARG_GT_THAN(num_vectors, 0);
+ EXPECT_ARG_NOT_NULL(x);
+ EXPECT_ARG_GT_THAN(leanvec_dims, 0);
+ EXPECT_ARG_GE_THAN(dim, leanvec_dims);
+ INVALID_ARGUMENT_IF(
+ (num_queries > 0 && x_q == nullptr),
+ "x_q should not be NULL when num_queries is greater than 0"
+ );
+
+ auto data = svs::data::ConstSimpleDataView(x, num_vectors, dim);
+ // A zero-sized view selects the in-distribution (PCA) path.
+ auto queries = svs::data::ConstSimpleDataView(
+ x_q, (x_q == nullptr) ? 0 : num_queries, dim
+ );
+
+ auto pool = ThreadPoolBuilder{}.build();
+ auto training_data = std::make_shared(
+ data, queries, leanvec_dims, pool
+ );
+
+ auto result = new svs_leanvec_training_data;
+ result->impl = std::move(training_data);
+ return result;
+#else
+ (void)dim;
+ (void)num_vectors;
+ (void)x;
+ (void)num_queries;
+ (void)x_q;
+ (void)leanvec_dims;
+ throw svs::c_runtime::not_implemented(
+ "LeanVec training data is not implemented in this build"
+ );
+#endif
+ },
+ out_err
+ );
+}
+
+extern "C" void svs_leanvec_training_data_free(svs_leanvec_training_data_h training_data) {
+ delete training_data;
+}
+
extern "C" svs_index_builder_h svs_index_builder_create(
svs_distance_metric_t metric,
size_t dimension,
diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp
index 6ba2d500..edf3e157 100644
--- a/bindings/c/tests/c_api_index.cpp
+++ b/bindings/c/tests/c_api_index.cpp
@@ -255,6 +255,102 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]")
svs_error_free(error);
}
+ CATCH_SECTION("Index Build and Search with pre-trained LeanVec") {
+ svs_error_h error = svs_error_create();
+
+ auto run_build_and_search = [&](svs_storage_h storage) {
+ CATCH_REQUIRE(storage != nullptr);
+ CATCH_REQUIRE(svs_error_ok(error));
+
+ svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error);
+ CATCH_REQUIRE(algorithm != nullptr);
+
+ svs_index_builder_h builder = svs_index_builder_create(
+ SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error
+ );
+ CATCH_REQUIRE(builder != nullptr);
+
+ bool success = svs_index_builder_set_threadpool(
+ builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error
+ );
+ CATCH_REQUIRE(success);
+
+ success = svs_index_builder_set_storage(builder, storage, error);
+ CATCH_REQUIRE(success);
+ CATCH_REQUIRE(svs_error_ok(error));
+
+ svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error);
+ CATCH_REQUIRE(index != nullptr);
+ CATCH_REQUIRE(svs_error_ok(error));
+
+ 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);
+
+ for (size_t i = 0; i < NUM_QUERIES; ++i) {
+ CATCH_REQUIRE(results.offsets[i + 1] - results.offsets[i] == K);
+ }
+
+ svs_search_results_free(&results);
+ svs_index_free(index);
+ svs_index_builder_free(builder);
+ svs_algorithm_free(algorithm);
+ svs_storage_free(storage);
+ };
+
+ // Builds a storage from matrices trained up front, then drops the training
+ // data handle to confirm the storage keeps its own reference to the matrices.
+ auto build_trained_storage = [&](size_t num_queries, const float* x_q) {
+ svs_leanvec_training_data_h training_data = svs_leanvec_training_data_build(
+ DIMENSION, NUM_VECTORS, data.data(), num_queries, x_q, DIMENSION / 2, error
+ );
+ CATCH_REQUIRE(check_training_data_support(training_data, error) == true);
+ if (!training_data_usable(training_data)) {
+ return static_cast(nullptr);
+ }
+
+ // leanvec_dims is taken from the training data.
+ svs_storage_h storage = svs_storage_create_leanvec_trained(
+ training_data, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error
+ );
+ svs_leanvec_training_data_free(training_data);
+ return storage;
+ };
+
+ // Out-of-distribution: matrices learned from a sample of queries.
+ svs_storage_h storage = build_trained_storage(NUM_QUERIES, queries.data());
+ if (storage_usable(storage)) {
+ run_build_and_search(storage);
+ }
+
+ // No training queries: in-distribution (PCA) matrices, trained up front.
+ storage = build_trained_storage(0, nullptr);
+ if (storage_usable(storage)) {
+ run_build_and_search(storage);
+ }
+
+ svs_error_free(error);
+ }
+
+ CATCH_SECTION("Pre-trained LeanVec storage rejects NULL training data") {
+ svs_error_h error = svs_error_create();
+
+ svs_storage_h storage = svs_storage_create_leanvec_trained(
+ nullptr, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error
+ );
+ CATCH_REQUIRE(storage == nullptr);
+#ifdef SVS_TEST_EXPECT_LVQ_LEANVEC
+ CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT);
+#else
+ CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_NOT_IMPLEMENTED);
+#endif
+
+ svs_error_free(error);
+ }
+
CATCH_SECTION("Index with Custom Threadpool") {
svs_error_h error = svs_error_create();
diff --git a/bindings/c/tests/c_api_test_utils.h b/bindings/c/tests/c_api_test_utils.h
index 003d6907..416df093 100644
--- a/bindings/c/tests/c_api_test_utils.h
+++ b/bindings/c/tests/c_api_test_utils.h
@@ -167,3 +167,25 @@ inline bool check_storage_support(svs_storage_h storage, svs_error_h error) {
/// True when compressed storage is expected to be usable on this host, so callers
/// can skip the build/search portion of a test that cannot run.
inline bool storage_usable(svs_storage_h storage) { return storage != nullptr; }
+
+/// Same build-configuration contract as check_storage_support(), for the LeanVec
+/// training-data constructor.
+inline bool
+check_training_data_support(svs_leanvec_training_data_h training_data, svs_error_h error) {
+#ifdef SVS_TEST_EXPECT_LVQ_LEANVEC
+ if (training_data != nullptr) {
+ return svs_error_ok(error) == true;
+ }
+ return svs_error_get_code(error) == SVS_ERROR_UNSUPPORTED_HW;
+#else
+ if (training_data != nullptr) {
+ return false; // LeanVec should not be available in a public build
+ }
+ return svs_error_get_code(error) == SVS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+/// True when LeanVec training data is expected to be usable on this host.
+inline bool training_data_usable(svs_leanvec_training_data_h training_data) {
+ return training_data != nullptr;
+}