Skip to content

Commit 5ccff2f

Browse files
committed
Fix: Replacement leaves isolated nodes
This patch addresses the issue #399, originally observed in the Swift layer. Reimplementing it in C++ helped locate the issue and lead to refactoring the `update` procedure in the lowest-layer `index_gt`. Now, `add` and `update` share less code. The `add` is one branch shorter (not that it would be noticeable), and `update` brings additional logic to avoid spilling `updated_slot` into top-results and consequently introducing self-loops. Closes #399
1 parent ffa5986 commit 5ccff2f

File tree

2 files changed

+227
-52
lines changed

2 files changed

+227
-52
lines changed

cpp/test.cpp

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ void expect(bool must_be_true) {
2626
__usearch_raise_runtime_error("Failed!");
2727
}
2828

29+
template <typename value_at> void expect_eq(value_at a, value_at b) { expect(a == b); }
30+
2931
/**
3032
* @brief Convinience wrapper combining combined allocation and construction of an index.
3133
*/
@@ -637,8 +639,71 @@ template <typename key_at, typename slot_at> void test_strings() {
637639
}
638640
}
639641

642+
/**
643+
* @brief Tests replacing and updating entries in index_dense_gt to ensure consistency after modifications.
644+
*/
645+
template <typename key_at, typename slot_at> void test_replacing_update() {
646+
647+
using vector_key_t = key_at;
648+
using slot_t = slot_at;
649+
650+
using index_punned_t = index_dense_gt<vector_key_t, slot_t>;
651+
metric_punned_t metric(1, metric_kind_t::l2sq_k, scalar_kind_t::f32_k);
652+
auto index_result = index_punned_t::make(metric);
653+
expect(bool(index_result));
654+
index_punned_t& index = index_result.index;
655+
656+
// Reserve space for 3 entries
657+
index.reserve(3);
658+
auto as_ptr = [](float v) {
659+
static float value;
660+
value = v;
661+
return &value;
662+
};
663+
664+
// Add 3 entries
665+
index.add(42, as_ptr(1.1f));
666+
index.add(43, as_ptr(2.1f));
667+
index.add(44, as_ptr(3.1f));
668+
expect_eq<std::size_t>(index.size(), 3);
669+
670+
// Assert initial state
671+
auto initial_search = index.search(as_ptr(1.0f), 3);
672+
expect_eq<std::size_t>(initial_search.size(), 3);
673+
expect_eq<vector_key_t>(initial_search[0].member.key, 42);
674+
expect_eq<vector_key_t>(initial_search[1].member.key, 43);
675+
expect_eq<vector_key_t>(initial_search[2].member.key, 44);
676+
677+
// Replace the second entry
678+
index.remove(43);
679+
index.add(43, as_ptr(2.2f));
680+
expect_eq<std::size_t>(index.size(), 3);
681+
682+
// Assert state after replacing second entry
683+
auto post_second_replacement = index.search(as_ptr(1.0f), 3);
684+
expect_eq<std::size_t>(post_second_replacement.size(), 3);
685+
expect_eq<vector_key_t>(post_second_replacement[0].member.key, 42);
686+
expect_eq<vector_key_t>(post_second_replacement[1].member.key, 43);
687+
expect_eq<vector_key_t>(post_second_replacement[2].member.key, 44);
688+
689+
// Replace the first entry
690+
index.remove(42);
691+
index.add(42, as_ptr(1.2f));
692+
expect_eq<std::size_t>(index.size(), 3);
693+
694+
// Assert state after replacing first entry
695+
auto final_search = index.search(as_ptr(1.0f), 3, 0);
696+
expect_eq<std::size_t>(final_search.size(), 3);
697+
expect_eq<vector_key_t>(final_search[0].member.key, 42);
698+
expect_eq<vector_key_t>(final_search[1].member.key, 43);
699+
expect_eq<vector_key_t>(final_search[2].member.key, 44);
700+
}
701+
640702
int main(int, char**) {
641703

704+
// Weird corner cases
705+
test_replacing_update<std::int64_t, std::uint32_t>();
706+
642707
// Exact search without constructing indexes.
643708
// Great for validating the distance functions.
644709
std::printf("Testing exact search\n");

0 commit comments

Comments
 (0)