{
  "id": "index_migration",
  "title": "Migrate an Index: Vector Quantization, Resume, Backup, and Wizard",
  "url": "https://redis.io/docs/latest/develop/ai/redisvl/user_guide/how_to_guides/index_migration/",
  "summary": "",
  "tags": [],
  "last_updated": "2026-06-11T16:10:09-04:00",
  "page_type": "content",
  "content_hash": "5667d5ed5d3e575832823de53ef2e7c1972f8ba90744d03c3c7c4d9b1d9530fb",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "The index migrator is an **experimental** feature. APIs, CLI commands, and\non-disk formats (plans, backups) may change in future releases. Review\nmigration plans carefully before applying them to production indexes.\n\n\nThis guide walks through a **vector quantization** migration\n(`float32` -> `float16`) end to end using the programmatic API. You will\nlearn how to:\n\n- Build a schema patch that describes the change\n- Generate and review a migration **plan** (read-only)\n- **Apply** the migration with a mandatory on-disk backup\n- Find **where the backup lives** and inspect its progress header\n- Understand **crash-safe resume** and safely re-run a migration\n- **Reload original vectors from the backup** (rollback)\n- Build and apply the same migration through the **wizard**\n\nFor conceptual background see\n[Index Migrations](https://redis.io/docs/latest/../../concepts/index-migrations) and the\n[Migrate an Index how-to](https://redis.io/docs/latest/how_to_guides/migrate-indexes).\n\n**Prerequisites:** a running Redis 8.0+ (or Redis Stack) at\n`redis://localhost:6379` and `redisvl` installed.\n\n\n[code example]"
    },
    {
      "id": "1-create-a-source-index-with-float32-vectors",
      "title": "1. Create a source index with float32 vectors",
      "role": "content",
      "text": "We start with a small Hash index whose `embedding` field stores full\nprecision `float32` vectors, then load some random data.\n\n\n[code example]\n\n    Loaded 600 documents into 'products'\n\n\n\n[code example]\n\n    Indices:\n    1. products"
    },
    {
      "id": "2-describe-the-change-with-a-schema-patch",
      "title": "2. Describe the change with a schema patch",
      "role": "content",
      "text": "A **schema patch** lists only what changes. Here we update the\n`embedding` field's datatype from `float32` to `float16` (a 2x memory\nreduction). We write it to a YAML file the planner can read.\n\n\n[code example]\n\n    version: 1\n    changes:\n      update_fields:\n      - name: embedding\n        attrs:\n          datatype: float16"
    },
    {
      "id": "3-create-a-migration-plan-read-only",
      "title": "3. Create a migration plan (read-only)",
      "role": "content",
      "text": "`create_plan` snapshots the live index, diffs it against the patch, and\nreturns a `MigrationPlan`. **No data is modified.** Review the warnings\nand the classified changes before applying.\n\n\n[code example]\n\n    Index:      products\n    Mode:       drop_recreate\n    Requested:  {'version': 1, 'changes': {'add_fields': [], 'remove_fields': [], 'update_fields': [{'name': 'embedding', 'attrs': {'datatype': 'float16'}, 'options': {}}], 'rename_fields': [], 'index': {}}}\n    Warnings:  \n      - Index downtime is required\n    \n    Saved plan to migration_plan.yaml"
    },
    {
      "id": "4-apply-the-migration-with-a-mandatory-backup",
      "title": "4. Apply the migration with a mandatory backup",
      "role": "content",
      "text": "The executor requires `backup_dir` before applying any migration. For\nquantization, it writes original vectors to disk before mutating them. If\nyou omit `backup_dir`, or pass a path that cannot be created or written,\nthe migration fails before touching the index. The returned report records\nthe resolved backup directory and any backup file prefixes used.\n\nWe also pass a `progress_callback` to watch each phase.\n\n\nThis drops and recreates the index definition. Documents are preserved;\nonly the index structure and vector encoding change. Pause writes during\nthe migration window.\n\n\n\n[code example]\n\n    Missing backup_dir is rejected before migration: A backup directory is required to apply migrations. Provide --backup-dir or backup_dir=...; migrations are not started without a backup directory.\n    Invalid backup_dir is rejected before migration: Could not create or access backup directory './not_a_backup_dir': [Errno 17] File exists: 'not_a_backup_dir'. A writable backup directory is required to safely migrate.\n    [enumerate] Enumerating indexed documents...\n    [enumerate] found 600 documents (0.003s)\n    [dump] Backing up original vectors...\n    [dump] 100/600 docs\n    [dump] 200/600 docs\n    [dump] 300/600 docs\n    [dump] 400/600 docs\n    [dump] 500/600 docs\n    [dump] 600/600 docs\n    [dump] done (0.009s)\n    [drop] Dropping index definition...\n    [drop] done (0.001s)\n    [quantize] Re-encoding vectors from backup...\n    [quantize] 100/600 docs\n    [quantize] 200/600 docs\n    [quantize] 300/600 docs\n    [quantize] 400/600 docs\n    [quantize] 500/600 docs\n    [quantize] 600/600 docs\n    [quantize] done (600 docs in 0.009s)\n    [create] Creating index with new schema...\n\n\n    [create] done (0.004s)\n    [index] Waiting for re-indexing...\n    [index] 22/115 docs (19%)\n\n\n    [index] 600/600 docs (100%)\n    [index] done (0.508s)\n    [validate] Validating migration...\n    [validate] done (0.01s)\n    \n    Result:            succeeded\n    Total duration:    0.554 s\n    Quantize duration: 0.009 s\n    Schema match:      True\n    Doc count match:   True\n    Backup dir:        /Users/nitin.kanukolanu/workspace/redis-vl-python/docs/user_guide/migration_backups\n    Backup prefixes:   ['/Users/nitin.kanukolanu/workspace/redis-vl-python/docs/user_guide/migration_backups/migration_backup_products_0a3e27b8']"
    },
    {
      "id": "5-where-is-the-backup-and-what-s-in-it",
      "title": "5. Where is the backup, and what's in it?",
      "role": "content",
      "text": "Backups are written under `report.backup.backup_dir`. For a single-worker\nHash quantization migration there are two files per index:\n\n- `migration_backup_<index>_<hash>.header` -- JSON: phase + progress counters\n- `migration_backup_<index>_<hash>.data` -- binary: original vectors, batched\n\nThe `<hash>` suffix is a short digest of the index name, which avoids\ncollisions. `report.backup.backup_paths` stores the path prefix without\n`.header` or `.data`. Multi-worker migrations record one prefix per\nworker.\n\nBackups are **retained after success** so you can audit or roll back;\ndelete them manually when no longer needed.\n\n\n[code example]\n\n    /Users/nitin.kanukolanu/workspace/redis-vl-python/docs/user_guide/migration_backups/migration_backup_products_0a3e27b8.data  (47,730 bytes)\n    /Users/nitin.kanukolanu/workspace/redis-vl-python/docs/user_guide/migration_backups/migration_backup_products_0a3e27b8.header  (209 bytes)\n\n\n\n[code example]\n\n    backup prefix:              /Users/nitin.kanukolanu/workspace/redis-vl-python/docs/user_guide/migration_backups/migration_backup_products_0a3e27b8\n    index_name:                 products\n    phase:                      completed\n    batch_size:                 100\n    dump_completed_batches:     6\n    quantize_completed_batches: 6"
    },
    {
      "id": "6-crash-safe-resume-and-checkpointing",
      "title": "6. Crash-safe resume and checkpointing",
      "role": "content",
      "text": "If a migration is interrupted by a crash, network drop, or `Ctrl+C`, just\nre-run the same command with the same `backup_dir`. The executor loads the\nbackup header, validates that it belongs to the planned source index, and\ncontinues from the next unfinished batch.\n\nThe header is the checkpoint for single-index migrations:\n\n- `phase` shows where the previous run stopped: `dump`, `ready`, `active`, or `completed`\n- `dump_completed_batches` counts original-vector batches safely written to `.data`\n- `quantize_completed_batches` counts batches already re-encoded and written back to Redis\n\n[code example]\n\nBatch migrations use the same per-index backup headers, plus a batch state\nYAML file that records the current index, completed indexes, failed\nindexes, and the `backup_dir`. Resume rejects a different backup directory\nso the checkpoint and backup files stay together.\n\nWhen `phase` is `completed`, re-running is safe if the live index already\nmatches the target schema: the executor detects the finished backup, skips\ncompleted work, and leaves the already-created index in place. If you have\nrolled back and the live index is back on the source schema, the old\ncompleted backup is stale for a new migration run; the executor discards\nthat checkpoint and writes a fresh backup.\n\n\n[code example]\n\n    Checkpoint says 6 batch(es) were already quantized.\n    Current phase: completed\n    \n    Re-running apply with the same backup_dir to exercise resume detection...\n    [enumerate] skipped (resume from backup)\n    [drop] skipped (already dropped)\n    [quantize] skipped (already completed)\n    [create] Creating index with new schema...\n    [create] done (0.004s)\n    [index] Waiting for re-indexing...\n    [index] 600/600 docs (100%)\n    [index] done (0.001s)\n    [validate] Validating migration...\n    [validate] done (0.008s)\n    Resume result:      succeeded\n    Resume backup dir:  /Users/nitin.kanukolanu/workspace/redis-vl-python/docs/user_guide/migration_backups\n    Resume prefixes:    ['/Users/nitin.kanukolanu/workspace/redis-vl-python/docs/user_guide/migration_backups/migration_backup_products_0a3e27b8']"
    },
    {
      "id": "7-verify-the-quantized-index",
      "title": "7. Verify the quantized index",
      "role": "content",
      "text": "The documents were preserved and the `embedding` field is now `float16`.\nWe reconnect to the live index and run a vector query (encoding the query\nvector to match the new datatype).\n\n\n[code example]\n\n    embedding datatype now: float16\n    product 0 | category: electronics | dist: 0\n    product 223 | category: books | dist: 0.0458984375\n    product 23 | category: books | dist: 0.04736328125"
    },
    {
      "id": "8-recover-original-vectors-from-the-backup-rollback",
      "title": "8. Recover original vectors from the backup (rollback)",
      "role": "content",
      "text": "Because the backup holds the original `float32` bytes, you can recover the\npre-migration vector data. The CLI provides a one-liner:\n\n[code example]\n\nBelow is the equivalent **Python API**: iterate the backup batches and\nwrite the original bytes back with `HSET`. Rollback restores **data only**;\nafterwards recreate the original index definition so the index encoding\nmatches the restored vectors again.\n\n\n[code example]\n\n    Restored original bytes for 600 vector field(s)\n    embedding datatype after rollback: float32"
    },
    {
      "id": "9-build-and-apply-a-migration-with-the-wizard",
      "title": "9. Build and apply a migration with the wizard",
      "role": "content",
      "text": "For exploratory work, `MigrationWizard` can build the same schema patch and\nmigration plan interactively. In a notebook, we script the answers so the\ncell can execute without blocking. The sequence below means: update a\nfield, choose `embedding`, keep the current algorithm, change datatype to\n`float16`, keep the distance metric, then finish.\n\nThe wizard still only creates the patch and plan. Applying the plan remains\na separate reviewed step, and `backup_dir` is still required.\n\n\n[code example]\n\n    Loaded 600 documents into 'wizard_products'\n    Building a migration plan for index 'wizard_products'\n    Current schema:\n    - Index name: wizard_products\n    - Storage type: hash\n      - name (text)\n      - category (tag)\n      - embedding (vector)\n    \n    Choose an action:\n    1. Add field        (text, tag, numeric, geo)\n    2. Update field     (sortable, weight, separator, vector config)\n    3. Remove field\n    4. Rename field     (rename field in all documents)\n    5. Rename index     (change index name)\n    6. Change prefix    (rename all keys)\n    7. Preview patch    (show pending changes as YAML)\n    8. Finish\n    Enter a number: 2\n    Updatable fields:\n    1. name (text)\n    2. category (tag)\n    3. embedding (vector)\n    Select a field to update by number or name: embedding\n    Current vector config for 'embedding':\n      algorithm: FLAT\n      datatype: float32\n      distance_metric: cosine\n      dims: 8 (cannot be changed)\n    \n    Leave blank to keep current value.\n      Algorithm: vector search method (FLAT=brute force, HNSW=graph, SVS-VAMANA=compressed graph)\n    Algorithm [current: FLAT]: \n      Datatype: float16, float32, bfloat16, float64, int8, uint8\n                (float16 reduces memory ~50%, int8/uint8 reduce ~75%)\n    Datatype [current: float32]: float16\n      Distance metric: how similarity is measured (cosine, l2, ip)\n    Distance metric [current: cosine]: \n    \n    Choose an action:\n    1. Add field        (text, tag, numeric, geo)\n    2. Update field     (sortable, weight, separator, vector config)\n    3. Remove field\n    4. Rename field     (rename field in all documents)\n    5. Rename index     (change index name)\n    6. Change prefix    (rename all keys)\n    7. Preview patch    (show pending changes as YAML)\n    8. Finish\n    Enter a number: 8\n    \n    Wizard patch:\n    version: 1\n    changes:\n      add_fields: []\n      remove_fields: []\n      update_fields:\n      - name: embedding\n        attrs:\n          datatype: float16\n        options: {}\n      rename_fields: []\n      index: {}\n    \n    Wizard plan mode: drop_recreate\n    Wizard warnings: ['Index downtime is required']\n\n\n\n[code example]\n\n    Wizard migration result: succeeded\n    Wizard schema match:     True\n    Wizard doc count match:  True\n    Wizard backup dir:       /Users/nitin.kanukolanu/workspace/redis-vl-python/docs/user_guide/wizard_migration_backups\n    Wizard backup prefixes:  ['/Users/nitin.kanukolanu/workspace/redis-vl-python/docs/user_guide/wizard_migration_backups/migration_backup_wizard_products_def8cdf8']\n    Wizard embedding dtype:  float16"
    },
    {
      "id": "10-cleanup-optional",
      "title": "10. Cleanup (optional)",
      "role": "content",
      "text": "Remove the demo indexes and the artifacts this notebook created. In\nproduction, delete backups only once you are certain rollback is no longer\nneeded.\n\n\n[code example]\n\n    Cleaned up demo indexes, demo keys, backups, and YAML files"
    },
    {
      "id": "learn-more",
      "title": "Learn more",
      "role": "related",
      "text": "- [Migrate an Index (how-to)](https://redis.io/docs/latest/how_to_guides/migrate-indexes) -- full CLI\n  workflow, batch migration, performance tuning, and troubleshooting\n- [Index Migrations (concepts)](https://redis.io/docs/latest/../../concepts/index-migrations) -- modes,\n  supported vs blocked changes, backup internals, sync vs async\n- For very large datasets, use `num_workers > 1` and the async executor\n  (`AsyncMigrationExecutor`) to parallelize re-encoding."
    }
  ],
  "examples": [
    {
      "id": "overview-ex0",
      "language": "python",
      "code": "import os\nimport glob\nimport numpy as np\nimport yaml\n\nfrom redisvl.index import SearchIndex\nfrom redisvl.redis.utils import array_to_buffer\nfrom redisvl.query import VectorQuery\n\nREDIS_URL = \"redis://localhost:6379\"\nINDEX_NAME = \"products\"\nDIMS = 8\nN_DOCS = 600\n\nnp.random.seed(42)\n\n\ndef delete_matching(client, pattern, batch_size=500):\n    deleted = 0\n    batch = []\n    for key in client.scan_iter(match=pattern, count=batch_size):\n        batch.append(key)\n        if len(batch) >= batch_size:\n            deleted += client.delete(*batch)\n            batch = []\n    if batch:\n        deleted += client.delete(*batch)\n    return deleted",
      "section_id": "overview"
    },
    {
      "id": "1-create-a-source-index-with-float32-vectors-ex0",
      "language": "python",
      "code": "schema = {\n    \"index\": {\n        \"name\": INDEX_NAME,\n        \"prefix\": \"product\",\n        \"storage_type\": \"hash\",\n    },\n    \"fields\": [\n        {\"name\": \"name\", \"type\": \"text\"},\n        {\"name\": \"category\", \"type\": \"tag\"},\n        {\n            \"name\": \"embedding\",\n            \"type\": \"vector\",\n            \"attrs\": {\n                \"algorithm\": \"flat\",\n                \"dims\": DIMS,\n                \"distance_metric\": \"cosine\",\n                \"datatype\": \"float32\",\n            },\n        },\n    ],\n}\n\nindex = SearchIndex.from_dict(schema, redis_url=REDIS_URL)\nif index.exists():\n    index.delete(drop=True)\nstale_keys = delete_matching(index.client, \"product:*\")\nif stale_keys:\n    print(f\"Removed {stale_keys} stale demo key(s)\")\nindex.create()\n\nvectors = np.random.rand(N_DOCS, DIMS).astype(np.float32)\ndata = [\n    {\n        \"name\": f\"product {i}\",\n        \"category\": \"electronics\" if i % 2 == 0 else \"books\",\n        \"embedding\": array_to_buffer(vectors[i], dtype=\"float32\"),\n    }\n    for i in range(N_DOCS)\n]\nkeys = index.load(data)\nprint(f\"Loaded {len(keys)} documents into '{INDEX_NAME}'\")",
      "section_id": "1-create-a-source-index-with-float32-vectors"
    },
    {
      "id": "1-create-a-source-index-with-float32-vectors-ex1",
      "language": "python",
      "code": "!rvl index listall --url redis://localhost:6379",
      "section_id": "1-create-a-source-index-with-float32-vectors"
    },
    {
      "id": "2-describe-the-change-with-a-schema-patch-ex0",
      "language": "python",
      "code": "patch = {\n    \"version\": 1,\n    \"changes\": {\n        \"update_fields\": [\n            {\"name\": \"embedding\", \"attrs\": {\"datatype\": \"float16\"}},\n        ]\n    },\n}\n\nwith open(\"schema_patch.yaml\", \"w\") as f:\n    yaml.safe_dump(patch, f, sort_keys=False)\n\nprint(open(\"schema_patch.yaml\").read())",
      "section_id": "2-describe-the-change-with-a-schema-patch"
    },
    {
      "id": "3-create-a-migration-plan-read-only-ex0",
      "language": "python",
      "code": "from redisvl.migration import MigrationPlanner, MigrationExecutor\nfrom redisvl.migration.utils import write_yaml\n\nplanner = MigrationPlanner()\nplan = planner.create_plan(\n    index_name=INDEX_NAME,\n    schema_patch_path=\"schema_patch.yaml\",\n    redis_url=REDIS_URL,\n)\n\nprint(\"Index:     \", plan.source.index_name)\nprint(\"Mode:      \", plan.mode)\nprint(\"Requested: \", plan.requested_changes)\nprint(\"Warnings:  \")\nfor w in plan.warnings:\n    print(\"  -\", w)\n\n# Plans can be persisted to YAML and reloaded later (or via the CLI)\nwrite_yaml(plan.model_dump(), \"migration_plan.yaml\")\nprint(\"\\nSaved plan to migration_plan.yaml\")",
      "section_id": "3-create-a-migration-plan-read-only"
    },
    {
      "id": "4-apply-the-migration-with-a-mandatory-backup-ex0",
      "language": "python",
      "code": "BACKUP_DIR = \"./migration_backups\"\n\n# The migration does not start without a usable backup directory.\nexecutor = MigrationExecutor()\ntry:\n    executor.apply(plan, redis_url=REDIS_URL, backup_dir=None)\nexcept ValueError as exc:\n    print(\"Missing backup_dir is rejected before migration:\", exc)\n\nBAD_BACKUP_DIR = \"./not_a_backup_dir\"\nif os.path.isdir(BAD_BACKUP_DIR):\n    os.rmdir(BAD_BACKUP_DIR)\nwith open(BAD_BACKUP_DIR, \"w\") as f:\n    f.write(\"this file intentionally blocks directory creation\")\ntry:\n    executor.apply(plan, redis_url=REDIS_URL, backup_dir=BAD_BACKUP_DIR)\nexcept ValueError as exc:\n    print(\"Invalid backup_dir is rejected before migration:\", exc)\nfinally:\n    if os.path.exists(BAD_BACKUP_DIR):\n        os.remove(BAD_BACKUP_DIR)\n\n\ndef on_progress(step, detail=None):\n    print(f\"[{step}] {detail or ''}\")\n\n\nreport = executor.apply(\n    plan,\n    redis_url=REDIS_URL,\n    backup_dir=BACKUP_DIR,\n    batch_size=100,\n    num_workers=1,\n    progress_callback=on_progress,\n)\n\nprint(\"\\nResult:           \", report.result)\nprint(\"Total duration:   \", report.timings.total_migration_duration_seconds, \"s\")\nprint(\"Quantize duration:\", report.timings.quantize_duration_seconds, \"s\")\nprint(\"Schema match:     \", report.validation.schema_match)\nprint(\"Doc count match:  \", report.validation.doc_count_match)\nprint(\"Backup dir:       \", report.backup.backup_dir)\nprint(\"Backup prefixes:  \", report.backup.backup_paths)\n\nBACKUP_PREFIX = report.backup.backup_paths[0]",
      "section_id": "4-apply-the-migration-with-a-mandatory-backup"
    },
    {
      "id": "5-where-is-the-backup-and-what-s-in-it-ex0",
      "language": "python",
      "code": "for path in sorted(glob.glob(os.path.join(report.backup.backup_dir, '*'))):\n    size = os.path.getsize(path)\n    print(f\"{path}  ({size:,} bytes)\")",
      "section_id": "5-where-is-the-backup-and-what-s-in-it"
    },
    {
      "id": "5-where-is-the-backup-and-what-s-in-it-ex1",
      "language": "python",
      "code": "from redisvl.migration.backup import VectorBackup\n\n# load() takes the path prefix, without .header or .data.\nbackup = VectorBackup.load(BACKUP_PREFIX)\nh = backup.header\nprint(\"backup prefix:             \", BACKUP_PREFIX)\nprint(\"index_name:                \", h.index_name)\nprint(\"phase:                     \", h.phase)\nprint(\"batch_size:                \", h.batch_size)\nprint(\"dump_completed_batches:    \", h.dump_completed_batches)\nprint(\"quantize_completed_batches:\", h.quantize_completed_batches)",
      "section_id": "5-where-is-the-backup-and-what-s-in-it"
    },
    {
      "id": "6-crash-safe-resume-and-checkpointing-ex0",
      "language": "bash",
      "code": "# Re-running the same CLI command resumes automatically:\nrvl migrate apply --plan migration_plan.yaml \\\n  --backup-dir ./migration_backups --url redis://localhost:6379",
      "section_id": "6-crash-safe-resume-and-checkpointing"
    },
    {
      "id": "6-crash-safe-resume-and-checkpointing-ex1",
      "language": "python",
      "code": "skip = backup.header.quantize_completed_batches\nprint(f\"Checkpoint says {skip} batch(es) were already quantized.\")\nprint(f\"Current phase: {backup.header.phase}\")\n\nprint(\"\\nRe-running apply with the same backup_dir to exercise resume detection...\")\nresume_report = executor.apply(\n    plan,\n    redis_url=REDIS_URL,\n    backup_dir=BACKUP_DIR,\n    batch_size=100,\n    num_workers=1,\n    progress_callback=on_progress,\n)\nprint(\"Resume result:     \", resume_report.result)\nprint(\"Resume backup dir: \", resume_report.backup.backup_dir)\nprint(\"Resume prefixes:   \", resume_report.backup.backup_paths)",
      "section_id": "6-crash-safe-resume-and-checkpointing"
    },
    {
      "id": "7-verify-the-quantized-index-ex0",
      "language": "python",
      "code": "restored = SearchIndex.from_existing(INDEX_NAME, redis_url=REDIS_URL)\nemb = next(f for f in restored.schema.to_dict()['fields'] if f['name'] == 'embedding')\nprint(\"embedding datatype now:\", emb['attrs']['datatype'])\n\nq = VectorQuery(\n    vector=vectors[0].tolist(),\n    vector_field_name=\"embedding\",\n    return_fields=[\"name\", \"category\"],\n    dtype=\"float16\",\n    num_results=3,\n)\nfor r in restored.query(q):\n    print(r[\"name\"], \"| category:\", r[\"category\"], \"| dist:\", r[\"vector_distance\"])",
      "section_id": "7-verify-the-quantized-index"
    },
    {
      "id": "8-recover-original-vectors-from-the-backup-rollback-ex0",
      "language": "bash",
      "code": "rvl migrate rollback --backup-dir ./migration_backups \\\n  --index products --url redis://localhost:6379",
      "section_id": "8-recover-original-vectors-from-the-backup-rollback"
    },
    {
      "id": "8-recover-original-vectors-from-the-backup-rollback-ex1",
      "language": "python",
      "code": "client = restored.client\n\nrestored_count = 0\nfor batch_keys, originals in backup.iter_batches():\n    pipe = client.pipeline(transaction=False)\n    for key in batch_keys:\n        if key in originals:\n            for field_name, original_bytes in originals[key].items():\n                pipe.hset(key, field_name, original_bytes)\n                restored_count += 1\n    pipe.execute()\n\nprint(f\"Restored original bytes for {restored_count} vector field(s)\")\n\n# Recreate the ORIGINAL float32 index definition over the restored data\noriginal_index = SearchIndex.from_dict(schema, redis_url=REDIS_URL)\noriginal_index.create(overwrite=True, drop=False)\n\ncheck = SearchIndex.from_existing(INDEX_NAME, redis_url=REDIS_URL)\nemb = next(f for f in check.schema.to_dict()['fields'] if f['name'] == 'embedding')\nprint(\"embedding datatype after rollback:\", emb['attrs']['datatype'])",
      "section_id": "8-recover-original-vectors-from-the-backup-rollback"
    },
    {
      "id": "9-build-and-apply-a-migration-with-the-wizard-ex0",
      "language": "python",
      "code": "import builtins\nimport copy\nfrom contextlib import contextmanager\n\nfrom redisvl.migration import MigrationWizard\nfrom redisvl.migration.utils import wait_for_index_ready\n\nWIZARD_INDEX_NAME = \"wizard_products\"\nWIZARD_PREFIX = \"wizard_product\"\nWIZARD_PATCH_PATH = \"wizard_schema_patch.yaml\"\nWIZARD_PLAN_PATH = \"wizard_migration_plan.yaml\"\nWIZARD_TARGET_SCHEMA_PATH = \"wizard_target_schema.yaml\"\nWIZARD_BACKUP_DIR = \"./wizard_migration_backups\"\n\nwizard_schema = copy.deepcopy(schema)\nwizard_schema[\"index\"][\"name\"] = WIZARD_INDEX_NAME\nwizard_schema[\"index\"][\"prefix\"] = WIZARD_PREFIX\n\n# Start from a clean wizard demo index and keyspace.\ntry:\n    existing_wizard_index = SearchIndex.from_existing(\n        WIZARD_INDEX_NAME, redis_url=REDIS_URL\n    )\n    existing_wizard_index.delete(drop=True)\nexcept Exception:\n    pass\ndelete_matching(client, f\"{WIZARD_PREFIX}:*\")\n\nwizard_index = SearchIndex.from_dict(wizard_schema, redis_url=REDIS_URL)\nwizard_index.create()\nwizard_index.load(data, id_field=None)\nwait_for_index_ready(wizard_index)\nprint(f\"Loaded {N_DOCS} documents into '{WIZARD_INDEX_NAME}'\")\n\n\n@contextmanager\ndef scripted_inputs(answers):\n    original_input = builtins.input\n    iterator = iter(answers)\n\n    def fake_input(prompt=\"\"):\n        answer = next(iterator)\n        print(f\"{prompt}{answer}\")\n        return answer\n\n    builtins.input = fake_input\n    try:\n        yield\n    finally:\n        builtins.input = original_input\n\n\nwizard_answers = [\n    \"2\",          # Update field\n    \"embedding\",  # Select the vector field\n    \"\",           # Keep algorithm\n    \"float16\",    # Quantize datatype\n    \"\",           # Keep distance metric\n    \"8\",          # Finish\n]\n\nwith scripted_inputs(wizard_answers):\n    wizard_plan = MigrationWizard().run(\n        index_name=WIZARD_INDEX_NAME,\n        redis_url=REDIS_URL,\n        plan_out=WIZARD_PLAN_PATH,\n        patch_out=WIZARD_PATCH_PATH,\n        target_schema_out=WIZARD_TARGET_SCHEMA_PATH,\n    )\n\nprint(\"\\nWizard patch:\")\nprint(open(WIZARD_PATCH_PATH).read())\nprint(\"Wizard plan mode:\", wizard_plan.mode)\nprint(\"Wizard warnings:\", wizard_plan.warnings)",
      "section_id": "9-build-and-apply-a-migration-with-the-wizard"
    },
    {
      "id": "9-build-and-apply-a-migration-with-the-wizard-ex1",
      "language": "python",
      "code": "wizard_report = MigrationExecutor().apply(\n    wizard_plan,\n    redis_url=REDIS_URL,\n    backup_dir=WIZARD_BACKUP_DIR,\n    batch_size=100,\n    num_workers=1,\n)\n\nwizard_live = SearchIndex.from_existing(WIZARD_INDEX_NAME, redis_url=REDIS_URL)\nwizard_embedding = next(\n    f for f in wizard_live.schema.to_dict()[\"fields\"] if f[\"name\"] == \"embedding\"\n)\n\nprint(\"Wizard migration result:\", wizard_report.result)\nprint(\"Wizard schema match:    \", wizard_report.validation.schema_match)\nprint(\"Wizard doc count match: \", wizard_report.validation.doc_count_match)\nprint(\"Wizard backup dir:      \", wizard_report.backup.backup_dir)\nprint(\"Wizard backup prefixes: \", wizard_report.backup.backup_paths)\nprint(\"Wizard embedding dtype: \", wizard_embedding[\"attrs\"][\"datatype\"])",
      "section_id": "9-build-and-apply-a-migration-with-the-wizard"
    },
    {
      "id": "10-cleanup-optional-ex0",
      "language": "python",
      "code": "delete_matching(client, \"product:*\")\nif check.exists():\n    check.delete(drop=False)\n\ndelete_matching(client, f\"{WIZARD_PREFIX}:*\")\nif wizard_live.exists():\n    wizard_live.delete(drop=False)\n\nfor backup_dir in (report.backup.backup_dir, wizard_report.backup.backup_dir):\n    for f in glob.glob(os.path.join(backup_dir, '*')):\n        os.remove(f)\n    if os.path.isdir(backup_dir):\n        os.rmdir(backup_dir)\n\nfor f in (\n    \"schema_patch.yaml\",\n    \"migration_plan.yaml\",\n    \"not_a_backup_dir\",\n    WIZARD_PATCH_PATH,\n    WIZARD_PLAN_PATH,\n    WIZARD_TARGET_SCHEMA_PATH,\n):\n    if os.path.exists(f):\n        os.remove(f)\nprint(\"Cleaned up demo indexes, demo keys, backups, and YAML files\")",
      "section_id": "10-cleanup-optional"
    }
  ]
}
