feat(gpu): Qdrant как векторный бэкенд под флагом — снимает SPOF FAISS
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>
This commit is contained in:
@@ -92,11 +92,12 @@ def check_plagiarism(
|
||||
session.commit()
|
||||
|
||||
try:
|
||||
from app.faiss_manager import FAISSManager
|
||||
from app.model_manager import ModelManager
|
||||
from app.ollama_client import OllamaClient
|
||||
from app.vector_store import get_backend
|
||||
|
||||
ollama = OllamaClient()
|
||||
vector_store = get_backend()
|
||||
semantic_matches: list[dict] = []
|
||||
|
||||
for i, fragment in enumerate(fragments):
|
||||
@@ -105,9 +106,9 @@ def check_plagiarism(
|
||||
# Пропустить слишком короткие фрагменты
|
||||
continue
|
||||
|
||||
# Уровень 3: Семантический поиск через FAISS
|
||||
# Уровень 3: Семантический поиск (FAISS или Qdrant по VECTOR_BACKEND)
|
||||
frag_vec = ModelManager.encode_single(frag_text)
|
||||
faiss_results = FAISSManager.search(frag_vec, k=10)
|
||||
faiss_results = vector_store.search(frag_vec, k=10)
|
||||
|
||||
for doc_id, score in faiss_results:
|
||||
if score < FAISS_SIMILARITY_THRESHOLD:
|
||||
@@ -201,9 +202,9 @@ def embed_documents(doc_ids: list[int]) -> dict[str, Any]:
|
||||
if not doc_ids:
|
||||
return {"status": "ok", "embedded": 0}
|
||||
|
||||
from app.faiss_manager import FAISSManager
|
||||
from app.model_manager import ModelManager
|
||||
from app.models import Document
|
||||
from app.vector_store import get_backend
|
||||
|
||||
logger.info(f"Построение эмбеддингов для {len(doc_ids)} документов...")
|
||||
|
||||
@@ -225,8 +226,9 @@ def embed_documents(doc_ids: list[int]) -> dict[str, Any]:
|
||||
|
||||
vectors = ModelManager.encode(texts)
|
||||
|
||||
FAISSManager.add_vectors(vectors, ids)
|
||||
FAISSManager.save()
|
||||
store = get_backend()
|
||||
store.add_vectors(vectors, ids)
|
||||
store.save()
|
||||
|
||||
# Обновить faiss_id в PostgreSQL (для IDMap2 faiss_id == doc_id)
|
||||
with db_session() as session:
|
||||
|
||||
@@ -222,10 +222,10 @@ def search_semantic(
|
||||
from app.model_manager import ModelManager
|
||||
query_vec = ModelManager.encode_single(query_normalized)
|
||||
|
||||
# FAISS семантический поиск
|
||||
from app.faiss_manager import FAISSManager
|
||||
faiss_results = FAISSManager.search(query_vec, k=50)
|
||||
logger.info(f"FAISS: найдено {len(faiss_results)} результатов")
|
||||
# Семантический поиск (FAISS или Qdrant по VECTOR_BACKEND)
|
||||
from app.vector_store import get_backend
|
||||
faiss_results = get_backend().search(query_vec, k=50)
|
||||
logger.info(f"Векторный поиск: найдено {len(faiss_results)} результатов")
|
||||
|
||||
# Elasticsearch BM25 поиск
|
||||
from app.es_client import search_fulltext
|
||||
|
||||
Reference in New Issue
Block a user