Skip to content

feat: reuse chunk vectors across re-ingest, and clear the ones we missed - #210

Merged
matthewjhunter merged 2 commits into
mainfrom
feat/chunk-vector-reuse
Aug 30, 2026
Merged

feat: reuse chunk vectors across re-ingest, and clear the ones we missed#210
matthewjhunter merged 2 commits into
mainfrom
feat/chunk-vector-reuse

Conversation

@matthewjhunter

Copy link
Copy Markdown
Owner

Closes the memstore-shaped half of task 3137.

The bug fix has to be read first

clearVectors (the recipe reconciler) and ResetEmbeddings (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-embeddings reported 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:

  • Included: content, heading path, symbol scope, declaration kind, document path. ChunkEmbedText renders 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.
  • Excluded: ordinal, byte offsets, line offsets. 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 even 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 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: reconcileEmbedder makes 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 is schemaVersion. File-level skipping of unchanged files already exists in the manifest sync.

🤖 Generated with Claude Code

matthewjhunter and others added 2 commits August 30, 2026 11:57
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>
@matthewjhunter
matthewjhunter merged commit b3f1738 into main Aug 30, 2026
8 checks passed
@matthewjhunter
matthewjhunter deleted the feat/chunk-vector-reuse branch August 30, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant