From ba8b2729c9c722b534bf1906963e09b5b9d9b6ea Mon Sep 17 00:00:00 2001 From: meiravgri <109056284+meiravgri@users.noreply.github.com> Date: Mon, 1 Aug 2022 09:08:55 +0300 Subject: [PATCH] resizing to align blocksize (#177) * modified resize logic of BF addvector so that the ids2vectors mapping size fits blockSize * fixed increasing count in case od re-using id, update brute_force_reindexing_same_vector test * clang format * HNSWIndex::addVector aligns the capacity to block size, HNSWIndex::deleteVector in case an entire block is removed, the capacity is aligned to blocksize and a decreased by block size, added test for resize cases in hnswlib * make format * update size estimation test * fixed after Alon's review * clang format * cahnged hnsw::remove point to return false in case not found, also changed HNSWIndex::delete vector to return false in this case (before checking if resizing is required) - this covers (hopedully) also empty index case and resizing only when actual deletion happend * added tests for empty HNSW index * added isLabelExists to hnswlib and modified test of id overrrides * hnsw::removepoint return void, checking if label exists happens only in the wrrapper * clang format * update test to removed checking if element count is bigger than max_ellements from addPoint (cant happen, we check this in addVector) * override test intial size * remove if exists in addvector update * Update src/VecSim/algorithms/hnsw/hnsw_wrapper.cpp Co-authored-by: alonre24 * Update tests/unit/test_hnswlib.cpp Co-authored-by: alonre24 * Update tests/unit/test_hnswlib.cpp Co-authored-by: alonre24 * removed redudent tests after alon's review * fixed tests * Update src/VecSim/algorithms/brute_force/brute_force.cpp Co-authored-by: DvirDukhan * Update tests/unit/test_hnswlib.cpp Co-authored-by: DvirDukhan * empty index and capacity = 0 tests. removed todo from hnsw_wrapper Co-authored-by: alonre24 Co-authored-by: DvirDukhan --- .../algorithms/brute_force/brute_force.cpp | 34 +-- src/VecSim/algorithms/hnsw/hnsw_wrapper.cpp | 50 +++- src/VecSim/algorithms/hnsw/hnswlib.h | 23 +- tests/unit/test_bruteforce.cpp | 103 +++++--- tests/unit/test_hnswlib.cpp | 228 +++++++++++++++--- 5 files changed, 333 insertions(+), 105 deletions(-) diff --git a/src/VecSim/algorithms/brute_force/brute_force.cpp b/src/VecSim/algorithms/brute_force/brute_force.cpp index 8a1dd6a2c..4977ce84e 100644 --- a/src/VecSim/algorithms/brute_force/brute_force.cpp +++ b/src/VecSim/algorithms/brute_force/brute_force.cpp @@ -85,16 +85,22 @@ int BruteForceIndex::addVector(const void *vector_data, size_t label) { if (this->deletedIds.size() != 0) { id = *this->deletedIds.begin(); this->deletedIds.erase(this->deletedIds.begin()); - this->count++; } else { - id = this->count++; + id = count; + // Save current id2vec size. + size_t ids_mapping_size = idToVectorBlockMemberMapping.size(); + + // If it's full - resize the index to be a multiplication of block size. + if (id >= ids_mapping_size) { + size_t last_block_vectors_count = count % vectorBlockSize; + this->idToVectorBlockMemberMapping.resize(ids_mapping_size + vectorBlockSize - + last_block_vectors_count); + } } } - // See if new id is bigger than current vector count. Needs to resize the index. - if (id >= this->idToVectorBlockMemberMapping.size()) { - this->idToVectorBlockMemberMapping.resize(std::ceil(this->count * 1.1)); - } + // Anyway - increse count. + ++count; // Get vector block to store the vector in. VectorBlock *vectorBlock; @@ -127,7 +133,7 @@ int BruteForceIndex::deleteVector(size_t label) { idType id; auto optionalId = this->labelToIdLookup.find(label); if (optionalId == this->labelToIdLookup.end()) { - // Nothing to delete; + // Nothing to delete. return true; } else { id = optionalId->second; @@ -142,21 +148,21 @@ int BruteForceIndex::deleteVector(size_t label) { VectorBlockMember *lastVectorBlockMember = lastVectorBlock->getMember(lastVectorBlock->getLength() - 1); - // Swap the last vector with the deleted vector; + // Swap the last vector with the deleted vector. vectorBlock->setMember(vectorIndex, lastVectorBlockMember); float *destination = vectorBlock->getVector(vectorIndex); float *origin = lastVectorBlock->removeAndFetchVector(); memmove(destination, origin, sizeof(float) * this->dim); - // Delete the vector block membership + // Delete the vector block membership. delete vectorBlockMember; this->idToVectorBlockMemberMapping[id] = NULL; // Add deleted id to reusable ids. this->deletedIds.emplace(id); this->labelToIdLookup.erase(label); - // If the last vector block is emtpy; + // If the last vector block is emtpy. if (lastVectorBlock->getLength() == 0) { delete lastVectorBlock; this->vectorBlocks.pop_back(); @@ -206,7 +212,7 @@ VecSimQueryResult_List BruteForceIndex::topKQuery(const void *queryBlob, size_t void *timeoutCtx = queryParams ? queryParams->timeoutCtx : NULL; this->last_mode = STANDARD_KNN; - float normalized_blob[this->dim]; // This will be use only if metric == VecSimMetric_Cosine + float normalized_blob[this->dim]; // This will be use only if metric == VecSimMetric_Cosine. if (this->metric == VecSimMetric_Cosine) { // TODO: need more generic memcpy(normalized_blob, queryBlob, this->dim * sizeof(float)); @@ -216,7 +222,7 @@ VecSimQueryResult_List BruteForceIndex::topKQuery(const void *queryBlob, size_t float upperBound = std::numeric_limits::lowest(); vecsim_stl::max_priority_queue> TopCandidates(this->allocator); - // For every block, compute its vectors scores and update the Top candidates max heap + // For every block, compute its vectors scores and update the Top candidates max heap. for (auto vectorBlock : this->vectorBlocks) { auto scores = computeBlockScores(vectorBlock, queryBlob, timeoutCtx, &rl.code); if (VecSim_OK != rl.code) { @@ -256,7 +262,7 @@ VecSimQueryResult_List BruteForceIndex::rangeQuery(const void *queryBlob, float void *timeoutCtx = queryParams ? queryParams->timeoutCtx : nullptr; this->last_mode = RANGE_QUERY; - float normalized_blob[this->dim]; // This will be use only if metric == VecSimMetric_Cosine + float normalized_blob[this->dim]; // This will be use only if metric == VecSimMetric_Cosine. if (this->metric == VecSimMetric_Cosine) { // TODO: need more generic when other types will be supported. memcpy(normalized_blob, queryBlob, this->dim * sizeof(float)); @@ -299,7 +305,7 @@ VecSimIndexInfo BruteForceIndex::info() const { VecSimInfoIterator *BruteForceIndex::infoIterator() { VecSimIndexInfo info = this->info(); - // For readability. Update this number when needed; + // For readability. Update this number when needed. size_t numberOfInfoFields = 8; VecSimInfoIterator *infoIterator = new VecSimInfoIterator(numberOfInfoFields); diff --git a/src/VecSim/algorithms/hnsw/hnsw_wrapper.cpp b/src/VecSim/algorithms/hnsw/hnsw_wrapper.cpp index fb375829d..8d4d5a4f6 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_wrapper.cpp +++ b/src/VecSim/algorithms/hnsw/hnsw_wrapper.cpp @@ -37,7 +37,7 @@ size_t HNSWIndex::estimateInitialSize(const HNSWParams *params) { sizeof(size_t); est += sizeof(*hnsw) + sizeof(size_t); est += sizeof(VisitedNodesHandler) + sizeof(size_t); - // used for synchronization only when parallel indexing / searching is enabled. + // Used for synchronization only when parallel indexing / searching is enabled. #ifdef ENABLE_PARALLELIZATION est += sizeof(VisitedNodesHandlerPool); #endif @@ -46,7 +46,7 @@ size_t HNSWIndex::estimateInitialSize(const HNSWParams *params) { est += sizeof(void *) * params->initialCapacity + sizeof(size_t); // link lists (for levels > 0) est += sizeof(size_t) * params->initialCapacity + sizeof(size_t); // element level est += sizeof(size_t) * params->initialCapacity + - sizeof(size_t); // labels lookup hash table buckets + sizeof(size_t); // Labels lookup hash table buckets. size_t size_links_level0 = sizeof(linklistsizeint) + params->M * 2 * sizeof(tableint) + sizeof(void *); @@ -92,16 +92,24 @@ size_t HNSWIndex::estimateElementMemory(const HNSWParams *params) { } int HNSWIndex::addVector(const void *vector_data, size_t id) { + + // If id already exists remove and re-add + if (this->hnsw->isLabelExist(id)) { + this->hnsw->removePoint(id); + } + try { - float normalized_data[this->dim]; // This will be use only if metric == VecSimMetric_Cosine + float normalized_data[this->dim]; // This will be use only if metric == VecSimMetric_Cosine. if (this->metric == VecSimMetric_Cosine) { // TODO: need more generic memcpy(normalized_data, vector_data, this->dim * sizeof(float)); float_vector_normalize(normalized_data, this->dim); vector_data = normalized_data; } - if (hnsw->getIndexSize() == this->hnsw->getIndexCapacity()) { - this->hnsw->resizeIndex(this->hnsw->getIndexCapacity() + this->blockSize); + size_t index_capacity = this->hnsw->getIndexCapacity(); + if (hnsw->getIndexSize() == index_capacity) { + size_t vectors_to_add = blockSize - index_capacity % blockSize; + this->hnsw->resizeIndex(index_capacity + vectors_to_add); } this->hnsw->addPoint(vector_data, id); return true; @@ -111,11 +119,29 @@ int HNSWIndex::addVector(const void *vector_data, size_t id) { } int HNSWIndex::deleteVector(size_t id) { - bool res = this->hnsw->removePoint(id); - if (hnsw->getIndexSize() + this->blockSize <= this->hnsw->getIndexCapacity()) { - this->hnsw->resizeIndex(this->hnsw->getIndexCapacity() - this->blockSize); + + // If id doesnt exist. + if (!this->hnsw->isLabelExist(id)) { + return false; } - return res; + + // Else, *delete* it from the graph. + this->hnsw->removePoint(id); + + size_t index_size = hnsw->getIndexSize(); + size_t curr_capacity = this->hnsw->getIndexCapacity(); + + // If we need to free a complete block & there is a least one block between the + // capacity and the size. + if (index_size % blockSize == 0 && index_size + blockSize <= curr_capacity) { + + // Check if the capacity is aligned to block size. + size_t extra_space_to_free = curr_capacity % blockSize; + + // Remove one block from the capacity. + this->hnsw->resizeIndex(curr_capacity - blockSize - extra_space_to_free); + } + return true; } double HNSWIndex::getDistanceFrom(size_t label, const void *vector_data) { @@ -132,7 +158,7 @@ VecSimQueryResult_List HNSWIndex::topKQuery(const void *query_data, size_t k, void *timeoutCtx = nullptr; try { this->last_mode = STANDARD_KNN; - float normalized_data[this->dim]; // This will be use only if metric == VecSimMetric_Cosine + float normalized_data[this->dim]; // This will be use only if metric == VecSimMetric_Cosine. if (this->metric == VecSimMetric_Cosine) { // TODO: need more generic memcpy(normalized_data, query_data, this->dim * sizeof(float)); @@ -158,7 +184,7 @@ VecSimQueryResult_List HNSWIndex::topKQuery(const void *query_data, size_t k, VecSimQueryResult_SetScore(rl.results[i], knn_res.top().first); knn_res.pop(); } - // Restore efRuntime + // Restore efRuntime. hnsw->setEf(originalEF); assert(hnsw->getEf() == originalEF); @@ -204,7 +230,7 @@ VecSimBatchIterator *HNSWIndex::newBatchIterator(const void *queryBlob, VecSimInfoIterator *HNSWIndex::infoIterator() { VecSimIndexInfo info = this->info(); - // For readability. Update this number when needed; + // For readability. Update this number when needed. size_t numberOfInfoFields = 12; VecSimInfoIterator *infoIterator = new VecSimInfoIterator(numberOfInfoFields); diff --git a/src/VecSim/algorithms/hnsw/hnswlib.h b/src/VecSim/algorithms/hnsw/hnswlib.h index 321ae810c..0db6c8255 100644 --- a/src/VecSim/algorithms/hnsw/hnswlib.h +++ b/src/VecSim/algorithms/hnsw/hnswlib.h @@ -160,13 +160,14 @@ class HierarchicalNSW : public VecsimBaseObject { linklistsizeint *get_linklist_at_level(tableint internal_id, size_t level) const; unsigned short int getListCount(const linklistsizeint *ptr) const; void resizeIndex(size_t new_max_elements); - bool removePoint(labeltype label); + void removePoint(labeltype label); void addPoint(const void *data_point, labeltype label); dist_t getDistanceByLabelFromPoint(labeltype label, const void *data_point); tableint searchBottomLayerEP(const void *query_data, void *timeoutCtx, VecSimQueryResult_Code *rc) const; vecsim_stl::max_priority_queue> searchKnn(const void *query_data, size_t k, void *timeoutCtx, VecSimQueryResult_Code *rc) const; + bool isLabelExist(labeltype label); }; /** @@ -322,6 +323,10 @@ VisitedNodesHandler *HierarchicalNSW::getVisitedList() const { return visited_nodes_handler.get(); } +template +bool HierarchicalNSW::isLabelExist(labeltype label) { + return (label_lookup_.find(label) != label_lookup_.end()); +} /** * helper functions */ @@ -945,11 +950,8 @@ void HierarchicalNSW::resizeIndex(size_t new_max_elements) { } template -bool HierarchicalNSW::removePoint(const labeltype label) { - // check that the label actually exists in the graph, and update the number of elements. - if (label_lookup_.find(label) == label_lookup_.end()) { - return true; - } +void HierarchicalNSW::removePoint(const labeltype label) { + tableint element_internal_id = label_lookup_[label]; vecsim_stl::vector neighbours_bitmap(allocator); @@ -1029,7 +1031,6 @@ bool HierarchicalNSW::removePoint(const labeltype label) { } else { SwapLastIdWithDeletedId(element_internal_id, last_element_internal_id); } - return true; } template @@ -1041,13 +1042,7 @@ void HierarchicalNSW::addPoint(const void *data_point, const labeltype l #ifdef ENABLE_PARALLELIZATION std::unique_lock templock_curr(cur_element_count_guard_); #endif - // Checking if an element with the given label already exists. if so, remove it. - if (label_lookup_.find(label) != label_lookup_.end()) { - removePoint(label); - } - if (cur_element_count >= max_elements_) { - throw std::runtime_error("The number of elements exceeds the specified limit"); - } + cur_c = max_id = cur_element_count++; label_lookup_[label] = cur_c; } diff --git a/tests/unit/test_bruteforce.cpp b/tests/unit/test_bruteforce.cpp index 79629bc14..06e278943 100644 --- a/tests/unit/test_bruteforce.cpp +++ b/tests/unit/test_bruteforce.cpp @@ -38,11 +38,13 @@ TEST_F(BruteForceTest, brute_force_vector_add_test) { TEST_F(BruteForceTest, resizeIndex) { size_t dim = 4; size_t n = 15; + size_t blockSize = 10; VecSimParams params{.algo = VecSimAlgo_BF, .bfParams = BFParams{.type = VecSimType_FLOAT32, .dim = dim, .metric = VecSimMetric_L2, - .initialCapacity = n}}; + .initialCapacity = n, + .blockSize = blockSize}}; VecSimIndex *index = VecSimIndex_New(¶ms); ASSERT_EQ(VecSimIndex_IndexSize(index), 0); @@ -55,12 +57,45 @@ TEST_F(BruteForceTest, resizeIndex) { } ASSERT_EQ(reinterpret_cast(index)->idToVectorBlockMemberMapping.size(), n); + // remove invalid id + VecSimIndex_DeleteVector(index, 3459); + + // This should do nothing + ASSERT_EQ(VecSimIndex_IndexSize(index), n); + ASSERT_EQ(reinterpret_cast(index)->idToVectorBlockMemberMapping.size(), n); // Add another vector, since index size equals to the capacity, this should cause resizing - // (by 10% factor from the new index size). + // (to fit a multiplication of block_size). VecSimIndex_AddVector(index, (const void *)a, n + 1); ASSERT_EQ(VecSimIndex_IndexSize(index), n + 1); + // Check alignment. + ASSERT_EQ(reinterpret_cast(index)->idToVectorBlockMemberMapping.size() % + blockSize, + 0); + // Check new capacity size, should be blockSize * 2. + ASSERT_EQ(reinterpret_cast(index)->idToVectorBlockMemberMapping.size(), + 2 * blockSize); + + // Now size = n + 1 = 16, capacity = 2* bs = 20. Test capacity overflow again + // to check that it stays aligned with blocksize. + + size_t add_vectors_count = 8; + for (size_t i = 0; i < add_vectors_count; i++) { + for (size_t j = 0; j < dim; j++) { + a[j] = (float)i; + } + VecSimIndex_AddVector(index, (const void *)a, n + 2 + i); + } + + // Size should be n + 1 + 8 = 24. + ASSERT_EQ(VecSimIndex_IndexSize(index), n + 1 + add_vectors_count); + // Check alignment of the capacity + ASSERT_EQ(reinterpret_cast(index)->idToVectorBlockMemberMapping.size() % + blockSize, + 0); + // Check new capacity size, should be blockSize * 3. ASSERT_EQ(reinterpret_cast(index)->idToVectorBlockMemberMapping.size(), - std::ceil(1.1 * (n + 1))); + 3 * blockSize); + VecSimIndex_Free(index); } @@ -173,7 +208,7 @@ TEST_F(BruteForceTest, brute_force_indexing_same_vector) { for (size_t i = 0; i < n; i++) { float f[dim]; for (size_t j = 0; j < dim; j++) { - f[j] = (float)(i / 10); // i / 10 is in integer (take the "floor" value) + f[j] = (float)(i / 10); // i / 10 is in integer (take the "floor" value). } VecSimIndex_AddVector(index, (const void *)f, i); } @@ -222,7 +257,7 @@ TEST_F(BruteForceTest, brute_force_reindexing_same_vector) { } ASSERT_EQ(VecSimIndex_IndexSize(index), 0); - // Reinsert the same vectors under the same ids + // Reinsert the same vectors under the same ids. for (size_t i = 0; i < n; i++) { float f[dim]; for (size_t j = 0; j < dim; j++) { @@ -232,7 +267,7 @@ TEST_F(BruteForceTest, brute_force_reindexing_same_vector) { } ASSERT_EQ(VecSimIndex_IndexSize(index), n); - // Run the same query again + // Run the same query again. runTopKSearchTest(index, query, k, verify_res); VecSimIndex_Free(index); @@ -271,7 +306,7 @@ TEST_F(BruteForceTest, brute_force_reindexing_same_vector_different_id) { } ASSERT_EQ(VecSimIndex_IndexSize(index), 0); - // Reinsert the same vectors under different ids than before + // Reinsert the same vectors under different ids than before. for (size_t i = 0; i < n; i++) { float f[dim]; for (size_t j = 0; j < dim; j++) { @@ -281,7 +316,7 @@ TEST_F(BruteForceTest, brute_force_reindexing_same_vector_different_id) { } ASSERT_EQ(VecSimIndex_IndexSize(index), n); - // Run the same query again + // Run the same query again. auto verify_res_different_id = [&](int id, float score, size_t index) { ASSERT_TRUE(id >= 60 && id < 70 && score <= 1); }; @@ -347,7 +382,7 @@ TEST_F(BruteForceTest, sanity_rinsert_1280) { auto *vectors = (float *)malloc(n * d * sizeof(float)); - // Generate random vectors in every iteration and inert them under different ids + // Generate random vectors in every iteration and inert them under different ids. for (size_t iter = 1; iter <= 3; iter++) { for (size_t i = 0; i < n; i++) { for (size_t j = 0; j < d; j++) { @@ -381,7 +416,7 @@ TEST_F(BruteForceTest, test_bf_info) { size_t n = 100; size_t d = 128; - // Build with default args + // Build with default args. VecSimParams params = { .algo = VecSimAlgo_BF, .bfParams = BFParams{ @@ -390,7 +425,7 @@ TEST_F(BruteForceTest, test_bf_info) { VecSimIndexInfo info = VecSimIndex_Info(index); ASSERT_EQ(info.algo, VecSimAlgo_BF); ASSERT_EQ(info.bfInfo.dim, d); - // Default args + // Default args. ASSERT_EQ(info.bfInfo.blockSize, DEFAULT_BLOCK_SIZE); ASSERT_EQ(info.bfInfo.indexSize, 0); VecSimIndex_Free(index); @@ -406,7 +441,7 @@ TEST_F(BruteForceTest, test_bf_info) { info = VecSimIndex_Info(index); ASSERT_EQ(info.algo, VecSimAlgo_BF); ASSERT_EQ(info.bfInfo.dim, d); - // User args + // User args. ASSERT_EQ(info.bfInfo.blockSize, 1); ASSERT_EQ(info.bfInfo.indexSize, 0); VecSimIndex_Free(index); @@ -418,7 +453,7 @@ TEST_F(BruteForceTest, test_basic_bf_info_iterator) { VecSimMetric metrics[3] = {VecSimMetric_Cosine, VecSimMetric_IP, VecSimMetric_L2}; for (size_t i = 0; i < 3; i++) { - // Build with default args + // Build with default args. VecSimParams params{ .algo = VecSimAlgo_BF, .bfParams = BFParams{ @@ -603,7 +638,7 @@ TEST_F(BruteForceTest, brute_force_search_empty_index) { float query[] = {50, 50, 50, 50}; - // We do not expect any results + // We do not expect any results. VecSimQueryResult_List res = VecSimIndex_TopKQuery(index, (const void *)query, k, NULL, BY_SCORE); ASSERT_EQ(VecSimQueryResult_Len(res), 0); @@ -630,7 +665,7 @@ TEST_F(BruteForceTest, brute_force_search_empty_index) { } ASSERT_EQ(VecSimIndex_IndexSize(index), 0); - // Again - we do not expect any results + // Again - we do not expect any results. res = VecSimIndex_TopKQuery(index, (const void *)query, k, NULL, BY_SCORE); ASSERT_EQ(VecSimQueryResult_Len(res), 0); it = VecSimQueryResult_List_GetIterator(res); @@ -701,7 +736,7 @@ TEST_F(BruteForceTest, brute_force_remove_vector_after_replacing_block) { } ASSERT_EQ(VecSimIndex_IndexSize(index), n); - // After deleting the first vector, the second one will be moved to the first block + // After deleting the first vector, the second one will be moved to the first block. for (size_t i = 0; i < n; i++) { VecSimIndex_DeleteVector(index, i); } @@ -730,7 +765,7 @@ TEST_F(BruteForceTest, brute_force_zero_minimal_capacity) { } ASSERT_EQ(VecSimIndex_IndexSize(index), n); - // After deleting the first vector, the second one will be moved to the first block + // After deleting the first vector, the second one will be moved to the first block. for (size_t i = 0; i < n; i++) { VecSimIndex_DeleteVector(index, i); } @@ -764,7 +799,7 @@ TEST_F(BruteForceTest, brute_force_batch_iterator) { } ASSERT_EQ(VecSimIndex_IndexSize(index), n); - // query for (n,n,...,n) vector (recall that n is the largest id in te index) + // Query for (n,n,...,n) vector (recall that n is the largest id in te index). float query[dim]; for (size_t j = 0; j < dim; j++) { query[j] = (float)n; @@ -772,7 +807,7 @@ TEST_F(BruteForceTest, brute_force_batch_iterator) { VecSimBatchIterator *batchIterator = VecSimBatchIterator_New(index, query, nullptr); size_t iteration_num = 0; - // get the 10 vectors whose ids are the maximal among those that hasn't been returned yet, + // Get the 10 vectors whose ids are the maximal among those that hasn't been returned yet, // in every iteration. The order should be from the largest to the lowest id. size_t n_res = 5; while (VecSimBatchIterator_HasNext(batchIterator)) { @@ -803,7 +838,7 @@ TEST_F(BruteForceTest, brute_force_batch_iterator_non_unique_scores) { .blockSize = 5}}; VecSimIndex *index = VecSimIndex_New(¶ms); - // run the test twice - for index of size 100, every iteration will run select-based search, + // Run the test twice - for index of size 100, every iteration will run select-based search, // as the number of results is 5, which is more than 0.1% of the index size. for index of size // 10000, we will run the heap-based search until we return 5000 results, and then switch to // select-based search. @@ -817,7 +852,7 @@ TEST_F(BruteForceTest, brute_force_batch_iterator_non_unique_scores) { } ASSERT_EQ(VecSimIndex_IndexSize(index), n); - // query for (n,n,...,n) vector (recall that n is the largest id in te index) + // Query for (n,n,...,n) vector (recall that n is the largest id in te index). float query[dim]; for (size_t j = 0; j < dim; j++) { query[j] = (float)n; @@ -825,13 +860,13 @@ TEST_F(BruteForceTest, brute_force_batch_iterator_non_unique_scores) { VecSimBatchIterator *batchIterator = VecSimBatchIterator_New(index, query, nullptr); size_t iteration_num = 0; - // get the 5 vectors whose ids are the maximal among those that hasn't been returned yet, in + // Get the 5 vectors whose ids are the maximal among those that hasn't been returned yet, in // every iteration. there are n/10 groups of 10 different vectors with the same score. size_t n_res = 5; bool even_iteration = false; std::set expected_ids; while (VecSimBatchIterator_HasNext(batchIterator)) { - // insert the maximal 10 ids in every odd iteration + // Insert the maximal 10 ids in every odd iteration. if (!even_iteration) { for (size_t i = 1; i <= 2 * n_res; i++) { expected_ids.insert(n - iteration_num * n_res - i); @@ -842,7 +877,7 @@ TEST_F(BruteForceTest, brute_force_batch_iterator_non_unique_scores) { expected_ids.erase(id); }; runBatchIteratorSearchTest(batchIterator, n_res, verify_res); - // make sure that the expected ids set is empty after two iterations. + // Make sure that the expected ids set is empty after two iterations. if (even_iteration) { ASSERT_TRUE(expected_ids.empty()); } @@ -876,14 +911,14 @@ TEST_F(BruteForceTest, brute_force_batch_iterator_reset) { } ASSERT_EQ(VecSimIndex_IndexSize(index), n); - // query for (n,n,...,n) vector (recall that n is the largest id in te index) + // Query for (n,n,...,n) vector (recall that n is the largest id in te index). float query[dim]; for (size_t j = 0; j < dim; j++) { query[j] = (float)n; } VecSimBatchIterator *batchIterator = VecSimBatchIterator_New(index, query, nullptr); - // get the 100 vectors whose ids are the maximal among those that hasn't been returned yet, in + // Get the 100 vectors whose ids are the maximal among those that hasn't been returned yet, in // every iteration. run this flow for 5 times, each time for 10 iteration, and reset the // iterator. size_t n_res = 100; @@ -924,7 +959,7 @@ TEST_F(BruteForceTest, brute_force_batch_iterator_corner_cases) { .initialCapacity = n}}; VecSimIndex *index = VecSimIndex_New(¶ms); - // query for (n,n,...,n) vector (recall that n is the largest id in te index) + // Query for (n,n,...,n) vector (recall that n is the largest id in te index) float query[dim]; for (size_t j = 0; j < dim; j++) { query[j] = (float)n; @@ -932,11 +967,11 @@ TEST_F(BruteForceTest, brute_force_batch_iterator_corner_cases) { // Create batch iterator for empty index. VecSimBatchIterator *batchIterator = VecSimBatchIterator_New(index, query, nullptr); - // try to get more results even though there are no. + // Try to get more results even though there are no. VecSimQueryResult_List res = VecSimBatchIterator_Next(batchIterator, 1, BY_SCORE); ASSERT_EQ(VecSimQueryResult_Len(res), 0); VecSimQueryResult_Free(res); - // retry to get results. + // Retry to get results. res = VecSimBatchIterator_Next(batchIterator, 1, BY_SCORE); ASSERT_EQ(VecSimQueryResult_Len(res), 0); VecSimQueryResult_Free(res); @@ -958,7 +993,7 @@ TEST_F(BruteForceTest, brute_force_batch_iterator_corner_cases) { ASSERT_EQ(VecSimQueryResult_Len(res), 0); VecSimQueryResult_Free(res); - // get all in first iteration, expect to use select search. + // Get all in first iteration, expect to use select search. size_t n_res = n; auto verify_res = [&](size_t id, float score, size_t index) { ASSERT_TRUE(id == n - 1 - index); @@ -966,7 +1001,7 @@ TEST_F(BruteForceTest, brute_force_batch_iterator_corner_cases) { runBatchIteratorSearchTest(batchIterator, n_res, verify_res); ASSERT_FALSE(VecSimBatchIterator_HasNext(batchIterator)); - // try to get more results even though there are no. + // Try to get more results even though there are no. res = VecSimBatchIterator_Next(batchIterator, n_res, BY_SCORE); ASSERT_EQ(VecSimQueryResult_Len(res), 0); VecSimQueryResult_Free(res); @@ -1108,7 +1143,7 @@ TEST_F(BruteForceTest, preferAdHocOptimization) { for (size_t index_size : {1000, 6000, 600000}) { for (size_t dim : {4, 80, 350, 780}) { - // create index and check for the expected output of "prefer ad-hoc" + // Create index and check for the expected output of "prefer ad-hoc". VecSimParams params{.algo = VecSimAlgo_BF, .bfParams = BFParams{.type = VecSimType_FLOAT32, .dim = dim, @@ -1174,7 +1209,7 @@ TEST_F(BruteForceTest, batchIteratorSwapIndices) { } ASSERT_EQ(VecSimIndex_IndexSize(index), n); - // query for (1,1,1,1) vector. + // Query for (1,1,1,1) vector. float query[dim]; query[0] = query[1] = query[2] = query[3] = 1.0; VecSimBatchIterator *batchIterator = VecSimBatchIterator_New(index, query, nullptr); @@ -1416,7 +1451,7 @@ TEST_F(BruteForceTest, rangeQuery) { } ASSERT_EQ(VecSimIndex_IndexSize(index), n); - size_t pivot_id = n / 2; // the id to return vectors around it. + size_t pivot_id = n / 2; // The id to return vectors around it. float query[] = {(float)pivot_id, (float)pivot_id, (float)pivot_id, (float)pivot_id}; // Validate invalid params are caught with runtime exception. diff --git a/tests/unit/test_hnswlib.cpp b/tests/unit/test_hnswlib.cpp index 063391fa1..0ed745ef8 100644 --- a/tests/unit/test_hnswlib.cpp +++ b/tests/unit/test_hnswlib.cpp @@ -39,11 +39,13 @@ TEST_F(HNSWLibTest, hnswlib_vector_add_test) { ASSERT_EQ(VecSimIndex_IndexSize(index), 1); VecSimIndex_Free(index); } +/**** resizing cases ****/ -TEST_F(HNSWLibTest, resizeIndex) { +// Add up to capacity. +TEST_F(HNSWLibTest, resizeNAlignIndex) { size_t dim = 4; - size_t n = 15; - size_t bs = 50; + size_t n = 10; + size_t bs = 3; VecSimParams params{.algo = VecSimAlgo_HNSWLIB, .hnswParams = HNSWParams{.type = VecSimType_FLOAT32, .dim = dim, @@ -54,19 +56,181 @@ TEST_F(HNSWLibTest, resizeIndex) { ASSERT_EQ(VecSimIndex_IndexSize(index), 0); float a[dim]; + + // Add up to n. + for (size_t i = 0; i < n; i++) { + for (size_t j = 0; j < dim; j++) { + a[j] = (float)i; + } + VecSimIndex_AddVector(index, (const void *)a, i); + } + // The size and the capacity should be equal. + ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), + VecSimIndex_IndexSize(index)); + // The capcity shouldnt be changed. + ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), n); + + // Add another vector to exceed the initial capacity. + VecSimIndex_AddVector(index, (const void *)a, n); + + // The capacity should be now aligned with the block size. + // bs = 3, size = 11 -> capacity = 12 + // New capacity = intiailcapacity + blockSize - intiailcapacity % blockSize. + ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), + n + bs - n % bs); + VecSimIndex_Free(index); +} + +// Case 1: initial cpapcity is larger than block size, and it is not aligned. +TEST_F(HNSWLibTest, resizeNAlignIndex_largeInitialCapacity) { + size_t dim = 4; + size_t n = 10; + size_t bs = 3; + VecSimParams params{.algo = VecSimAlgo_HNSWLIB, + .hnswParams = HNSWParams{.type = VecSimType_FLOAT32, + .dim = dim, + .metric = VecSimMetric_L2, + .initialCapacity = n, + .blockSize = bs}}; + VecSimIndex *index = VecSimIndex_New(¶ms); + ASSERT_EQ(VecSimIndex_IndexSize(index), 0); + + float a[dim]; + + // add up to blocksize + 1 = 3 + 1 = 4 + for (size_t i = 0; i < bs + 1; i++) { + for (size_t j = 0; j < dim; j++) { + a[j] = (float)i; + } + VecSimIndex_AddVector(index, (const void *)a, i); + } + + // The capcity shouldnt change, should remain n. + ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), n); + + // Delete last vector, to get size % block_size == 0. size = 3 + VecSimIndex_DeleteVector(index, bs); + + // Index size = bs = 3. + ASSERT_EQ(VecSimIndex_IndexSize(index), bs); + + // New capacity = intiailcapacity - block_size - number_of_vectors_to_align = + // 10 - 3 - 10 % 3 (1) = 6 + size_t curr_capacity = reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(); + ASSERT_EQ(curr_capacity, n - bs - n % bs); + + // Delete all the vectors to decrease capacity by another bs. + size_t i = 0; + while (VecSimIndex_IndexSize(index) > 0) { + VecSimIndex_DeleteVector(index, i); + ++i; + } + ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), bs); + // Add and delete a vector to acheive: + // size % block_size == 0 && size + bs <= capacity(3). + // the capacity should be resized to zero + VecSimIndex_AddVector(index, (const void *)a, 0); + VecSimIndex_DeleteVector(index, 0); + ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), 0); + + // Do it again. This time after adding a vector the capacity is increased by bs. + // Upon deletion it will be resized to zero again. + VecSimIndex_AddVector(index, (const void *)a, 0); + ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), bs); + VecSimIndex_DeleteVector(index, 0); + ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), 0); + + VecSimIndex_Free(index); +} + +// Case 2: initial cpapcity is smaller than block_size. +TEST_F(HNSWLibTest, resizeNAlignIndex_largerBlockSize) { + size_t dim = 4; + size_t n = 4; + size_t bs = 6; + VecSimParams params{.algo = VecSimAlgo_HNSWLIB, + .hnswParams = HNSWParams{.type = VecSimType_FLOAT32, + .dim = dim, + .metric = VecSimMetric_L2, + .initialCapacity = n, + .blockSize = bs}}; + VecSimIndex *index = VecSimIndex_New(¶ms); + ASSERT_EQ(VecSimIndex_IndexSize(index), 0); + + float a[dim]; + + // Add up to initial capacity. for (size_t i = 0; i < n; i++) { for (size_t j = 0; j < dim; j++) { a[j] = (float)i; } VecSimIndex_AddVector(index, (const void *)a, i); } + + // The capcity shouldnt change. ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), n); - // Add another vector, since index size equals to the capacity, this should cause resizing - // by the block size parameter. - VecSimIndex_AddVector(index, (const void *)a, n + 1); + // Size equals capacity. + ASSERT_EQ(VecSimIndex_IndexSize(index), n); + + // Add another vector - > the capacity is increased to a multiplication of block_size. + VecSimIndex_AddVector(index, (const void *)a, n); + ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), bs); + + // Size increased by 1. ASSERT_EQ(VecSimIndex_IndexSize(index), n + 1); - ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), bs + n); + + // Delete random vector. + VecSimIndex_DeleteVector(index, 1); + + // The capacity should remain the same. + ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), bs); + + VecSimIndex_Free(index); +} + +// Test empty index edge cases. +TEST_F(HNSWLibTest, emptyIndex) { + size_t dim = 4; + size_t n = 20; + size_t bs = 6; + VecSimParams params{.algo = VecSimAlgo_HNSWLIB, + .hnswParams = HNSWParams{.type = VecSimType_FLOAT32, + .dim = dim, + .metric = VecSimMetric_L2, + .initialCapacity = n, + .blockSize = bs}}; + VecSimIndex *index = VecSimIndex_New(¶ms); + ASSERT_EQ(VecSimIndex_IndexSize(index), 0); + + // Try to remove from an empty index - should faul because label doesnt exist. + VecSimIndex_DeleteVector(index, 0); + + // Add one vector. + float a[dim]; + for (size_t j = 0; j < dim; j++) { + a[j] = (float)1.7; + } + + VecSimIndex_AddVector(index, (const void *)a, 1); + // Try to remove it. + VecSimIndex_DeleteVector(index, 1); + // The capacity should change to be aligned with the vector size. + + size_t new_capcaity = reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(); + ASSERT_EQ(new_capcaity, n - n % bs - bs); + + // Size equals 0. + ASSERT_EQ(VecSimIndex_IndexSize(index), 0); + + // Try to remove it again. + // The capacity should remain unchanged, as we are trying to delete a label that doesnt exist. + VecSimIndex_DeleteVector(index, 1); + ASSERT_EQ(reinterpret_cast(index)->getHNSWIndex()->getIndexCapacity(), + new_capcaity); + // Nor the size. + ASSERT_EQ(VecSimIndex_IndexSize(index), 0); + VecSimIndex_Free(index); } @@ -284,7 +448,7 @@ TEST_F(HNSWLibTest, sanity_rinsert_1280) { auto *vectors = (float *)malloc(n * d * sizeof(float)); - // Generate random vectors in every iteration and inert them under different ids + // Generate random vectors in every iteration and inert them under different ids. for (size_t iter = 1; iter <= 3; iter++) { for (size_t i = 0; i < n; i++) { for (size_t j = 0; j < d; j++) { @@ -330,7 +494,7 @@ TEST_F(HNSWLibTest, test_hnsw_info) { VecSimIndexInfo info = VecSimIndex_Info(index); ASSERT_EQ(info.algo, VecSimAlgo_HNSWLIB); ASSERT_EQ(info.hnswInfo.dim, d); - // Default args + // Default args. ASSERT_EQ(info.hnswInfo.blockSize, DEFAULT_BLOCK_SIZE); ASSERT_EQ(info.hnswInfo.M, HNSW_DEFAULT_M); ASSERT_EQ(info.hnswInfo.efConstruction, HNSW_DEFAULT_EF_C); @@ -352,7 +516,7 @@ TEST_F(HNSWLibTest, test_hnsw_info) { info = VecSimIndex_Info(index); ASSERT_EQ(info.algo, VecSimAlgo_HNSWLIB); ASSERT_EQ(info.hnswInfo.dim, d); - // User args + // User args. ASSERT_EQ(info.hnswInfo.blockSize, bs); ASSERT_EQ(info.hnswInfo.efConstruction, 1000); ASSERT_EQ(info.hnswInfo.M, 200); @@ -366,7 +530,7 @@ TEST_F(HNSWLibTest, test_basic_hnsw_info_iterator) { VecSimMetric metrics[3] = {VecSimMetric_Cosine, VecSimMetric_IP, VecSimMetric_L2}; for (size_t i = 0; i < 3; i++) { - // Build with default args + // Build with default args. VecSimParams params{ .algo = VecSimAlgo_HNSWLIB, .hnswParams = HNSWParams{ @@ -468,7 +632,7 @@ TEST_F(HNSWLibTest, test_query_runtime_params_default_build_args) { size_t d = 4; size_t k = 11; - // Build with default args + // Build with default args. VecSimParams params{.algo = VecSimAlgo_HNSWLIB, .hnswParams = HNSWParams{.type = VecSimType_FLOAT32, .dim = d, @@ -496,17 +660,17 @@ TEST_F(HNSWLibTest, test_query_runtime_params_default_build_args) { runTopKSearchTest(index, query, k, verify_res); VecSimIndexInfo info = VecSimIndex_Info(index); - // Check that default args did not change + // Check that default args did not change. ASSERT_EQ(info.hnswInfo.M, HNSW_DEFAULT_M); ASSERT_EQ(info.hnswInfo.efConstruction, HNSW_DEFAULT_EF_C); ASSERT_EQ(info.hnswInfo.efRuntime, HNSW_DEFAULT_EF_RT); - // Run same query again, set efRuntime to 300 + // Run same query again, set efRuntime to 300. VecSimQueryParams queryParams{.hnswRuntimeParams = HNSWRuntimeParams{.efRuntime = 300}}; runTopKSearchTest(index, query, k, verify_res, &queryParams); info = VecSimIndex_Info(index); - // Check that default args did not change + // Check that default args did not change. ASSERT_EQ(info.hnswInfo.M, HNSW_DEFAULT_M); ASSERT_EQ(info.hnswInfo.efConstruction, HNSW_DEFAULT_EF_C); ASSERT_EQ(info.hnswInfo.efRuntime, HNSW_DEFAULT_EF_RT); @@ -531,7 +695,7 @@ TEST_F(HNSWLibTest, test_query_runtime_params_user_build_args) { size_t M = 100; size_t efConstruction = 300; size_t efRuntime = 500; - // Build with default args + // Build with default args. VecSimParams params{.algo = VecSimAlgo_HNSWLIB, .hnswParams = HNSWParams{.type = VecSimType_FLOAT32, .dim = d, @@ -562,17 +726,17 @@ TEST_F(HNSWLibTest, test_query_runtime_params_user_build_args) { runTopKSearchTest(index, query, k, verify_res); VecSimIndexInfo info = VecSimIndex_Info(index); - // Check that user args did not change + // Check that user args did not change. ASSERT_EQ(info.hnswInfo.M, M); ASSERT_EQ(info.hnswInfo.efConstruction, efConstruction); ASSERT_EQ(info.hnswInfo.efRuntime, efRuntime); - // Run same query again, set efRuntime to 300 + // Run same query again, set efRuntime to 300. VecSimQueryParams queryParams{.hnswRuntimeParams = HNSWRuntimeParams{.efRuntime = 300}}; runTopKSearchTest(index, query, k, verify_res, &queryParams); info = VecSimIndex_Info(index); - // Check that user args did not change + // Check that user args did not change. ASSERT_EQ(info.hnswInfo.M, M); ASSERT_EQ(info.hnswInfo.efConstruction, efConstruction); ASSERT_EQ(info.hnswInfo.efRuntime, efRuntime); @@ -605,7 +769,7 @@ TEST_F(HNSWLibTest, hnsw_search_empty_index) { float query[] = {50, 50, 50, 50}; - // We do not expect any results + // We do not expect any results. VecSimQueryResult_List res = VecSimIndex_TopKQuery(index, (const void *)query, k, NULL, BY_SCORE); ASSERT_EQ(VecSimQueryResult_Len(res), 0); @@ -628,7 +792,7 @@ TEST_F(HNSWLibTest, hnsw_search_empty_index) { } ASSERT_EQ(VecSimIndex_IndexSize(index), 0); - // Again - we do not expect any results + // Again - we do not expect any results. res = VecSimIndex_TopKQuery(index, (const void *)query, k, NULL, BY_SCORE); ASSERT_EQ(VecSimQueryResult_Len(res), 0); it = VecSimQueryResult_List_GetIterator(res); @@ -748,6 +912,7 @@ TEST_F(HNSWLibTest, hnsw_override) { VecSimIndex *index = VecSimIndex_New(¶ms); ASSERT_TRUE(index != nullptr); + // Insert n == 100 vectors. for (size_t i = 0; i < n; i++) { float f[dim]; for (size_t j = 0; j < dim; j++) { @@ -766,6 +931,7 @@ TEST_F(HNSWLibTest, hnsw_override) { } VecSimIndex_AddVector(index, (const void *)f, i); } + float query[dim]; for (size_t j = 0; j < dim; j++) { query[j] = (float)n; @@ -1386,7 +1552,7 @@ TEST_F(HNSWLibTest, testCosine) { VecSimBatchIterator *batchIterator = VecSimBatchIterator_New(index, query, nullptr); size_t iteration_num = 0; - // get the 10 vectors whose ids are the maximal among those that hasn't been returned yet, + // Get the 10 vectors whose ids are the maximal among those that hasn't been returned yet, // in every iteration. The order should be from the largest to the lowest id. size_t n_res = 10; while (VecSimBatchIterator_HasNext(batchIterator)) { @@ -1447,7 +1613,7 @@ TEST_F(HNSWLibTest, testSizeEstimation) { } // Estimate the memory delta of adding a full new block. - estimation = VecSimIndex_EstimateElementSize(¶ms) * bs; + estimation = VecSimIndex_EstimateElementSize(¶ms) * (bs % n + bs); actual = 0; for (size_t i = 0; i < bs; i++) { @@ -1477,7 +1643,7 @@ TEST_F(HNSWLibTest, testTimeoutReturn) { VecSimIndex_AddVector(index, vec, 0); VecSim_SetTimeoutCallbackFunction([](void *ctx) { return 1; }); // Always times out - // Checks return code on timeout + // Checks return code on timeout. rl = VecSimIndex_TopKQuery(index, vec, 1, NULL, BY_ID); ASSERT_EQ(rl.code, VecSim_QueryResult_TimedOut); ASSERT_EQ(VecSimQueryResult_Len(rl), 0); @@ -1489,7 +1655,7 @@ TEST_F(HNSWLibTest, testTimeoutReturn) { while (VecSimIndex_Info(index).hnswInfo.max_level == 0) { VecSimIndex_AddVector(index, vec, next++); } - VecSim_SetTimeoutCallbackFunction([](void *ctx) { return 1; }); // Always times out + VecSim_SetTimeoutCallbackFunction([](void *ctx) { return 1; }); // Always times out. rl = VecSimIndex_TopKQuery(index, vec, 2, NULL, BY_ID); ASSERT_EQ(rl.code, VecSim_QueryResult_TimedOut); @@ -1497,7 +1663,7 @@ TEST_F(HNSWLibTest, testTimeoutReturn) { VecSimQueryResult_Free(rl); VecSimIndex_Free(index); - VecSim_SetTimeoutCallbackFunction([](void *ctx) { return 0; }); // cleanup + VecSim_SetTimeoutCallbackFunction([](void *ctx) { return 0; }); // Cleanup. } TEST_F(HNSWLibTest, testTimeoutReturn_batch_iterator) { @@ -1519,7 +1685,7 @@ TEST_F(HNSWLibTest, testTimeoutReturn_batch_iterator) { ASSERT_EQ(VecSimIndex_IndexSize(index), n); - // Fail on second batch (after some calculation already completed in the first one) + // Fail on second batch (after some calculation already completed in the first one). VecSimBatchIterator *batchIterator = VecSimBatchIterator_New(index, vec, nullptr); rl = VecSimBatchIterator_Next(batchIterator, 1, BY_ID); @@ -1527,7 +1693,7 @@ TEST_F(HNSWLibTest, testTimeoutReturn_batch_iterator) { ASSERT_NE(VecSimQueryResult_Len(rl), 0); VecSimQueryResult_Free(rl); - VecSim_SetTimeoutCallbackFunction([](void *ctx) { return 1; }); // Always times out + VecSim_SetTimeoutCallbackFunction([](void *ctx) { return 1; }); // Always times out. rl = VecSimBatchIterator_Next(batchIterator, 1, BY_ID); ASSERT_EQ(rl.code, VecSim_QueryResult_TimedOut); ASSERT_EQ(VecSimQueryResult_Len(rl), 0); @@ -1535,7 +1701,7 @@ TEST_F(HNSWLibTest, testTimeoutReturn_batch_iterator) { VecSimBatchIterator_Free(batchIterator); - // Fail on first batch (while calculating) + // Fail on first batch (while calculating). auto timeoutcb = [](void *ctx) { static size_t flag = 1; if (flag) { @@ -1561,7 +1727,7 @@ TEST_F(HNSWLibTest, testTimeoutReturn_batch_iterator) { while (VecSimIndex_Info(index).hnswInfo.max_level == 0) { VecSimIndex_AddVector(index, vec, next++); } - VecSim_SetTimeoutCallbackFunction([](void *ctx) { return 1; }); // Always times out + VecSim_SetTimeoutCallbackFunction([](void *ctx) { return 1; }); // Always times out. batchIterator = VecSimBatchIterator_New(index, vec, nullptr); rl = VecSimBatchIterator_Next(batchIterator, 2, BY_ID); @@ -1572,6 +1738,6 @@ TEST_F(HNSWLibTest, testTimeoutReturn_batch_iterator) { VecSimBatchIterator_Free(batchIterator); VecSimIndex_Free(index); - VecSim_SetTimeoutCallbackFunction([](void *ctx) { return 0; }); // cleanup + VecSim_SetTimeoutCallbackFunction([](void *ctx) { return 0; }); // Cleanup. } } // namespace hnswlib