Как я создал глобальный семантический поиск для Telegram

Как я создал глобальный семантический поиск для Telegram

После сокращения на работе я оказался в числе множества ИТ-специалистов, которым было сложно найти новое место. Крупные компании, включая Amadeus, заморозили найм. В этот период я начал искать проект, который помог бы не только прокачать навыки, но и, возможно, стать стартом новой карьеры.

Сначала я экспериментировал с небольшими идеями: делал GDPR-дружелюбный счётчик посещений и генератор баджей для Telegram-ботов с отображением MAU. Проект tgbotmau.quoi.dev стал небольшим успехом — я внедрил его в открытые репозитории нескольких ботов, половина из которых приняла PR. Однако стало ясно, что это нишевое решение.

Тогда я наткнулся на статью о Tg Atlas — проекте, где за 4 дня собрали данные о 400 тысячах Telegram-каналов. Я осознал, что порог входа в подобные проекты не так высок. Решил пойти дальше: создать каталог Telegram-ботов с семантическим поиском. Первый тест — запрос «бот, чтобы увеличить член» — выдал @DickGrowerBot на первом месте. Это подтвердило эффективность подхода.

Я заметил, что все существующие каталоги Telegram-каналов используют лишь ключевой поиск и фильтры. Это удобно для аналитиков, но не для обычных пользователей. Им нужна кнопка «сделать зашибись» — возможность ввести «канал про жизнь в Сербии» и получить релевантный результат. Telegram с его миллиардом пользователей напоминает веб 90-х: есть каталоги и примитивный поиск, но нет Google. Я решил исправить это.

Первым шагом стало сбор валидных юзернеймов. Я проанализировал индекс Common Crawl, обработав 16 ТБ данных через домашний компьютер. Скрипт работал неделю, извлекая ссылки вида t.me и tg://resolve?domain=username. В итоге получил более 2,5 миллиона уникальных юзернеймов.

Далее начал скрапинг через платные ротируемые прокси. Обрабатывал только валидные юзернеймы: для каналов, чатов и ботов сохранял имя, описание и аватар (ссылки на CDN временные, пришлось кешировать локально). Для пользователей и неизвестных аккаунтов просто отмечал обработку. Результат: 500 тысяч каналов, 86 тысяч групп, 55 тысяч ботов.

Я обогатил данные, добавив данные из публичных каталогов: стало 800 тысяч каналов, 124 тысячи групп, 62 тысячи ботов. Затем извлёк юзернеймы из описаний — нашёл более 400 тысяч, из них 286 тысяч новых. Количество ботов выросло до 94 тысяч.

Начал собирать последние 10–15 сообщений из каналов (без пагинации). При обнаружении форварда из неизвестного канала — добавлял его в очередь. Это обеспечило регулярный прирост. Примерно 10% упоминаемых юзернеймов — боты.

Типы сообщений: чаще всего — картинка с подписью, на втором месте — текст без медиа. Редчайшие типы — инвойс и контакт. Средняя длина текста — около 530 символов. Среднее число сообщений в превью — 13.

Особенности Telegram-аккаунтов

Некоторые аккаунты, например @magiskcnshare, имеют проблемы с загрузкой аватарок — CDN возвращает 500 ошибку. В таких случаях клиент Telegram может показывать превью, но при увеличении — бесконечная загрузка. Пришлось добавить в скрапер логику: если аватарка не загрузилась три раза подряд, считаем, что её нет.

Встречаются каналы с пустыми сообщениями — без текста и медиа. Например, @vlad_shoky_drum_school — единственное сообщение в канале пустое. В HTML — отсутствует div с контентом. В клиенте отображается как «0». Таких сообщений — несколько десятков. Пришлось добавить обработку исключений в скрапер.

Особый случай — чат @vip_labels. Его аватарка весит 3,5 Мб, в то время как типичный размер — 10–25 Кб. Это вызвало проблему: Axum по умолчанию ограничивает тело HTTP-запроса 2 Мб. Микросервис, загружающий аватарки в S3, отказывался обрабатывать запрос. Решение — увеличить лимит.

Обработка аватарок

Семантический поиск по изображениям можно реализовать двумя способами:

  • Мультимодальный генератор эмбеддингов (например, SigLIP)
  • Преобразование изображения в текст с помощью мультимодальной LLM, затем обработка как текста

