Skip to content
Merged
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
21 changes: 11 additions & 10 deletions src/VecSim/algorithms/hnsw/hnsw.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Copyright (c) 2006-Present, Redis Ltd.
* All rights reserved.
* SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates <open-source-office@arm.com>
*
* Licensed under your choice of the Redis Source Available License 2.0
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
Expand Down Expand Up @@ -544,7 +545,7 @@ void HNSWIndex<DataType, DistType>::processCandidate(

elements_tags[candidate_id] = visited_tag;

DistType cur_dist = this->calcDistance(query_data, cur_data);
DistType cur_dist = this->calcDistanceForQuery(cur_data, query_data);
if (lowerBound > cur_dist || top_candidates.size() < ef) {

candidate_set.emplace(-cur_dist, candidate_id);
Expand Down Expand Up @@ -572,7 +573,7 @@ void HNSWIndex<DataType, DistType>::processCandidate(

elements_tags[candidate_id] = visited_tag;

DistType cur_dist = this->calcDistance(query_data, cur_data);
DistType cur_dist = this->calcDistanceForQuery(cur_data, query_data);
if (lowerBound > cur_dist || top_candidates.size() < ef) {
candidate_set.emplace(-cur_dist, candidate_id);

Expand Down Expand Up @@ -629,7 +630,7 @@ void HNSWIndex<DataType, DistType>::processCandidate_RangeSearch(

elements_tags[candidate_id] = visited_tag;

DistType cur_dist = this->calcDistance(query_data, cur_data);
DistType cur_dist = this->calcDistanceForQuery(cur_data, query_data);
if (cur_dist < dyn_range) {
candidate_set.emplace(-cur_dist, candidate_id);

Expand All @@ -647,7 +648,7 @@ void HNSWIndex<DataType, DistType>::processCandidate_RangeSearch(

elements_tags[candidate_id] = visited_tag;

DistType cur_dist = this->calcDistance(query_data, cur_data);
DistType cur_dist = this->calcDistanceForQuery(cur_data, query_data);
if (cur_dist < dyn_range) {
candidate_set.emplace(-cur_dist, candidate_id);

Expand All @@ -674,7 +675,7 @@ HNSWIndex<DataType, DistType>::searchLayer(idType ep_id, const void *data_point,

DistType lowerBound;
if (!isMarkedDeleted(ep_id)) {
DistType dist = this->calcDistance(data_point, getDataByInternalId(ep_id));
DistType dist = this->calcDistanceForQuery(getDataByInternalId(ep_id), data_point);
lowerBound = dist;
top_candidates.emplace(dist, ep_id);
candidate_set.emplace(-dist, ep_id);
Expand Down Expand Up @@ -1219,7 +1220,7 @@ void HNSWIndex<DataType, DistType>::greedySearchLevel(const void *vector_data, s
if (isInProcess(candidate)) {
continue;
}
DistType d = this->calcDistance(vector_data, getDataByInternalId(candidate));
DistType d = this->calcDistanceForQuery(getDataByInternalId(candidate), vector_data);
if (d < curDist) {
curDist = d;
bestCand = candidate;
Expand Down Expand Up @@ -1557,7 +1558,7 @@ void HNSWIndex<DataType, DistType>::insertElementToGraph(idType element_id,
size_t max_common_level;
if (element_max_level < global_max_level) {
max_common_level = element_max_level;
cur_dist = this->calcDistance(vector_data, getDataByInternalId(curr_element));
cur_dist = this->calcDistanceForQuery(getDataByInternalId(curr_element), vector_data);
for (auto level = static_cast<int>(global_max_level);
level > static_cast<int>(element_max_level); level--) {
// this is done for the levels which are above the max level
Expand Down Expand Up @@ -1878,7 +1879,7 @@ idType HNSWIndex<DataType, DistType>::searchBottomLayerEP(const void *query_data
if (curr_element == INVALID_ID)
return curr_element; // index is empty.

DistType cur_dist = this->calcDistance(query_data, getDataByInternalId(curr_element));
DistType cur_dist = this->calcDistanceForQuery(getDataByInternalId(curr_element), query_data);
for (size_t level = max_level; level > 0 && curr_element != INVALID_ID; --level) {
greedySearchLevel<true>(query_data, level, curr_element, cur_dist, timeoutCtx, rc);
}
Expand All @@ -1901,7 +1902,7 @@ HNSWIndex<DataType, DistType>::searchBottomLayer_WithTimeout(idType ep_id, const
if (!isMarkedDeleted(ep_id)) {
// If ep is not marked as deleted, get its distance and set lower bound and heaps
// accordingly
DistType dist = this->calcDistance(data_point, getDataByInternalId(ep_id));
DistType dist = this->calcDistanceForQuery(getDataByInternalId(ep_id), data_point);
lowerBound = dist;
top_candidates->emplace(dist, getExternalLabel(ep_id));
candidate_set.emplace(-dist, ep_id);
Expand Down Expand Up @@ -2009,7 +2010,7 @@ VecSimQueryResultContainer HNSWIndex<DataType, DistType>::searchRangeBottomLayer
dynamic_range_search_boundaries = dynamic_range = ep_dist;
} else {
// If ep is not marked as deleted, get its distance and set ranges accordingly
ep_dist = this->calcDistance(data_point, getDataByInternalId(ep_id));
ep_dist = this->calcDistanceForQuery(getDataByInternalId(ep_id), data_point);
dynamic_range = ep_dist;
if (ep_dist <= radius) {
// Entry-point is within the radius - add it to the results.
Expand Down
13 changes: 7 additions & 6 deletions src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Copyright (c) 2006-Present, Redis Ltd.
* All rights reserved.
* SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates <open-source-office@arm.com>
*
* Licensed under your choice of the Redis Source Available License 2.0
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
Expand Down Expand Up @@ -138,8 +139,8 @@ VecSimQueryReply_Code HNSW_BatchIterator<DataType, DistType>::scanGraphInternal(
this->visitNode(candidate_id);

const char *candidate_data = this->index->getDataByInternalId(candidate_id);
DistType candidate_dist =
this->index->calcDistance(this->getQueryBlob(), (const void *)candidate_data);
DistType candidate_dist = this->index->calcDistanceForQuery(
(const void *)candidate_data, this->getQueryBlob());

candidates.emplace(candidate_dist, candidate_id);
}
Expand All @@ -150,8 +151,8 @@ VecSimQueryReply_Code HNSW_BatchIterator<DataType, DistType>::scanGraphInternal(
this->visitNode(candidate_id);

const char *candidate_data = this->index->getDataByInternalId(candidate_id);
DistType candidate_dist =
this->index->calcDistance(this->getQueryBlob(), (const void *)candidate_data);
DistType candidate_dist = this->index->calcDistanceForQuery(
(const void *)candidate_data, this->getQueryBlob());

candidates.emplace(candidate_dist, candidate_id);
}
Expand All @@ -175,8 +176,8 @@ HNSW_BatchIterator<DataType, DistType>::scanGraph(VecSimQueryReply_Code *rc) {
if (this->getResultsCount() == 0 && this->top_candidates_extras.empty() &&
this->candidates.empty()) {
if (!index->isMarkedDeleted(this->entry_point)) {
this->lower_bound = this->index->calcDistance(
this->getQueryBlob(), this->index->getDataByInternalId(this->entry_point));
this->lower_bound = this->index->calcDistanceForQuery(
this->index->getDataByInternalId(this->entry_point), this->getQueryBlob());
} else {
this->lower_bound = std::numeric_limits<DistType>::max();
}
Expand Down
3 changes: 2 additions & 1 deletion src/VecSim/algorithms/hnsw/hnsw_multi.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Copyright (c) 2006-Present, Redis Ltd.
* All rights reserved.
* SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates <open-source-office@arm.com>
*
* Licensed under your choice of the Redis Source Available License 2.0
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
Expand Down Expand Up @@ -153,7 +154,7 @@ double HNSWIndex_Multi<DataType, DistType>::getDistanceFromInternal(labelType la

// Iterate over the ids and find the minimum distance.
for (auto id : IDs) {
DistType d = this->calcDistance(this->getDataByInternalId(id), vector_data);
DistType d = this->calcDistanceForQuery(this->getDataByInternalId(id), vector_data);
dist = std::fmin(dist, d);
}

Expand Down
3 changes: 2 additions & 1 deletion src/VecSim/algorithms/hnsw/hnsw_single.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Copyright (c) 2006-Present, Redis Ltd.
* All rights reserved.
* SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates <open-source-office@arm.com>
*
* Licensed under your choice of the Redis Source Available License 2.0
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
Expand Down Expand Up @@ -129,7 +130,7 @@ HNSWIndex_Single<DataType, DistType>::getDistanceFromInternal(labelType label,
}
idType id = it->second;

return this->calcDistance(vector_data, this->getDataByInternalId(id));
return this->calcDistanceForQuery(this->getDataByInternalId(id), vector_data);
}

template <typename DataType, typename DistType>
Expand Down
31 changes: 26 additions & 5 deletions src/VecSim/spaces/computer/calculator.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Copyright (c) 2006-Present, Redis Ltd.
* All rights reserved.
* SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates <open-source-office@arm.com>
*
* Licensed under your choice of the Redis Source Available License 2.0
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
Expand All @@ -24,8 +25,13 @@ class IndexCalculatorInterface : public VecsimBaseObject {

virtual DistType calcDistance(const void *v1, const void *v2, size_t dim) const = 0;

virtual DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector,
size_t dim) const = 0;

// Raw distance function; cached by the index to skip the vtable on the hot path.
virtual spaces::dist_func_t<DistType> getDistFunc() const = 0;

virtual spaces::dist_func_t<DistType> getDistFuncForQuery() const = 0;
};

/**
Expand All @@ -39,26 +45,41 @@ class IndexCalculatorInterface : public VecsimBaseObject {
template <typename DistType, typename DistFuncType>
class DistanceCalculatorInterface : public IndexCalculatorInterface<DistType> {
public:
DistanceCalculatorInterface(std::shared_ptr<VecSimAllocator> allocator, DistFuncType dist_func)
: IndexCalculatorInterface<DistType>(allocator), dist_func(dist_func) {}
DistanceCalculatorInterface(std::shared_ptr<VecSimAllocator> allocator, DistFuncType dist_func,
DistFuncType query_dist_func = nullptr)
: IndexCalculatorInterface<DistType>(allocator), dist_func(dist_func),
query_dist_func(query_dist_func ? query_dist_func : dist_func) {}
virtual DistType calcDistance(const void *v1, const void *v2, size_t dim) const = 0;
virtual DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector,
size_t dim) const = 0;

protected:
DistFuncType dist_func;
DistFuncType query_dist_func;
};

template <typename DistType>
class DistanceCalculatorCommon
: public DistanceCalculatorInterface<DistType, spaces::dist_func_t<DistType>> {
public:
DistanceCalculatorCommon(std::shared_ptr<VecSimAllocator> allocator,
spaces::dist_func_t<DistType> dist_func)
: DistanceCalculatorInterface<DistType, spaces::dist_func_t<DistType>>(allocator,
dist_func) {}
spaces::dist_func_t<DistType> dist_func,
spaces::dist_func_t<DistType> query_dist_func = nullptr)
: DistanceCalculatorInterface<DistType, spaces::dist_func_t<DistType>>(allocator, dist_func,
query_dist_func) {}

DistType calcDistance(const void *v1, const void *v2, size_t dim) const override {
return this->dist_func(v1, v2, dim);
}

DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector,
size_t dim) const override {
return this->query_dist_func(candidate_vector, query_vector, dim);
}

spaces::dist_func_t<DistType> getDistFunc() const override { return this->dist_func; }

spaces::dist_func_t<DistType> getDistFuncForQuery() const override {
return this->query_dist_func;
}
};
22 changes: 21 additions & 1 deletion src/VecSim/vec_sim_index.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Copyright (c) 2006-Present, Redis Ltd.
* All rights reserved.
* SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates <open-source-office@arm.com>
*
* Licensed under your choice of the Redis Source Available License 2.0
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
Expand Down Expand Up @@ -87,7 +88,9 @@ struct VecSimIndexAbstract : public VecSimIndexInterface {
private:
IndexCalculatorInterface<DistType> *indexCalculator; // Distance calculator.
spaces::dist_func_t<DistType> cachedDistFunc; // Cached dist func, used on the hot path.
PreprocessorsContainerAbstract *preprocessors; // Storage and query preprocessors.
spaces::dist_func_t<DistType>
cachedDistFuncForQuery; // Cached dist func for query, used on the hot path.
PreprocessorsContainerAbstract *preprocessors; // Storage and query preprocessors.

size_t inputBlobSize; // The size of input vectors/queries blob in bytes. May differ from dim *
// sizeof(vecType) when vectors have been externally preprocessed (e.g.,
Expand Down Expand Up @@ -128,6 +131,9 @@ struct VecSimIndexAbstract : public VecSimIndexInterface {
indexCalculator(components.indexCalculator),
cachedDistFunc(components.indexCalculator ? components.indexCalculator->getDistFunc()
: nullptr),
cachedDistFuncForQuery(components.indexCalculator
? components.indexCalculator->getDistFuncForQuery()
: nullptr),
preprocessors(components.preprocessors), inputBlobSize(params.inputBlobSize),
storedDataSize(params.storedDataSize) {
assert(VecSimType_sizeof(vecType));
Expand Down Expand Up @@ -168,6 +174,20 @@ struct VecSimIndexAbstract : public VecSimIndexInterface {
return cachedDistFunc(vector_data1, vector_data2, this->dim);
}

/**
* @brief Calculate the distance between a stored candidate vector and a query vector.
* Allows asymmetric distance computation (e.g., for quantized stored vectors).
*
* @note Precondition: @c cachedDistFuncForQuery must be non-null. Subclasses that construct
* this index with a null @c indexCalculator (e.g. SVS, which uses its own
* internal distance kernels) must not call this method.
*
* @return the distance between the candidate and the query.
*/
DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector) const {
return cachedDistFuncForQuery(candidate_vector, query_vector, this->dim);
}

/**
* @brief Preprocess a blob for both storage and query.
*
Expand Down
37 changes: 30 additions & 7 deletions tests/unit/test_components.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,58 @@ class IndexCalculatorTest : public ::testing::Test {};
namespace dummyCalcultor {

using DummyType = int;
using dummy_dist_func_t = DummyType (*)(int);
using dummy_dist_func_t = DummyType (*)(int, int);

int dummyDistFunc(int value) { return value; }
int dummyDistFunc(int v1, int v2) { return v1 + v2; }
int dummyQueryDistFunc(int candidate, int query) { return candidate - query; }

template <typename DistType>
class DistanceCalculatorDummy : public DistanceCalculatorInterface<DistType, dummy_dist_func_t> {
public:
DistanceCalculatorDummy(std::shared_ptr<VecSimAllocator> allocator, dummy_dist_func_t dist_func)
: DistanceCalculatorInterface<DistType, dummy_dist_func_t>(allocator, dist_func) {}
DistanceCalculatorDummy(std::shared_ptr<VecSimAllocator> allocator, dummy_dist_func_t dist_func,
dummy_dist_func_t query_dist_func = nullptr)
: DistanceCalculatorInterface<DistType, dummy_dist_func_t>(allocator, dist_func,
query_dist_func) {}

virtual DistType calcDistance(const void *v1, const void *v2, size_t dim) const {
return this->dist_func(7);
int v1_int = *static_cast<const int *>(v1);
int v2_int = *static_cast<const int *>(v2);
return this->dist_func(v1_int, v2_int);
}

virtual DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector,
size_t dim) const {
int candidate_int = *static_cast<const int *>(candidate_vector);
int query_int = *static_cast<const int *>(query_vector);
return this->query_dist_func(candidate_int, query_int);
}

// Dummy uses a non-standard dist func signature, so the standard slot is unavailable.
spaces::dist_func_t<DistType> getDistFunc() const override { return nullptr; }
spaces::dist_func_t<DistType> getDistFuncForQuery() const override { return nullptr; }
};

} // namespace dummyCalcultor

TEST(IndexCalculatorTest, TestIndexCalculator) {
int v1 = 20, v2 = 10;

std::shared_ptr<VecSimAllocator> allocator = VecSimAllocator::newVecsimAllocator();

// Test computer with a distance function signature different from dim(v1, v2, dim()).
using namespace dummyCalcultor;
auto distance_calculator = DistanceCalculatorDummy<DummyType>(allocator, dummyDistFunc);

ASSERT_EQ(distance_calculator.calcDistance(nullptr, nullptr, 0), 7);
ASSERT_EQ(distance_calculator.calcDistance(&v1, &v2, 0), 30);
ASSERT_EQ(distance_calculator.calcDistance(&v2, &v1, 0), 30);
ASSERT_EQ(distance_calculator.calcDistanceForQuery(&v1, &v2, 0), 30);
ASSERT_EQ(distance_calculator.calcDistanceForQuery(&v2, &v1, 0), 30);

auto asymmetric_distance_calculator =
DistanceCalculatorDummy<DummyType>(allocator, dummyDistFunc, dummyQueryDistFunc);
ASSERT_EQ(asymmetric_distance_calculator.calcDistance(&v1, &v2, 0), 30);
ASSERT_EQ(asymmetric_distance_calculator.calcDistance(&v2, &v1, 0), 30);
ASSERT_EQ(asymmetric_distance_calculator.calcDistanceForQuery(&v1, &v2, 0), 10);
ASSERT_EQ(asymmetric_distance_calculator.calcDistanceForQuery(&v2, &v1, 0), -10);
}

class PreprocessorsTest : public ::testing::Test {};
Expand Down
7 changes: 5 additions & 2 deletions tests/unit/test_hnsw_tiered.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Copyright (c) 2006-Present, Redis Ltd.
* All rights reserved.
* SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates <open-source-office@arm.com>
*
* Licensed under your choice of the Redis Source Available License 2.0
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
Expand Down Expand Up @@ -4382,7 +4383,8 @@ TYPED_TEST(HNSWTieredIndexTestBasic, HNSWWithPreprocessor) {
expected_score = frontend_index->calcDistance(normalized_query, normalized_vec);
}
if (label == 1) { // the result in the hnsw index
expected_score = hnsw_index->calcDistance(norm_double_query, norm_double_vec);
expected_score =
hnsw_index->calcDistanceForQuery(norm_double_vec, norm_double_query);
}
ASSERT_EQ(score, expected_score) << "label: " << label;
};
Expand All @@ -4408,7 +4410,8 @@ TYPED_TEST(HNSWTieredIndexTestBasic, HNSWWithPreprocessor) {
// Both vector were processed by the hnsw preprocessor, as well as the query.
// The score should be the distance from the doubled query to the doubled vector.
auto verify_res = [&](size_t label, double score, size_t result_rank) {
double expected_score = hnsw_index->calcDistance(norm_double_query, norm_double_vec);
double expected_score =
hnsw_index->calcDistanceForQuery(norm_double_vec, norm_double_query);
ASSERT_EQ(score, expected_score) << "label: " << label;
};

Expand Down