Repository navigation
feat(reembed): stage replacement vectors beside the active column - #64
Conversation
reembed.js used to clear memories.vector before re-embedding when the dimensionality changed, so vector search returned nothing until the backfill finished (raised in #51). Replace it with a staged dual-column migration: - dry-run by default; --stage --apply embeds into vector_next with the embedding generation and each row's content_hash, resumably; - --activate --apply checks full coverage and the recorded provenance under an advisory lock and an ACCESS EXCLUSIVE table lock, copies the current vectors to vector_prev, promotes vector_next in one transaction and rebuilds the HNSW index (halfvec above 2000 dims); - activation refuses partial coverage, rows edited after staging, rows with a vector but no text, a provenance mismatch and --limit or --collection; the active column is never nulled while staging. Pure SQL builders and validators live in services/embedding-migration.js (reusing pgvector.js's halfvecMode); getEmbeddingProvenance() joins the embedder interface; provenance is kept in zengram_meta. Driver errors are logged as fingerprints, the migration's own refusals verbatim. Behaviour change: --commit now means --stage --apply. Refs #51 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedEnable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Next included review available in 26 minutes. View limit details
Comment |
[High impact] The migration can change vector search behavior across the full memory corpus. Fix wrong-model vector reuse and the whitespace-only coverage failure before merging.
|
- Refuse to stage when EMBEDDING_GENERATION is pinned but the embedding identity changed: vectors staged under the old model would otherwise be reused and promoted under the new one (checked before any write, and reported by the --stage preview). - Use one embeddable-text rule in the CLI and the coverage SQL (first non-empty text field; whitespace-only text counts, as the API embeds it), so a whitespace-only memory no longer blocks activation forever. - Keep embedding_migration_previous paired with vector_prev: staging records the active identity under embedding_migration_pending_previous and only activation moves it, in the cutover transaction. - Reject malformed --limit/--collection values, and refuse --activate when either flag is supplied at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Reviewed by the author agent for 436945b. |
|
@coderabbitai review |
|
|
Merging 436945b without every selected review; the reviewers stay advisory, so this is recorded instead of counted as reviewed:
|
Problem
Refs #51. Thanks to @denisvmedia for pointing out the real issue: when
api/scripts/reembed.jschanges dimensions, it re-typesmemories.vectorwithUSING NULLbefore re-embedding, so vector search returns nothing for the whole backfill (and any failed row stays NULL).Change
reembed.jsbecomes a staged dual-column migration, the same one we run in our production deployment, adapted to Zengram's modules:node api/scripts/reembed.js--stage --apply(preview without--apply)vector_next+vector_next_generation+vector_next_content_hash; resumable--activate --apply(preview without--apply)vector→vector_prev,vector_next→vector, HNSW rebuiltSafety properties:
vector; every staging SQL statement passes throughassertNoActiveVectorWipe;ACCESS EXCLUSIVElock onmemories, then refuses (and rolls back) on any gap: a memory with text that is unstaged, staged under another generation, or edited after staging (content_hashchanged); a memory with a vector but no text; a recorded provenance that does not match the current embedding env;--limit/--collection;vector_cosine_opsup to 2000 dims, thehalfveccast above, identical to whatinitPgvector()creates;vector_prevkeeps one generation for manual rollback.Files:
api/src/services/embedding-migration.js: pure SQL builders and validators (reuseshalfvecModefrompgvector.js).api/scripts/reembed.js: the CLI. It uses its ownpg.PoolfromPOSTGRES_URLas before, logs driver errors aserrorSummaryfingerprints, and prints the migration's own refusals verbatim. Ends with a[reembed-summary]JSON line and a nonzero exit on failure or refusal.api/src/services/embedders/interface.js:getEmbeddingProvenance()(provider, model, dims, prefix fingerprints, normalization, generation id; optionalEMBEDDING_GENERATIONoverride). It stores fingerprints only, never the raw prefix text.zengram_metatable (same DDL as the structured store).docs/operations.md, plus.env.example,docs/configuration.md,CHANGELOG.md(Unreleased) and the README.Behaviour change:
--commitused to re-embed in place. It now means--stage --apply, and--activate --applydoes the cutover.Tests
api/tests/embedding-migration.test.js: 39 tests covering CLI modes, coverage validation, provenance matching, stage and prev column planning, index selection, the no-wipe guard, metadata I/O against a stub pool, error redaction and cleanup.cd api && npm test: 389/389 pass locally (Node 22). Typecheck warning count is unchanged (84).pgvector/pgvector:pg16container (pgvector 0.8.2) with a deterministic OpenAI-compatible stub embedder:vector(2048), ahalfvec(2048)HNSW index andvector_prev vector(1536);vectorindex.🤖 Generated with Claude Code