«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами. telegram.. telegram. исследование.

Мы привыкли видеть Telegram как список чатов и каналов на своём устройстве. Но это лишь малая часть большой экосистемы, ограниченная подписками каждого пользователя. А как выглядит вся экосистема целиком?

Чтобы ответить на этот вопрос, мы — команда сервиса TGpages, провели масштабное исследование публичных Telegram-каналов. Результатом исследования стала интерактивная карта, которая поможет пользователям находить новые интересные каналы, а авторам, экспертам и брендам — лучше понимать структуру рынка, находить ниши и анализировать конкурентное окружение.

Эта статья — о том, как мы провели исследование и разработали карту. Я постарался описать методику исследования и основные технические барьеры, которые нам удалось решить в этом проекте.

«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 1

Принцип построения карты

Задача создать карту, на которой пользователям будет легко находить похожие каналы, ниши, оценивать масштаб тематических категорий, и немаловажно — чтобы её было интересно исследовать.

Представьте человека, который впервые смотрит на звёздное небо через телескоп. Каждое скопление звёзд — загадка. Человек изучает небо, открывает для себя новые галактики и созвездия. Нам хотелось, чтобы на нашей карте Telegram-каналов пользователь тоже мог «путешествовать» по тематическим «галактикам», открывая для себя новые «звёзды» — каналы.

Чтобы этого добиться, в основу карты было заложено два принципа:

  • Если каналы пишут схожий контент, они находятся рядом на карте

  • Чем дальше каналы друг от друга, тем меньше у них общего

— так на карте возникают целые острова и тематические архипелаги.

Микро- и макрокатегории

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

Было решено ввести двухуровневую тематическую категоризацию:

  • Макрокатегории — широкие тематические области: «Новости», «Развлечения», «Крипта», «Спорт»,…

  • Микрокатегории — узкие тематические ниши: «Разработка игр и геймдизайн», «Новости и фанаты LEGO», «Строительство бань и саун», «Сообщества мам», «Кожевенное мастерство»,…

«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 2

Такой подход к тематическому моделированию помог сделать карту удобнее: пользователь начинает с общего обзора всей экосистемы, находит интересующую его область и постепенно погружается в детали. Точно так же устроена навигация в бумажных атласах — от континентов к странам, от стран к городам.

Подготовка данных

Список каналов мы собрали из нескольких источников:

  • Собственная база нашего сервиса — TGPages

  • Открытые каталоги каналов

  • Курируемые списки каналов от наших партнёров

В итоге мы получили около 600 000 каналов для исследования.

Автоматизированный сбор данных из Telegram

Чтобы собрать данные о 600 000 каналах за разумное время, и не столкнуться с блокировками со стороны Telegram, мы построили распределённую систему на облачных функциях (AWS Lambda). Архитектура выглядит так:

  1. Центральный бэкенд хранит очередь заданий (usernames каналов) и структурированные метаданные каналов

  2. Lambda-воркеры в 17 регионах AWS параллельно считывают данные каналов с веб-интерфейса Telegram

  3. S3 хранит «сырые» данные каналов — публикации

