Инженерный блог

Алматинские пробки vs BigQuery: Как мы анализировали 1 000 000 маршрутов курьеров и оптимизировали локальную рекламу

12.08.2026
Шарафутдинов Р.

Пятница. 19:00 вечера. Алматы, проспект Аль-Фараби бордового цвета. Водители бьют по рулю, смотрят в Яндекс Навигатор и прожигают время впустую. И именно этот момент — золотая жила для рекламодателей. Почему? Потому что внимание человека свободно на 100%, а его геолокация не изменится ближайшие 40 минут. Что если направить это скучающее внимание на аудиокниги, заказ ужина из ближайшего ресторана или приложения для медитации?

Сегодня мы разберем на архитектурном уровне, как мы взяли миллионы координат машин в городе, научились строить «полигоны пробок» в реальном времени с помощью Google Cloud (BigQuery GIS, Pub/Sub) и применили эти данные, чтобы сделать баннерную рекламу (через Google Publisher Tag) эффективнее до 300%.

Архитектура: От хаоса координат к четким полигонам

У нас есть непрерывный поток гео-данных от партнерской курьерской службы (да, те самые ребята с желтыми и зелеными рюкзаками). Их приложение отправляет локацию каждые 10 секунд по MQTT. Это тысячи событий (events) в секунду.

Если мы будем писать все это напрямую в базу данных (PostgreSQL), она умрет через пару минут. Поэтому мы выбрали бессерверную (serverless) потоковую архитектуру.

Шаг 1: Прием данных через Pub/Sub и Dataflow

Все координаты попадают в топик Google Cloud Pub/Sub. Это наш амортизатор. Он может принимать миллионы сообщений в секунду без всякой настройки.

Чтобы читать данные из Pub/Sub, мы подняли пайплайн Cloud Dataflow (Apache Beam). Его задача — очистить "мусор": убрать отстрелы GPS (аномалии), обогатить данные (например, добавить H3 хэши) и сложить в хранилище.

# Фрагмент пайплайна Dataflow (Apache Beam)
import apache_beam as beam
import json

def parse_and_clean_gps(message):
    data = json.loads(message)
    # Фильтруем аномалии GPS (например, отстрелы в Капчагай)
    if 43.0 < data['lat'] < 43.6 and 76.6 < data['lon'] < 77.2:
        yield data

(p 
 | 'ReadFromPubSub' >> beam.io.ReadFromPubSub(subscription='projects/ozat-kz/subscriptions/gps-sub')
 | 'CleanData' >> beam.FlatMap(parse_and_clean_gps)
 | 'WriteToBigQuery' >> beam.io.WriteToBigQuery(
       table='ozat-kz:traffic_dataset.raw_gps',
       create_disposition=beam.io.BigQueryDisposition.CREATE_NEVER,
       write_disposition=beam.io.BigQueryDisposition.WRITE_APPEND
   )
)

Смотреть код на GitHub (OZAT-kz)

Шаг 2: BigQuery GIS — Магия поиска заторов

Итак, у нас лежат миллионы точек в таблице raw_gps. Теперь начинается самое интересное. Нам нужны не просто точки, а полигоны (площади) пробок.

Мы используем функции BigQuery GIS (Geographic Information Systems). Это инструмент, который позволяет делать геометрические вычисления прямо поверх петабайтов данных через SQL за считанные секунды.

Логика следующая: 1. Мы вычисляем скорость курьеров за последние 5 минут (если меньше 15 км/ч — значит стоит в пробке). 2. Группируем такие "медленные" точки в кластеры с помощью ST_CLUSTERDBSCAN, если они находятся в радиусе 50 метров друг от друга. 3. Затем натягиваем виртуальный полигон поверх этих кластерных точек (ST_CONVEXHULL).

-- Создаем полигон пробки
SELECT
  ST_CONVEXHULL(ST_UNION_AGG(geo_point)) as traffic_jam_polygon,
  COUNT(DISTINCT courier_id) as stuck_couriers_count
FROM clustered_data
WHERE cluster_id IS NOT NULL
GROUP BY cluster_id;

Смотреть код на GitHub (OZAT-kz)

Функция ST_CONVEXHULL натягивает виртуальную «резинку» поверх всех стоящих курьеров, формируя готовый полигон пробки. И этот процесс повторяется каждые 3 минуты!

Синхронизация с клиентом: Доставка полигонов в приложение

Вычислять пробки в BigQuery — это круто. Но как приложению, которое сейчас открыто на телефоне у пользователя, узнать, что оно находится внутри этого полигона?

