TurboVec Vector Deletion: How Swap-Remove Enables Fast O(1) Removal Without Preserving Order
TurboVec does not preserve insertion order during vector deletion; instead, it performs an O(1) swap-remove that moves the last vector into the deleted slot and updates internal id tables.
TurboVec is an approximate-nearest-neighbor library that stores quantized vectors positionally inside a TurboQuantIndex for fast search. When you delete a vector through the high-level IdMapIndex API, the implementation trades sequence stability for constant-time performance. Understanding how TurboVec handles vector deletion requires a close look at the swap-remove logic in RyanCodrai/turbovec.
The Swap-Remove Mechanism in TurboVec
Inside the core index, vectors are held in a flat positional array. When a deletion request arrives, the index does not shift subsequent elements forward. Instead, it invokes a swap-remove primitive on the underlying storage.
This operation copies the vector currently occupying the last slot into the now-vacant deleted slot, then truncates the storage by one. Because the moved element originates from the end of the array, the original ordering of the remaining vectors is destroyed.
How IdMapIndex::remove Works
The IdMapIndex wrapper in turbovec/src/id_map.rs orchestrates the deletion while keeping external id mappings consistent. The method executes four discrete steps:
-
Resolve the slot. It looks up the internal slot index for the given external
idin theid_to_slotmap. If the id is absent, it returnsfalseimmediately. -
Swap-remove the vector. It calls
self.inner.swap_remove(slot)on the innerTurboQuantIndex, which returns the former last index. -
Remap the id tables. If the deleted slot was not the last position, it retrieves the id of the vector that was moved, writes that id into
slot_to_id[slot], and updatesid_to_slotto point to the new slot. -
Shrink the slot list. It pops the final entry from
slot_to_id, dropping the duplicate trailing record.
All four steps are O(1), giving IdMapIndex::remove constant-time complexity regardless of index size.
Source Code Breakdown
The implementation in turbovec/src/id_map.rs is concise and explicitly relies on swap_remove:
/// Remove the vector with the given external id.
/// Returns `true` if the id was present and removed, `false` otherwise.
/// O(1) via the inner [`TurboQuantIndex::swap_remove`].
pub fn remove(&mut self, id: u64) -> bool {
let Some(slot) = self.id_to_slot.remove(&id) else {
return false;
};
let last = self.slot_to_id.len() - 1;
// Perform the swap-remove on the inner index.
let moved_from = self.inner.swap_remove(slot);
debug_assert_eq!(moved_from, last);
// Mirror the swap-and-pop in our id tables.
if slot != last {
let moved_id = self.slot_to_id[last];
self.slot_to_id[slot] = moved_id;
self.id_to_slot.insert(moved_id, slot);
}
self.slot_to_id.pop();
true
}
The debug_assert_eq!(moved_from, last) confirms the invariant that the inner index always pulls from the final slot. After the swap, the bidirectional maps (slot_to_id and id_to_slot) are patched so that the moved vector’s external id resolves to its new position.
Observing Non-Deterministic Order After Deletion
You can verify this behavior with a small Rust program that inserts three vectors and deletes the middle one:
use turbovec::IdMapIndex;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create an index for 768-dimensional vectors, 4-bit quantization.
let mut idx = IdMapIndex::new(768, 4)?;
// Add three vectors with stable external IDs.
let vectors = vec![0.0_f32; 768 * 3];
idx.add_with_ids(&vectors, &[10, 20, 30])?;
// Delete the vector with id 20.
assert!(idx.remove(20));
// The remaining vectors are now in slots 0 and 1, but the
// original order (10, 30) is not guaranteed – the vector that
// was at the last slot (id 30) may have been swapped into slot 1.
println!("Current ids: {:?}", idx.slot_to_id);
Ok(())
}
Running this snippet typically prints slot_to_id with id 30 occupying slot 1, proving that TurboVec deletion is order-agnostic. The documentation in turbovec/src/lib.rs and the test suite in turbovec/tests/swap_remove.rs reinforce this contract.
Preserving Insertion Order Outside TurboVec
If your application requires strict insertion order after deletes, TurboVec’s API does not provide a linear-removal mode. You have two alternative paths:
- Rebuild the index. Collect the surviving ids in order, create a fresh
IdMapIndex, and re-add the vectors sequentially. - Use external buffering. Maintain an ordered
Vecof ids outside TurboVec and treat the index as an unordered pool.
Both strategies trade the O(1) deletion speed of TurboVec for sequence stability.
Performance Characteristics
- Time complexity:
IdMapIndex::removeruns in O(1) time because hash-map lookups and a single swap-remove are constant-time operations. - Space complexity: The id tables
slot_to_idandid_to_slotshrink by one entry, so no extra allocations are required. - Order guarantee: None. After deletion, the element at the final index is relocated into the vacated slot.
Summary
- TurboVec deletes vectors using a swap-remove strategy inside
TurboQuantIndex. - The
IdMapIndex::removemethod inturbovec/src/id_map.rscompletes in O(1) time by swapping the last vector into the deleted slot. - External id mappings are patched in place via
slot_to_idandid_to_slot. - Insertion order is not preserved after a deletion.
- Applications that need ordered deletion must rebuild the index or handle ordering outside of TurboVec.
Frequently Asked Questions
Does TurboVec preserve insertion order when deleting vectors?
No. TurboVec uses swap-remove semantics, so the vector at the last slot is moved into the position of the deleted vector. This destroys the original insertion sequence to achieve O(1) performance.
What is the time complexity of IdMapIndex::remove?
IdMapIndex::remove is O(1). It performs a constant-time hash-map lookup, a single swap_remove on the inner index, and updates the bidirectional id tables in place.
How does TurboVec update id mappings during a deletion?
After the inner index swaps the last vector into the deleted slot, IdMapIndex::remove reads the moved vector’s id from the end of slot_to_id, writes it into the vacated slot, and updates id_to_slot so that future lookups resolve correctly. Finally it pops the trailing entry from slot_to_id.
Can I force TurboVec to maintain order during deletion?
No, TurboVec does not expose an ordered removal API. If order must be kept, you should rebuild the index with the surviving vectors in the desired sequence, or manage an ordered layer outside of the index.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →