341 lines
12 KiB
Markdown
341 lines
12 KiB
Markdown
# ТЗ: Семантический поиск и 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. Настроить инкрементальное обновление
|