FAISS-индекс — файловый синглтон в RAM одного воркера (save на каждую запись, без блокировок, без HA, не горизонтален). Добавлен альтернативный бэкенд Qdrant с тем же classmethod-интерфейсом, выбор через VECTOR_BACKEND — аддитивно и безопасно: дефолт остаётся faiss, ничего не ломается, пока не переключат. - app/qdrant_manager.py — search/add_vectors/save(no-op)/total_vectors поверх qdrant-client (коллекция Cosine, id точки = doc_id, upsert идемпотентен); - app/vector_store.py — get_backend() по настройке; задачи search/plagiarism переведены на него (больше не импортируют FAISSManager напрямую); - app/migrate_faiss_to_qdrant.py — перелив существующих векторов (reconstruct из IndexIDMap2 → upsert), идемпотентно; - docker-compose.prod.yml — сервис qdrant под профилем `qdrant` (по умолчанию не поднимается, ресурсов не ест) + volume; README — раздел про переключение. Тесты (9) гоняют QdrantManager против ВСТРОЕННОГО Qdrant (qdrant-client :memory:, не моки) + диспетчеризацию бэкенда. Всего тестов: 82 (indexer 24, gost 24, gpu 34). Плюсы Qdrant активируются только после явного переключения + миграции. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
48 lines
2.0 KiB
Python
48 lines
2.0 KiB
Python
"""Одноразовая миграция векторов из файлового FAISS в Qdrant.
|
|
|
|
Запуск (после старта qdrant и выставления VECTOR_BACKEND=qdrant в .env):
|
|
docker compose -f docker-compose.prod.yml --profile qdrant up -d qdrant
|
|
docker compose -f docker-compose.prod.yml exec worker-gpu python -m app.migrate_faiss_to_qdrant
|
|
|
|
Идемпотентна: повторный запуск перезапишет те же точки по doc_id, дублей не будет.
|
|
После миграции стоит сверить: QdrantManager.total_vectors() == count(documents).
|
|
"""
|
|
|
|
import logging
|
|
|
|
import numpy as np
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
logger = logging.getLogger("migrate_faiss_to_qdrant")
|
|
|
|
|
|
def migrate(batch_size: int = 1000) -> int:
|
|
"""Перелить все векторы из FAISS-индекса в Qdrant. Возвращает число векторов."""
|
|
import faiss
|
|
|
|
from app.config import settings
|
|
from app.qdrant_manager import QdrantManager
|
|
|
|
index = faiss.read_index(settings.FAISS_INDEX_PATH)
|
|
if not hasattr(index, "id_map"):
|
|
raise SystemExit("FAISS-индекс без id_map (несовместимый тип) — миграция невозможна")
|
|
|
|
ids = faiss.vector_to_array(index.id_map).astype(np.int64)
|
|
total = int(len(ids))
|
|
logger.info("FAISS: %d векторов к миграции в Qdrant", total)
|
|
|
|
migrated = 0
|
|
for start in range(0, total, batch_size):
|
|
chunk = ids[start : start + batch_size]
|
|
vecs = np.vstack([index.reconstruct(int(i)) for i in chunk]).astype(np.float32)
|
|
QdrantManager.add_vectors(vecs, [int(i) for i in chunk])
|
|
migrated += len(chunk)
|
|
logger.info("Мигрировано %d/%d", migrated, total)
|
|
|
|
logger.info("Готово. Точек в Qdrant: %d", QdrantManager.total_vectors())
|
|
return migrated
|
|
|
|
|
|
if __name__ == "__main__":
|
|
migrate()
|