Делать запрос из мобилки в BigQuery нельзя — это медленно (2-3 секунды) и разорит нас на костах. Поэтому мы настроили Cloud Function, которая раз в 3 минуты забирает массив актуальных полигонов пробок из BigQuery и кладет их в Redis (Cloud Memorystore).

Когда пользователь открывает наше приложение (или сайт с нашей рекламой), мы берем его GPS-координаты (с его согласия, конечно) и делаем сверхбыстрый запрос к нашему микросервису на Golang. Микросервис проверяет пересечение точки с полигонами в памяти (Point-in-Polygon) за 1 миллисекунду.

AdTech: Динамический таргетинг через Google Publisher Tag

Самое вкусное — монетизация. Мы интегрированы с Google Ad Manager. Обычно рекламная сеть сама решает, что показать. Но мы можем ей помочь с помощью технологии Key-Value Targeting (Таргетинг по ключу-значению) в библиотеке Google Publisher Tag (GPT).

Если наш микросервис вернул ответ, что пользователь в пробке (isInJam: true), мы динамически добавляем параметр в рекламный слот ПЕРЕД тем, как запросить баннер у гугла.

// Клиентский код в приложении/на сайте
// Полный пример интеграции GPT + React ищите на GitHub

async function initAds(userLat, userLon) {
  // 1. Быстро проверяем наш кэш пробок
  const trafficStatus = await checkTrafficJam(userLat, userLon); 
  
  window.googletag = window.googletag || {cmd: []};
  googletag.cmd.push(function() {
    var slot = googletag.defineSlot('/1234567/almaty_app_banner', [300, 250], 'div-gpt-ad-123')
      .addService(googletag.pubads());

    // 2. СЕКРЕТНЫЙ ИНГРЕДИЕНТ: Передаем контекст пробки в Ad Manager
    if (trafficStatus.isInJam) {
      slot.setTargeting('context', 'traffic_jam');
      slot.setTargeting('jam_severity', trafficStatus.severity); // e.g. '10_points'
    } else {
      slot.setTargeting('context', 'moving');
    }

    googletag.enableServices();
    googletag.display('div-gpt-ad-123');
  });
}

Смотреть код на GitHub (OZAT-kz)

В интерфейсе самого Google Ad Manager наши Trafficking-специалисты (рекламщики) создали специальные премиальные кампании (Line Items). Эти кампании активируются ТОЛЬКО если context=traffic_jam. Рекламодатели (сервисы аудиокниг, онлайн-кинотеатры, локальные рестораны на районе) платят нам за эти показы повышенный CPM, потому что знают, что конверсия будет сумасшедшей.

Результаты: А/В Тестирование и Метрики

Мы запустили А/В тест на 1 месяц. Половине пользователей, стоящих в пробке, мы показывали обычную рекламу (Random). Другой половине — таргетированную "пробочную" рекламу (Audiobooks, Food Pickup, Anti-stress apps).

  • CTR (Click-Through Rate): Вырос с унылых 1.2% до невероятных 4.5% в пиковые часы (18:00 - 20:00).
  • eCPM (Эффективная стоимость 1000 показов): Выросла на 210%. Рекламодатели начали биться в аукционе за нашу "запертую" аудиторию.
  • User Feedback: Как ни странно, негатива стало меньше. Люди писали в саппорт: «Ваша реклама аудиоспектакля спасла меня от нервного срыва на Саина/Жандосова». Релевантная реклама воспринимается как контент.

Сравнение кликабельности (CTR) рекламы аудиокниг и фастфуда среди пользователей, находящихся в пробке, и обычных пользователей.

Выводы

Гео-данные — это новая нефть. Но сырая нефть не заправит вашу машину. Инструменты вроде Google Cloud Pub/Sub и BigQuery GIS выступают тем самым нефтеперерабатывающим заводом, который позволяет извлекать смысл из хаоса из миллионов координат.

Соединение глубокой бэкенд-аналитики (Data Engineering) с фронтенд-технологиями монетизации (AdTech / GPT) открывает совершенно новые ниши для заработка на мобильных приложениях и веб-сайтах в Казахстане.

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

Рустам Шарафутдинов

Рустам Шарафутдинов

Автор инженерного блога

Эксперт в области архитектуры Google Cloud и Senior Full-Stack разработчик с более чем 15-летним опытом. Специализируется на отказоустойчивых архитектурах, оптимизации высоконагруженных проектов и интеграции AI (Vertex AI).

Экспертность: GCP, Kubernetes, Микросервисы, React, Node.js

Комментарии (0)