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