feat: reuse chunk vectors across re-ingest, and clear the ones we missed - #210
Merged
Conversation
Both promised every stored vector and cleared only the fact side. Document chunks were given vectors yesterday and neither path was updated, so `memstore admin reset-embeddings` left them behind and a recipe change rebuilt facts under the new recipe while documents stayed in the old one -- two vector spaces in one store, and a document ranking that degrades without saying so. That is precisely the failure the embedder fingerprint exists to prevent, reintroduced underneath it. reset-embeddings now counts both corpora as well as clearing both, so the number it reports is the number it cleared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes task 3137's memstore-shaped half. Re-ingest replaces a document's whole chunk set -- the corpus design is explicit that chunks are replaced, not merged -- so every chunk of a changed file was re-embedded even when its own bytes never moved. On a repo sync that is most of the embedding work in the run, and all of it is waste: the same text under the same header renders the same input to the same model. The reuse key is everything that reaches the embedder and nothing else. ChunkEmbedText renders the section heading, the symbol scope and the declaration kind into a header ahead of the body, so identical text under a different heading is a different input and must be embedded again -- there is a test for that, because matching on content alone is the obvious implementation and it would silently keep a vector for text the model never saw in that form. Ordinal and the byte and line offsets are deliberately absent: a chunk that shifted down the file because something above it grew is the same input, and re-embedding it is the waste being removed. The document path is in the key as well, though reusableChunkVectors already scopes its query to one document. The path is the header's source field, so a key without it would permit cross-document reuse the moment someone widened that query, and the reused vector would carry another file's name in its header. What makes any of this sound is the store-level embedder fingerprint: reconcileEmbedder refuses to open when the model or dimension changed and clears every vector when the recipe changed, so vectors in the table are always from the embedder currently configured. Reuse must not outlive that guarantee, which is why the preceding commit had to land first. Fields in the key are length-prefixed rather than delimited, so no field value can impersonate a boundary between two others. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Closes the memstore-shaped half of task 3137.
The bug fix has to be read first
clearVectors(the recipe reconciler) andResetEmbeddings(memstore admin reset-embeddings) both promised every stored vector and cleared only the fact side. Document chunks were given vectors in #208 and neither path was updated.So
reset-embeddingsreported a count that excluded document vectors and left them in place, and a recipe change would have rebuilt facts under the new recipe while documents stayed in the old one -- two vector spaces in one store, and a document ranking that degrades without announcing itself. That is exactly the failure the embedder fingerprint exists to prevent, reintroduced underneath it.This commit lands first because vector reuse is only sound while that guarantee holds.
Reuse
Re-ingest replaces a document's whole chunk set -- the corpus design is explicit that chunks are replaced, not merged -- so every chunk of a changed file was re-embedded even when its own bytes never moved. On a repo sync that is most of the embedding work in the run, and all of it is waste: the same text under the same header renders the same input to the same model.
The key is everything that reaches the embedder and nothing else:
ChunkEmbedTextrenders the heading and scope into a header ahead of the body, so identical text under a moved heading is a different input and must be embedded again. There is a test for that, because matching on content alone is the obvious implementation and it would silently keep a vector for text the model never saw in that form.The document path is in the key even though
reusableChunkVectorsalready scopes its query to one document. The path is the header'ssourcefield, so a key without it would permit cross-document reuse the moment someone widened that query, and the borrowed vector would carry another file's name in its header. Both are pinned by tests.Fields are length-prefixed rather than delimited, so no field value can impersonate a boundary between two others.
What 3137 asked for that was already here
The task's headline safeguard -- an embedding-model fingerprint that refuses rather than returning silently degraded rankings -- already exists and is stronger than the pattern it cites:
reconcileEmbeddermakes a model or dimension change a hard error at store-open with a remediation hint, and clears vectors automatically on a recipe-only change. Its torn-write and atomic-rename safeguards are Postgres transactions here, and its cache-version constant isschemaVersion. File-level skipping of unchanged files already exists in the manifest sync.🤖 Generated with Claude Code