Repository navigation
fix(core): let update_index choose passage IDs for unclaimed chunks - #428
Merged
ASuresh0524 merged 3 commits intoOct 9, 2026
Conversation
add_text() stamps str(len(self.chunks)) -- this builder's own insertion position -- into every chunk without an explicit ID, and update_index() then compared that position against the index being updated, so the first appended chunk was always rejected as a duplicate of passage "0". Resolve auto-assigned IDs against the target index instead, and leave IDs the caller chose (metadata["id"]) and content-hash IDs alone. (cherry picked from commit af06d1ff3aae68f0bd09654e5a06c74c3c5875e6)
The append tests called into the IVF backend's real `add_with_ids()` / `remove_ids()`, which needs the FAISS class wrappers that make those methods accept NumPy arrays. A full test-suite environment can instead resolve `faiss` to the SWIG bindings shipped beside the HNSW backend, where those overloads are absent, so the append died in the binding rather than in the code under test. The passage-ID choice this change fixes happens in `update_index()` before any backend runs, so the IVF cases now assert what `update_index()` asks the backend to add and remove. The last test keeps a real, end-to-end append through the HNSW path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
add_text()withoutmetadata["id"]stampschunk["id"] = str(len(self.chunks))— this builder'sinsertion position — and
update_index()compared that stamp against the target index. So thefirst appended chunk was always rejected as a duplicate of passage
0:and the whole append aborted.
Fix
Treat an ID that came from the sequential scheme rather than from the caller as unclaimed, and resolve
those chunks against the index being updated (skipping IDs already taken there). Caller-chosen
duplicates still raise; content-hash IDs stay content-hash.
Why it matters
examples/dynamic_update_no_recompute.pyonly works today because it hands every chunk a globallyunique
id. A caller who lets the builder choose IDs cannot append to an existing index at all.Tests
New
tests/test_update_index_auto_passage_ids.py(5 tests): 3 failed / 2 passed on the base commit,every failure the
ValueErrorabove; 5 passed with the fix (4 of them run everywhere; the end-to-endHNSW one is skipped on Intel macOS runners, see below).
The IVF cases assert the IDs
update_index()hands toadd_vectors()/remove_ids(), with thosetwo calls standing in for the backend, because they go straight to a FAISS
IndexIVF— and the FAISSbindings the installed HNSW wheel brings into a full-suite environment do not expose the NumPy-friendly
add_with_ids()/remove_ids()overloads, so the append dies in the binding instead of in the codeunder test. The passage-ID decision this change fixes happens before any backend runs. The last test
keeps a real end-to-end append through the non-compact HNSW path, so the seam is not the only thing
covered. That last test is skipped on
darwin+x86_64: thereupdate_index()'s nativeindex.add()call (api.py:1194) aborts inside the FAISS bindings (Fatal Python error: Aborted,exit code 134) and takes the whole pytest session with it. It passes on the arm64 macOS, Ubuntu and
Windows runners, so this is the Intel build of the bindings rather than the code under test -- worth a
separate look, and out of scope here.
pre-commit(pinned ruff / ruff-format) andty==0.0.17are green on both files.Scope
This change leaves the non-compact HNSW branch's own ID assignment alone: after this stage it
re-derives each appended ID as
str(index.ntotal + offset)(api.py:1125-1129), becauseupdate_indexdoes not extend
<index>.ids.txt— the label→ID map the HNSW searcher reads, which falls back to theraw label once the map runs out (
leann_backend_hnsw/hnsw_backend.py:274-279). So on that path:0..n-1that the builder itself produces (the case the newtests cover) now appends instead of aborting;
0, 1, 3and appending onechunk stores it as
3, which was already taken, so the offset map keeps a single entry for3andthe earlier passage loses its own — and since
.ids.txtstill has 3 lines, a hit on that earliervector resolves to the newly appended passage. Making a chosen ID survive that update means
extending
.ids.txtrather than renumbering, which is a separate change; flagging it here so theboundary of this fix is explicit.
AI assistance was used for this change.