@@ -157,6 +157,10 @@ class MutableVamanaIndex {
157157 std::vector<SlotMetadata> status_;
158158 size_t first_empty_ = 0 ;
159159 IDTranslator translator_;
160+ // Count of Valid slots. Maintained atomically in add_points/delete_entry.
161+ // Wrapped in unique_ptr because std::atomic is not movable.
162+ std::unique_ptr<std::atomic<size_t >> num_valid_{
163+ std::make_unique<std::atomic<size_t >>(0 )};
160164 // Protects translator access: exclusive for writes (add/consolidate/compact),
161165 // shared for reads (delete/search). Wrapped in unique_ptr for movability.
162166 std::unique_ptr<std::shared_mutex> translator_mutex_{
@@ -200,6 +204,7 @@ class MutableVamanaIndex {
200204 , status_(data_.size(), SlotMetadata::Valid)
201205 , first_empty_{data_.size ()}
202206 , translator_()
207+ , num_valid_{std::make_unique<std::atomic<size_t >>(data_.size ())}
203208 , distance_{std::move (distance_function)}
204209 , threadpool_{threads::as_threadpool (std::move (threadpool_proto))}
205210 , search_parameters_{vamana::construct_default_search_parameters (data_)}
@@ -227,6 +232,7 @@ class MutableVamanaIndex {
227232 , status_(data_.size(), SlotMetadata::Valid)
228233 , first_empty_{data_.size ()}
229234 , translator_()
235+ , num_valid_{std::make_unique<std::atomic<size_t >>(data_.size ())}
230236 , distance_(std::move(distance_function))
231237 , threadpool_(threads::as_threadpool(std::move(threadpool_proto)))
232238 , search_parameters_(vamana::construct_default_search_parameters(data_))
@@ -293,6 +299,7 @@ class MutableVamanaIndex {
293299 , status_{data_.size (), SlotMetadata::Valid}
294300 , first_empty_{data_.size ()}
295301 , translator_{std::move (translator)}
302+ , num_valid_{std::make_unique<std::atomic<size_t >>(data_.size ())}
296303 , distance_{distance_function}
297304 , threadpool_{std::move (threadpool)}
298305 , search_parameters_{config.search_parameters }
@@ -368,7 +375,15 @@ class MutableVamanaIndex {
368375 // /
369376 // / @brief Check whether the external ID `e` exists in the index.
370377 // /
371- bool has_id (size_t e) const { return translator_.has_external (e); }
378+ bool has_id (size_t e) const {
379+ if (!translator_.has_external (e)) {
380+ return false ;
381+ }
382+ // Check slot is not Deleted (deferred translator cleanup).
383+ auto internal = translator_.get_internal (e);
384+ return std::atomic_ref<const SlotMetadata>(status_[internal])
385+ .load (std::memory_order_acquire) == SlotMetadata::Valid;
386+ }
372387
373388 // /
374389 // / @brief Get the external ID mapped to be `i`.
@@ -390,8 +405,13 @@ class MutableVamanaIndex {
390405 // / each external ID in the index.
391406 // /
392407 template <typename F> void on_ids (F&& f) const {
408+ // Skip entries whose slot is Deleted (deferred translator cleanup).
393409 for (auto pair : translator_) {
394- f (pair.first );
410+ auto internal = pair.second ;
411+ if (std::atomic_ref<const SlotMetadata>(status_[internal])
412+ .load (std::memory_order_acquire) == SlotMetadata::Valid) {
413+ f (pair.first );
414+ }
395415 }
396416 }
397417
@@ -405,11 +425,7 @@ class MutableVamanaIndex {
405425 }
406426
407427 // / @brief Return the number of **valid** (non-deleted) entries in the index.
408- size_t size () const {
409- // NB: Index translation should always be kept in-sync with the number of valid
410- // elements.
411- return translator_.size ();
412- }
428+ size_t size () const { return num_valid_->load (std::memory_order_acquire); }
413429
414430 // /
415431 // / @brief Translate in-place a collection of internal IDs to external IDs.
@@ -734,6 +750,7 @@ class MutableVamanaIndex {
734750 std::atomic_ref<SlotMetadata>(status_[i])
735751 .store (SlotMetadata::Valid, std::memory_order_release);
736752 }
753+ num_valid_->fetch_add (slots.size (), std::memory_order_acq_rel);
737754
738755 return slots;
739756 }
@@ -778,10 +795,17 @@ class MutableVamanaIndex {
778795
779796 void delete_entry (size_t i) {
780797 auto & meta = getindex (status_, i);
781- // allow silent double-deletions, requred for concurent deletions
782- std::atomic_ref<SlotMetadata>(meta).store (
783- SlotMetadata::Deleted, std::memory_order_release
784- );
798+ // CAS Valid → Deleted. Only the thread that successfully transitions
799+ // decrements num_valid_; double-deletes silently no-op.
800+ SlotMetadata expected = SlotMetadata::Valid;
801+ if (std::atomic_ref<SlotMetadata>(meta).compare_exchange_strong (
802+ expected,
803+ SlotMetadata::Deleted,
804+ std::memory_order_acq_rel,
805+ std::memory_order_relaxed
806+ )) {
807+ num_valid_->fetch_sub (1 , std::memory_order_acq_rel);
808+ }
785809 }
786810
787811 bool is_deleted (size_t i) const { return status_[i] != SlotMetadata::Valid; }
0 commit comments