Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions include/svs/orchestrators/dynamic_vamana.h
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,20 @@ class DynamicVamana : public manager::IndexManager<DynamicVamanaInterface> {

/// @brief Check if the index has been consolidated - no `Deleted` entries remain.
bool is_consolidated() const { return impl_->is_consolidated(); }

///// Range search
/// @copydoc svs::index::vamana::MutableVamanaIndex::range_search
template <typename QueryType>
std::vector<std::vector<Neighbor<size_t>>> range_search(
data::ConstSimpleDataView<QueryType> queries,
float radius,
const VamanaSearchParameters& search_parameters,
const lib::DefaultPredicate& cancel = lib::Returns(lib::Const<false>())
) {
return impl_->range_search(
AnonymousArray<2>(queries), radius, search_parameters, cancel
);
}
};

///
Expand Down
63 changes: 63 additions & 0 deletions include/svs/orchestrators/vamana.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,26 @@ class VamanaInterface {
// through the wrong slot with no link-time diagnostic.
virtual svs::index::vamana::MemoryBreakdown get_memory_breakdown() const = 0;
virtual svs::index::vamana::VamanaIndexParameters parameters() const = 0;

///// Range search
virtual std::vector<std::vector<Neighbor<size_t>>> range_search(
AnonymousArray<2> queries,
float radius,
const index::vamana::VamanaSearchParameters& search_parameters,
const lib::DefaultPredicate& cancel = lib::Returns(lib::Const<false>())
) = 0;
};

/// Whether the index implementation `Impl` provides ``range_search`` for `Queries`.
template <typename Impl, typename Queries>
concept HasRangeSearch = requires(
Impl& impl,
const Queries& queries,
float radius,
const index::vamana::VamanaSearchParameters& search_parameters,
const lib::DefaultPredicate& cancel
) { impl.range_search(queries, radius, search_parameters, cancel); };

template <lib::TypeList QueryTypes, typename Impl, typename IFace = VamanaInterface>
class VamanaImpl : public manager::ManagerImpl<QueryTypes, Impl, IFace> {
private:
Expand Down Expand Up @@ -283,6 +301,37 @@ class VamanaImpl : public manager::ManagerImpl<QueryTypes, Impl, IFace> {
svs::index::vamana::VamanaIndexParameters parameters() const override {
return impl().parameters();
}

///// Range search
std::vector<std::vector<Neighbor<size_t>>> range_search(
AnonymousArray<2> queries,
float radius,
const VamanaSearchParameters& search_parameters,
const lib::DefaultPredicate& cancel = lib::Returns(lib::Const<false>())
) override {
return lib::match(
QueryTypes{},
queries.type(),
[&]<typename T>(lib::Type<T> SVS_UNUSED(type)) {
const auto view = data::ConstSimpleDataView<T>(queries);
if constexpr (HasRangeSearch<Impl, data::ConstSimpleDataView<T>>) {
return this->impl().range_search(
view, radius, search_parameters, cancel
);
} else {
throw ANNEXCEPTION("Range search is not supported by this index!");
return std::vector<std::vector<Neighbor<size_t>>>{};
}
},
[&](svs::DataType data_type) -> std::vector<std::vector<Neighbor<size_t>>> {
throw ANNEXCEPTION(
"Unsupported datatype! Got: {}. Expected one of: {}.",
data_type,
fmt::join(QueryTypes::data_types(), ", ")
);
}
);
}
};

///// Forward declarations
Expand Down Expand Up @@ -776,6 +825,20 @@ class Vamana : public manager::IndexManager<VamanaInterface> {
return impl_->parameters();
}

///// Range search
/// @copydoc svs::index::vamana::VamanaIndex::range_search
template <typename QueryType>
std::vector<std::vector<Neighbor<size_t>>> range_search(
data::ConstSimpleDataView<QueryType> queries,
float radius,
const VamanaSearchParameters& search_parameters,
const lib::DefaultPredicate& cancel = lib::Returns(lib::Const<false>())
) {
return impl_->range_search(
AnonymousArray<2>(queries), radius, search_parameters, cancel
);
}

private:
template <
manager::QueryTypeDefinition QueryTypes,
Expand Down
16 changes: 16 additions & 0 deletions tests/svs/orchestrators/concurrent_dynamic_vamana.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "svs/lib/file.h"

// tests
#include "tests/utils/range_search.h"
#include "tests/utils/test_dataset.h"
#include "tests/utils/utils.h"
#include "tests/utils/vamana_reference.h"
Expand Down Expand Up @@ -406,3 +407,18 @@ CATCH_TEST_CASE(
CATCH_REQUIRE(index.has_id(i));
}
}

CATCH_TEST_CASE(
"ConcurrentDynamicVamana Range Search", "[managers][concurrent_dynamic_vamana]"
) {
auto data = load_data();
auto queries = svs::data::SimpleData<float>::load(test_dataset::query_file());
auto index = svs::ConcurrentDynamicVamana::build<float>(
svs::index::vamana::VamanaBuildParameters{1.2, 32, 64, 750, 28, true},
load_data(),
iota_ids(data.size()),
svs::DistanceType::L2,
num_threads
);
svs_test::check_orchestrator_range_search(index, data, queries);
}
16 changes: 16 additions & 0 deletions tests/svs/orchestrators/dynamic_vamana.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "svs/core/distance.h"

// Test dataset utilities
#include "tests/utils/range_search.h"
#include "tests/utils/test_dataset.h"
#include "tests/utils/utils.h"
#include "tests/utils/vamana_reference.h"
Expand Down Expand Up @@ -310,3 +311,18 @@ CATCH_TEST_CASE("DynamicVamana Per-Index Logger", "[managers][dynamic_vamana][lo
CATCH_REQUIRE(custom.messages->empty());
}
}

