# 🔍 Система поиска ## Обзор Система поиска использует **семантические эмбединги** для точного поиска по публикациям. Поддерживает две архитектуры: 1. **BiEncoder** (SentenceTransformers) - быстрая, стандартное качество 2. **ColBERT** (pylate) - медленнее на ~50ms, но **+175% recall** 🎯 Обе реализации используют FDE (Fast Document Encoding) для оптимизации хранения. ## 🎯 Выбор модели Управление через `SEARCH_MODEL_TYPE` в env: ```bash # ColBERT - лучшее качество (по умолчанию) SEARCH_MODEL_TYPE=colbert # BiEncoder - быстрее, но хуже recall SEARCH_MODEL_TYPE=biencoder ``` ### Сравнение моделей | Аспект | BiEncoder | ColBERT | |--------|-----------|---------| | **Recall@10** | ~0.16 | **0.44** ✅ | | **Query time** | ~395ms | ~447ms | | **Indexing** | ~26s | ~12s ✅ | | **Архитектура** | 1 doc = 1 vector | 1 doc = N vectors (multi-vector) | | **Лучше для** | Скорость | Качество | 💋 **Рекомендация**: используйте `colbert` для production, если качество важнее скорости. ## 🚀 Основные возможности ### **1. Семантический поиск** - Понимание смысла запросов, а не только ключевых слов - Поддержка русского и английского языков - Multi-vector retrieval (ColBERT) для точных результатов ### **2. Оптимизированная индексация** - Batch-обработка для больших объёмов данных - Тихий режим для массовых операций - FDE кодирование для сжатия векторов ### **3. Высокая производительность** - MaxSim scoring (ColBERT) или косинусное сходство (BiEncoder) - Кеширование результатов - Асинхронная обработка ## 📋 API ### GraphQL запросы ```graphql # Поиск по публикациям query SearchShouts($text: String!, $options: ShoutsOptions) { load_shouts_search(text: $text, options: $options) { id title body topics { title } } } # Поиск по авторам query SearchAuthors($text: String!, $limit: Int, $offset: Int) { load_authors_search(text: $text, limit: $limit, offset: $offset) { id name email } } ``` ### Параметры поиска ```python options = { "limit": 10, # Количество результатов "offset": 0, # Смещение для пагинации "filters": { # Дополнительные фильтры "community": 1, "status": "published" } } ``` ## 🛠️ Техническая архитектура ### Компоненты системы ``` 📦 Search System ├── 🎯 SearchService # API интерфейс + выбор модели │ ├── 🔵 BiEncoder Path (MuveraWrapper) │ ├── 🧠 SentenceTransformer # paraphrase-multilingual-MiniLM-L12-v2 │ ├── 🗜️ Muvera FDE # Сжатие векторов │ └── 📊 Cosine Similarity # Ранжирование │ ├── 🟢 ColBERT Path (MuveraPylateWrapper) 🎯 NEW! │ ├── 🧠 pylate ColBERT # answerdotai/answerai-colbert-small-v1 │ ├── 🗜️ Native MUVERA # Multi-vector FDE (каждый токен → FDE) │ ├── 🚀 FAISS Prefilter # O(log N) → top-1000 кандидатов (опционально) │ └── 📊 TRUE MaxSim Scoring # Token-level similarity на кандидатах │ └── 💾 File Persistence # Сохранение в /dump ``` ### Модели эмбедингов #### BiEncoder (стандарт) **Модель**: `paraphrase-multilingual-MiniLM-L12-v2` - Поддержка 50+ языков включая русский - Размерность: 384D - Fallback: `all-MiniLM-L6-v2` - Алгоритм: average pooling + cosine similarity #### ColBERT (улучшенная версия) **Модель**: `answerdotai/answerai-colbert-small-v1` - Многоязычная ColBERT модель - Размерность: 768D - Алгоритм: max pooling + MaxSim scoring - 🤖 **Внимание**: модели, тренированные через дистилляцию, могут иметь проблемы с нормализацией скоров ([pylate#142](https://github.com/lightonai/pylate/issues/142)) ### Процесс индексации #### BiEncoder ```python # 1. Извлечение текста doc_content = f"{title} {subtitle} {lead} {body}".strip() # 2. Генерация single-vector эмбединга embedding = encoder.encode(doc_content) # [384D] # 3. FDE кодирование (average pooling) compressed = muvera.encode_fde(embedding, buckets=128, method="avg") # 4. Сохранение в индекс embeddings[doc_id] = compressed ``` #### ColBERT (native MUVERA multi-vector) 🎯 ```python # 1. Извлечение текста doc_content = f"{title} {subtitle} {lead} {body}".strip() # 2. Генерация multi-vector эмбединга (по токену) doc_embeddings = encoder.encode([doc_content], is_query=False) # [N_tokens, 768D] # 3. 🎯 NATIVE MUVERA: FDE encode КАЖДЫЙ токен отдельно doc_fdes = [] for token_vec in doc_embeddings[0]: token_fde = muvera.encode_fde(token_vec.reshape(1, -1), buckets=128, method="avg") doc_fdes.append(token_fde) # 4. Сохранение в индекс как СПИСОК векторов embeddings[doc_id] = doc_fdes # List of FDE vectors, not single! ``` ### Алгоритм поиска #### BiEncoder (косинусное сходство) ```python # 1. Эмбединг запроса query_embedding = encoder.encode(query_text) query_fde = muvera.encode_fde(query_embedding, buckets=128, method="avg") # 2. Косинусное сходство for doc_id, doc_embedding in embeddings.items(): similarity = np.dot(query_fde, doc_embedding) / ( np.linalg.norm(query_fde) * np.linalg.norm(doc_embedding) ) results.append({"id": doc_id, "score": similarity}) # 3. Ранжирование results.sort(key=lambda x: x["score"], reverse=True) ``` #### ColBERT (TRUE MaxSim с native MUVERA) 🎯 ```python # 1. Multi-vector эмбединг запроса query_embeddings = encoder.encode([query_text], is_query=True) # [N_tokens, 768D] # 2. 🎯 NATIVE MUVERA: FDE encode КАЖДЫЙ query токен query_fdes = [] for token_vec in query_embeddings[0]: token_fde = muvera.encode_fde(token_vec.reshape(1, -1), buckets=128, method="avg") query_fdes.append(token_fde) # 3. 🎯 TRUE MaxSim scoring (ColBERT-style) for doc_id, doc_fdes in embeddings.items(): # Для каждого query токена находим максимальное сходство с doc токенами max_sims = [] for query_fde in query_fdes: token_sims = [ np.dot(query_fde, doc_fde) / (np.linalg.norm(query_fde) * np.linalg.norm(doc_fde)) for doc_fde in doc_fdes ] max_sims.append(max(token_sims)) # Final score = average of max similarities final_score = np.mean(max_sims) results.append({"id": doc_id, "score": final_score}) # 4. Ранжирование results.sort(key=lambda x: x["score"], reverse=True) ``` **💡 Ключевое отличие**: Настоящий MaxSim через native MUVERA multi-vector, а не упрощенный через max pooling! ## 🚀 FAISS Acceleration (для больших индексов) ### Проблема масштабируемости **Без FAISS** (brute force): ```python # O(N) сложность - перебор ВСЕХ документов for doc_id in all_50K_documents: # 😱 50K iterations! score = maxsim(query, doc) ``` **С FAISS** (двухэтапный поиск): ```python # Stage 1: FAISS prefilter - O(log N) candidates = faiss_index.search(query_avg, k=1000) # Только 1K кандидатов # Stage 2: TRUE MaxSim только на кандидатах for doc_id in candidates: # ✅ 1K iterations (50x быстрее!) score = maxsim(query, doc) ``` ### Когда включать FAISS? | Документов | Без FAISS | С FAISS | Рекомендация | |------------|-----------|---------|--------------| | < 1K | ~50ms | ~30ms | 🤷 Опционально | | 1K-10K | ~200ms | ~40ms | ✅ Желательно | | 10K-50K | ~1-2s | ~60ms | ✅ **Обязательно** | | > 50K | ~5s+ | ~100ms | ✅ **Критично** | ### Архитектура с FAISS ``` 📦 ColBERT + MUVERA + FAISS: Indexing: ├── ColBERT → [token1_vec, token2_vec, ...] ├── MUVERA → [token1_fde, token2_fde, ...] └── FAISS → doc_avg в индекс (для быстрого поиска) Search: ├── ColBERT query → [q1_vec, q2_vec, ...] ├── MUVERA → [q1_fde, q2_fde, ...] │ ├── 🚀 Stage 1 (FAISS - грубый): │ └── query_avg → top-1000 candidates (быстро!) │ └── 🎯 Stage 2 (MaxSim - точный): └── TRUE MaxSim только для candidates (качественно!) ``` ### Конфигурация FAISS ```bash # Включить FAISS (default: true) SEARCH_USE_FAISS=true # Сколько кандидатов брать для rerank SEARCH_FAISS_CANDIDATES=1000 # Больше = точнее, но медленнее ``` **💋 Рекомендация**: Оставьте `SEARCH_USE_FAISS=true` если планируется >10K документов. ## ⚙️ Конфигурация ### Переменные окружения ```bash # 🎯 Выбор модели (ключевая настройка!) SEARCH_MODEL_TYPE=colbert # "biencoder" | "colbert" (default: colbert) # 🚀 FAISS acceleration (рекомендуется для >10K документов) SEARCH_USE_FAISS=true # Включить FAISS prefilter (default: true) SEARCH_FAISS_CANDIDATES=1000 # Сколько кандидатов для rerank (default: 1000) # Индексация и кеширование MUVERA_INDEX_NAME=discours SEARCH_MAX_BATCH_SIZE=25 SEARCH_PREFETCH_SIZE=200 SEARCH_CACHE_ENABLED=true SEARCH_CACHE_TTL_SECONDS=300 ``` ### Настройки производительности ```python # Batch размеры SINGLE_DOC_THRESHOLD = 10 # Меньше = одиночная обработка BATCH_SIZE = 32 # Размер batch для SentenceTransformers FDE_BUCKETS = 128 # Количество bucket для сжатия # Logging SILENT_BATCH_MODE = True # Тихий режим для batch операций DEBUG_SINGLE_DOCS = True # Подробные логи для одиночных документов ``` ## 🔧 Использование ### Индексация новых документов ```python from services.search import search_service # Одиночный документ search_service.index(shout) # Batch индексация (тихий режим) await search_service.bulk_index(shouts_list) ``` ### Поиск ```python # Поиск публикаций results = await search_service.search("машинное обучение", limit=10, offset=0) # Поиск авторов authors = await search_service.search_authors("Иван Петров", limit=5) ``` ### Проверка статуса ```python # Информация о сервисе info = await search_service.info() # Статус индекса status = await search_service.check_index_status() # Проверка документов verification = await search_service.verify_docs(["1", "2", "3"]) ``` ## 🐛 Отладка ### Логирование ```python # Включить debug логи import logging logging.getLogger("services.search").setLevel(logging.DEBUG) # Проверить загрузку модели logger.info("🔍 SentenceTransformer model loaded successfully") ``` ### Диагностика ```python # Проверить количество проиндексированных документов info = await search_service.info() print(f"Documents: {info['muvera_info']['documents_count']}") # Найти отсутствующие документы missing = await search_service.verify_docs(expected_doc_ids) print(f"Missing: {missing['missing']}") ``` ## 📈 Метрики производительности ### Benchmark (dataset: NanoFiQA2018, 50 queries) #### BiEncoder (MuveraWrapper) ``` 📊 BiEncoder Performance: ├── Indexing time: ~26s ├── Avg query time: ~395ms ├── Recall@10: 0.16 (16%) └── Memory: ~50MB per 1000 docs ``` #### ColBERT (MuveraPylateWrapper) ✅ ``` 📊 ColBERT Performance: ├── Indexing time: ~12s ✅ (faster!) ├── Avg query time: ~447ms (+52ms) ├── Recall@10: 0.44 (44%) 🎯 +175%! └── Memory: ~60MB per 1000 docs ``` ### Выбор модели: когда что использовать? | Сценарий | Рекомендация | Причина | |----------|-------------|---------| | Production поиск | **ColBERT + FAISS** | Качество + скорость | | Dev/testing | BiEncoder | Быстрый старт | | Ограниченная память | BiEncoder | -20% память | | < 10K документов | ColBERT без FAISS | Overhead не нужен | | > 10K документов | **ColBERT + FAISS** | Обязательно для скорости | | Нужен максимальный recall | **ColBERT** | +175% recall | ### Оптимизация 1. **Batch обработка** - для массовых операций используйте `bulk_index()` 2. **Тихий режим** - отключает детальное логирование 3. **Кеширование** - результаты поиска кешируются (опционально) 4. **FDE сжатие** - уменьшает размер векторов в 2-3 раза 5. **GPU ускорение** - установите `device="cuda"` в ColBERT для 10x speedup ## 💾 Персистентность и восстановление ### Автоматическое сохранение в файлы Система автоматически сохраняет индекс в файлы после каждой успешной индексации: ```python # Автосохранение после индексации await self.save_index_to_file("/dump") logger.info("💾 Индекс автоматически сохранен в файл") ``` ### Структура файлов ``` /dump/ (или ./dump/) ├── discours.pkl.gz # BiEncoder индекс (gzip) └── discours_colbert.pkl.gz # ColBERT индекс (gzip) ``` Каждый файл содержит: - `documents` - контент и метаданные - `embeddings` - FDE-сжатые векторы - `vector_dimension` - размерность - `buckets` - FDE buckets - `model_name` (ColBERT only) - название модели ### Восстановление при запуске При запуске сервиса система автоматически восстанавливает индекс из файла: ```python # В initialize_search_index() await search_service.async_init() # Восстанавливает из файла # Fallback path: /dump (priority) или ./dump ``` ## 🆕 Преимущества file-based хранения ### По сравнению с БД - **📦 Простота**: Нет зависимости от Redis/БД для индекса - **💾 Эффективность**: Gzip сжатие (pickle) - быстрое сохранение/загрузка - **🔄 Портативность**: Легко копировать между серверами - **🔒 Целостность**: Атомарная запись через gzip ### Производительность ``` 📊 Хранение индекса: ├── File (gzip): ~25MB disk, быстрая загрузка ✅ ├── Memory only: ~50MB RAM, потеря при рестарте ❌ └── БД: ~75MB RAM, медленное восстановление ``` ## 🔄 Миграция и обновления ### Переиндексация ```python # Полная переиндексация from main import initialize_search_index_with_data await initialize_search_index_with_data() ``` ### Обновление модели #### Переключение BiEncoder ↔ ColBERT ```bash # Изменить в .env SEARCH_MODEL_TYPE=colbert # или biencoder # Перезапустить сервис dokku ps:restart core # Система автоматически: # 1. Загрузит нужную модель # 2. Восстановит соответствующий индекс из файла # 3. Если индекса нет - создаст новый при первой индексации ``` #### Смена конкретной модели 1. Остановить сервис 2. Обновить зависимости (`pip install -U sentence-transformers pylate`) 3. Изменить `model_name` в `MuveraWrapper` или `MuveraPylateWrapper` 4. Удалить старый индекс файл 5. Запустить переиндексацию ### Резервное копирование ```bash # Создание бэкапа файлов индекса cp /dump/discours*.pkl.gz /backup/ # Восстановление из бэкапа cp /backup/discours*.pkl.gz /dump/ # Или использовать dokku storage dokku storage:mount core /host/path:/dump ``` ## 🔗 Связанные документы - [API Documentation](api.md) - GraphQL эндпоинты - [Testing](testing.md) - Тестирование поиска - [Performance](performance.md) - Оптимизация производительности