12 KiB
ТЗ: Семантический поиск и 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
Вход: парсированные сообщения
Действие:
- Загрузить модель sentence-transformers
- Сгенерировать embedding для каждого текста
- Записать в коллекцию 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-загрузки новых сообщений:
- Парсинг только новых batch-файлов
- Добавление в Meilisearch (add/update)
- Генерация embeddings и добавление в ChromaDB
- Индекс обновляется без прерывания поиска
Ограничения
- Данные: только текстовые сообщения, медиа не индексируются
- 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)
Следующие шаги
- Dev-агент: создать скрипты парсинга + индексации
- Настроить Docker Meilisearch
- Протестировать поиск на 5-10 запросах
- Добавить Flask API
- Настроить инкрементальное обновление