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

12 KiB
Raw Blame History

ТЗ: Семантический поиск и 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):

{
  "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)

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)

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}

Ответ:

{
  "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. Настроить инкрементальное обновление