perf(search): incremental vector reconcile — stop re-embedding all N per write (11.3)#86
Merged
Merged
Conversation
…per write (11.3) The search reindex fires (debounced) on every entity write and re-embedded EVERY entity each time. Cheap for FTS, but a packaged build now loads the real embedder (post-#84), so with embeddings on a single edit re-embedded the whole vault — pegging a core continuously. This is a blocker for shipping embeddings enabled; it's the '11.3 incremental vector maintenance' tail. - `VectorIndexer.reconcile(entities)` — embeds ONLY entities whose indexable content changed since this indexer last saw them (a per-session entityId→sha256(embedText) cache), and reaps vectors for entities that were deleted or went blank. First reconcile of a session embeds everything (matches rebuild) + reaps any prior-session store rows not in the current set. A single edit now re-embeds one entity, not N. - `VectorStore.snapshotIds()` (both impls) — lets reconcile reap without re-reading embeddings. `rebuild` seeds the cache so a following reconcile skips the just-embedded set. Lexical stays a full rebuild (FTS is cheap). - index.ts reindex path calls `reconcile` instead of the vector `rebuild`. Change detection keys on the embed text (title\nbody), so a property write the index doesn't surface skips a re-embed; a fresh indexer (vault open / model swap) starts empty → one full re-embed. +6 tests (embed-count: first=all, unchanged=0, changed=1; reaps deleted/blank/stale). 133 search tests green; tsc + lint clean. Co-Authored-By: Claude Fable 5 <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 incremental vector maintenance 11.3 tail — a blocker for shipping embeddings enabled.
The problem
The search reindex fires (debounced ~250ms) on every entity write and re-embedded every entity each time. Cheap for FTS, but a packaged build now loads the real embedder (post-#84) — so with embeddings on, a single note edit re-embedded the whole vault, pegging a core continuously.
The fix (localized to the vector subsystem)
VectorIndexer.reconcile(entities)— embeds ONLY entities whose indexable content changed since this indexer last saw them (a per-sessionentityId → sha256(embedText)cache), and reaps vectors for entities that were deleted or went blank. First reconcile of a session embeds everything (matchesrebuild) + reaps any prior-session store rows not in the current set. A single edit re-embeds one entity, not N.VectorStore.snapshotIds()(both impls) so reconcile can reap without re-reading embeddings.reconcileinstead of the vectorrebuild; lexical stays a full rebuild (FTS is cheap).Change detection keys on the embed text (
title\nbody), so a property write the index doesn't surface skips a re-embed; a fresh indexer (vault open / model swap) starts empty → one full re-embed (one-time, tolerable). Persisted content hashes (to make vault-open cheap too) are a later refinement — the critical per-write cost is what this fixes.Verified
+6 tests (embed-count: first=all, unchanged=0, changed=1; reaps deleted / blank / stale-store-row). 133 search tests green;
tsc+bun run lintclean.🤖 Generated with Claude Code