Каждый воркер:

  1. Получает задание от центрального API

  2. Загружает публичное веб-превью канала (https://t.me/s/{username})

  3. Парсит HTML, извлекает метаданные и текст до 100 последних постов

  4. Загружает аватар канала в хранилище с CDN (будет нужен для интерактивной карты)

  5. Сохраняет посты в S3 для дальнейшего анализа

При скорости около 10 запросов в минуту для каждого воркера обработка всех 600 000 каналов заняла 2-3 дня.

Для каждого канала извлекаем:

  • Метаданные: название, описание (bio), аватар, количество подписчиков

  • Контент: тексты постов (до 100 последних публикаций)

  • Статистика: просмотры, реакции, даты публикаций

Фильтрация каналов

Не все каналы подошли для анализа:

  • Каналы без публичного веб-превью (https://t.me/s/{username})

  • Каналы с недостаточным количеством текста

  • Каналы с контентом преимущественно из изображений/видео без текста

После фильтрации для дальнейшего анализа осталось около 500 000 каналов.

Вычисление эмбеддингов

📚 Что такое эмбеддинг? Представьте, что каждый текст — это точка в пространстве. Похожие тексты находятся близко друг к другу, разные — далеко. Эмбеддинг — это координаты этой точки. Нейросеть читает текст и выдаёт список чисел (в нашем случае — вектор из 1536 числел) — это и есть «адрес» текста в многомерном семантическом пространстве. Тексты про гейминг будут иметь похожие координаты, а тексты про кулинарию — совсем другие.

Для тематического моделирования и построения двухмерной карты, необходимо было «понять» и представить в числовой форме о чём каждый канал — построить эмбеддинги. Есть два источника информации:

  1. Как канал себя позиционирует — название и описание (bio)

  2. О чём канал фактически пишет — тексты постов

Для каждого канала мы сгенерировали два отдельных эмбеддинга с помощью модели OpenAI text-embedding-3-small — для контента канала и для названия и описания.

Два отдельных эмбеддинга мы сделали, чтобы можно было в дальнейшем «регулировать» насколько каждый из них играет роль в процессе кластеризации и построении карты

Генерация эмбеддингов для 500 000 каналов — это около 1 000 000 запросов к API (два эмбеддинга на канал). Чтобы снизить затраты, мы использовали OpenAI Batch API (-50% к стоимости использования). Весь процесс вычисления эмбеддингов занял около 3 дней.

Объединение двух эмбеддингов в один

У нас получилось два 1536-мерных вектора для каждого канала. Для кластеризации и построения карты нужен только один. Мы использовали взвешенное среднее с последующей нормализацией:

# Комбинируем с равным весом 50/50
combined = 0.5 * identity_emb + 0.5 * content_emb

# L2-нормализация: приводим к единичной длине
combined = combined / np.linalg.norm(combined)
«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 3

Что здесь происходит:

  1. Взвешенное сложение. Каждый эмбеддинг — это вектор из 1536 чисел. Мы берём 50% от каждого компонента идентификационного эмбеддинга и 50% от контентного, складываем их. Получается новый вектор, который «помнит» оба сигнала. Вес 50/50 — эмпирический выбор. Мы пробовали разные соотношения (0.3/0.7, 0.7/0.3) и визуально оценивали качество кластеров.

  2. L2-нормализация. Исходные эмбеддинги от OpenAI имеют единичную длину. Но после сложения двух векторов длина результата зависит от угла между ними: если контентный и идентификационный эмбеддинги указывают в одном направлении (канал пишет о том же, о чём заявляет) — длина близка к 1; если они расходятся — длина меньше. Мы приводим все векторы к единичной длине, чтобы это не влияло на кластеризацию. После нормализации евклидово расстояние между векторами становится эквивалентно косинусному сходству — метрике широко применяемой для сравнения текстов.

📚 Что такое L2-нормализация? Представьте стрелку из центра координат. L2-нормализация растягивает или сжимает её так, чтобы длина стала ровна 1. После этого мы сравниваем только направления векторов, а не их величины. Два текста разной длины после нормализации сравниваются по семантике.

Кластеризация каналов

📚 Кластеризация — процесс автоматической группировки объектов на основе их схожести. Представьте карту с точками — кластеризация находит «сгустки» и говорит: «вот это одна группа, а вот это — другая». Алгоритм не знает заранее, сколько групп искать — он просто находит структуру в данных.

Теперь у нас получилось 500 000 комбинированных векторов по 1536 измерений каждый. Но кластеризовать их «как есть» нельзя — мешает «проклятие размерности».

📚 Что такое «проклятие размерности»? Представьте, что вы ищете похожего на себя человека. Если критерий один — рост — задача простая: найти людей примерно вашего роста. Если критериев два (рост и вес), уже сложнее, но всё ещё реально. А теперь представьте 1536 критериев: рост, вес, цвет глаз, любимый фильм, скорость печати, предпочтения в музыке… По каждому критерию люди немного отличаются, и эти маленькие различия накапливаются. В итоге все оказываются примерно одинаково «далеки» от вас — понятие «похожий» размывается. Хуже того: разница между «самым похожим» и «самым непохожим» человеком становится ничтожной на фоне общего «расстояния» до каждого из них.

Шаг 1: UMAP — снижение размерности. Алгоритм UMAP выолняет сжатие размерности эмбеддингов так чтобы сохранить локальную структуру в данных: каналы, которые были ближайшими в 1536-мерном пространстве эмбеддингов, останутся ближайшими и после сжатия. Мы «сжимаем» 1536 измерений до 15:

reducer = umap.UMAP(
    n_components=15,       # целевая размерность
    n_neighbors=30,        # сколько соседей учитывать
    metric='cosine',       # косинусное расстояние для текстов
    min_dist=0.0,          # разрешаем плотные кластеры
    random_state=42
)
reduced = reducer.fit_transform(combined_embeddings)
«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 4

Шаг 2: HDBSCAN — поиск кластеров. Был выбран HDBSCAN, потому что он не требует заранее указывать число кластеров — алгоритм сам находит группы на основе плотности данных и умеет помечать «шумовые» точки, которые не принадлежат ни одному кластеру:

# L2-нормализуем после UMAP
normalized = reduced / np.linalg.norm(reduced, axis=1, keepdims=True)

clusterer = hdbscan.HDBSCAN(
    min_cluster_size=50,   # минимальный размер кластера
    min_samples=15,        # плотность ядра кластера
    metric='euclidean'
)
labels = clusterer.fit_predict(normalized)
«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 5

Мы выполнили кластеризацию дважды с разными параметрами для получения двух уровней иерархии:

Параметр

Глубокая кластеризация

Широкая кластеризация

min_cluster_size

50

500

min_samples

15

100

Результат

~650 микрокатегорий

~100 макрокатегорий

Глубокая кластеризация (с меньшим min_cluster_size) находит узкие ниши: «Крипто-трейдинг», «Турецкие сериалы», «Городские СМИ». Широкая (с большим min_cluster_size) — общие области: «Гейминг», «Бизнес», «Спорт».

HDBSCAN помечает неоднозначные точки как шум (метка -1). Мы классифицируем такие каналы в категорию «Разное». В результате, около 4% каналов со смешанной тематикой или слишком уникальным контентом попали в этот раздел.

Генерация названий кластеров

Для использования в нашем проекте полученным группам необходимо было дать репрезентативные имена: «Лайфстайл и блоги», «Маркетинг», «Здоровье», «Психология» и так далее

📚 Что такое центроид кластера? Это «центр тяжести» кластера — усреднённый вектор всех точек в группе. Если кластер — это облако точек, центроид — точка в самом центре этого облака.

Для каждого кластера:

  1. Выбрали самые репрезентативные каналы — 40 каналов, ближайших к центроиду по косинусному сходству

  2. Собрали контент — название, описание (до 200 символов) и 3 самых длинных поста (до 800 символов каждый)

  3. Отправили в GPT-5.2 Completion API с промптом похожим на следующий (сокращен для статьи):

Ниже представлены семплы из тематических категорий Telegram-каналов полученных в ходе процесса кластеризации.

На основе семпла данных Telegram-каналов необходимо сформировать названия для рубрик (например: 'Бизнес', 'Спорт', 'Кулинария', 'Видеоигры', 'Политика', 'Музыка'). Для каждого кластера каналов сгенерируй одно название категории (1–4 слова, на русском языке)

...
«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 6

При обработке большого числа кластеров батчами неизбежно появляются похожие названия:

  • «Крипто-трейдинг»

  • «Торговля криптовалютой»

  • «Криптосигналы»

— это проблема, которую митигировали через семантическую дедупликацию:

  1. Генерируем эмбеддинги всех названий категорий (тем же text-embedding-3-small)

  2. Вычисляем попарные косинусные сходства

  3. Для пар со сходством > 0.85 — перегенерировали название меньшего кластера, передав языковой модели список названий, которых нужно избегать.

Визуализация: t-SNE

Теперь нам нужно было спроецировать 500 000 каналов на 2D-карте так, чтобы запечатлеть семантическую близость: похожие каналы рядом, разные — далеко. Для этого мы используем алгоритм понижения размерности данных t-SNE. На входе алгоритма — эмбеддинги каналов, а на выходе мы получаем две координаты: x и y, которые будут использоваться для проекции маркера канала на карту.

«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 7

Почему t-SNE, а не UMAP? Ведь UMAP мы уже использовали для кластеризации — почему бы не применить его и для 2D-проекции? Дело в визуальном эффекте. UMAP склонен создавать «вытянутые» связные структуры, где темы плавно перетекают друг в друга. t-SNE, напротив, агрессивно разделяет кластеры — между группами образуется чистое пустое пространство. Для нашей задачи это иде��льно: мы хотели сделать так, чтобы тематические категории выглядели как отдельные «острова».

Строго говоря, алгоритм t-SNE не гарантирует, что близость на плоскости будет отражать близость точек в исходном пространстве. Но для нашей задачи визуализации его было достаточно — тем более, что «подсветить» схожие каналы можно применив цветовое кодирование по тематическим категориям, выявленным на этапе кластеризации.

Интерактивная карта: от данных к продукту

К этому моменту у нас появились все данные для построения карты: точки с 2D-координатами, именованные кластеры и метаданные каналов. Оставалось превратить это в интерфейс, в котором пользователь сможет исследовать данные — примерно как в Google Maps, но для Telegram-каналов.

Три уровня детализации

Карта имеет три масштаба:

  • Обзорный уровень. Каналы отображаются как точки с цветовым кодированием по макрокатегориям, яркость точки — размер канала в подписчиках. Видны подписи с названием тематики на кластерах макрокатегорий.

  • Средний уровень. Цветовое кодирование для макрокатегорий переключается на кодирование для микрокатегорий. Появляются подписи кластеров микрокатегорий.

  • Глубокий уровень. Точки превращаются в «пузырьки» с аватарками каналов. Размер пузырька зависит от числа подписчиков — крупные каналы визуально выделяются.

«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 8

При зуме карты переключение между уровнями анимировано: цвета точек плавно переходят от одного уровня цветового кодирования к следующему, подписи кластеров появляются и исчезают с fade-эффектом, размер точек изменяется постепенно приближаясь к глубокому уровню детализации. Для интерполяции цветовых значений и размеров используем кубические easing-функции от значения уровня зума, что делает анимацию приятнее для глаза.

Технически карта состоит из четырёх наложенных друг на друга слоёв:

  • WebGL-слой — отрисовка точек каналов

  • Canvas-слой для подписей — названия кластеров поверх карты для удобной навигации

  • Canvas-слой для аватарок каналов — на глубоком уровне зума поверх точек отображаются аватарки каналов

  • UI-слой — элементы интерфейса: строка поиска, фильтр катаегорий, карточки каналов и тултипы, модальные окна и пр.

Рендеринг 500 000 точек

Одним из главных вызовов этого проекта была отрисовка полумиллиона точек с плавной анимацией и приемлемой частотой кадров. В противном случае пользоваться картой было бы просто неудобно. Canvas для этого не подходит: каждый вызов arc() и fill() — это отдельная операция на CPU и на среднем по мощности устройстве можно ожидать FPS не больше 10.

Решение — WebGL. Вместо того чтобы рисовать точки по одной, мы загружаем все координаты в GPU одним массивом и выполняем отрисовку за один draw call. GPU спроектирован именно для таких задач — параллельной обработки множества вершин.

Ключевое решение — статический буфер. Все 500 000 точек загружаются в видеопамять один раз при старте приложения. Каждый кадр мы передаём в шейдер только «глобальные» параметры — позицию камеры, уровень зума, время для анимаций — а координаты точек остаются в GPU.

На глубоком уровне масштаба ситуация другая: в видимую область попадает всего несколько десятков или сотен каналов, и для режима «пузырьков» нужны скорректированные позиции с разрешением коллизий (об этом далее). Для этого случая мы использовали пространственный индекс, чтобы быстро найти только видимые точки (viewport culling), пересчитывать их позиции и загружать в GPU небольшой буфер для отрисовки.

Подписи кластеров: проблема центроида

Координаты каналов (точек на карте) были получены в ходе расчета t-SNE, но как понять на каких координатах размещать подписи названий кластеров? Наивный ответ — в центроиде (среднем арифметическом координат всех точек кластера). Но t-SNE создаёт кластеры неправильной формы. Большой кластер может состоять из нескольких плотных «островов», и подпись в геометрическом центре окажется в пустоте между ними.

Решение — density-based positioning:

  1. Разбиваем точки кластера на grid-ячейки

  2. Находим ячейки с максимальной плотностью

  3. Размещаем подписи в «визуальных центрах» — там, где больше всего точек

Для крупных кластеров (более 500 каналов) показываем сразу несколько подписей в разных плотных областях с достаточным количеством точек. Чтобы не загружать устройство пользователя этими вычислениями, расчет выполняется «оффлайн» — на этапе сборки проекта.

«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 9

Интерактивность карты и пространственный индекс

Когда пользователь водит курсором по карте, необходимо показывать тултип с информацией о канале на который указывает курсор. При клике на точку нужно открыть окно с подробным описанием канала. Для обеих задач нужно находить идентификатор канала под курсором, чтобы извлечь его информацию. Наивным решением было бы перебирать все 500 000 точек чтобы ближайшую к курсору. Но это O(n) на каждое событие мыши, что даст заметные лаги для такого количества точек. Решение которое мы применили — пространственный индекс.

📚 Что такое пространственный индекс? Представьте шахматную доску, наложенную на карту. Каждая клетка «знает», какие точки в ней находятся. Когда пользователь кликает, мы сначала определяем клетку под курсором, а затем проверяем только точки в этой клетке и соседних. Вместо 500 000 точек проверяем лишь малую долю из них.

Мы использовали grid-based индекс с ячейками размером 0.05 координатных единиц (координаты точек в пределах [-1, 1]):

class SpatialIndex {
  private grid: Map<number, GridCell> = new Map();
  private cellSize: number = 0.05;
  
  getCellKey(x: number, y: number): number {
    const cx = Math.floor(x / this.cellSize) + OFFSET;
    const cy = Math.floor(y / this.cellSize) + OFFSET;
    return cx + cy * MULTIPLIER; // Компактный числовой ключ
  }
}
«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 10

При таком подходе поиск занимает O(k), где k — количество каналов в нескольких ячейках. Этот же индекс мы затем использовали для построения списка похожих каналов в карточке канала.

Режим «пузырей»: разрешение коллизий

На глубоком зуме нашей карты каналы отображаются как «пузырьки» с аватарками. Размер «пузырька» зависит от числа подписчиков. При реализации мы столкнулись с проблемой: пузырьки крупных каналов перекрывают мелких соседей. Нужно было придумать как «расталкивать» пересекающиеся «пузырьки» каналов, чтобы все были видны на карте.

Для этого мы использовали алгоритм итеративной релаксации:

function relaxPositions(channels: Channel[], zoom: number, baseScale: number, iterations = 12) {
  const scale = baseScale * zoom;
  const padding = 3 / scale;
  const springStrength = 0.08;

  const channelData = channels.map(ch => ({
    id: ch.id,
    originalX: ch.x,
    originalY: ch.y,
    x: ch.x,
    y: ch.y,
    radius: getRadius(ch.normalizedSize, zoom) / scale,
  }));

  for (let iter = 0; iter < iterations; iter++) {
    // Расталкиваем пересекающиеся пузырьки
    for (let i = 0; i < channelData.length; i++) {
      for (let j = i + 1; j < channelData.length; j++) {
        const a = channelData[i];
        const b = channelData[j];

        const dx = b.x - a.x;
        const dy = b.y - a.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        const minDist = a.radius + b.radius + padding;

        if (dist < minDist && dist > 0.0001) {
          const overlap = minDist - dist;
          const push = overlap * 0.5;
          a.x -= (dx / dist) * push;
          a.y -= (dy / dist) * push;
          b.x += (dx / dist) * push;
          b.y += (dy / dist) * push;
        }
      }
    }

    for (const ch of channelData) {
      ch.x -= (ch.x - ch.originalX) * springStrength;
      ch.y -= (ch.y - ch.originalY) * springStrength;
    }
  }

  return channelData;
}
«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 11
«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 12

Симуляция запускается когда пользователь панорамирует или меняет масштаб карты на глубоком уровне зума с debounce 150мс и завершается за 12 итераций. За счет debounce, пока пользователь активно зумит, пузыри остаются на исходных позициях — это предотвращает излишнее «дёргание» интерфейса и улучшает производительность.

📚 Что такое debounce? Это техника, при которой ресурсоёмкие вычисления откладываются во времени до тех пор, пока пользователь не прекратит взаимодействие. Если пользователь в данный момент меняет масштаб или позицию на карте, мы не запускаем тяжёлые вычисления. Жд��м 150 мс после того как пользователь закончил и только тогда пересчитываем позиции пузырьков.

Загрузка данных: чанки и prefetch

500 000 каналов с метаданными (название, описание, координаты, кластеры, размер) — это десятки мегабайт данных. Мы разбили данные на 15 чанков. Формат — MessagePack вместо JSON. Это бинарный формат, который парсится браузером быстрее. Стратегия загрузки — prefetch с перекрытием: пока мы обрабатываем текущий чанк, уже запрашиваем следующий. Сетевое ожидание и обработка данных происходят параллельно.

Deep linking

Мы подумали, что пользователи наверняка захотят делиться найденным на карте контентом — как собственным каналом, так и новыми интересными находками. Для этого состояние карты должно полностью восстанавливаться из URL.

Мы сохраняем все параметры в query-строке:

/?x=-0.362722&y=0.522171&z=115&channel=e1ad3ff8-35ec-4bd3-9da4-14c0d3dae6e4&zoom=115
«Атлас Telegram» — как создавалась интерактивная карта с 500 000 каналами - 13
  • x, y — координаты центра viewport

  • z — уровень зума

  • channel — выбранный канал

  • search — поисковый запрос

При каждом изменении состояния (панорамирование, зум, выбор канала) URL обновляется. При открытии ссылки происходит обратный процесс: парсим query-параметры, восстанавливаем позицию камеры и, если указан канал, открываем карточку с информацией.

Что дальше

В результате у нас получилось большое исследование экосистемы Telegram-каналов с визуально впечатляющим и при этом функциональным интерфейсом. Мы получили интерактивную карту с 500 000 телеграм каналами классифицированные на ~100 макро- и ~600 микрокатегорий. Карта доступна у нас на сайте https://tgpages.com/atlas/map. Мы будем дополнять карту новыми каналами и улучшать качество анализа данных.

Вопросы или предложения по реализации карты пишите в комментариях. Если вы автор Telegram-канала и не нашли свой канал на карте, вы можете добавить его через нашего бота и он попадёт в следующую версию карты.

Автор: BorisChumichev

Источник

Rambler's Top100