diff --git a/bindings/cpp/tests/runtime_test.cpp b/bindings/cpp/tests/runtime_test.cpp index 4ebc25df7..25e7c6d2d 100644 --- a/bindings/cpp/tests/runtime_test.cpp +++ b/bindings/cpp/tests/runtime_test.cpp @@ -1346,6 +1346,86 @@ CATCH_TEST_CASE("RangeSearchFunctionalStatic", "[runtime][static_vamana]") { svs::runtime::v0::VamanaIndex::destroy(index); } +CATCH_TEST_CASE("SearchBufferCapacityRaisedToWindowSize", "[runtime]") { + const auto& test_data = get_test_data(); + const int nq = 5; + const float* xq = test_data.data(); + const int k = 20; + std::vector distances(nq * k); + std::vector result_labels(nq * k); + + svs::runtime::v0::VamanaIndex::SearchParams small_capacity; + small_capacity.search_window_size = 50; + small_capacity.search_buffer_capacity = 10; + + CATCH_SECTION("Static, per-search parameters") { + svs::runtime::v0::VamanaIndex* index = nullptr; + auto status = svs::runtime::v0::VamanaIndex::build( + &index, + test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::FP32, + svs::runtime::v0::VamanaIndex::BuildParams{64} + ); + CATCH_REQUIRE(status.ok()); + status = index->add(test_n, test_data.data()); + CATCH_REQUIRE(status.ok()); + + status = index->search( + nq, xq, k, distances.data(), result_labels.data(), &small_capacity + ); + CATCH_REQUIRE(status.ok()); + for (int i = 0; i < nq; ++i) { + CATCH_REQUIRE(result_labels[i * k] == static_cast(i)); + } + svs::runtime::v0::VamanaIndex::destroy(index); + } + + CATCH_SECTION("Static, default search parameters") { + svs::runtime::v0::VamanaIndex* index = nullptr; + auto status = svs::runtime::v0::VamanaIndex::build( + &index, + test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::FP32, + svs::runtime::v0::VamanaIndex::BuildParams{64}, + small_capacity + ); + CATCH_REQUIRE(status.ok()); + status = index->add(test_n, test_data.data()); + CATCH_REQUIRE(status.ok()); + + status = index->search(nq, xq, k, distances.data(), result_labels.data(), nullptr); + CATCH_REQUIRE(status.ok()); + svs::runtime::v0::VamanaIndex::destroy(index); + } + + CATCH_SECTION("Dynamic, per-search parameters") { + svs::runtime::v0::DynamicVamanaIndex* index = nullptr; + auto status = svs::runtime::v0::DynamicVamanaIndex::build( + &index, + test_d, + svs::runtime::v0::MetricType::L2, + svs::runtime::v0::StorageKind::FP32, + svs::runtime::v0::VamanaIndex::BuildParams{64} + ); + CATCH_REQUIRE(status.ok()); + std::vector labels(test_n); + std::iota(labels.begin(), labels.end(), 0); + status = index->add(test_n, labels.data(), test_data.data()); + CATCH_REQUIRE(status.ok()); + + status = index->search( + nq, xq, k, distances.data(), result_labels.data(), &small_capacity + ); + CATCH_REQUIRE(status.ok()); + for (int i = 0; i < nq; ++i) { + CATCH_REQUIRE(result_labels[i * k] == static_cast(i)); + } + svs::runtime::v0::DynamicVamanaIndex::destroy(index); + } +} + CATCH_TEST_CASE("GetDistanceDynamic", "[runtime]") { const auto& test_data = get_test_data(); svs::runtime::v0::DynamicVamanaIndex* index = nullptr; diff --git a/bindings/python/src/vamana_common.cpp b/bindings/python/src/vamana_common.cpp index 46ed3ac78..fb74996e7 100644 --- a/bindings/python/src/vamana_common.cpp +++ b/bindings/python/src/vamana_common.cpp @@ -85,7 +85,7 @@ See also: :py:class:`svs.VamanaSearchParameters`, search_window_size (int, read-only): The number of valid entries in the buffer that will be used to determine stopping conditions for graph search. search_buffer_capacity (int, read-only): The (expected) number of valid entries that - will be available. Must be at least as large as `search_window_size`. + will be available. Raised to `search_window_size` if smaller. )"}; config.def(py::init<>()) @@ -102,7 +102,8 @@ Configure with a given search window size. This constructor implicitly defaults py::arg("search_buffer_capacity"), R"( Configure with a given search window size and capacity. -Raises ``svs.ANNException`` if ``search_buffer_capacity < search_window_size``.)" +If ``search_buffer_capacity < search_window_size``, the capacity is raised to +``search_window_size``.)" ) .def_property_readonly( "search_window_size", &SearchBufferConfig::get_search_window_size diff --git a/include/svs/index/vamana/search_buffer.h b/include/svs/index/vamana/search_buffer.h index 62c255b1b..2f0b8274c 100644 --- a/include/svs/index/vamana/search_buffer.h +++ b/include/svs/index/vamana/search_buffer.h @@ -43,28 +43,17 @@ class SearchBufferConfig { size_t search_window_size_{0}; size_t total_capacity_{0}; - // Tags to bypass invariant checks. - struct BypassInvariantCheckTag {}; - public: constexpr SearchBufferConfig() = default; - SearchBufferConfig( - BypassInvariantCheckTag, size_t search_window_size, size_t total_capacity - ) - : search_window_size_{search_window_size} - , total_capacity_{total_capacity} {} - + // A capacity smaller than the window size is raised to the window size. + // Callers such as FAISS pass a default capacity alongside a user-chosen window + // size, so rejecting the pair would fail searches the user never misconfigured. SearchBufferConfig(size_t search_window_size, size_t total_capacity) - : SearchBufferConfig( - BypassInvariantCheckTag(), search_window_size, total_capacity - ) { - check_invariants(); - } + : search_window_size_{search_window_size} + , total_capacity_{std::max(search_window_size, total_capacity)} {} /*not explicit*/ SearchBufferConfig(size_t search_window_size) - : SearchBufferConfig( - BypassInvariantCheckTag(), search_window_size, search_window_size - ) {} + : SearchBufferConfig(search_window_size, search_window_size) {} // Increment both the window size and capacity. void increment(size_t by) { @@ -81,17 +70,6 @@ class SearchBufferConfig { size_t get_search_window_size() const { return search_window_size_; } size_t get_total_capacity() const { return total_capacity_; } - void check_invariants() const { - if (search_window_size_ > total_capacity_) { - throw ANNEXCEPTION( - "Improper configuration for search buffer! Effective size ({}) cannot be " - "less than capacity ({}).", - search_window_size_, - total_capacity_ - ); - } - } - friend bool operator==(SearchBufferConfig, SearchBufferConfig) = default; }; diff --git a/tests/svs/index/vamana/search_buffer.cpp b/tests/svs/index/vamana/search_buffer.cpp index 8ecb698a5..d9f2babd9 100644 --- a/tests/svs/index/vamana/search_buffer.cpp +++ b/tests/svs/index/vamana/search_buffer.cpp @@ -324,9 +324,10 @@ CATCH_TEST_CASE("Testing Search Buffer", "[core][search_buffer]") { CATCH_REQUIRE(config.get_search_window_size() == 10); CATCH_REQUIRE(config.get_total_capacity() == 20); - // Ensure we get an error if mis-configuring. - auto f = []() { return vamana::SearchBufferConfig{10, 9}; }; - CATCH_REQUIRE_THROWS_AS(f(), svs::ANNException); + // A capacity smaller than the window size is raised to the window size. + config = vamana::SearchBufferConfig{10, 9}; + CATCH_REQUIRE(config.get_search_window_size() == 10); + CATCH_REQUIRE(config.get_total_capacity() == 10); } // Type Traits