Files
wiki/tasks/snowbike-rag/TZ.md
2026-04-12 21:55:33 +03:00

341 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ТЗ: Семантический поиск и RAG по данным Telegram (Сноубайк Россия)
## Общее описание
Система семантического поиска и RAG (Retrieval-Augmented Generation) по 155K сообщений Telegram-группы «Сноубайк Россия». Гибридный подход: Meilisearch (ключевые слова) + ChromaDB (семантика) + Sonnet (суммаризация).
**Цель:** ответы на вопросы типа «какие масла рекомендуют для Polaris 850?» — не найти сообщение, а получить агрегированный ответ на основе всех данных.
---
## Исходные данные
**Расположение:** `/home/node/.openclaw/workspace/data/telegram-collector/raw/1242788123/`
**Структура:**
```
raw/1242788123/
├── meta.json — метаданные канала (12 топиков)
├── 1/ — Основная (92K сообщений, 1.3 ГБ)
├── 63155/ — Барахолка (1.5K, 267 МБ)
├── 63467/ — Техничка (21.6K, 306 МБ)
├── 63469/ — Экип (3.6K, 57 МБ)
├── 64805/ — Обзоры (11K, 166 МБ)
├── 76611/ — Инструкции и 3D (96 msgs, 386 МБ)
├── 97494/ — Электрички (1.6K, 32 МБ)
├── 99795/ — Китай (15.7K, 213 МБ)
├── 103316/ — ОФФТОП (5.8K, 63 МБ)
├── 103317/ — Локации (1.6K, 55 МБ)
├── 117112/ — Опросы (24 msgs)
└── 161840/ — Соревнования (24 msgs, 11 МБ)
```
**Формат сообщения (batch_NNNN.json):**
```json
{
"id": 165211,
"date": "2026-03-24T17:55:39Z",
"text": "Текст сообщения",
"from_id": 5774548432,
"reply_to_msg_id": null,
"reply_to_top_id": null,
"quote_text": null,
"edit_date": null,
"pinned": false,
"media": null
}
```
**Общий объём:** 2.9 ГБ, 155K сообщений, 12 топиков
**Обновление:** инкрементальное, ежедневно в 00:00 МСК (cron `860e23a4`)
---
## Архитектура
```
Запрос пользователя
┌─────────────┐
│ Flask API │ ← HTTP сервер
└──────┬──────┘
┌─────┴─────┐
▼ ▼
┌─────────┐ ┌─────────┐
│Meili- │ │ChromaDB │ ← два индекса параллельно
│search │ │(векторы)│
└────┬────┘ └────┬────┘
│ │
└─────┬─────┘
┌─────────────┐
│ Объединение │ ← reranking контекста
│ контекста │
└──────┬──────┘
┌─────────────┐
│ Sonnet │ ← суммаризация + ответ
│ (LLM) │
└──────┬──────┘
Ответ пользователю
```
---
## Компоненты
### 1. Meilisearch (полнотекстовый поиск)
**Назначение:** поиск по ключевым словам, допускающий опечатки
**Роль:** быстрый отсев релевантных сообщений по точным словам
**Дocker:** `getmeili/meilisearch:latest`, порт 7700
**Индекс:** `snowbike_messages`
**Поля индекса:**
- `id` — ID сообщения (уникальный)
- `text` — текст сообщения (основное поле для поиска)
- `date` — дата сообщения
- `topic_id` — ID топика
- `topic_title` — название топика
- `from_id` — ID автора
- `reply_to_msg_id` — ID сообщения, на которое отвечаем (для цепочек)
**Настройки индекса:**
- `filterableAttributes`: `["topic_id", "date"]`
- `sortableAttributes`: `["date"]`
- `typoTolerance`: `true` (по умолчанию)
- `searchableAttributes`: `["text"]`
- `stopWords`: `["и", "в", "на", "с", "для", "это", "что", "как", "не", "а"]` (русские стоп-слова)
**Размер индекса:** ~200 МБ на 155K сообщений
### 2. ChromaDB (семантический поиск)
**Назначение:** поиск по смыслу (не по словам)
**Роль:** найти ответы, которые говорят о том же, но другими словами
**Пакет:** `chromadb` (pip), без Docker
**Коллекция:** `snowbike_embeddings`
**Embeddings:**
- **Модель:** `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2`
- Бесплатная, локальная, 384-мерные вектора
- Поддержка русского языка
- Размер модели: ~470 МБ (скачается при первом запуске)
- Скорость: ~100 сообщений/сек на CPU
- **Альтернатива:** OpenAI `text-embedding-3-small` ($0.02/1M токенов, ~$0.50 за все данные)
**Структура записи в ChromaDB:**
```
id: str(message_id)
embedding: List[float] (384-мерный вектор)
metadata: {
"topic_id": int,
"topic_title": str,
"date": str,
"from_id": int
}
document: str(text)
```
**Размер коллекции:** ~500 МБ (155K × 384 × 4 байта + metadata)
### 3. Sonnet (суммаризация)
**Назначение:** агрегация контекста и формирование ответа
**Модель:** `openrouter/anthropic/claude-sonnet-4.6` (через OpenRouter)
**Роль:** на основе найденных сообщений — сформировать полезный ответ
**Промпт-шаблон:**
```
Ты — помощник по сноубайкам. На основе найденных сообщений ответь на вопрос.
Если информации недостаточно — скажи об этом.
Всегда указывай, откуда взята информация (дата, автор, топик).
Вопрос: {question}
Найденные сообщения:
{context}
Ответ:
```
---
## Pipeline
### Шаг 1: Парсинг сырых данных
**Скрипт:** `scripts/parse_messages.py`
**Вход:** `/data/telegram-collector/raw/1242788123/{topic_id}/batch_*.json`
**Выход:** плоский список сообщений (JSON lines)
```python
for each topic_id in raw/1242788123/:
for each batch_NNNN.json in topic_id/:
for each message in batch:
yield {
"id": message["id"],
"text": message["text"],
"date": message["date"],
"topic_id": topic_id,
"topic_title": meta["topics"][topic_id],
"from_id": message["from_id"],
"reply_to_msg_id": message["reply_to_msg_id"],
"media": bool(message["media"])
}
```
### Шаг 2: Индексация в Meilisearch
**Скрипт:** `scripts/index_meilisearch.py`
**Вход:** парсированные сообщения
**Действие:** batch upload в Meilisearch (по 1000 сообщений за раз)
**Таймаут:** ~5 минут на все 155K сообщений
### Шаг 3: Генерация embeddings и запись в ChromaDB
**Скрипт:** `scripts/index_chromadb.py`
**Вход:** парсированные сообщения
**Действие:**
1. Загрузить модель sentence-transformers
2. Сгенерировать embedding для каждого текста
3. Записать в коллекцию ChromaDB
**Оптимизация:**
- Батчинг: по 32 сообщения за раз
- Фильтрация пустых сообщений (text = "")
- Skip медиа-сообщений без текста
**Время:** ~25 минут на CPU, ~5 минут на GPU
### Шаг 4: Поиск (основной flow)
```python
def search(query: str, topic_ids: list[int] = None):
# 1. Meilisearch — точные совпадения
meili_results = meili_index.search(query, limit=20)
# 2. ChromaDB — семантический поиск
query_embedding = model.encode(query)
chroma_results = collection.query(
query_embeddings=[query_embedding],
n_results=20
)
# 3. Объединение и дедупликация
all_results = merge_and_deduplicate(meili_results, chroma_results)
# 4. Реранкинг (по релевантности + дате)
ranked = rerank(all_results, query)
# 5. Формирование контекста
context = format_context(ranked[:10])
# 6. LLM ответ
answer = sonnet_summarize(query, context)
return answer, sources
```
### Шаг 5: API endpoint
**Скрипт:** `server.py`
**Стек:** Flask, порт 5557
**URL:** `/search?q={query}&topics={topic_ids}&limit={limit}`
**Ответ:**
```json
{
"query": "какие масла рекомендуют для Polaris 850",
"answer": "Для Polaris 850 рекомендуют...",
"sources": [
{"id": 123456, "date": "2026-01-15", "topic": "Техничка", "author": "Иван"},
{"id": 789012, "date": "2026-02-20", "topic": "Техничка", "author": "Петр"}
],
"count": 20,
"time_ms": 1500
}
```
---
## Технологии и зависимости
### Python пакеты (requirements.txt)
```
meilisearch==0.31.0
chromadb==0.4.22
sentence-transformers==2.3.1
flask==3.0.0
```
### Docker
```
getmeili/meilisearch:latest — порт 7700
```
### LLM API
- OpenRouter (Sonnet 4.6) — через существующий ключ в `.env`
---
## Расположение файлов
```
tasks/snowbike-rag/
├── TZ.md — это документ
├── scripts/
│ ├── parse_messages.py — парсинг сырых данных
│ ├── index_meilisearch.py — загрузка в Meilisearch
│ ├── index_chromadb.py — embeddings + ChromaDB
│ └── search.py — поиск + LLM
├── server.py — Flask API
├── requirements.txt
└── docker-compose.yml — Meilisearch
```
**Данные (только чтение):**
- Сырые: `/data/telegram-collector/raw/1242788123/`
- Мета: `/data/telegram-collector/raw/1242788123/meta.json`
---
## Инкрементальное обновление
Ежедневно после cron-загрузки новых сообщений:
1. Парсинг только новых batch-файлов
2. Добавление в Meilisearch (add/update)
3. Генерация embeddings и добавление в ChromaDB
4. Индекс обновляется без прерывания поиска
---
## Ограничения
- **Данные:** только текстовые сообщения, медиа не индексируются
- **Embeddings:** локальная модель, ~25 минут на CPU (первый прогон)
- **LLM:** стоимость ~$0.005 за запрос (Sonnet 4.6, ~5K токенов контекста)
- **Память:** ~700 МБ для Meilisearch + ~500 МБ для ChromaDB + ~500 МБ для модели
- **Язык:** данные на русском, модель многоязычная
---
## Стоимость
- **Индексация:** бесплатно (локальная модель)
- **Поиск (embeddings):** бесплатно (локальная модель)
- **LLM ответ:** ~$0.005 за запрос (Sonnet 4.6)
- **Docker:** бесплатно (Meilisearch community)
---
## Следующие шаги
1. Dev-агент: создать скрипты парсинга + индексации
2. Настроить Docker Meilisearch
3. Протестировать поиск на 5-10 запросах
4. Добавить Flask API
5. Настроить инкрементальное обновление