Migrate to a new embedding model

Available on NAMS: No. The procedure on this page needs the Bolt backend. With the hosted NAMS backend its operations are unavailable: depending on the call, the client raises NotSupportedError, AttributeError or TypeError, or ignores a Bolt-only setting or argument (NAMS manages embedding and extraction server-side). See the backend capabilities reference for what NAMS provides instead.

Change the embedding model for an existing Bolt database while preserving the source data needed to regenerate vectors. A model change requires a backfill even when the old and new models have the same number of dimensions: their vectors are not interchangeable.

Prerequisites

  • A database backup or disposable test copy, plus the original model identifier and settings.

  • Source text for every vector you intend to preserve, and access to the new embedding provider.

  • A maintenance window that pauses writes and vector searches until backfill and validation finish.

  • The Python SDK with the extra for the new embedding provider, for example pip install 'neo4j-agent-memory[openai]==0.7.0'. This runbook uses public Bolt graph/schema APIs; it is not a hosted workspace migration tool.

1. Inspect the existing indexes and choose a migration path

Run in Neo4j Query or through a connected Bolt client’s graph.execute_read:

SHOW VECTOR INDEXES YIELD name, state, options
RETURN name, state, options

The library-managed vector index names are message_embedding_idx, entity_embedding_idx, preference_embedding_idx, fact_embedding_idx, task_embedding_idx, and step_embedding_idx.

MemoryClient.connect() sets up schema and validates existing managed vector index dimensions. A mismatch raises EmbeddingDimensionMismatchError with expected_dimensions, actual_dimensions, and index_name. The message lists all mismatches; the latter two attributes describe the first one. Validation compares dimensions, not model identity or every stored vector.

Situation Action

Keep the original model

Restore its settings and retain the existing vectors and indexes.

Disposable data

Re-import source data into a fresh database configured for the new model.

Preserve existing entities and IDs

Backfill vectors in a paused database or a separate copy, following the remaining steps.

Missing source text or unknown original model

Recover that information before claiming equivalent retrieval. Rebuilding indexes alone cannot reconstruct vectors.

2. Remove only the indexes that must change dimension

Keep the application paused. Use a Neo4j administration connection, or connect a Bolt client with the old embedding settings so dimension validation succeeds. Do not connect with the new model and expect to drop old indexes from inside that context: connection fails before reaching the body.

For a migration of all six managed vector indexes, execute these statements individually in Neo4j Query:

DROP INDEX message_embedding_idx IF EXISTS;
DROP INDEX entity_embedding_idx IF EXISTS;
DROP INDEX preference_embedding_idx IF EXISTS;
DROP INDEX fact_embedding_idx IF EXISTS;
DROP INDEX task_embedding_idx IF EXISTS;
DROP INDEX step_embedding_idx IF EXISTS;

Dropping an index does not remove node embedding properties. Old vectors remain until you overwrite or explicitly remove them. For a same-dimension model change, index recreation is not required, but the complete backfill is.

Avoid client.schema.drop_all() for a vector-only migration: it removes constraints and indexes selected by library name prefixes, including conversation_, entity_, and message_. A user-created schema element sharing a prefix can also be selected.

3. Connect with the new model and backfill from source text

Configure the new provider as in Configure an embedding provider. After the old incompatible indexes are gone, connecting with the new settings creates missing indexes at the new dimensions. Keep traffic paused: this step has not regenerated existing vectors.

Use a separate provider instance with the same configuration for the backfill. The following helper covers entities only and expects an already connected Bolt client and an EmbeddingProvider named embedder. Adapt it for each populated memory layer in the table below before resuming traffic.

async def reembed_entities(client, embedder, batch_size=200):
    if batch_size < 1:
        raise ValueError("batch_size must be positive")
    after_id = ""
    updated = 0
    while True:
        rows = await client.graph.execute_read(
            "MATCH (e:Entity) WHERE e.id > $after_id AND e.name IS NOT NULL "
            "RETURN e.id AS id, e.name AS text ORDER BY e.id LIMIT $limit",
            {"after_id": after_id, "limit": batch_size},
        )
        if not rows:
            return updated
        vectors = await embedder.embed([row["text"] for row in rows])
        if len(vectors) != len(rows) or any(
            len(vector) != embedder.dimensions for vector in vectors
        ):
            raise ValueError("Embedding provider returned an unexpected shape")
        await client.graph.execute_write(
            "UNWIND $rows AS row MATCH (e:Entity {id: row.id}) "
            "SET e.embedding = row.vector",
            {"rows": [{"id": row["id"], "vector": vector}
                      for row, vector in zip(rows, vectors)]},
        )
        updated += len(rows)
        after_id = rows[-1]["id"]

The helper awaits each batch write. It uses stable ID ordering, requires non-null string IDs, and can be restarted from the beginning with the same model. It is not a resumable migration service. Record progress and preserve the backup if a provider call or database write fails.

Match the source text used by the normal write APIs:

Node Property Text to embed

Message

embedding

content

Entity

embedding

name

Preference

embedding

category + ": " + preference, followed by " (" + context + ")" when context is present

Fact

embedding

subject + " " + predicate + " " + object

ReasoningTrace

task_embedding

task

ReasoningStep

embedding

Nonempty thought, action, and observation values prefixed with Thought: , Action: , and Observation: (each followed by a space), joined with a space

Inspect records with missing source text rather than silently counting them as migrated. If you choose different text construction for your application, apply it consistently to future writes and validate retrieval quality.

memory.write_mode="buffered" does not defer these graph.execute_write calls. An explicit buffered migration would need separate submission, draining, and error handling; synchronous batch writes make completion easier to verify.

4. Verify the migration before restoring traffic

  1. Compare expected and updated node counts for every populated layer. Audit missing IDs and missing source text separately.

  2. Check vector lengths and missing-vector counts. For example:

    MATCH (e:Entity)
    RETURN size(e.embedding) AS dimensions, count(*) AS entities
    ORDER BY dimensions
  3. Confirm all six managed indexes that your database uses are ONLINE with the intended dimension in SHOW VECTOR INDEXES.

  4. With the new provider, run await client.schema.validate_vector_index_dimensions(embedder.dimensions). A successful connection alone is not evidence that every old vector was replaced.

  5. Run labeled retrieval cases from Evaluate memory quality, including older records, before restoring traffic.

The dimension validator skips validation if the server rejects the SHOW VECTOR INDEXES syntax. Other query failures propagate. Do not treat a skipped check as proof that the schema or migration is correct.

If validation fails, keep traffic paused and restore the database backup with the original model settings. Reverting settings alone after a partial backfill would leave mixed-model vectors.