"""
向量存储迁移和备份工具
提供向量存储系统间的数据迁移、备份恢复和数据验证功能。
支持内存存储、FAISS、Milvus等不同后端之间的无缝迁移。
"""
import hashlib
import json
import logging
import os
import shutil
from datetime import datetime
from typing import Any, Dict, Iterator, List, Optional, Protocol, runtime_checkable
from haystack import Document as HaystackDocument
from haystack.document_stores.in_memory import InMemoryDocumentStore
from document_stores.implementations.base_vector_store import BaseVectorStore, VectorStoreError
from document_stores.vector_store_factory import create_vector_store
@runtime_checkable
class _HaystackDocumentSource(Protocol):
def get_haystack_store(self) -> InMemoryDocumentStore: ...
@runtime_checkable
class _MappedDocumentSource(Protocol):
documents: Dict[str, HaystackDocument]
[docs]
class VectorStoreMigrator:
"""
向量存储迁移工具类。
提供完整的迁移、备份、恢复和验证功能。
"""
[docs]
def __init__(self) -> None:
"""初始化迁移工具"""
self.logger = logging.getLogger("VectorStoreMigrator")
[docs]
def migrate(
self,
source_config: Dict[str, Any],
target_config: Dict[str, Any],
batch_size: int = 1000,
verify_migration: bool = True,
backup_before_migration: bool = True,
) -> bool:
"""
执行向量存储迁移。
Args:
source_config: 源存储配置
target_config: 目标存储配置
batch_size: 批量迁移大小
verify_migration: 是否验证迁移结果
backup_before_migration: 迁移前是否备份
Returns:
迁移成功返回True,失败返回False
"""
try:
self.logger.info("开始向量存储迁移...")
# 创建源和目标存储
source_store = create_vector_store(source_config, auto_fallback=False)
target_store = create_vector_store(target_config, auto_fallback=False)
# 迁移前备份
backup_path = None
if backup_before_migration:
backup_path = self.backup_storage(
source_store, f"backup_before_migration_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
)
if not backup_path:
self.logger.error("迁移前备份失败")
return False
# 执行迁移
success = self._execute_migration(source_store, target_store, batch_size)
if not success:
self.logger.error("迁移执行失败")
return False
# 验证迁移结果
if verify_migration:
if not self.verify_migration(source_store, target_store):
self.logger.error("迁移验证失败")
return False
self.logger.info("向量存储迁移完成")
# 清理资源
source_store.close()
target_store.close()
return True
except Exception as e:
self.logger.error(f"迁移过程发生错误: {str(e)}")
return False
def _execute_migration(self, source_store: BaseVectorStore, target_store: BaseVectorStore, batch_size: int) -> bool:
"""
执行实际的迁移操作。
Args:
source_store: 源存储
target_store: 目标存储
batch_size: 批处理大小
Returns:
迁移成功返回True
"""
total_docs = source_store.get_document_count()
if total_docs == 0:
self.logger.info("源存储为空,无需迁移")
return True
self.logger.info(f"开始迁移 {total_docs} 个文档")
migrated_count = 0
try:
# 批量迁移文档
for batch_docs in self._get_documents_in_batches(source_store, batch_size):
if not batch_docs:
continue
# 添加到目标存储
if not target_store.add_documents(batch_docs):
self.logger.error(f"批量添加文档失败,已迁移 {migrated_count} 个文档")
return False
migrated_count += len(batch_docs)
self.logger.info(f"已迁移 {migrated_count}/{total_docs} 个文档")
self.logger.info(f"迁移完成,总共迁移 {migrated_count} 个文档")
return migrated_count == total_docs
except Exception as e:
self.logger.error(f"迁移执行失败: {str(e)}")
return False
def _get_documents_in_batches(self, store: BaseVectorStore, batch_size: int) -> Iterator[List[HaystackDocument]]:
"""
批量获取文档的生成器。
Args:
store: 向量存储
batch_size: 批处理大小
Yields:
文档批次列表
"""
if isinstance(store, _HaystackDocumentSource):
all_docs = store.get_haystack_store().filter_documents({})
elif isinstance(store, _MappedDocumentSource) and isinstance(store.documents, dict):
all_docs = list(store.documents.values())
else:
raise VectorStoreError("Storage backend does not expose documents for migration")
for i in range(0, len(all_docs), batch_size):
yield all_docs[i : i + batch_size]
[docs]
def backup_storage(self, store: BaseVectorStore, backup_name: str, backup_dir: str = "./backups") -> Optional[str]:
"""
备份向量存储。
Args:
store: 要备份的存储
backup_name: 备份名称
backup_dir: 备份目录
Returns:
备份路径,失败返回None
"""
try:
# 创建备份目录
os.makedirs(backup_dir, exist_ok=True)
backup_path = os.path.join(backup_dir, backup_name)
os.makedirs(backup_path, exist_ok=True)
self.logger.info(f"开始备份到 {backup_path}")
# 保存存储索引
if not store.save_index(backup_path):
self.logger.error("保存存储索引失败")
return None
# 保存配置和元数据
backup_metadata = {
"backup_time": datetime.now().isoformat(),
"storage_info": store.get_storage_info(),
"document_count": store.get_document_count(),
"backup_version": "1.0",
}
metadata_path = os.path.join(backup_path, "backup_metadata.json")
with open(metadata_path, "w", encoding="utf-8") as f:
json.dump(backup_metadata, f, indent=2, ensure_ascii=False)
# 创建备份校验和
checksum = self._calculate_backup_checksum(backup_path)
checksum_path = os.path.join(backup_path, "backup.checksum")
with open(checksum_path, "w") as f:
f.write(checksum)
self.logger.info(f"备份完成: {backup_path}")
return backup_path
except Exception as e:
self.logger.error(f"备份失败: {str(e)}")
return None
[docs]
def restore_storage(self, backup_path: str, target_config: Dict[str, Any], verify_backup: bool = True) -> bool:
"""
从备份恢复向量存储。
Args:
backup_path: 备份路径
target_config: 目标存储配置
verify_backup: 是否验证备份完整性
Returns:
恢复成功返回True
"""
try:
self.logger.info(f"开始从 {backup_path} 恢复存储")
# 验证备份完整性
if verify_backup and not self.verify_backup(backup_path):
self.logger.error("备份验证失败")
return False
# 加载备份元数据
metadata_path = os.path.join(backup_path, "backup_metadata.json")
if not os.path.exists(metadata_path):
self.logger.error("备份元数据文件不存在")
return False
with open(metadata_path, "r", encoding="utf-8") as f:
backup_metadata = json.load(f)
# 创建目标存储
target_store = create_vector_store(target_config, auto_fallback=False)
# 加载索引
if not target_store.load_index(backup_path):
self.logger.error("加载索引失败")
return False
# 验证恢复结果
restored_count = target_store.get_document_count()
expected_count = backup_metadata.get("document_count", 0)
if restored_count != expected_count:
self.logger.warning(f"恢复的文档数量 ({restored_count}) 与备份记录 ({expected_count}) 不匹配")
self.logger.info(f"存储恢复完成,恢复了 {restored_count} 个文档")
target_store.close()
return True
except Exception as e:
self.logger.error(f"恢复失败: {str(e)}")
return False
@staticmethod
def _documents_match(source: HaystackDocument, target: Optional[HaystackDocument]) -> bool:
return (
target is not None
and target.content == source.content
and target.meta == source.meta
and target.embedding == source.embedding
)
[docs]
def verify_migration(
self, source_store: BaseVectorStore, target_store: BaseVectorStore, sample_ratio: float = 0.1
) -> bool:
"""
验证迁移结果。
Args:
source_store: 源存储
target_store: 目标存储
sample_ratio: 抽样验证比例
Returns:
验证通过返回True
"""
try:
self.logger.info("开始验证迁移结果...")
# 验证文档数量
source_count = source_store.get_document_count()
target_count = target_store.get_document_count()
if source_count != target_count:
self.logger.error(f"文档数量不匹配: 源存储 {source_count}, 目标存储 {target_count}")
return False
if source_count == 0:
self.logger.info("存储为空,验证通过")
return True
# 抽样验证文档内容和向量
sample_size = max(1, int(source_count * sample_ratio))
self.logger.info(f"抽样验证 {sample_size} 个文档")
verified = 0
for batch in self._get_documents_in_batches(source_store, max(1, sample_size)):
for source_doc in batch:
target_doc = target_store.get_document_by_id(source_doc.id)
if not self._documents_match(source_doc, target_doc):
return False
verified += 1
if verified >= sample_size:
break
if verified >= sample_size:
break
success_rate = verified / sample_size
self.logger.info(f"迁移验证通过,成功率: {success_rate:.2%}")
return verified > 0
except Exception as e:
self.logger.error(f"迁移验证失败: {str(e)}")
return False
[docs]
def verify_backup(self, backup_path: str) -> bool:
"""
验证备份完整性。
Args:
backup_path: 备份路径
Returns:
验证通过返回True
"""
try:
# 检查必要文件
required_files = ["backup_metadata.json", "backup.checksum"]
for file_name in required_files:
file_path = os.path.join(backup_path, file_name)
if not os.path.exists(file_path):
self.logger.error(f"备份文件缺失: {file_name}")
return False
# 验证校验和
checksum_path = os.path.join(backup_path, "backup.checksum")
with open(checksum_path, "r") as f:
stored_checksum = f.read().strip()
calculated_checksum = self._calculate_backup_checksum(backup_path)
if stored_checksum != calculated_checksum:
self.logger.error("备份校验和不匹配")
return False
self.logger.info("备份验证通过")
return True
except Exception as e:
self.logger.error(f"备份验证失败: {str(e)}")
return False
def _calculate_backup_checksum(self, backup_path: str) -> str:
"""
计算备份的校验和。
Args:
backup_path: 备份路径
Returns:
校验和字符串
"""
hasher = hashlib.sha256()
# 遍历所有文件计算校验和
for root, dirs, files in os.walk(backup_path):
for file in sorted(files):
if file == "backup.checksum": # 跳过校验和文件本身
continue
file_path = os.path.join(root, file)
try:
with open(file_path, "rb") as f:
while chunk := f.read(8192):
hasher.update(chunk)
except Exception as e:
self.logger.warning(f"无法读取文件 {file_path}: {str(e)}")
return hasher.hexdigest()
[docs]
def list_backups(self, backup_dir: str = "./backups") -> List[Dict[str, Any]]:
"""
列出所有备份。
Args:
backup_dir: 备份目录
Returns:
备份信息列表
"""
backups: List[Dict[str, Any]] = []
if not os.path.exists(backup_dir):
return backups
try:
for backup_name in os.listdir(backup_dir):
backup_path = os.path.join(backup_dir, backup_name)
if not os.path.isdir(backup_path):
continue
metadata_path = os.path.join(backup_path, "backup_metadata.json")
if not os.path.exists(metadata_path):
continue
try:
with open(metadata_path, "r", encoding="utf-8") as f:
metadata = json.load(f)
backup_info = {
"name": backup_name,
"path": backup_path,
"backup_time": metadata.get("backup_time"),
"document_count": metadata.get("document_count", 0),
"storage_type": metadata.get("storage_info", {}).get("storage_type"),
"size_mb": self._get_directory_size(backup_path) / 1024 / 1024,
}
backups.append(backup_info)
except Exception as e:
self.logger.warning(f"无法读取备份元数据 {backup_name}: {str(e)}")
except Exception as e:
self.logger.error(f"列出备份失败: {str(e)}")
# 按备份时间排序
backups.sort(key=lambda x: x.get("backup_time", ""), reverse=True)
return backups
def _get_directory_size(self, path: str) -> int:
"""
获取目录大小(字节)。
Args:
path: 目录路径
Returns:
目录大小
"""
total_size = 0
try:
for dirpath, dirnames, filenames in os.walk(path):
for filename in filenames:
file_path = os.path.join(dirpath, filename)
if os.path.exists(file_path):
total_size += os.path.getsize(file_path)
except Exception as e:
self.logger.warning(f"计算目录大小失败: {str(e)}")
return total_size
[docs]
def cleanup_old_backups(self, backup_dir: str = "./backups", keep_count: int = 10) -> int:
"""
清理旧备份。
Args:
backup_dir: 备份目录
keep_count: 保留的备份数量
Returns:
删除的备份数量
"""
try:
backups = self.list_backups(backup_dir)
if len(backups) <= keep_count:
self.logger.info(f"备份数量 ({len(backups)}) 未超过保留限制 ({keep_count})")
return 0
# 删除最旧的备份
backups_to_delete = backups[keep_count:]
deleted_count = 0
for backup in backups_to_delete:
try:
shutil.rmtree(backup["path"])
self.logger.info(f"已删除旧备份: {backup['name']}")
deleted_count += 1
except Exception as e:
self.logger.error(f"删除备份失败 {backup['name']}: {str(e)}")
self.logger.info(f"清理完成,删除了 {deleted_count} 个旧备份")
return deleted_count
except Exception as e:
self.logger.error(f"清理备份失败: {str(e)}")
return 0
# 便捷函数
[docs]
def migrate_vector_storage(
source_config: Dict[str, Any], target_config: Dict[str, Any], batch_size: int = 1000
) -> bool:
"""
向量存储迁移的便捷函数。
Args:
source_config: 源存储配置
target_config: 目标存储配置
batch_size: 批处理大小
Returns:
迁移成功返回True
"""
migrator = VectorStoreMigrator()
return migrator.migrate(source_config, target_config, batch_size)
[docs]
def backup_vector_storage(store: BaseVectorStore, backup_name: str) -> Optional[str]:
"""
向量存储备份的便捷函数。
Args:
store: 要备份的存储
backup_name: 备份名称
Returns:
备份路径,失败返回None
"""
migrator = VectorStoreMigrator()
return migrator.backup_storage(store, backup_name)