Repository navigation
Python: fix FAISS delete leaving stale vectors after removing several records - #14702
alexcastgonz (pcbeingused333) wants to merge 2 commits into
Conversation
… records remove_ids shifts every later vector down by one position, but the key map kept the old positions, so later deletes removed the wrong vectors. Update the remaining positions after each removal.
There was a problem hiding this comment.
🟡 Changes recommended
Deletion can corrupt key mappings when index removal fails and incorrectly renumber stable-ID indexes, leaving orphaned vectors.
2 open issues
What changed in this PR
This PR addresses #14695 by updating the Python FAISS connector’s key mappings after flat-index deletions, preventing subsequent deletes from targeting stale positions.
Changes:
- Decrement later mapped positions after each vector removal.
- Add regression tests for deleting all records and maintaining correct positions after partial deletes.
| File | Description |
|---|---|
python/tests/unit/connectors/memory/test_faiss.py |
Adds full and partial deletion regression tests. |
python/semantic_kernel/connectors/faiss.py |
Updates remaining key positions after vector deletion. |
Review effort: Balanced
| vector_index = key_map.pop(key) | ||
| self.indexes[vector_field].remove_ids(np.array([vector_index])) |
| # removing a vector shifts every later vector down by one position | ||
| for other_key, position in key_map.items(): | ||
| if position > vector_index: | ||
| key_map[other_key] = position - 1 |
…n-flat indexes Remove the vector from the index before dropping the key mapping. Only flat indexes compact ids on removal, which the key map relies on (positions in search, start offset in upsert), so deleting from other indexes (e.g. IVF, HNSW) is rejected before anything is mutated.
|
Thanks, both Copilot points were valid and are addressed in the new commit:
Both new tests fail against the previous revision; This comment was drafted with Claude Code. |
PRABHU KIRAN VANDRANKI (VANDRANKI)
left a comment
There was a problem hiding this comment.
I ran this on current main (df1cb33) and on main with this diff applied (python venv, faiss 1.15.1, real FaissCollection, no mocks). The diff applied cleanly.
Main reproduces the bug and it is worse than a stale count. remove_ids shifts later vectors down, and main kept the old positions in the key map, so the next delete in the same call or a later call removes the wrong vector. With 5 records a to e:
delete(["a", "b"]): main ends withntotal3 and the map{c: 2, d: 3, e: 4}. The PR ends with{c: 0, d: 1, e: 2}.delete("a")thendelete("c"): on main a search withc's own vector returnsd, and a search withd's vector returnsb. On the PR the surviving recordsb,d,eeach find themselves.deleteof all five in one call: main leavesntotalat 2 with an empty map. The PR leavesntotal0.
I then checked an invariant instead of single cases. After the deletes, for every key still in the map: the vector at its list position (the order search uses to turn a faiss position into a key) must be that record's own vector, ntotal must equal the key count, and a search with the record's vector must return that record. I used 6 records and 7 delete patterns, each with one and with two vector fields:
- main breaks the invariant in 4 of the 7:
["k0","k1"],"k0"then"k2"(a search withk3's vector returnsk1),["k1","k3","k4"](a search fork5raisesVectorSearchExecutionException: list index out of range), and deleting all six. - the other 3 are fine on main for data (deleting only the last record, deleting only
k2, deletingk0twice), though the last two leave stale positions in the map. - the PR holds the invariant in all 7, with one and with two vector fields.
Then 300 random sequences of deletes of 1 to 3 keys out of 6 records: 74 end consistent on main, 300 of 300 on the PR.
Non-flat indexes. This is a behavior change worth knowing about. With an IndexIVFFlat (trained), delete("a") on main returns without an error. Afterwards a search for e's vector raises list index out of range and a search for b's vector returns key c. On the PR the same delete("a") raises VectorStoreOperationException: Deleting records is only supported for flat indexes ... before anything is removed, and searches still return the right keys (e gives e, b gives b). With IndexHNSWFlat, main already raised (a wrapped faiss error, map left alone) and the PR raises the clearer message. So users with an IVF index who call delete now get an error where they used to get a silently broken index. I think that is the right call, and it may deserve a release note.
Tests. tests/unit/connectors/memory/test_faiss.py: 21 passed on main, 25 on the PR. If I copy the PR's test file onto main, 3 of the 4 new tests fail (delete_multiple_keeps_index_in_sync, delete_updates_remaining_positions, delete_rejected_for_index_that_preserves_ids). The HNSW test passes on main because main already raises there.
Limits. I did not test upserting an existing key and then deleting it, search with filters after deletes, GPU indexes, or any other index type.
Approving.

Fixes #14695
Motivation and Context
FaissCollection.deletewith several keys leaves stale vectors in the FAISS index: the repro in #14695 ends withntotal == 1and an empty key map, so later searches can return vectors for records that no longer exist.Root cause:
_inner_deleteremoves a vector withremove_ids, which shifts every later vector in a flat index down by one position, but the key map kept the old positions. Every delete after the first then targeted the wrong position.Description
After removing a key's vector, decrement the stored position of every key that sat after it in the same vector field's map. The change is limited to
_inner_deleteinpython/semantic_kernel/connectors/faiss.py.Tests added in
tests/unit/connectors/memory/test_faiss.py:test_delete_multiple_keeps_index_in_sync: the issue's scenario; index and key map are empty after deleting all records.test_delete_updates_remaining_positions: positions stay correct after partial deletes.Both fail on main and pass with the change; the rest of
test_faiss.pypasses, andruff format --check/ruff checkare clean on the touched files.Contribution Checklist
This change was prepared with the help of an AI coding assistant.