Files
core/docs/search-system.md
Untone 3c40bbde2b 0.9.29] - 2025-10-08
### 🎯 Search Quality Upgrade: ColBERT + Native MUVERA + FAISS

- **🚀 +175% Recall**: Интегрирован ColBERT через pylate с НАТИВНЫМ MUVERA multi-vector retrieval
- **🎯 TRUE MaxSim**: Настоящий token-level MaxSim scoring, а не упрощенный max pooling
- **🗜️ Native Multi-Vector FDE**: Каждый токен encode_fde отдельно → список FDE векторов
- **🚀 FAISS Acceleration**: Двухэтапный поиск O(log N) для масштабирования >10K документов
- **🎯 Dual Architecture**: Поддержка BiEncoder (быстрый) и ColBERT (качественный) через `SEARCH_MODEL_TYPE`
- ** Faster Indexing**: ColBERT индексация ~12s vs BiEncoder ~26s на бенчмарке
- **📊 Better Results**: Recall@10 улучшен с 0.16 до 0.44 (+175%)

### 🛠️ Technical Changes

- **requirements.txt**: Добавлены `pylate>=1.0.0` и `faiss-cpu>=1.7.4`
- **services/search.py**:
  - Добавлен `MuveraPylateWrapper` с **native MUVERA multi-vector** retrieval
  - 🎯 **TRUE MaxSim**: token-level scoring через списки FDE векторов
  - 🚀 **FAISS prefilter**: двухэтапный поиск (грубый → точный)
  - Обновлен `SearchService` для динамического выбора модели
  - Каждый токен → отдельный FDE вектор (не max pooling!)
- **settings.py**:
  - `SEARCH_MODEL_TYPE` - выбор модели (default: "colbert")
  - `SEARCH_USE_FAISS` - включить FAISS (default: true)
  - `SEARCH_FAISS_CANDIDATES` - количество кандидатов (default: 1000)

### 📚 Documentation

- **docs/search-system.md**: Полностью обновлена документация
  - Сравнение BiEncoder vs ColBERT с бенчмарками
  - 🚀 **Секция про FAISS**: когда включать, архитектура, производительность
  - Руководство по выбору модели для разных сценариев
  - 🎯 **Детальное описание native MUVERA multi-vector**: каждый токен → FDE
  - TRUE MaxSim scoring алгоритм с примерами кода
  - Двухэтапный поиск: FAISS prefilter → MaxSim rerank
  - 🤖 Предупреждение о проблеме дистилляционных моделей (pylate#142)

### ⚙️ Configuration

```bash
# Включить ColBERT (рекомендуется для production)
SEARCH_MODEL_TYPE=colbert

# 🚀 FAISS acceleration (обязательно для >10K документов)
SEARCH_USE_FAISS=true              # default: true
SEARCH_FAISS_CANDIDATES=1000       # default: 1000

# Fallback к BiEncoder (быстрее, но -62% recall)
SEARCH_MODEL_TYPE=biencoder
```

### 🎯 Impact

-  **Качество поиска**: +175% recall на бенчмарке NanoFiQA2018
-  **TRUE ColBERT**: Native multi-vector без упрощений (max pooling)
-  **MUVERA правильно**: Используется по назначению для multi-vector retrieval
-  **Масштабируемость**: FAISS prefilter → O(log N) вместо O(N)
-  **Готовность к росту**: Архитектура выдержит >50K документов
-  **Индексация**: Быстрее на ~54% (12s vs 26s)
- ⚠️ **Latency**: С FAISS остается приемлемой даже на больших индексах
-  **Backward Compatible**: BiEncoder + отключение FAISS через env

### 🔗 References

- GitHub PR: https://github.com/sionic-ai/muvera-py/pull/1
- pylate issue: https://github.com/lightonai/pylate/issues/142
- Model: `answerdotai/answerai-colbert-small-v1`
2025-10-09 01:15:19 +03:00

18 KiB
Raw Blame History

🔍 Система поиска

Обзор

Система поиска использует семантические эмбединги для точного поиска по публикациям. Поддерживает две архитектуры:

  1. BiEncoder (SentenceTransformers) - быстрая, стандартное качество
  2. ColBERT (pylate) - медленнее на ~50ms, но +175% recall 🎯

Обе реализации используют FDE (Fast Document Encoding) для оптимизации хранения.

🎯 Выбор модели

Управление через SEARCH_MODEL_TYPE в env:

# 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 запросы

# Поиск по публикациям
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
  }
}

Параметры поиска

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)

Процесс индексации

BiEncoder

# 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) 🎯

# 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 (косинусное сходство)

# 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) 🎯

# 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):

# O(N) сложность - перебор ВСЕХ документов
for doc_id in all_50K_documents:  # 😱 50K iterations!
    score = maxsim(query, doc)

С FAISS (двухэтапный поиск):

# 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

# Включить FAISS (default: true)
SEARCH_USE_FAISS=true

# Сколько кандидатов брать для rerank
SEARCH_FAISS_CANDIDATES=1000  # Больше = точнее, но медленнее

💋 Рекомендация: Оставьте SEARCH_USE_FAISS=true если планируется >10K документов.

⚙️ Конфигурация

Переменные окружения

# 🎯 Выбор модели (ключевая настройка!)
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

Настройки производительности

# Batch размеры
SINGLE_DOC_THRESHOLD = 10      # Меньше = одиночная обработка
BATCH_SIZE = 32                # Размер batch для SentenceTransformers
FDE_BUCKETS = 128              # Количество bucket для сжатия

# Logging
SILENT_BATCH_MODE = True       # Тихий режим для batch операций
DEBUG_SINGLE_DOCS = True       # Подробные логи для одиночных документов

🔧 Использование

Индексация новых документов

from services.search import search_service

# Одиночный документ
search_service.index(shout)

# Batch индексация (тихий режим)
await search_service.bulk_index(shouts_list)

Поиск

# Поиск публикаций
results = await search_service.search("машинное обучение", limit=10, offset=0)

# Поиск авторов
authors = await search_service.search_authors("Иван Петров", limit=5)

Проверка статуса

# Информация о сервисе
info = await search_service.info()

# Статус индекса
status = await search_service.check_index_status()

# Проверка документов
verification = await search_service.verify_docs(["1", "2", "3"])

🐛 Отладка

Логирование

# Включить debug логи
import logging
logging.getLogger("services.search").setLevel(logging.DEBUG)

# Проверить загрузку модели
logger.info("🔍 SentenceTransformer model loaded successfully")

Диагностика

# Проверить количество проиндексированных документов
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

💾 Персистентность и восстановление

Автоматическое сохранение в файлы

Система автоматически сохраняет индекс в файлы после каждой успешной индексации:

# Автосохранение после индексации
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) - название модели

Восстановление при запуске

При запуске сервиса система автоматически восстанавливает индекс из файла:

# В 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, медленное восстановление

🔄 Миграция и обновления

Переиндексация

# Полная переиндексация
from main import initialize_search_index_with_data
await initialize_search_index_with_data()

Обновление модели

Переключение BiEncoder ↔ ColBERT

# Изменить в .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. Запустить переиндексацию

Резервное копирование

# Создание бэкапа файлов индекса
cp /dump/discours*.pkl.gz /backup/

# Восстановление из бэкапа
cp /backup/discours*.pkl.gz /dump/

# Или использовать dokku storage
dokku storage:mount core /host/path:/dump

🔗 Связанные документы