CATCH_TEST_CASE("DynamicVamana Range Search", "[managers][dynamic_vamana]") {
auto data = svs::data::SimpleData<float>::load(test_dataset::data_svs_file());
auto queries = svs::data::SimpleData<float>::load(test_dataset::query_file());
std::vector<size_t> ids(data.size());
std::iota(ids.begin(), ids.end(), 0);
svs::DynamicVamana index = svs::DynamicVamana::build<float>(
svs::index::vamana::VamanaBuildParameters{1.2, 32, 64, 750, 28, true},
data,
ids,
svs::DistanceL2(),
2
);
svs_test::check_orchestrator_range_search(index, data, queries);
}
13 changes: 13 additions & 0 deletions tests/svs/orchestrators/vamana.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "svs/core/distance.h"

// Test dataset utilities
#include "tests/utils/range_search.h"
#include "tests/utils/test_dataset.h"
#include "tests/utils/utils.h"
#include "tests/utils/vamana_reference.h"
Expand Down Expand Up @@ -280,3 +281,15 @@ CATCH_TEST_CASE("Vamana Per-Index Logger", "[managers][vamana][logging]") {
CATCH_REQUIRE(custom.messages->empty());
}
}

CATCH_TEST_CASE("Vamana Range Search", "[managers][vamana]") {
auto data = svs::data::SimpleData<float>::load(test_dataset::data_svs_file());
auto queries = svs::data::SimpleData<float>::load(test_dataset::query_file());
svs::Vamana index = svs::Vamana::build<float>(
svs::index::vamana::VamanaBuildParameters{1.2, 32, 64, 750, 28, true},
data,
svs::DistanceL2(),
2
);
svs_test::check_orchestrator_range_search(index, data, queries);
}
125 changes: 125 additions & 0 deletions tests/utils/range_search.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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

// svs
#include "svs/core/data/simple.h"
#include "svs/core/distance.h"
#include "svs/index/vamana/dynamic_search_buffer.h"
#include "svs/lib/exception.h"

// catch2
#include "catch2/catch_test_macros.hpp"

// stl
#include <algorithm>
#include <cstdint>
#include <unordered_set>
#include <vector>

namespace svs_test {

///
/// @brief Check the ``range_search`` of an orchestrator built with L2 over ``data`` and
/// queried with float queries, with external IDs equal to the row in ``data``.
///
template <typename Index, typename Data>
void check_orchestrator_range_search(
Index& index, const Data& data, const svs::data::SimpleData<float>& all_queries
) {
const size_t num_queries = 20;
auto queries = svs::data::SimpleData<float>(num_queries, all_queries.dimensions());
for (size_t q = 0; q < num_queries; ++q) {
queries.set_datum(q, all_queries.get_datum(q));
}
auto distance = svs::distance::DistanceL2();

// Brute-force distances and a radius that holds about 30 neighbors per query.
auto distances = std::vector<std::vector<float>>(num_queries);
float radius = 0;
for (size_t q = 0; q < num_queries; ++q) {
for (size_t i = 0; i < data.size(); ++i) {
distances[q].push_back(
svs::distance::compute(distance, queries.get_datum(q), data.get_datum(i))
);
}
auto sorted = distances[q];
std::nth_element(sorted.begin(), sorted.begin() + 30, sorted.end());
radius += sorted[30];
}
radius /= static_cast<float>(num_queries);

auto check = [&](const auto& results, size_t max_results) {
CATCH_REQUIRE(results.size() == num_queries);
size_t num_expected = 0;
size_t num_found = 0;
for (size_t q = 0; q < num_queries; ++q) {
const auto& result = results[q];
CATCH_REQUIRE(result.size() <= max_results);
auto seen = std::unordered_set<size_t>{};
float last = 0;
for (const auto& neighbor : result) {
CATCH_REQUIRE(neighbor.id() < data.size());
CATCH_REQUIRE(neighbor.distance() < radius);
CATCH_REQUIRE(neighbor.distance() >= last);
CATCH_REQUIRE(seen.insert(neighbor.id()).second);
last = neighbor.distance();
}
for (size_t i = 0; i < data.size(); ++i) {
if (distances[q][i] < radius) {
++num_expected;
num_found += seen.contains(i);
}
}
}
return static_cast<double>(num_found) / static_cast<double>(num_expected);
};

using buffer_type = svs::index::vamana::MutableBuffer<uint32_t>;
auto sp = index.get_search_parameters();

// Unbounded. The shared test dataset is hard for graph search (top-30 at window 20);
// the index tests check recall more strictly.
sp.buffer_config({20, buffer_type::unbounded_capacity});
CATCH_REQUIRE(
check(index.range_search(queries.cview(), radius, sp), data.size()) >= 0.85
);

// Bounded by the buffer capacity.
sp.buffer_config({10, 20});
auto bounded = index.range_search(queries.cview(), radius, sp);
check(bounded, 20);
CATCH_REQUIRE(std::any_of(bounded.begin(), bounded.end(), [](const auto& r) {
return r.size() == 20;
}));

// Cancelled before starting.
sp.buffer_config({20, buffer_type::unbounded_capacity});
auto none = index.range_search(queries.cview(), radius, sp, []() { return true; });
CATCH_REQUIRE(none.size() == num_queries);
for (const auto& result : none) {
CATCH_REQUIRE(result.empty());
}

// Unsupported query type.
auto bytes = svs::data::SimpleData<uint8_t>(1, queries.dimensions());
CATCH_REQUIRE_THROWS_AS(
index.range_search(bytes.cview(), radius, sp), svs::ANNException
);
}

} // namespace svs_test
Loading