Files
gbrain/docs/embedding-migrations.md
T
jbarol e19709b052 docs(migrations): align the rationale list with the corrected recipe order
The 'Why we don't do this automatically' list still said alter-then-wipe;
reorder to wipe-then-alter and replace the fragile 'step 3' numeric
cross-reference with a name-based one.
2026-06-10 14:49:24 -07:00

7.0 KiB

Switching embedding models or dimensions on an existing brain

GBrain stores embeddings in a fixed-dimension vector(N) column on content_chunks. If you switch to a model with a different dimension (e.g. openai:text-embedding-3-large 1536 → zeroentropyai:zembed-1 1280, or voyage:voyage-4-large 2048), the on-disk column type doesn't change automatically.

gbrain init, gbrain doctor, and gbrain embed --stale all detect this mismatch and refuse to silently proceed. This doc is the recipe they point at.

Same-dimension model swaps (v0.41.31.0 — automatic)

If you switch to a different model at the same dimension count (e.g. one 1536-dim provider to another, or a re-tuned model that keeps its width), the column type doesn't change, so no ALTER/wipe recipe is needed. As of v0.41.31.0, gbrain stamps an embedding-provenance signature (<provider:model>:<dims>) onto each page when its chunks are embedded. After you point the config at the new model, the stored signatures differ from the current one, and gbrain embed --stale re-embeds exactly those pages:

# After switching to the new same-dim model in your config:
gbrain embed --stale          # re-embeds signature-drifted pages
gbrain embed --stale --dry-run # preview the count without re-embedding

Under federated_v2, the same drift is picked up by the per-source embed-backfill jobs that gbrain sync --all enqueues (capped $X/source/24h). Grandfather: pages embedded before v0.41.31.0 carry a NULL signature and are NEVER flagged stale, so upgrading to v0.41.31.0 does NOT trigger a whole-corpus re-embed. Signatures only get stamped going forward.

A dimension change still requires the wipe-and-reinit (PGLite) or column-alter (Postgres) recipe below — the on-disk vector(N) width genuinely has to change.

Why we don't do this automatically

Switching dimensions requires:

  1. Dropping the HNSW vector index (pgvector won't survive an ALTER COLUMN TYPE).
  2. Wiping every existing embedding (the old vectors are unusable in the new space — and pgvector refuses to cast them across dimensions, so this must happen before the alter).
  3. Altering the column type (Postgres only — PGLite cannot do this).
  4. Re-embedding the entire corpus (can take hours on a 50K-page brain and costs $1-100 in API calls depending on model).
  5. Conditionally recreating the index (HNSW supports up to 2000 dimensions per pgvector; above that you must use exact scans).

That's not an upgrade-time auto-run. It's a deliberate, expensive operation. Run it when you've decided you actually want the new model.

PGLite (default install)

PGLite cannot ALTER COLUMN TYPE vector(N). pgvector ships as embedded WASM, not a native extension, and the WASM build rejects the column-type alter with could not access file "$libdir/vector". The SQL recipe below works against Postgres only.

The path that works on PGLite is wipe-and-reinit. v0.37 ships a single-command wrapper:

gbrain reinit-pglite \
  --embedding-model zeroentropyai:zembed-1 \
  --embedding-dimensions 1280

This backs up the existing brain to <path>.bak, runs gbrain init with the new flags (preserving every other field in ~/.gbrain/config.json), and re-syncs the brain repo. Add --no-sync to skip the resync, --yes to skip the TTY confirmation, --json for structured output.

Equivalent by hand:

# 1. Back up the existing brain (in case you want to roll back).
mv ~/.gbrain/brain.pglite ~/.gbrain/brain.pglite.bak

# 2. Re-init with the new model + dimensions. `gbrain init` writes
#    the schema sized to the new dim, and (as of v0.37) preserves
#    every other field in ~/.gbrain/config.json (chat model,
#    expansion model, API keys).
gbrain init --pglite \
  --embedding-model zeroentropyai:zembed-1 \
  --embedding-dimensions 1280

# 3. Re-import your brain repo. `gbrain sync` reads the brain repo
#    from disk and re-creates the page rows.
gbrain sync

# 4. Re-embed. The embed pipeline now uses the new model and the
#    column accepts the new dim.
gbrain embed --stale

If your brain repo is large enough that re-syncing from disk is expensive (>50K pages), see the Postgres section below — migrating to Postgres temporarily lets you run the SQL recipe, then migrate back to PGLite.

GBRAIN_HOME users: substitute the active database path (or use gbrain config get database_path to find it).

Postgres (Supabase / self-hosted)

Postgres supports the in-place column alter. Replace <NEW_DIMS> with your target dimension count.

BEGIN;

-- 1. Drop the HNSW index. It can't survive the column type change.
DROP INDEX IF EXISTS idx_chunks_embedding;

-- 2. Clear stale embeddings FIRST. This must happen BEFORE the column
--    alter: pgvector refuses to cast existing vectors across dimensions
--    ("expected <NEW_DIMS> dimensions, not <OLD_DIMS>"), so altering a
--    column that still holds old-width vectors aborts the transaction.
--    NULLs cast fine. (The old vectors are unusable in the new space
--    anyway — this is the wipe step from the rationale above.)
UPDATE content_chunks SET embedding = NULL, embedded_at = NULL;

-- 3. Alter the column type (all rows are NULL now, so the cast succeeds).
ALTER TABLE content_chunks ALTER COLUMN embedding TYPE vector(<NEW_DIMS>);

-- 4. Recreate the HNSW index ONLY IF dims <= 2000. Above that, leave it
--    indexless and rely on exact scans (gbrain searchVector handles this
--    automatically — search just gets slower, not broken).
-- For dims <= 2000 (e.g. 1024, 1280, 1536, 768):
CREATE INDEX IF NOT EXISTS idx_chunks_embedding
  ON content_chunks USING hnsw (embedding vector_cosine_ops);
-- For dims > 2000 (e.g. 2048 Voyage 4 Large): skip step 4.

COMMIT;

Then re-init config with the new model:

gbrain init --supabase \
  --embedding-model <provider:model> \
  --embedding-dimensions <NEW_DIMS>

And re-embed:

gbrain embed --stale

A note on gbrain config set

Pre-v0.37 docs recommended gbrain config set embedding_model X to switch models. This is a no-op for the embed pipeline. config set writes the DB plane; the embed gateway reads the file plane (~/.gbrain/config.json). The pre-v0.37 recipe shipped the lie because the contract wasn't surfaced.

As of v0.37, gbrain config set embedding_model and gbrain config set embedding_dimensions REFUSE and print the wipe-and-reinit recipe.

To change schema-sizing fields, use gbrain init (PGLite) or the SQL recipe (Postgres). Both update the file plane AND the schema together.

Verify

After the recipe lands, gbrain doctor --fast should report green and gbrain doctor should pass the embedding_width_consistency check:

✓ embedding_width_consistency   dim parity: config 1280 / column vector(1280)

If it doesn't, file an issue with the doctor output and the steps you ran.

v0.37+ followups

  • Auto-fallback to alternative embedding providers when the primary fails quota/auth. Tracked; requires explicit --try-fallback consent because mixing provider vectors silently corrupts retrieval.