Sanctum Migration Guide
Guide for migrating Sanctum memory storage between adapters, upgrading infrastructure, and managing data transitions.
Table of Contents
- Migration Scenarios
- InMemory to Qdrant Migration
- Qdrant Version Upgrades
- Changing Vector Dimensions
- Zero-Downtime Migration
- Rollback Procedures
- Data Validation
- Troubleshooting
Migration Scenarios
Common Migration Paths
- Development to Production: InMemory → Qdrant
- Scaling Up: Local Qdrant → Qdrant Cluster
- Cloud Migration: Self-hosted → Qdrant Cloud
- Dimension Change: 384 → 1536 dimensions (model upgrade)
- Version Upgrade: Qdrant v1.6 → v1.7
InMemory to Qdrant Migration
Overview
Migrate from ephemeral InMemory storage to persistent Qdrant for production use.
Prerequisites
- Running Qdrant instance (local, cluster, or cloud)
- Sufficient storage capacity
- Matching embedding model dimensions
- Paladin application with both adapters available
Migration Steps
Step 1: Export from InMemory
Create an export utility:
// src/bin/export_sanctum.rs use paladin::paladin_ports::output::sanctum_port::{SanctumPort, SanctumFilter}; use paladin::core::platform::container::sanctum::SanctumEntry; use std::fs::File; use std::io::Write; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // Initialize InMemory adapter with existing data let in_memory = InMemorySanctum::new(); // Export all memories let filter = SanctumFilter::new(); // No filter = all memories let count = in_memory.count(Some(filter.clone())).await?; println!("Exporting {} memories...", count); // For InMemory, we need to implement an export method // This is a simplified example let memories = export_all_memories(&in_memory).await?; // Serialize to JSON let json = serde_json::to_string_pretty(&memories)?; let mut file = File::create("sanctum_export.json")?; file.write_all(json.as_bytes())?; println!("Export complete: {} memories written to sanctum_export.json", memories.len()); Ok(()) } async fn export_all_memories( sanctum: &dyn SanctumPort ) -> Result<Vec<SanctumEntry>, Box<dyn std::error::Error>> { // Implementation depends on your specific setup // May need to add export methods to SanctumPort trait todo!("Implement export logic") }
Serialized Format:
{
"version": "1.0",
"exported_at": "2024-01-30T10:00:00Z",
"total_entries": 10000,
"entries": [
{
"memory": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"paladin_id": "paladin-123",
"content": "User asked about Rust programming",
"memory_type": "Episodic",
"importance": 0.8,
"access_count": 5,
"created_at": "2024-01-30T09:00:00Z",
"last_accessed": "2024-01-30T09:30:00Z",
"metadata": {}
},
"embedding": [0.1, -0.2, 0.3, ...]
}
]
}
Step 2: Set Up Qdrant
Option A: Docker
docker run -d \
--name paladin-qdrant \
-p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrant:v1.7.4
Option B: Kubernetes
kubectl apply -f k8s/qdrant-statefulset.yaml
Option C: Qdrant Cloud
Sign up at https://qdrant.to/cloud and create a cluster.
Verify connectivity:
curl http://localhost:6333/health
# Expected: {"title":"qdrant - vector search engine","version":"1.7.4"}
Step 3: Configure Paladin for Qdrant
Update configuration:
# config.yml
sanctum:
enabled: true
adapter_type: "qdrant"
qdrant:
url: "http://localhost:6334"
collection_name: "migrated_memories"
vector_dimension: 1536 # Match your embeddings
Or via environment variables:
export APP_SANCTUM_ADAPTER_TYPE=qdrant
export APP_SANCTUM_QDRANT_URL=http://localhost:6334
export APP_SANCTUM_QDRANT_COLLECTION_NAME=migrated_memories
export APP_SANCTUM_QDRANT_VECTOR_DIMENSION=1536
Step 4: Import to Qdrant
Create an import utility:
// src/bin/import_sanctum.rs use paladin::infrastructure::adapters::sanctum::QdrantSanctumAdapter; use paladin::core::platform::container::sanctum::SanctumEntry; use std::fs::File; use std::io::Read; #[derive(Deserialize)] struct ExportData { version: String, total_entries: usize, entries: Vec<SanctumEntry>, } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // Read export file let mut file = File::open("sanctum_export.json")?; let mut contents = String::new(); file.read_to_string(&mut contents)?; let export: ExportData = serde_json::from_str(&contents)?; println!("Importing {} memories...", export.total_entries); // Initialize Qdrant adapter let qdrant = QdrantSanctumAdapter::new( "http://localhost:6334", "migrated_memories", 1536, ).await?; // Import in batches for efficiency let batch_size = 100; for chunk in export.entries.chunks(batch_size) { qdrant.store_batch(chunk.to_vec()).await?; println!("Imported batch of {} memories", chunk.len()); } // Verify count let count = qdrant.count(None).await?; println!("Import complete! Total memories in Qdrant: {}", count); if count != export.total_entries { eprintln!("WARNING: Count mismatch! Expected {}, got {}", export.total_entries, count); } Ok(()) }
Run the import:
cargo run --bin import_sanctum
Expected output:
Importing 10000 memories...
Imported batch of 100 memories
Imported batch of 100 memories
...
Import complete! Total memories in Qdrant: 10000
Step 5: Validate Migration
Run validation checks:
// src/bin/validate_migration.rs use paladin::infrastructure::adapters::sanctum::QdrantSanctumAdapter; use paladin::paladin_ports::output::sanctum_port::{SanctumPort, SanctumQuery}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let qdrant = QdrantSanctumAdapter::new( "http://localhost:6334", "migrated_memories", 1536, ).await?; // 1. Count check let total = qdrant.count(None).await?; println!("✓ Total memories: {}", total); // 2. Sample search test let test_embedding = vec![0.1; 1536]; // Dummy embedding let query = SanctumQuery::new(test_embedding, 5); let results = qdrant.search(query).await?; println!("✓ Search returned {} results", results.len()); // 3. Specific memory retrieval // Test with a known memory ID from export println!("✓ Validation complete!"); Ok(()) }
Step 6: Switch Production Traffic
Graceful Cutover:
- Deploy new Paladin version with Qdrant configuration
- Monitor for errors in logs
- Compare search results between old and new
- Gradually increase traffic to new adapter
Configuration Update:
# Update environment and restart
kubectl set env deployment/paladin \
APP_SANCTUM_ADAPTER_TYPE=qdrant \
APP_SANCTUM_QDRANT_URL=http://qdrant:6334
kubectl rollout status deployment/paladin
Step 7: Cleanup
After successful validation:
# Remove export file
rm sanctum_export.json
# Stop old InMemory instances
# Update documentation
# Remove InMemory-specific code if no longer needed
Migration Checklist
- Export all memories from InMemory adapter
- Verify export file integrity and count
- Deploy Qdrant infrastructure
- Test Qdrant connectivity
- Configure Paladin for Qdrant
- Import memories in batches
- Validate total count matches
- Run sample searches
- Test specific memory retrieval
- Monitor application logs for errors
- Compare performance metrics
- Update production configuration
- Document new architecture
- Schedule backups
- Remove temporary export files
Qdrant Version Upgrades
Upgrade Path
Qdrant follows semantic versioning. Minor version upgrades (1.6 → 1.7) are generally safe.
Upgrade Process
Step 1: Create Backup
# Create snapshot of all collections
curl -X POST http://localhost:6333/collections/paladin_memories/snapshots
Step 2: Test in Staging
Deploy new version to staging environment first:
# docker-compose.staging.yml
services:
qdrant-new:
image: qdrant/qdrant:v1.7.4 # New version
# ... rest of config
Step 3: Verify Compatibility
# Test with staging data
cargo test --test qdrant_integration
Step 4: Production Upgrade
Blue-Green Deployment:
- Deploy new Qdrant instance (green)
- Replicate data from old instance (blue)
- Switch traffic to green
- Monitor for issues
- Decommission blue
Rolling Update (Kubernetes):
kubectl set image statefulset/qdrant \
qdrant=qdrant/qdrant:v1.7.4
kubectl rollout status statefulset/qdrant
Changing Vector Dimensions
Scenario
Upgrading embedding model (e.g., 384 → 1536 dimensions) requires re-embedding all content.
Process
Step 1: Re-embed All Content
// src/bin/reembed_memories.rs use paladin::infrastructure::adapters::sanctum::QdrantSanctumAdapter; use paladin::paladin_ports::output::{SanctumPort, EmbeddingPort}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // Old adapter (384 dimensions) let old_qdrant = QdrantSanctumAdapter::new( "http://localhost:6334", "old_memories", 384, ).await?; // New adapter (1536 dimensions) let new_qdrant = QdrantSanctumAdapter::new( "http://localhost:6334", "new_memories", 1536, ).await?; // New embedding provider let embedding_service = OpenAIEmbeddingAdapter::new(...); // Re-embed and transfer let batch_size = 100; // ... implementation to fetch, re-embed, and store Ok(()) }
Step 2: Update Configuration
sanctum:
enabled: true
adapter_type: "qdrant"
qdrant:
url: "http://localhost:6334"
collection_name: "new_memories" # New collection
vector_dimension: 1536 # Updated dimension
Step 3: Cutover
Switch application to new collection and dimension.
Zero-Downtime Migration
Strategy: Dual-Write Pattern
Write to both old and new adapters simultaneously during migration.
#![allow(unused)] fn main() { pub struct DualWriteSanctum { primary: Arc<dyn SanctumPort>, secondary: Arc<dyn SanctumPort>, } #[async_trait] impl SanctumPort for DualWriteSanctum { async fn store(&self, entry: SanctumEntry) -> Result<(), SanctumError> { // Write to both, but only require primary to succeed let primary_result = self.primary.store(entry.clone()).await; // Log secondary failures but don't fail the operation if let Err(e) = self.secondary.store(entry).await { warn!("Secondary write failed: {}", e); } primary_result } async fn search(&self, query: SanctumQuery) -> Result<Vec<SanctumSearchResult>, SanctumError> { // Always read from primary self.primary.search(query).await } // ... other methods } }
Migration Steps with Dual-Write
-
Phase 1: Dual-Write (Primary=Old, Secondary=New)
- Configure dual-write adapter
- Deploy application
- New writes go to both adapters
- Reads come from old adapter
-
Phase 2: Backfill Historical Data
- Run background job to copy old data to new adapter
- Monitor progress
-
Phase 3: Validation
- Compare counts
- Spot-check search results
- Validate data integrity
-
Phase 4: Flip Primary
- Switch to Primary=New, Secondary=Old
- Monitor for issues
-
Phase 5: Remove Dual-Write
- Stop dual-write
- Use only new adapter
- Decommission old adapter
Rollback Procedures
Immediate Rollback
If critical issues occur during migration:
# Kubernetes
kubectl rollout undo deployment/paladin
# Docker Compose
docker-compose down
docker-compose -f docker-compose.old.yml up -d
# Environment variables
export APP_SANCTUM_ADAPTER_TYPE=in_memory # Revert to old config
systemctl restart paladin
Data Rollback
Restore from snapshot:
# List snapshots
curl http://localhost:6333/collections/paladin_memories/snapshots
# Recover from snapshot
curl -X PUT http://localhost:6333/collections/paladin_memories/snapshots/recover \
-H "Content-Type: application/json" \
-d '{"location": "snapshot-name"}'
Validation After Rollback
# Verify service health
curl http://localhost:8080/health
# Check memory count
cargo run --bin count_memories
# Run smoke tests
cargo test --test smoke_test
Data Validation
Automated Validation Script
// src/bin/validate_sanctum.rs #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let sanctum = initialize_adapter().await?; // 1. Count validation let count = sanctum.count(None).await?; assert!(count > 0, "No memories found"); println!("✓ Count: {}", count); // 2. Search functionality let test_results = test_search(&sanctum).await?; assert!(!test_results.is_empty(), "Search returned no results"); println!("✓ Search: {} results", test_results.len()); // 3. Memory integrity for result in test_results.iter().take(10) { validate_memory(&result.entry.memory)?; } println!("✓ Memory integrity"); // 4. Embedding dimensions let expected_dim = 1536; for result in test_results.iter().take(5) { assert_eq!(result.entry.embedding.len(), expected_dim, "Embedding dimension mismatch"); } println!("✓ Embedding dimensions"); println!("\n✅ All validation checks passed!"); Ok(()) }
Manual Validation Checklist
- Total count matches expected
- Search returns relevant results
- All memory types present (Episodic, Semantic, Procedural)
- Importance scores in valid range (0.0-1.0)
- Timestamps are valid
- Metadata preserved
- Embedding dimensions correct
- No duplicate memories
- Performance within acceptable limits
Troubleshooting
Issue: Count Mismatch After Migration
Problem: Fewer memories in Qdrant than expected
Solutions:
-
Check import logs for errors:
grep -i error import.log -
Verify batch import completed:
# Check Qdrant collection info curl http://localhost:6333/collections/paladin_memories -
Re-run import for missing data:
#![allow(unused)] fn main() { // Identify missing memories and re-import }
Issue: Search Returns Incorrect Results
Problem: Search results don't match expectations
Solutions:
-
Verify embedding dimensions match:
vector_dimension: 1536 # Must match embedding model -
Check distance metric configuration:
#![allow(unused)] fn main() { distance: Distance::Cosine # Should match old setup } -
Rebuild HNSW index:
curl -X POST http://localhost:6333/collections/paladin_memories/index
Issue: Slow Import Performance
Problem: Import takes too long
Solutions:
-
Increase batch size:
#![allow(unused)] fn main() { let batch_size = 500; // Up from 100 } -
Disable indexing during import:
#![allow(unused)] fn main() { indexing_threshold: Some(0), // Index after import complete } -
Use parallel imports:
#![allow(unused)] fn main() { use futures::stream::StreamExt; futures::stream::iter(chunks) .for_each_concurrent(4, |chunk| async move { adapter.store_batch(chunk).await.unwrap(); }) .await; }
Issue: Out of Memory During Migration
Problem: Qdrant OOM killed during import
Solutions:
-
Reduce batch size:
#![allow(unused)] fn main() { let batch_size = 50; // Smaller batches } -
Enable quantization:
#![allow(unused)] fn main() { quantization_config: Some(QuantizationConfig::Scalar(...)) } -
Move vectors to disk temporarily:
#![allow(unused)] fn main() { on_disk: true } -
Increase node resources:
resources: limits: memory: "16Gi" # Increase from 8Gi
Best Practices
- Always Backup First: Create snapshots before any migration
- Test in Staging: Never migrate production data untested
- Gradual Rollout: Use blue-green or canary deployments
- Monitor Closely: Watch metrics during and after migration
- Have Rollback Plan: Know how to revert quickly
- Validate Thoroughly: Don't assume migration succeeded
- Document Everything: Record procedures and learnings
- Schedule Appropriately: Migrate during low-traffic periods
Support
For migration assistance:
- GitHub Issues: paladin-dev-env/issues
- Qdrant Discord: https://qdrant.to/discord
- Qdrant Documentation: https://qdrant.tech/documentation/
Next Steps: