Skip to content

perf(search): incremental vector reconcile — stop re-embedding all N per write (11.3)#86

Merged
th3-br41n merged 1 commit into
mainfrom
feat/incremental-vector-index
Jul 3, 2026
Merged

perf(search): incremental vector reconcile — stop re-embedding all N per write (11.3)#86
th3-br41n merged 1 commit into
mainfrom
feat/incremental-vector-index

Conversation

@th3-br41n

Copy link
Copy Markdown
Contributor

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-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 re-embeds one entity, not N.
  • VectorStore.snapshotIds() (both impls) so reconcile can reap without re-reading embeddings.
  • index.ts reindex path calls reconcile instead of the vector rebuild; 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 lint clean.

🤖 Generated with Claude Code

…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>
@th3-br41n th3-br41n merged commit 3369898 into main Jul 3, 2026
3 checks passed
@th3-br41n th3-br41n deleted the feat/incremental-vector-index branch July 3, 2026 16:51
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