From aca812532d8a280e9300df1ac2767838135ee8ef Mon Sep 17 00:00:00 2001 From: Zhang Xiangze Date: Mon, 20 Apr 2026 09:38:01 +0000 Subject: [PATCH 1/2] IndexCalculatorInterface change Add calcDistanceForQuery() API and use it in HNSW index --- src/VecSim/algorithms/hnsw/hnsw.h | 21 ++++++------ .../algorithms/hnsw/hnsw_batch_iterator.h | 13 ++++---- src/VecSim/algorithms/hnsw/hnsw_multi.h | 3 +- src/VecSim/algorithms/hnsw/hnsw_single.h | 3 +- src/VecSim/spaces/computer/calculator.h | 33 ++++++++++++++++--- src/VecSim/vec_sim_index.h | 22 ++++++++++++- tests/unit/test_components.cpp | 7 ++++ tests/unit/test_hnsw_tiered.cpp | 7 ++-- 8 files changed, 83 insertions(+), 26 deletions(-) diff --git a/src/VecSim/algorithms/hnsw/hnsw.h b/src/VecSim/algorithms/hnsw/hnsw.h index 2510ddbfb..ffccf32f7 100644 --- a/src/VecSim/algorithms/hnsw/hnsw.h +++ b/src/VecSim/algorithms/hnsw/hnsw.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates * * 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 @@ -544,7 +545,7 @@ void HNSWIndex::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); @@ -572,7 +573,7 @@ void HNSWIndex::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); @@ -629,7 +630,7 @@ void HNSWIndex::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); @@ -647,7 +648,7 @@ void HNSWIndex::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); @@ -674,7 +675,7 @@ HNSWIndex::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); @@ -1219,7 +1220,7 @@ void HNSWIndex::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; @@ -1557,7 +1558,7 @@ void HNSWIndex::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(global_max_level); level > static_cast(element_max_level); level--) { // this is done for the levels which are above the max level @@ -1878,7 +1879,7 @@ idType HNSWIndex::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(query_data, level, curr_element, cur_dist, timeoutCtx, rc); } @@ -1901,7 +1902,7 @@ HNSWIndex::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); @@ -2009,7 +2010,7 @@ VecSimQueryResultContainer HNSWIndex::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. diff --git a/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h b/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h index 77e44738d..6be783446 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h +++ b/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates * * 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 @@ -138,8 +139,8 @@ VecSimQueryReply_Code HNSW_BatchIterator::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); } @@ -150,8 +151,8 @@ VecSimQueryReply_Code HNSW_BatchIterator::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); } @@ -175,8 +176,8 @@ HNSW_BatchIterator::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::max(); } diff --git a/src/VecSim/algorithms/hnsw/hnsw_multi.h b/src/VecSim/algorithms/hnsw/hnsw_multi.h index 50ff1a37d..348aadfc5 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_multi.h +++ b/src/VecSim/algorithms/hnsw/hnsw_multi.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates * * 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 @@ -153,7 +154,7 @@ double HNSWIndex_Multi::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); } diff --git a/src/VecSim/algorithms/hnsw/hnsw_single.h b/src/VecSim/algorithms/hnsw/hnsw_single.h index 61899a142..8955ec179 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_single.h +++ b/src/VecSim/algorithms/hnsw/hnsw_single.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates * * 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 @@ -129,7 +130,7 @@ HNSWIndex_Single::getDistanceFromInternal(labelType label, } idType id = it->second; - return this->calcDistance(vector_data, this->getDataByInternalId(id)); + return this->calcDistanceForQuery(this->getDataByInternalId(id), vector_data); } template diff --git a/src/VecSim/spaces/computer/calculator.h b/src/VecSim/spaces/computer/calculator.h index 539325d8e..cac550666 100644 --- a/src/VecSim/spaces/computer/calculator.h +++ b/src/VecSim/spaces/computer/calculator.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates * * 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 @@ -24,8 +25,15 @@ class IndexCalculatorInterface : public VecsimBaseObject { virtual DistType calcDistance(const void *v1, const void *v2, size_t dim) const = 0; + // Used for stored candidate distance vs query vector. + // Default implementation delegates to calcDistance for backward compatibility. + 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 getDistFunc() const = 0; + + virtual spaces::dist_func_t getDistFuncForQuery() const = 0; }; /** @@ -39,12 +47,17 @@ class IndexCalculatorInterface : public VecsimBaseObject { template class DistanceCalculatorInterface : public IndexCalculatorInterface { public: - DistanceCalculatorInterface(std::shared_ptr allocator, DistFuncType dist_func) - : IndexCalculatorInterface(allocator), dist_func(dist_func) {} + DistanceCalculatorInterface(std::shared_ptr allocator, DistFuncType dist_func, + DistFuncType query_dist_func = nullptr) + : IndexCalculatorInterface(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 @@ -52,13 +65,23 @@ class DistanceCalculatorCommon : public DistanceCalculatorInterface> { public: DistanceCalculatorCommon(std::shared_ptr allocator, - spaces::dist_func_t dist_func) - : DistanceCalculatorInterface>(allocator, - dist_func) {} + spaces::dist_func_t dist_func, + spaces::dist_func_t query_dist_func = nullptr) + : DistanceCalculatorInterface>(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 getDistFunc() const override { return this->dist_func; } + + spaces::dist_func_t getDistFuncForQuery() const override { + return this->query_dist_func; + } }; diff --git a/src/VecSim/vec_sim_index.h b/src/VecSim/vec_sim_index.h index 38d47a2ca..049f6ca4b 100644 --- a/src/VecSim/vec_sim_index.h +++ b/src/VecSim/vec_sim_index.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates * * 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 @@ -87,7 +88,9 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { private: IndexCalculatorInterface *indexCalculator; // Distance calculator. spaces::dist_func_t cachedDistFunc; // Cached dist func, used on the hot path. - PreprocessorsContainerAbstract *preprocessors; // Storage and query preprocessors. + spaces::dist_func_t + 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., @@ -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)); @@ -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. * diff --git a/tests/unit/test_components.cpp b/tests/unit/test_components.cpp index eb465fa92..40c097a7c 100644 --- a/tests/unit/test_components.cpp +++ b/tests/unit/test_components.cpp @@ -34,8 +34,14 @@ class DistanceCalculatorDummy : public DistanceCalculatorInterfacedist_func(7); } + virtual DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector, + size_t dim) const { + return this->query_dist_func(7); + } + // Dummy uses a non-standard dist func signature, so the standard slot is unavailable. spaces::dist_func_t getDistFunc() const override { return nullptr; } + spaces::dist_func_t getDistFuncForQuery() const override { return nullptr; } }; } // namespace dummyCalcultor @@ -49,6 +55,7 @@ TEST(IndexCalculatorTest, TestIndexCalculator) { auto distance_calculator = DistanceCalculatorDummy(allocator, dummyDistFunc); ASSERT_EQ(distance_calculator.calcDistance(nullptr, nullptr, 0), 7); + ASSERT_EQ(distance_calculator.calcDistanceForQuery(nullptr, nullptr, 0), 7); } class PreprocessorsTest : public ::testing::Test {}; diff --git a/tests/unit/test_hnsw_tiered.cpp b/tests/unit/test_hnsw_tiered.cpp index b64885b97..3bbb2f141 100644 --- a/tests/unit/test_hnsw_tiered.cpp +++ b/tests/unit/test_hnsw_tiered.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates * * 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 @@ -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; }; @@ -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; }; From 2586df94ccfb16dd8cf65718571321a2fbc5822e Mon Sep 17 00:00:00 2001 From: Zhang Xiangze Date: Tue, 14 Jul 2026 03:54:46 +0000 Subject: [PATCH 2/2] Improve tests --- src/VecSim/spaces/computer/calculator.h | 2 -- tests/unit/test_components.cpp | 34 ++++++++++++++++++------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/VecSim/spaces/computer/calculator.h b/src/VecSim/spaces/computer/calculator.h index cac550666..3dc66aa1a 100644 --- a/src/VecSim/spaces/computer/calculator.h +++ b/src/VecSim/spaces/computer/calculator.h @@ -25,8 +25,6 @@ class IndexCalculatorInterface : public VecsimBaseObject { virtual DistType calcDistance(const void *v1, const void *v2, size_t dim) const = 0; - // Used for stored candidate distance vs query vector. - // Default implementation delegates to calcDistance for backward compatibility. virtual DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector, size_t dim) const = 0; diff --git a/tests/unit/test_components.cpp b/tests/unit/test_components.cpp index 40c097a7c..718a0c16a 100644 --- a/tests/unit/test_components.cpp +++ b/tests/unit/test_components.cpp @@ -20,23 +20,30 @@ 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 class DistanceCalculatorDummy : public DistanceCalculatorInterface { public: - DistanceCalculatorDummy(std::shared_ptr allocator, dummy_dist_func_t dist_func) - : DistanceCalculatorInterface(allocator, dist_func) {} + DistanceCalculatorDummy(std::shared_ptr allocator, dummy_dist_func_t dist_func, + dummy_dist_func_t query_dist_func = nullptr) + : DistanceCalculatorInterface(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(v1); + int v2_int = *static_cast(v2); + return this->dist_func(v1_int, v2_int); } virtual DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector, size_t dim) const { - return this->query_dist_func(7); + int candidate_int = *static_cast(candidate_vector); + int query_int = *static_cast(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. @@ -47,15 +54,24 @@ class DistanceCalculatorDummy : public DistanceCalculatorInterface allocator = VecSimAllocator::newVecsimAllocator(); // Test computer with a distance function signature different from dim(v1, v2, dim()). using namespace dummyCalcultor; auto distance_calculator = DistanceCalculatorDummy(allocator, dummyDistFunc); - - ASSERT_EQ(distance_calculator.calcDistance(nullptr, nullptr, 0), 7); - ASSERT_EQ(distance_calculator.calcDistanceForQuery(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(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 {};