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
68 changes: 50 additions & 18 deletions include/svs/index/vamana/dynamic_index.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,32 @@ inline constexpr std::string_view name(SlotMetadata metadata) {

class ValidBuilder {
public:
ValidBuilder(const std::vector<SlotMetadata>& status)
: status_{status} {}
// Optional `pred` (with `translator`): an entry is also invalid if `pred(external_id)`
// is false. Invalid entries are navigated through but never returned.
ValidBuilder(
const std::vector<SlotMetadata>& status,
const IDTranslator* translator = nullptr,
const std::function<bool(size_t)>* pred = nullptr
)
: status_{status}
, translator_{translator}
, pred_{pred} {}

template <typename I>
constexpr PredicatedSearchNeighbor<I> operator()(I i, float distance) const {
bool invalid = getindex(status_, i) == SlotMetadata::Deleted;
// This neighbor should be skipped if the metadata corresponding to the given index
// marks this slot as deleted.
if (!invalid && pred_ != nullptr) {
invalid = !(*pred_)(translator_->get_external(i));
}
return PredicatedSearchNeighbor<I>(i, distance, !invalid);
}

private:
const std::vector<SlotMetadata>& status_;
const IDTranslator* translator_;
const std::function<bool(size_t)>* pred_;
};

template <graphs::MemoryGraph Graph, typename Data, typename Dist>
Expand Down Expand Up @@ -532,6 +545,9 @@ class MutableVamanaIndex {
[&](const auto is, uint64_t /*tid*/) {
for (auto i : is) {
for (size_t j = 0, jmax = getsize<1>(ids); j < jmax; ++j) {
if (ids.at(i, j) == static_cast<size_t>(-1)) {
continue; // Padding from a filtered search.
}
auto internal = lib::narrow_cast<Idx>(ids.at(i, j));
ids.at(i, j) = translate_internal_id(internal);
}
Expand Down Expand Up @@ -573,13 +589,17 @@ class MutableVamanaIndex {

// Return a `greedy_search` compatible builder for this index.
// This is an internal method, mostly used to help implement the batch iterator.
ValidBuilder internal_search_builder() const { return ValidBuilder{status_}; }
ValidBuilder internal_search_builder(const std::function<bool(size_t)>* pred = nullptr
) const {
return ValidBuilder{status_, &translator_, pred};
}

auto greedy_search_closure(
GreedySearchPrefetchParameters prefetch_parameters,
const lib::DefaultPredicate& cancel = lib::Returns(lib::Const<false>())
const lib::DefaultPredicate& cancel = lib::Returns(lib::Const<false>()),
const std::function<bool(size_t)>* pred = nullptr
) const {
return [&, prefetch_parameters](
return [&, prefetch_parameters, pred](
const auto& query, auto& accessor, auto& distance, auto& buffer
) {
// Perform the greedy search using the provided resources.
Expand All @@ -591,7 +611,7 @@ class MutableVamanaIndex {
distance,
buffer,
vamana::EntryPointInitializer<Idx>{lib::as_const_span(entry_point_)},
internal_search_builder(),
internal_search_builder(pred),
prefetch_parameters,
cancel
);
Expand Down Expand Up @@ -623,8 +643,12 @@ class MutableVamanaIndex {
QueryResultView<I> results,
const Queries& queries,
const search_parameters_type& sp,
const lib::DefaultPredicate& cancel = lib::Returns(lib::Const<false>())
const lib::DefaultPredicate& cancel = lib::Returns(lib::Const<false>()),
const std::function<bool(size_t)>* pred = nullptr
) {
// Optional `pred`: only entries whose external id passes are returned. It is called
// from all search threads. Rows with fewer passing entries than requested are
// padded with id `size_t(-1)` and the worst score.
threads::parallel_for(
threadpool_,
threads::StaticPartition{queries.size()},
Expand All @@ -643,17 +667,25 @@ class MutableVamanaIndex {

auto scratch = extensions::per_thread_batch_search_setup(data_, distance_);

extensions::per_thread_batch_search(
data_,
buffer,
scratch,
queries,
results,
threads::UnitRange{is},
greedy_search_closure(prefetch_parameters, cancel),
*this,
cancel
);
auto run = [&](const auto& search) {
extensions::per_thread_batch_search(
data_,
buffer,
scratch,
queries,
results,
threads::UnitRange{is},
search,
*this,
cancel
);
};
auto search = greedy_search_closure(prefetch_parameters, cancel, pred);
if (pred == nullptr) {
run(search);
} else {
run(extensions::FilteredSearch<decltype(search)>{search, pred});
}
}
);

Expand Down
36 changes: 34 additions & 2 deletions include/svs/index/vamana/extensions.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
#include "svs/lib/preprocessor.h"
#include "svs/lib/threads.h"

#include <functional>
#include <limits>

namespace svs::index::vamana::extensions {

/////
Expand Down Expand Up @@ -432,12 +435,18 @@ struct VamanaSingleSearchType {
/// with additional results.
template <typename Index, typename SearchBuffer, typename Query>
void check_and_supplement_search_buffer(
const Index& index, SearchBuffer& search_buffer, const Query& query
const Index& index,
SearchBuffer& search_buffer,
const Query& query,
const std::function<bool(size_t)>* pred = nullptr
) {
if (search_buffer.valid() < search_buffer.target_window() &&
search_buffer.valid() < index.size()) {
search_buffer.sort();
for (auto external_id : index.external_ids()) {
if (pred != nullptr && !(*pred)(external_id)) {
continue;
}
auto internal_id = index.translate_external_id(external_id);
auto dist = index.get_distance(external_id, query);
auto builder = index.internal_search_builder();
Expand All @@ -449,6 +458,15 @@ void check_and_supplement_search_buffer(
}
}

/// A search closure that also carries the filter of a filtered search.
template <typename Search> struct FilteredSearch {
Search search;
const std::function<bool(size_t)>* pred;
template <typename... Args> void operator()(Args&&... args) const {
search(std::forward<Args>(args)...);
}
};

/// Customization point object for processing single queries.
inline constexpr VamanaSingleSearchType single_search{};

Expand Down Expand Up @@ -481,7 +499,11 @@ SVS_FORCE_INLINE void svs_invoke(
search(query, accessor, distance, search_buffer);

if constexpr (Index::needs_id_translation) {
check_and_supplement_search_buffer(index, search_buffer, query);
const std::function<bool(size_t)>* pred = nullptr;
if constexpr (requires { search.pred; }) {
pred = search.pred; // Filtered search: keep filtered-out ids out of the top-up.
}
check_and_supplement_search_buffer(index, search_buffer, query, pred);
}
}

Expand Down Expand Up @@ -586,6 +608,16 @@ void svs_invoke(

// Copy back results.
for (size_t j = 0; j < num_neighbors; ++j) {
if constexpr (requires { search.pred; }) {
if (j >= search_buffer.size()) { // Filtered: fewer than k passed.
result.index(i, j) = static_cast<I>(-1);
result.distance(i, j) =
distance::comparator(distance)(0.0f, 1.0f)
? std::numeric_limits<float>::infinity()
: -std::numeric_limits<float>::infinity();
continue;
}
}
result.set(search_buffer[j], i, j);
}
}
Expand Down
67 changes: 67 additions & 0 deletions include/svs/orchestrators/dynamic_vamana.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,21 @@ class DynamicVamanaInterface : public VamanaInterface {

// Check if the index has been consolidated - no `Deleted` entries remain.
virtual bool is_consolidated() const = 0;

// Filtered search; false if the data type does not support it. Keep last (ABI).
virtual bool filtered_search(
svs::QueryResultView<size_t> results,
AnonymousArray<2> queries,
const index::vamana::VamanaSearchParameters& search_parameters,
const std::function<bool(size_t)>& filter
) = 0;
};

// Filtered search is only enabled for uncompressed data.
template <typename T> inline constexpr bool is_simple_data = false;
template <typename T, size_t Extent, typename Alloc>
inline constexpr bool is_simple_data<data::SimpleData<T, Extent, Alloc>> = true;

template <lib::TypeList QueryTypes, typename Impl>
class DynamicVamanaImpl : public VamanaImpl<QueryTypes, Impl, DynamicVamanaInterface> {
public:
Expand Down Expand Up @@ -109,6 +122,46 @@ class DynamicVamanaImpl : public VamanaImpl<QueryTypes, Impl, DynamicVamanaInter

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

bool filtered_search(
svs::QueryResultView<size_t> results,
AnonymousArray<2> queries,
const index::vamana::VamanaSearchParameters& search_parameters,
const std::function<bool(size_t)>& filter
) override {
// Indexes whose `search` has no filter argument (e.g. the concurrent index) report
// "unsupported" like other data types.
constexpr bool supported =
is_simple_data<typename Impl::data_type> &&
requires(
Impl & idx,
svs::QueryResultView<size_t> r,
data::ConstSimpleDataView<float> q,
const index::vamana::VamanaSearchParameters& sp,
const std::function<bool(size_t)>* pred
) { idx.search(r, q, sp, lib::Returns(lib::Const<false>()), pred); };
if constexpr (supported) {
lib::match(
QueryTypes{},
queries.type(),
[&]<typename T>(lib::Type<T> SVS_UNUSED(type)) {
impl().search(
results,
data::ConstSimpleDataView<T>(queries),
search_parameters,
lib::Returns(lib::Const<false>()),
&filter
);
},
[&](svs::DataType data_type) {
throw ANNEXCEPTION("Unsupported query datatype! Got: {}.", data_type);
}
);
return true;
} else {
return false;
}
}
};

// Forward Declaractions.
Expand Down Expand Up @@ -609,6 +662,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(); }

/// @brief Filtered search: only ids for which ``filter(id)`` is true are returned.
/// Returns false if the data type does not support it.
template <typename QueryType>
bool filtered_search(
QueryResultView<size_t> results,
data::ConstSimpleDataView<QueryType> queries,
const VamanaSearchParameters& search_parameters,
const std::function<bool(size_t)>& filter
) {
return impl_->filtered_search(
results, AnonymousArray<2>(queries), search_parameters, filter
);
}
};

///
Expand Down
Loading
Loading