Spaces:
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
- When to Run This Runbook
- Pre-flight Checks
- Local / Dev Migration
- Production Migration
- Post-Cutover Verification
- Rollback Procedure
- Known Limitations
1. When to Run This Runbook
Run this when:
EMBEDDING_MODELorQDRANT_VECTOR_SIZEis changing in any environment- The backend startup log shows
Vector store readiness check failedwith a dimension mismatch error /healthreturns"qdrant": "degraded"after an embedding model change
You do not need this runbook for:
- Rebuilding one tenant's index (use
POST /api/v1/kb/reindexorscripts/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:
# 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)
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
docker exec chatbot_backend_dev uv run python scripts/migrate_qdrant_collection.py \
--target smart_chatbot_kb_v2
The script will:
- Create
smart_chatbot_kb_v2at the target dimension - Re-embed all tenant KB entries
- Run a health probe
- Print the cutover env var
Step 3 β Cutover
Update .env:
QDRANT_COLLECTION_NAME=smart_chatbot_kb_v2
Restart the backend:
docker compose -f docker-compose.dev.yml restart smart-chatbot
Step 4 β Verify (see Β§5)
Step 5 β Retire old collection (after verification)
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
ssh <prod-server>
docker exec -it <prod-backend-container> bash
Step 2 β Dry run first
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
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:
docker compose -f docker-compose.prod.yml restart smart-chatbot
With Kubernetes:
kubectl rollout restart deployment/<backend-deployment>
Step 6 β Verify (see Β§5)
Step 7 β Post-cutover reindex (for any missed entries)
# 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:
# 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:
# 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
# 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 |