2025-10-09 01:15:19 +03:00
# 🔍 Система поиска
2025-08-31 19:20:43 +03:00
## Обзор
2025-10-09 01:15:19 +03:00
Система поиска использует **семантические эмбединги** для точного поиска по публикациям. Поддерживает две архитектуры:
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, если качество важнее скорости.
2025-08-31 19:20:43 +03:00
## 🚀 Основные возможности
### **1. Семантический поиск**
- Понимание смысла запросов, а не только ключевых слов
- Поддержка русского и английского языков
2025-10-09 01:15:19 +03:00
- Multi-vector retrieval (ColBERT) для точных результатов
2025-08-31 19:20:43 +03:00
### **2. Оптимизированная индексация**
- Batch-обработка для больших объёмов данных
- Тихий режим для массовых операций
- FDE кодирование для сжатия векторов
### **3. Высокая производительность**
2025-10-09 01:15:19 +03:00
- MaxSim scoring (ColBERT) или косинусное сходство (BiEncoder)
2025-08-31 19:20:43 +03:00
- Кеширование результатов
- Асинхронная обработка
## 📋 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
2025-10-09 01:15:19 +03:00
├── 🎯 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
2025-08-31 19:20:43 +03:00
```
2025-10-09 01:15:19 +03:00
### Модели эмбедингов
2025-08-31 19:20:43 +03:00
2025-10-09 01:15:19 +03:00
#### BiEncoder (стандарт)
**Модель**: `paraphrase-multilingual-MiniLM-L12-v2`
2025-08-31 19:20:43 +03:00
- Поддержка 50+ языков включая русский
- Размерность: 384D
- Fallback: `all-MiniLM-L6-v2`
2025-10-09 01:15:19 +03:00
- Алгоритм: 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 ))
2025-08-31 19:20:43 +03:00
### Процесс индексации
2025-10-09 01:15:19 +03:00
#### BiEncoder
2025-08-31 19:20:43 +03:00
```python
# 1. Извлечение текста
doc_content = f"{title} {subtitle} {lead} {body}".strip()
2025-10-09 01:15:19 +03:00
# 2. Генерация single-vector эмбединга
embedding = encoder.encode(doc_content) # [384D]
2025-08-31 19:20:43 +03:00
2025-10-09 01:15:19 +03:00
# 3. FDE кодирование (average pooling)
2025-08-31 19:20:43 +03:00
compressed = muvera.encode_fde(embedding, buckets=128, method="avg")
# 4. Сохранение в индекс
embeddings[doc_id] = compressed
2025-10-09 01:15:19 +03:00
```
2025-09-01 15:09:36 +03:00
2025-10-09 01:15:19 +03:00
#### 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!
2025-08-31 19:20:43 +03:00
```
### Алгоритм поиска
2025-10-09 01:15:19 +03:00
#### BiEncoder (косинусное сходство)
2025-08-31 19:20:43 +03:00
```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():
2025-10-09 01:15:19 +03:00
similarity = np.dot(query_fde, doc_embedding) / (
np.linalg.norm(query_fde) * np.linalg.norm(doc_embedding)
)
2025-08-31 19:20:43 +03:00
results.append({"id": doc_id, "score": similarity})
# 3. Ранжирование
results.sort(key=lambda x: x["score"], reverse=True)
```
2025-10-09 01:15:19 +03:00
#### 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 документов.
2025-08-31 19:20:43 +03:00
## ⚙️ Конфигурация
### Переменные окружения
```bash
2025-10-09 01:15:19 +03:00
# 🎯 Выбор модели (ключевая настройка!)
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
2025-08-31 19:20:43 +03:00
SEARCH_PREFETCH_SIZE=200
SEARCH_CACHE_ENABLED=true
2025-10-09 01:15:19 +03:00
SEARCH_CACHE_TTL_SECONDS=300
2025-08-31 19:20:43 +03:00
```
### Настройки производительности
```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']}")
```
## 📈 Метрики производительности
2025-10-09 01:15:19 +03:00
### 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
```
2025-08-31 19:20:43 +03:00
2025-10-09 01:15:19 +03:00
#### ColBERT (MuveraPylateWrapper) ✅
2025-08-31 19:20:43 +03:00
```
2025-10-09 01:15:19 +03:00
📊 ColBERT Performance:
├── Indexing time: ~12s ✅ (faster!)
├── Avg query time: ~447ms (+52ms)
├── Recall@10: 0.44 (44%) 🎯 +175%!
└── Memory: ~60MB per 1000 docs
2025-08-31 19:20:43 +03:00
```
2025-10-09 01:15:19 +03:00
### Выбор модели: когда что использовать?
| Сценарий | Рекомендация | Причина |
|----------|-------------|---------|
| Production поиск | **ColBERT + FAISS** | Качество + скорость |
| Dev/testing | BiEncoder | Быстрый старт |
| Ограниченная память | BiEncoder | -20% память |
| < 10K документов | ColBERT без FAISS | Overhead не нужен |
| > 10K документов | **ColBERT + FAISS** | Обязательно для скорости |
| Нужен максимальный recall | **ColBERT** | +175% recall |
2025-08-31 19:20:43 +03:00
### Оптимизация
1. **Batch обработка** - для массовых операций используйте `bulk_index()`
2. **Тихий режим** - отключает детальное логирование
2025-10-09 01:15:19 +03:00
3. **Кеширование** - результаты поиска кешируются (опционально)
2025-08-31 19:20:43 +03:00
4. **FDE сжатие** - уменьшает размер векторов в 2-3 раза
2025-10-09 01:15:19 +03:00
5. **GPU ускорение** - установите `device="cuda"` в ColBERT для 10x speedup
2025-08-31 19:20:43 +03:00
2025-09-01 15:09:36 +03:00
## 💾 Персистентность и восстановление
2025-10-09 01:15:19 +03:00
### Автоматическое сохранение в файлы
2025-09-01 15:09:36 +03:00
2025-10-09 01:15:19 +03:00
Система автоматически сохраняет индекс в файлы после каждой успешной индексации:
2025-09-01 15:09:36 +03:00
```python
# Автосохранение после индексации
2025-10-09 01:15:19 +03:00
await self.save_index_to_file("/dump")
logger.info("💾 Индекс автоматически сохранен в файл")
2025-09-01 15:09:36 +03:00
```
2025-10-09 01:15:19 +03:00
### Структура файлов
2025-09-01 15:09:36 +03:00
```
2025-10-09 01:15:19 +03:00
/dump/ (или ./dump/)
├── discours.pkl.gz # BiEncoder индекс (gzip)
└── discours_colbert.pkl.gz # ColBERT индекс (gzip)
2025-09-01 15:09:36 +03:00
```
2025-10-09 01:15:19 +03:00
Каждый файл содержит:
- `documents` - контент и метаданные
- `embeddings` - FDE-сжатые векторы
- `vector_dimension` - размерность
- `buckets` - FDE buckets
- `model_name` (ColBERT only) - название модели
2025-09-01 15:09:36 +03:00
### Восстановление при запуске
2025-10-09 01:15:19 +03:00
При запуске сервиса система автоматически восстанавливает индекс из файла:
2025-09-01 15:09:36 +03:00
```python
# В initialize_search_index()
2025-10-09 01:15:19 +03:00
await search_service.async_init() # Восстанавливает из файла
# Fallback path: /dump (priority) или ./dump
2025-09-01 15:09:36 +03:00
```
2025-10-09 01:15:19 +03:00
## 🆕 Преимущества file-based хранения
2025-09-01 15:09:36 +03:00
2025-10-09 01:15:19 +03:00
### По сравнению с БД
2025-09-01 15:09:36 +03:00
2025-10-09 01:15:19 +03:00
- **📦 Простота**: Нет зависимости от Redis/БД для индекса
- **💾 Эффективность**: Gzip сжатие (pickle) - быстрое сохранение/загрузка
- **🔄 Портативность**: Легко копировать между серверами
- **🔒 Целостность**: Атомарная запись через gzip
2025-09-01 15:09:36 +03:00
### Производительность
```
2025-10-09 01:15:19 +03:00
📊 Хранение индекса:
├── File (gzip): ~25MB disk, быстрая загрузка ✅
├── Memory only: ~50MB RAM, потеря при рестарте ❌
└── БД: ~75MB RAM, медленное восстановление
2025-09-01 15:09:36 +03:00
```
2025-08-31 19:20:43 +03:00
## 🔄 Миграция и обновления
### Переиндексация
```python
# Полная переиндексация
from main import initialize_search_index_with_data
await initialize_search_index_with_data()
```
### Обновление модели
2025-10-09 01:15:19 +03:00
#### Переключение BiEncoder ↔ ColBERT
```bash
# Изменить в .env
SEARCH_MODEL_TYPE=colbert # или biencoder
# Перезапустить сервис
dokku ps:restart core
# Система автоматически:
# 1. Загрузит нужную модель
# 2. Восстановит соответствующий индекс из файла
# 3. Если индекса нет - создаст новый при первой индексации
```
#### Смена конкретной модели
2025-08-31 19:20:43 +03:00
1. Остановить сервис
2025-10-09 01:15:19 +03:00
2. Обновить зависимости (`pip install -U sentence-transformers pylate` )
3. Изменить `model_name` в `MuveraWrapper` или `MuveraPylateWrapper`
4. Удалить старый индекс файл
5. Запустить переиндексацию
2025-08-31 19:20:43 +03:00
2025-09-01 15:09:36 +03:00
### Резервное копирование
```bash
2025-10-09 01:15:19 +03:00
# Создание бэкапа файлов индекса
cp /dump/discours*.pkl.gz /backup/
2025-09-01 15:09:36 +03:00
# Восстановление из бэкапа
2025-10-09 01:15:19 +03:00
cp /backup/discours*.pkl.gz /dump/
# Или использовать dokku storage
dokku storage:mount core /host/path:/dump
2025-09-01 15:09:36 +03:00
```
2025-08-31 19:20:43 +03:00
## 🔗 Связанные документы
- [API Documentation ](api.md ) - GraphQL эндпоинты
- [Testing ](testing.md ) - Тестирование поиска
- [Performance ](performance.md ) - Оптимизация производительности