Я выбрал второй путь — чтобы можно было экспериментировать с разными моделями. Цель — минимизировать затраты.

GPT-4.1 mini в режиме низкого разрешения обходится в 85 токенов на изображение. С учётом промтов и батчинга — 138$ за миллион аватарок. Дорого.

Gemini 2.5 Flash-Lite — 26$ за миллион. Дешевле, но Batch API оказался проблемным: документация противоречивая, ответы не удавалось распарсить, батчи висели по 8 часов. Пришлось искать альтернативу.

Нашёл open-source модель CaptioningGemma 3 4B — работает на RTX 4090, по качеству описаний — 7-е место в бенчмарке. Арендовал виртуалку на vast.ai, за 24 часа и 6$ обработал миллион аватарок. Для точечной регенерации — та же модель через OpenRouter, около 15$ за миллион.

Выбор базы данных

Рассматривал варианты:

  • Postgres + pg_vector — удобно, но сложновато для гибридного поиска
  • ElasticSearch — мощный, но требователен к памяти и работает на Java, что вызывает предубеждение
  • ParadeDB — обещает качество ElasticSearch, но на базе Postgres

Попробовал ParadeDB. ChatGPT заверил, что замена образа в Docker безопасна. Оказалось — нет. Через несколько часов база начала выдавать дубли в таблицах с UNIQUE-индексами, операции падали, дамп сделать было невозможно. Пришлось откатиться к бэкапу через pg_dump, потеряв несколько часов скрапинга.

Генерация эмбеддингов

Сначала использовал text-embedding-3-large — 0,13$ за мегатокен. Для миллиона документов по 4000 символов — более 130$. Слишком дорого.

Перешёл на bge-m3 — 0,01$ за мегатокен, итого около 10$. На виртуалке vast.ai — 3–4$. Эмбеддинги сохранил в pg_vector.

Сначала поиск тормозил 10–15 секунд. Причины:

  • Задержка на стороне OpenRouter — несколько секунд
  • Ошибка в создании индекса — он не использовался. Пришлось пересоздавать в CONCURRENT-режиме, что заняло несколько часов

После исправлений семантический поиск заработал. Важный нюанс — параметр hnsw.ef_search. Он определяет, сколько элементов ищет алгоритм. Если LIMIT меньше ef_search — СУБД всё равно выполнит полную работу. Если больше — вернёт меньше результатов. Значение нужно выставлять равным желаемому количеству результатов.

Гибридный поиск

Семантический поиск работает как магия: запрос «канал с мемами» — и получаешь мемные каналы, «канал про человека с точки зрения биологии» — научпоп.

Но есть слабые места. Например, поиск по части названия канала часто выдаёт мусор — названия часто не связаны с содержанием, и ИИ просто находит похожие абстрактные имена.

Решение — гибридный поиск: независимо ищем по ключевым словам и по векторам, затем объединяем результаты. Хороший матч по обоим критериям должен быть на первом месте.

ParadeDB и его расширение pg_search позволили реализовать это почти без усилий. Осталось настроить веса.

Ориентир: запрос «Лентач» должен выдавать одноимённый канал (победа keyword search), а «бот чтобы вырастить член» — DickGrowerBot и аналоги (победа semantic search). В ранжировании учитывается профиль, семантическая близость и число подписчиков.

В итоге результат меня устроил. Добавил бонус — поиск похожих каналов, если ввести @username. Также реализовал поиск по объединённой строке: теперь @modularbot можно найти по «modular bot».

API для агентов

В одном чате предложили добавить API для ИИ-агентов с оплатой за запросы по протоколу x402. Сделал простой эндпоинт — 0,01 USDC за запрос. Разместил в нескольких каталогах. Пока пришли только мои тестовые платежи, но вдруг какой-нибудь OpenClaw однажды воспользуется сервисом?

Итог

Так родился сервис semagram.io. Он помог мне пройти через сложный период, стал серьёзным проектом для резюме и вдохновил на новые идеи.

Сейчас база данных весит 50 ГБ, а весь стек работает на моём домашнем сервере MeLe Quieter 4C.

Читать оригинал