diff --git a/include/usearch/index_dense.hpp b/include/usearch/index_dense.hpp index b0d6766a..57dd475f 100644 --- a/include/usearch/index_dense.hpp +++ b/include/usearch/index_dense.hpp @@ -1050,17 +1050,18 @@ class index_dense_gt { */ bool try_reserve(index_limits_t limits) { + // Reserving is monotonic: reducing thread limits must not shrink member-addressed storage. + limits.members = (std::max)(limits.members, typed_->capacity()); + limits.members = (std::max)(limits.members, vectors_lookup_.size()); + // The slot lookup system will generally prefer power-of-two sizes. if (config_.enable_key_lookups) { unique_lock_t lock(slot_lookup_mutex_); if (!slot_lookup_.try_reserve(limits.members)) return false; - limits.members = slot_lookup_.capacity(); } - // Once the `slot_lookup_` grows, let's use its capacity as the new - // target for the `vectors_lookup_` to synchronize allocations and - // expensive index re-organizations. + // Hash-table load-factor slack does not need corresponding vector or graph slots. if (limits.members != vectors_lookup_.size()) { vectors_lookup_t new_vectors_lookup(limits.members); if (!new_vectors_lookup) diff --git a/rust/lib.rs b/rust/lib.rs index 4bac26fc..6c6e3304 100644 --- a/rust/lib.rs +++ b/rust/lib.rs @@ -2200,6 +2200,28 @@ mod tests { assert_eq!(index.size(), 0); } + #[test] + fn reserve_uses_requested_member_capacity_and_never_shrinks() { + let options = IndexOptions { + dimensions: 4, + metric: MetricKind::IP, + quantization: ScalarKind::F32, + ..Default::default() + }; + let index = Index::new(&options).unwrap(); + + index.reserve(10).unwrap(); + assert_eq!(index.capacity(), 10); + + index.reserve(4).unwrap(); + assert_eq!(index.capacity(), 10); + + for key in 0..10 { + index.add(key, &[key as f32, 0.0, 0.0, 0.0]).unwrap(); + } + assert_eq!(index.size(), 10); + } + #[test] fn stats_variants() { let options = IndexOptions {