Spaces:
Runtime error
Runtime error
| # Qdrant Collection Migration Runbook | |
| **Purpose:** Step-by-step operator procedure for migrating the Qdrant vector store to a new | |
| embedding model (and therefore a new collection dimension) without downtime. | |
| **Background:** Each Qdrant collection is created with a fixed vector dimension. When the embedding | |
| model changes (e.g. from `text-embedding-3-small` at 1536 dims to `gemini-embedding-2` at 3072 dims), | |
| the old collection cannot accept new-dimension vectors. This runbook covers the safe migration path: | |
| create a new collection, backfill, verify, then cut over. | |
| --- | |
| ## Table of Contents | |
| 1. [When to Run This Runbook](#1-when-to-run-this-runbook) | |
| 2. [Pre-flight Checks](#2-pre-flight-checks) | |
| 3. [Local / Dev Migration](#3-local--dev-migration) | |
| 4. [Production Migration](#4-production-migration) | |
| 5. [Post-Cutover Verification](#5-post-cutover-verification) | |
| 6. [Rollback Procedure](#6-rollback-procedure) | |
| 7. [Known Limitations](#7-known-limitations) | |
| --- | |
| ## 1. When to Run This Runbook | |
| Run this when: | |
| - `EMBEDDING_MODEL` or `QDRANT_VECTOR_SIZE` is changing in any environment | |
| - The backend startup log shows `Vector store readiness check failed` with a dimension mismatch error | |
| - `/health` returns `"qdrant": "degraded"` after an embedding model change | |
| You do **not** need this runbook for: | |
| - Rebuilding one tenant's index (use `POST /api/v1/kb/reindex` or `scripts/rebuild_tenant_index.py`) | |
| - Changing the LLM model (no vector dimension impact) | |
| - Scaling Qdrant replicas or upgrading Qdrant server version | |
| --- | |
| ## 2. Pre-flight Checks | |
| Run these before starting in any environment: | |
| ```bash | |
| # 1. Confirm current collection dimension in Qdrant | |
| docker exec chatbot_qdrant_dev python3 -c " | |
| from qdrant_client import QdrantClient | |
| c = QdrantClient('http://localhost:6333') | |
| info = c.get_collection('smart_chatbot_kb') | |
| print('Current dim:', info.config.params.vectors.size) | |
| " | |
| # Or from the backend container: | |
| docker exec chatbot_backend_dev python3 -c " | |
| from app.services.dependencies import get_vector_store | |
| print('Configured dim:', get_vector_store().vector_size) | |
| print('Actual dim:', get_vector_store().get_collection_dimension()) | |
| " | |
| # 2. Confirm the test suite is green | |
| docker exec chatbot_backend_dev uv run pytest tests -q --tb=no | |
| # 3. Note the current collection name (from .env) | |
| grep QDRANT_COLLECTION_NAME .env | |
| ``` | |
| --- | |
| ## 3. Local / Dev Migration | |
| ### Step 1 β Dry run (inspect only) | |
| ```bash | |
| docker exec chatbot_backend_dev uv run python scripts/migrate_qdrant_collection.py \ | |
| --target smart_chatbot_kb_v2 \ | |
| --dry-run | |
| ``` | |
| Expected output: | |
| ``` | |
| Source collection : smart_chatbot_kb | |
| Actual dimension: 384 | |
| Target collection : smart_chatbot_kb_v2 | |
| Target dimension: 3072 | |
| Dry run: would migrate smart_chatbot_kb (384 β 3072) β smart_chatbot_kb_v2 | |
| No writes performed. | |
| ``` | |
| If you see `No migration needed`, the collection already matches. Stop here. | |
| ### Step 2 β Run the migration | |
| ```bash | |
| docker exec chatbot_backend_dev uv run python scripts/migrate_qdrant_collection.py \ | |
| --target smart_chatbot_kb_v2 | |
| ``` | |
| The script will: | |
| 1. Create `smart_chatbot_kb_v2` at the target dimension | |
| 2. Re-embed all tenant KB entries | |
| 3. Run a health probe | |
| 4. Print the cutover env var | |
| ### Step 3 β Cutover | |
| Update `.env`: | |
| ``` | |
| QDRANT_COLLECTION_NAME=smart_chatbot_kb_v2 | |
| ``` | |
| Restart the backend: | |
| ```bash | |
| docker compose -f docker-compose.dev.yml restart smart-chatbot | |
| ``` | |
| ### Step 4 β Verify (see Β§5) | |
| ### Step 5 β Retire old collection (after verification) | |
| ```bash | |
| docker exec chatbot_backend_dev python3 -c " | |
| from qdrant_client import QdrantClient; import os | |
| c = QdrantClient(os.getenv('QDRANT_URL', 'http://qdrant:6333')) | |
| c.delete_collection('smart_chatbot_kb') | |
| print('Deleted smart_chatbot_kb') | |
| " | |
| ``` | |
| --- | |
| ## 4. Production Migration | |
| Production uses a live Qdrant instance. Follow this zero-impact sequence. | |
| ### Step 1 β Connect to the production backend container | |
| ```bash | |
| ssh <prod-server> | |
| docker exec -it <prod-backend-container> bash | |
| ``` | |
| ### Step 2 β Dry run first | |
| ```bash | |
| uv run python scripts/migrate_qdrant_collection.py --target smart_chatbot_kb_v2 --dry-run | |
| ``` | |
| Verify the output shows the expected dimension change. If it says "no migration needed", stop. | |
| ### Step 3 β Run the migration during low-traffic hours | |
| ```bash | |
| uv run python scripts/migrate_qdrant_collection.py --target smart_chatbot_kb_v2 | |
| ``` | |
| > β **Important:** While the migration runs, the live system continues to serve queries from the | |
| > **old** collection. New KB entries written during migration land in the old collection and may not | |
| > appear in the new one. Schedule during low-traffic hours or put the KB write endpoints behind | |
| > a maintenance flag during migration. | |
| ### Step 4 β Update the environment variable | |
| Update `QDRANT_COLLECTION_NAME=smart_chatbot_kb_v2` in your production env file or secret manager | |
| (AWS SSM / Vault / Heroku config var / etc.). | |
| ### Step 5 β Rolling restart | |
| Restart the backend service. With Docker: | |
| ```bash | |
| docker compose -f docker-compose.prod.yml restart smart-chatbot | |
| ``` | |
| With Kubernetes: | |
| ```bash | |
| kubectl rollout restart deployment/<backend-deployment> | |
| ``` | |
| ### Step 6 β Verify (see Β§5) | |
| ### Step 7 β Post-cutover reindex (for any missed entries) | |
| ```bash | |
| # For each tenant that had activity during the migration window: | |
| curl -X POST https://your-domain.com/api/v1/kb/reindex \ | |
| -H "X-API-Key: <tenant-api-key>" | |
| ``` | |
| ### Step 8 β Retire old collection (after 24-hour observation window) | |
| Only after you are confident the new collection is serving correctly: | |
| ```bash | |
| # From the backend container: | |
| python3 -c " | |
| from qdrant_client import QdrantClient; import os | |
| c = QdrantClient(os.getenv('QDRANT_URL')) | |
| c.delete_collection('smart_chatbot_kb') | |
| print('Retired smart_chatbot_kb') | |
| " | |
| ``` | |
| --- | |
| ## 5. Post-Cutover Verification | |
| After restarting the backend with the new collection name: | |
| ```bash | |
| # 1. Check /health reports qdrant as healthy | |
| curl https://your-domain.com/health | python3 -m json.tool | grep qdrant | |
| # 2. Send a test chat message to confirm RAG is working | |
| curl -X POST https://your-domain.com/api/v1/chat \ | |
| -H "X-API-Key: <test-tenant-api-key>" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"message": "What do you help with?"}' | |
| # 3. Run the full test suite (on non-prod environments) | |
| docker exec chatbot_backend_dev uv run pytest tests -q --tb=no | |
| ``` | |
| --- | |
| ## 6. Rollback Procedure | |
| If anything goes wrong after cutover: | |
| ### Immediate rollback | |
| ```bash | |
| # 1. Revert QDRANT_COLLECTION_NAME to the old collection name in .env | |
| QDRANT_COLLECTION_NAME=smart_chatbot_kb | |
| # 2. Restart backend | |
| docker compose -f docker-compose.dev.yml restart smart-chatbot | |
| # Or in production: kubectl rollout restart / docker compose restart | |
| # 3. Verify /health shows qdrant: healthy | |
| ``` | |
| The old collection is untouched throughout the migration β this rollback is instant and safe. | |
| > Only delete the old collection **after** you are fully satisfied with the new one. | |
| --- | |
| ## 7. Known Limitations | |
| | Limitation | Impact | Workaround | | |
| |---|---|---| | |
| | KB writes during migration miss the target collection | New entries are in old collection only until reindex | After cutover, call `POST /api/v1/kb/reindex` for affected tenants | | |
| | Partial failure mid-backfill leaves target partially populated | Some tenants missing from target | Re-run the migration script (it is idempotent β deletes and re-upserts per tenant) | | |
| | No atomic cutover | Brief window where backend serves from old collection while restart is in progress | Acceptable for V1 β use rolling restart to minimize window | | |
| | `_qdrant_ready` flag not re-evaluated at runtime | `/health` may stay `degraded` even after Qdrant recovers mid-run | Restart backend to re-evaluate; tracked as future improvement | | |