diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index be466bd7..aa721def 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -27,6 +27,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/SVS_C_API_Design.md b/bindings/c/SVS_C_API_Design.md index 5c5bcbfe..e244d942 100644 --- a/bindings/c/SVS_C_API_Design.md +++ b/bindings/c/SVS_C_API_Design.md @@ -437,6 +437,7 @@ svs_storage_h svs_storage_create_lvq( ); // LeanVec two-level hierarchical storage +// Reduction matrices are computed from the dataset (PCA) at build time. svs_storage_h svs_storage_create_leanvec( size_t leanvec_dims, // Primary dimensions (usually much smaller) svs_data_type_t primary, // Primary storage type @@ -444,6 +445,17 @@ svs_storage_h svs_storage_create_leanvec( svs_error_h out_err ); +// LeanVec storage using matrices trained up front, e.g. out-of-distribution +// matrices learned from a sample of queries. `leanvec_dims` comes from the +// training data. The storage keeps its own reference to the matrices, so the +// training data handle may be freed as soon as this returns. +svs_storage_h svs_storage_create_leanvec_trained( + svs_leanvec_training_data_h training_data, + 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); ``` diff --git a/bindings/c/include/svs/c_api/svs_c.h b/bindings/c/include/svs/c_api/svs_c.h index dccccb97..a28033d7 100644 --- a/bindings/c/include/svs/c_api/svs_c.h +++ b/bindings/c/include/svs/c_api/svs_c.h @@ -125,6 +125,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; @@ -305,6 +306,47 @@ SVS_API svs_storage_h svs_storage_create_sq( /// @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 13ba58a5..53d06c35 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..963a434f --- /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_api/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 b35927ca..bcf82c77 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 40baf635..847392c7 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 @@ -55,6 +59,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" svs_algorithm_h svs_algorithm_create_vamana( size_t graph_degree, size_t build_window_size, @@ -260,6 +270,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, @@ -270,27 +301,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 ); @@ -342,6 +395,63 @@ svs_storage_create_sq(svs_data_type_t data_type, svs_error_h out_err) { 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 d9ab2446..eefe569f 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -255,6 +255,157 @@ 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 LeanVec OOD Training Data") { + svs_error_h error = svs_error_create(); + + const size_t leanvec_dims = DIMENSION / 2; + + // Train out-of-distribution matrices from the data and a sample of queries. + svs_leanvec_training_data_h training_data = svs_leanvec_training_data_build( + DIMENSION, + NUM_VECTORS, + data.data(), + NUM_QUERIES, + queries.data(), + leanvec_dims, + error + ); + + // LeanVec is only available on supported hardware/builds; skip otherwise. + if (training_data == nullptr) { + auto code = svs_error_get_code(error); + CATCH_REQUIRE( + (code == SVS_ERROR_NOT_IMPLEMENTED || code == SVS_ERROR_UNSUPPORTED_HW) + ); + svs_error_free(error); + return; + } + CATCH_REQUIRE(svs_error_ok(error)); + + // The number of LeanVec dimensions comes from the training data. + svs_storage_h storage = svs_storage_create_leanvec_trained( + training_data, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error + ); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // The storage holds its own reference to the trained matrices, so the + // training data handle can be released before the index is built. + svs_leanvec_training_data_free(training_data); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + 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_index_search_topK( + index, queries.data(), NUM_QUERIES, K, nullptr, nullptr, error + ); + CATCH_REQUIRE(results != nullptr); + 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->results_per_query[i] == K); + } + + svs_search_results_free(results); + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_storage_free(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); + auto code = svs_error_get_code(error); + CATCH_REQUIRE( + (code == SVS_ERROR_INVALID_ARGUMENT || code == SVS_ERROR_NOT_IMPLEMENTED) + ); + + svs_error_free(error); + } + + CATCH_SECTION("Index Build and Search with pre-trained in-distribution LeanVec") { + svs_error_h error = svs_error_create(); + + // No training queries: in-distribution (PCA) matrices, trained up front. + svs_leanvec_training_data_h training_data = svs_leanvec_training_data_build( + DIMENSION, NUM_VECTORS, data.data(), 0, nullptr, DIMENSION / 2, error + ); + if (training_data == nullptr) { + auto code = svs_error_get_code(error); + CATCH_REQUIRE( + (code == SVS_ERROR_NOT_IMPLEMENTED || code == SVS_ERROR_UNSUPPORTED_HW) + ); + svs_error_free(error); + return; + } + + svs_storage_h storage = svs_storage_create_leanvec_trained( + training_data, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error + ); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Freeing here exercises the storage's shared ownership of the matrices: + // the build below still uses them. + svs_leanvec_training_data_free(training_data); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + 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_index_search_topK( + index, queries.data(), NUM_QUERIES, K, nullptr, nullptr, error + ); + CATCH_REQUIRE(results != nullptr); + 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->results_per_query[i] == K); + } + + svs_search_results_free(results); + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_storage_free(storage); + svs_error_free(error); + } + CATCH_SECTION("Index with Custom Threadpool") { svs_error_h error = svs_error_create();