chore(repo): заложен репозиторий v2 — контракт, спека, исследование
- Зачем:
- спека «Боевой реализм стенда» исполняется в новом репозитории:
предшественник замораживается как стабильный учебный стенд,
v2 стартует пустым и переносит только нужное
- Что:
- README: что это, статус «строится по спеке», ссылки на спеку и на
репозиторий-предшественник
- AGENTS.md написан заново, а не скопирован: язык, uv, обязательная
проверка API через MCP Context7, контракт трекера Gitea (спека —
источник истины, корневой issue тонкий), метки триажа, новые доки в docs/
- .gitignore: Python и uv, .env, секреты, IDE, логи
- docs/specs/2026-07-30-stand-v2-realism.md перенесена из v1; содержание
не менялось, поправлены только ссылки: добавлена строка о переезде,
ссылка на generator-realism.md переведена на абсолютный URL v1
- docs/research/2026-07-26-yandex-clickstream-format.md перенесено:
источник истины по формату широкого события
- лицензии у предшественника нет, переносить нечего
- Проверка:
- git show --stat: 5 файлов
- относительные ссылки спеки ведут на существующие файлы репозитория
This commit is contained in:
+45
@@ -0,0 +1,45 @@
|
||||
# Python и uv
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
.venv/
|
||||
.python-version
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.mypy_cache/
|
||||
|
||||
# Настройки окружения: в git попадает только образец .env.example
|
||||
.env
|
||||
.env.local
|
||||
*.env.local
|
||||
|
||||
# Секреты
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
secrets/
|
||||
credentials/
|
||||
|
||||
# Редакторы и IDE
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Логи и временные файлы
|
||||
logs/
|
||||
*.log
|
||||
*.tmp
|
||||
*.bak
|
||||
*~
|
||||
|
||||
# Файлы операционных систем
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Локальные настройки Claude Code
|
||||
.claude/*
|
||||
!.claude/agents/
|
||||
@@ -0,0 +1,67 @@
|
||||
# AGENTS.md
|
||||
|
||||
Короткий контракт для работы в репозитории учебной дата-платформы кликстрима.
|
||||
|
||||
## Цель репозитория
|
||||
|
||||
Проект учебный: учебная ценность разработок — одна из его базовых ценностей.
|
||||
Стенд строится по спеке
|
||||
[«Боевой реализм стенда (v2)»](docs/specs/2026-07-30-stand-v2-realism.md).
|
||||
|
||||
## Язык
|
||||
|
||||
Пиши на ясном русском языке. Иностранные слова оставляй только там, где у
|
||||
термина нет устоявшегося русского аналога: имена технологий (Kafka, ClickHouse,
|
||||
Airflow) и названия из кода. Если для понятия есть обычное русское слово —
|
||||
используй его, не выдумывай транслитерации.
|
||||
|
||||
Документы и комментарии держи короткими и понятными читателю, который их не
|
||||
писал: простые слова, короткие фразы, сложную мысль поясняй при первом
|
||||
упоминании. Если понятность и буквальная точность спорят — выбирай понятность.
|
||||
|
||||
Внутренние рассуждения и промежуточные пометки по ходу работы веди на
|
||||
английском — он экономнее по токенам. На русском остаётся всё, что видит и
|
||||
хранит проект: итоговые ответы, документы, комментарии в коде и SQL, сообщения
|
||||
коммитов.
|
||||
|
||||
## Код и данные
|
||||
|
||||
- Python — только через `uv`.
|
||||
- Изменения держать минимальными и в границах задания.
|
||||
- Секреты не коммитить: настройки — через `.env`, образец — `.env.example`.
|
||||
- При изменении инфраструктуры или DDL обновлять документацию тем же PR.
|
||||
- Коммиты — Conventional Commits: заголовок `type(scope): результат`, тело на
|
||||
русском по схеме Зачем / Что / Проверка.
|
||||
- «Грязные» записи не должны валить пайплайн: ошибки разбора уходят в таблицы
|
||||
`*_errors`.
|
||||
- Данные не читать целиком без необходимости: по умолчанию малый срез. Для
|
||||
демо и тестов быстрый повторяемый прогон важнее полноты данных.
|
||||
|
||||
## Проверка API через MCP Context7 (обязательно)
|
||||
|
||||
Для спорных или меняющихся API (Airflow и провайдеры, DDL ClickHouse) сначала
|
||||
уточнять актуальную версию: `resolve-library-id` -> `query-docs`. Принятое
|
||||
решение кратко фиксировать в коде или документации: что проверили и почему
|
||||
выбрали этот вариант.
|
||||
|
||||
## Задачи
|
||||
|
||||
Трекер — Gitea на `git.dementev.space`, работа через CLI `tea` (логин по
|
||||
умолчанию настроен, репозиторий определяется по git remote). Команды и
|
||||
подводные камни описаны в
|
||||
[доке предшественника](https://git.dementev.space/ddmitry/clickstream-ch-kafka-superset-demo/src/branch/main/docs/agents/issue-tracker.md).
|
||||
|
||||
- Спека фичи — файл в `docs/specs/`, источник истины, версионируется с кодом.
|
||||
- Корневой issue фичи — тонкий: ссылка на спеку и чек-лист дочерних issues
|
||||
(`- [ ] #NN`). Содержание спеки в issue не дублируется.
|
||||
- Дочерние issues — самодостаточные постановки: цель, критерии приёмки
|
||||
чекбоксами, границы, «сначала прочитать», команды проверки.
|
||||
- Итоговые решения переносятся в спеку или ADR тем же PR.
|
||||
- Метки триажа — пять ролей: `needs-triage`, `needs-info`, `ready-for-agent`,
|
||||
`ready-for-human`, `wontfix`. Карта и её тикеты — метки `wayfinder:*`.
|
||||
|
||||
## Структура
|
||||
|
||||
- Новые документы — в `docs/` или в профильных подпапках, не в корне.
|
||||
- Состав доков v2 определяется по ходу этапов, набор предшественника не
|
||||
копируется (спека, раздел 12).
|
||||
@@ -0,0 +1,36 @@
|
||||
# Учебная дата-платформа кликстрима
|
||||
|
||||
Стенд для работы с кликстримом: Kafka, кластер ClickHouse (2 шарда и
|
||||
clickhouse-keeper), Airflow, Superset. Преемник учебного стенда
|
||||
[clickstream-ch-kafka-superset-demo](https://git.dementev.space/ddmitry/clickstream-ch-kafka-superset-demo).
|
||||
|
||||
## Статус
|
||||
|
||||
Репозиторий строится по спеке
|
||||
[«Боевой реализм стенда (v2)»](docs/specs/2026-07-30-stand-v2-realism.md).
|
||||
Работающего стенда пока нет: здесь только спека, исследование формата данных и
|
||||
контракт работы ([AGENTS.md](AGENTS.md)). Быстрый старт появится вместе с
|
||||
этапом «Каркас стенда».
|
||||
|
||||
## Что здесь будет
|
||||
|
||||
- одно широкое событие кликстрима по образцу выгрузки Яндекс Метрики вместо
|
||||
четырёх топиков;
|
||||
- второй источник — заказы бэкенда, ежедневным слепком в ту же Kafka;
|
||||
- сверка клиентской покупки с заказом бэкенда: деньги считаем по бэкенду,
|
||||
поведение и атрибуцию — по трекеру;
|
||||
- анонимный кликстрим и склейка кука↔пользователь через покупки;
|
||||
- ClickHouse кластером как единственным режимом.
|
||||
|
||||
## Чем отличается от предшественника
|
||||
|
||||
Предшественник остаётся стабильным учебным стендом и заморожен для новых фич:
|
||||
там событие разрезано на четыре топика, есть только просмотры страниц,
|
||||
посетители опознаны по email, ClickHouse — одна нода. Развитие идёт здесь.
|
||||
|
||||
## Документация
|
||||
|
||||
- [docs/specs/](docs/specs/) — спеки: источник истины о задуманном.
|
||||
- [docs/research/](docs/research/) — исследования; сейчас это формат
|
||||
кликстрима Яндекса, по которому строится модель события.
|
||||
- [AGENTS.md](AGENTS.md) — контракт работы в репозитории.
|
||||
@@ -0,0 +1,723 @@
|
||||
# Формат кликстрима Яндекса: что копировать стенду
|
||||
|
||||
Status: Research
|
||||
Дата: 2026-07-26
|
||||
Тикет: [#27](https://github.com/dementev-dev/clickstream-ch-kafka-superset-demo/issues/27)
|
||||
|
||||
## Зачем это исследование
|
||||
|
||||
Владелец стенда выбирает целевую модель данных. Сейчас событие разрезано на
|
||||
четыре топика (`browser`, `location`, `device`, `geo`), которые склеиваются по
|
||||
`event_id` и `click_id`. Вопрос: переходить ли на одно широкое событие и по
|
||||
какому образцу его строить.
|
||||
|
||||
Образец выбран — Яндекс. В России Метрика и AppMetrica — основной источник
|
||||
кликстрима, и менти должен узнать формат, когда столкнётся с ним на работе.
|
||||
|
||||
Всё ниже — по документации Яндекса, ClickHouse, Snowplow, Segment и Amplitude.
|
||||
Где источник найти не удалось, это написано прямо.
|
||||
|
||||
## Коротко: ответы на главные вопросы
|
||||
|
||||
**Форма — плоское ядро с массивами, не вложенный JSON.** У Яндекса нет
|
||||
вложенных объектов вроде `context` или `properties`. Есть примерно 140 плоских
|
||||
колонок на хит, и всё, что бывает «много раз в одном событии» (цели, товары,
|
||||
покупки, свои параметры), лежит в **параллельных массивах**: `productID`,
|
||||
`productName`, `productPrice` — три отдельных массива одной длины, а не массив
|
||||
объектов. Именно эту форму стенду и стоит копировать.
|
||||
|
||||
**Доставка — батч для всех и поток для платных.** Обычный путь (Logs API) —
|
||||
батч: запрос готовится, потом скачивается TSV-файл. Данные за текущий день
|
||||
недоступны. В тарифе «Метрика Про» есть поток в управляемый ClickHouse с
|
||||
задержкой до 15 минут. Kafka в этой картине нет ни в одном официальном
|
||||
варианте.
|
||||
|
||||
**Kafka на стенде — учебная замена, и так это и надо называть.** Подробный
|
||||
разбор — в разделе «Батч или поток».
|
||||
|
||||
Главные находки:
|
||||
|
||||
1. Метрика отдаёт **две разные сущности**: хиты (`hits`, просмотры страниц) и
|
||||
визиты (`visits`, сессии). Визит — уже свёрнутая сессия с массивами внутри.
|
||||
2. **56 полей хитов — массивы** (по подсчёту на странице полей Logs API).
|
||||
Массивы — основной способ Яндекса выразить «много значений в одном событии».
|
||||
3. **User agent строкой Метрика не отдаёт.** Отдаются уже разобранные поля:
|
||||
`browser`, `browserMajorVersion`, `operatingSystem`, `deviceCategory`.
|
||||
Поле `browser_user_agent` на стенде — наша выдумка, у Яндекса аналога нет.
|
||||
4. **Координат в выгрузке Метрики нет.** Гео — это `regionCountry`,
|
||||
`regionCity` и числовые `regionCountryID`, `regionCityID`. Наши
|
||||
`geo_latitude` и `geo_longitude` тоже без аналога.
|
||||
5. **Метки времени: серверная одна.** В хитовой выгрузке в облако есть
|
||||
`UTCEventTime` и смещение часового пояса клиента `ClientTimeZone` (в
|
||||
минутах). Отдельной клиентской метки времени нет. Разделение «время у
|
||||
клиента» и «время приёма на сервере» есть у AppMetrica
|
||||
(`event_datetime` против `event_receive_datetime`), не у Метрики.
|
||||
6. **Поток даёт версии одной записи.** В потоковой выгрузке есть колонки `Sign`
|
||||
(Int8) и `HitVersion` / `VisitVersion`. Старая версия приходит со `Sign =
|
||||
-1`, новая с `Sign = 1`; складывать надо через `sum(Sign)`. Это ровно та же
|
||||
механика, что у движка `CollapsingMergeTree` в ClickHouse.
|
||||
7. **Ecommerce и выручка в выгрузке есть, но плоско.** Сумма заказа —
|
||||
`purchaseRevenue` типа `Array(Float64)`, валюта — `purchaseCurrency` типа
|
||||
`Array(String)`. Плюс отдельно лежит поле `ecommerce` типа `String` — сырой
|
||||
JSON события ecommerce.
|
||||
|
||||
## 1. Logs API Метрики: состав данных
|
||||
|
||||
Источники: [список полей — хиты](https://yandex.ru/dev/metrika/ru/logs/fields/hits),
|
||||
[список полей — визиты](https://yandex.ru/dev/metrika/ru/logs/fields/visits),
|
||||
[введение в Logs API](https://yandex.ru/dev/metrika/ru/logs/).
|
||||
|
||||
### Две сущности вместо одной
|
||||
|
||||
- **Хит (`hits`)** — одно действие: просмотр страницы, клик по ссылке, скачивание
|
||||
файла. Имена полей начинаются с `ym:pv:`.
|
||||
- **Визит (`visits`)** — сессия целиком. Имена полей начинаются с `ym:s:`.
|
||||
Внутри визита лежит массив идентификаторов его хитов: `ym:s:watchIDs` типа
|
||||
`Array(UInt64)`, не более 500 просмотров.
|
||||
|
||||
Это важно для стенда: у Яндекса «широкое событие» — это хит, а сессия —
|
||||
отдельная широкая запись, собранная Яндексом на своей стороне. У нас сессии
|
||||
считаются в DDS/DM; у Яндекса они приходят готовыми.
|
||||
|
||||
### Идентификаторы
|
||||
|
||||
| Поле | Тип | Что это |
|
||||
|---|---|---|
|
||||
| `ym:pv:watchID` | UInt64 | Идентификатор хита |
|
||||
| `ym:pv:pageViewID` | UInt32 | Идентификатор просмотра страницы |
|
||||
| `ym:pv:visitID` | UInt64 | Идентификатор визита (в хитах доступен с 10.10.2025) |
|
||||
| `ym:pv:counterID` | UInt32 | Номер счётчика (то есть сайта) |
|
||||
| `ym:pv:clientID` | UInt64 | Анонимный идентификатор посетителя через свою cookie |
|
||||
| `ym:pv:counterUserIDHash` | UInt64 | Идентификатор посетителя для подсчёта уникальных |
|
||||
| `ym:s:visitID`, `ym:s:clientID`, `ym:s:counterUserIDHash` | те же типы | То же на уровне визита |
|
||||
|
||||
Все идентификаторы — **числа**, не UUID.
|
||||
|
||||
Про `UserID` — свой идентификатор пользователя со стороны сайта. По справке
|
||||
Метрики он передаётся методом `setUserID` и связывается с `ClientID`, если
|
||||
метод вызвали во время сессии
|
||||
([setUserID](https://yandex.com/support/metrica/en/objects/set-user-id.html)).
|
||||
Поля `UserID` **в списке выгружаемых полей Logs API найти не удалось** — ни в
|
||||
хитах, ни в визитах. В выгрузке хитов в облако его тоже нет. Считаем: свой
|
||||
идентификатор пользователя в кликстриме Метрики напрямую не выдаётся.
|
||||
|
||||
Про рекламные метки:
|
||||
|
||||
- `ym:pv:hasGCLID` (UInt8) и `ym:pv:GCLID` (String) — метка клика Google.
|
||||
- `ym:pv:hasSBCLID` (UInt8) и `ym:pv:SBCLID` (String) — метка SBCLID.
|
||||
- **`YCLID` в списке полей Logs API не найден.** Он есть в примерном датасете
|
||||
ClickHouse как колонка `YCLID UInt64` (см. раздел 5) и упоминается в справке
|
||||
Метрики как идентификатор клика по объявлению Яндекс Директа. Вывод: в
|
||||
выгрузке Logs API поле `yclid` не подтверждено; в схеме ClickHouse оно есть.
|
||||
|
||||
### Метки времени и часовой пояс
|
||||
|
||||
| Поле | Тип | Что это |
|
||||
|---|---|---|
|
||||
| `ym:pv:date` | Date | Дата события |
|
||||
| `ym:pv:dateTime` | DateTime | Дата и время события **в часовом поясе счётчика** |
|
||||
| `ym:pv:clientTimeZone` | Int16 | Смещение часового пояса посетителя от UTC **в минутах** |
|
||||
| `ym:s:dateTime` | DateTime | Время визита в часовом поясе счётчика |
|
||||
| `ym:s:dateTimeUTC` | DateTime | Время визита в UTC+3 |
|
||||
|
||||
Две вещи, которые стоит запомнить:
|
||||
|
||||
- Часовой пояс — не UTC по умолчанию. У хита это «пояс счётчика», у визита есть
|
||||
и вторая колонка в UTC+3 (то есть по московскому времени, а не по нулевому
|
||||
меридиану).
|
||||
- Клиентское время события отдельным полем не приходит. Приходит только
|
||||
**смещение пояса клиента**. То есть «две метки времени, клиентская и
|
||||
серверная» — это про AppMetrica и про западные трекеры, но не про Метрику.
|
||||
|
||||
### Массивы
|
||||
|
||||
Массивов очень много. Страница полей хитов помечает **56 полей как массивы**.
|
||||
Основные группы:
|
||||
|
||||
- **Цели:** `ym:pv:goalsID` — `Array(UInt32)`. У визита целая группа
|
||||
параллельных массивов: `ym:s:goalsID`, `ym:s:goalsSerialNumber`,
|
||||
`ym:s:goalsDateTime`, `ym:s:goalsPrice`, `ym:s:goalsOrder`,
|
||||
`ym:s:goalsCurrency`.
|
||||
- **Свои параметры:** `ym:pv:parsedParamsKey1` … `parsedParamsKey10`, каждый —
|
||||
`Array(String)`. Это десять уровней вложенности, разложенные по десяти
|
||||
массивам. Плюс поле `ym:pv:params` типа `String` — исходный JSON параметров.
|
||||
- **Покупки:** `ym:pv:purchaseID` `Array(String)`, `ym:pv:purchaseRevenue`
|
||||
`Array(Float64)`, `ym:pv:purchaseCurrency` `Array(String)` и другие (в хитах
|
||||
доступны с 19.06.2025).
|
||||
- **Товары:** `ym:pv:productID`, `productName`, `productBrand`,
|
||||
`productCategory`, `productCategoryLevel1`…`Level5`, `productPrice`
|
||||
`Array(Int64)`, `productQuantity` `Array(UInt64)`, `productEventType`
|
||||
`Array(String)` со значениями `view_item_list`, `click`, `detail`, `add`,
|
||||
`purchase`, `remove`.
|
||||
- **Промоакции:** `ym:pv:promotionID`, `promotionName`, `promotionCreative`,
|
||||
`promotionEventType`.
|
||||
|
||||
У визита к этому добавляются ещё три больших блока массивов:
|
||||
`ym:s:purchasedProduct*` (что купили), `ym:s:impressions*` (что показали) и
|
||||
`ym:s:offlineCall*` (звонки).
|
||||
|
||||
Ключевое наблюдение: **это параллельные массивы, а не массив объектов**. Третий
|
||||
товар в событии — это третий элемент в каждом из массивов `productID`,
|
||||
`productName`, `productPrice`. Собирается такое в ClickHouse через `ARRAY JOIN`.
|
||||
|
||||
### Что ещё есть в хите (короткий список категорий)
|
||||
|
||||
- Страница: `ym:pv:URL`, `ym:pv:referer`, `ym:pv:title`, `ym:pv:pageCharset`.
|
||||
- Атрибуция: `UTMSource`, `UTMMedium`, `UTMCampaign`, `UTMContent`, `UTMTerm`;
|
||||
`lastTrafficSource`, `lastSearchEngineRoot`, `lastSearchEngine`,
|
||||
`lastAdvEngine`, `lastSocialNetwork`; четыре поля Openstat; `ym:pv:from`.
|
||||
- Браузер: `browser`, `browserMajorVersion`, `browserMinorVersion`,
|
||||
`browserEngine` и четыре части его версии, `browserLanguage`,
|
||||
`browserCountry`, `cookieEnabled`, `javascriptEnabled`.
|
||||
- Устройство и экран: `deviceCategory` (1 = десктоп, 2 = телефон, 3 = планшет,
|
||||
4 = TV), `mobilePhone`, `mobilePhoneModel`, `operatingSystem`,
|
||||
`operatingSystemRoot`, `screenWidth`, `screenHeight`, `physicalScreenWidth`,
|
||||
`physicalScreenHeight`, `windowClientWidth`, `windowClientHeight`,
|
||||
`screenColors`, `screenFormat`, `screenOrientation`.
|
||||
- Гео и сеть: `ipAddress`, `regionCountry` (код ISO), `regionCity` (название
|
||||
по-английски), `regionCountryID`, `regionCityID`.
|
||||
- Признаки: `isPageView`, `isTurboPage`, `iFrame`, `link`, `download`,
|
||||
`notBounce`, `artificial`, `httpError`.
|
||||
|
||||
### Ограничения выгрузки
|
||||
|
||||
Со страницы [введения в Logs API](https://yandex.ru/dev/metrika/ru/logs/):
|
||||
|
||||
- Данные за **текущий день недоступны** — они могут быть неполными.
|
||||
- Данные «доформировываются» примерно **до 3 дней**; запрашивать рекомендуют
|
||||
начиная с предыдущих дней.
|
||||
- Максимальный период одного запроса — **1 год**.
|
||||
- Параметр со списком полей — **не более 3000 символов**.
|
||||
- Общая квота на объём подготовленных логов — **10 ГБ**. Подготовленные файлы
|
||||
надо регулярно удалять, иначе квота кончится. Квоту расширяет тариф
|
||||
«Метрика Про».
|
||||
- Результат Logs API может расходиться с интерфейсом Метрики из-за разных
|
||||
алгоритмов обработки и округления чисел с плавающей точкой.
|
||||
|
||||
Ограничение «не более 1 ГБ на файл» встречается в поиске по документации
|
||||
Метрики, но дословно подтвердить его на официальной странице ограничений не
|
||||
удалось — **не подтверждено**.
|
||||
|
||||
Порядок работы (по официальным описаниям API):
|
||||
|
||||
1. `POST /management/v1/counter/{counterId}/logrequests` — создать запрос.
|
||||
2. `GET /management/v1/counter/{counterId}/logrequest/{requestId}` — проверить
|
||||
статус; статус `processed` означает, что лог готов.
|
||||
3. `GET .../logrequest/{requestId}/part/{partNumber}/download` — скачать
|
||||
часть.
|
||||
|
||||
Формат выгрузки — **TSV**. Дословно из официального блога Метрики: «Сырые
|
||||
данные передаются в стандартном формате tsv»
|
||||
([блог Метрики про Logs API](https://yandex.ru/blog/metrika/vygruzhayte-syrye-dannye-iz-metriki-cherez-logs-api)).
|
||||
Там же прямо сказано, что типовой приёмник таких данных — ClickHouse.
|
||||
|
||||
## 2. Батч или поток
|
||||
|
||||
Коротко: **у Метрики есть оба пути, но Kafka нет ни в одном.**
|
||||
|
||||
### Путь 1. Logs API — батч
|
||||
|
||||
Запрос готовится, потом скачивается TSV-файл частями. Текущий день недоступен.
|
||||
Это не поток ни в каком смысле: минимальная задержка — сутки.
|
||||
|
||||
### Путь 2. Data Streaming в Yandex Cloud — поток, но не Kafka
|
||||
|
||||
Источник: [Data Streaming (интеграция с Yandex
|
||||
Cloud)](https://yandex.ru/support/metrica/ru/uploading-data/cloud), [как
|
||||
работать с данными](https://yandex.ru/support/metrica/ru/pro/data-work),
|
||||
[учебник Yandex
|
||||
Cloud](https://yandex.cloud/ru/docs/tutorials/dataplatform/metrika-to-clickhouse).
|
||||
|
||||
Что там есть:
|
||||
|
||||
- Неагрегированные данные Метрики попадают в **свой управляемый
|
||||
ClickHouse-кластер** в Yandex Cloud.
|
||||
- Задержка от события до записи в ClickHouse — **до 15 минут**.
|
||||
- Перенос делает **Yandex Data Transfer**, тип трансфера — «Репликация».
|
||||
- Хиты и визиты переносятся отдельными таблицами.
|
||||
- Историю до создания коннектора эта версия не переносит. Если трансфер
|
||||
выключить и включить, данные за простой потеряются.
|
||||
- Нужен тариф **«Метрика Про»**.
|
||||
- Визит меняется по мере поступления новых событий, поэтому в выгрузке лежат
|
||||
**разные версии одного визита**. Разбираются они через `Sign`: старая версия
|
||||
приходит со `Sign = -1`, новая со `Sign = 1`, считать надо через `sum(Sign)`
|
||||
в `GROUP BY`. Можно использовать модификатор `FINAL`, но он медленнее.
|
||||
|
||||
Приёмник здесь — ClickHouse напрямую. **Экспорт кликстрима Метрики в Object
|
||||
Storage или в Yandex Data Streams (сервис с Kafka-совместимым интерфейсом)
|
||||
официальной документацией не подтверждён.**
|
||||
|
||||
### Путь 3. AppMetrica Data Stream — пятиминутные окна
|
||||
|
||||
Источник: [Data Stream API,
|
||||
описание](https://appmetrica.yandex.ru/docs/ru/mobile-api/datastream/about).
|
||||
Поток представлен последовательностью **пятиминутных окон**, каждое окно
|
||||
скачивается запросом. Задержка — не меньше 10 минут. Хранение — 7 дней.
|
||||
Формат — CSV (RFC 4180) или JSON.
|
||||
|
||||
### Честно ли рассказывать про Kafka
|
||||
|
||||
Да, если называть вещи своими именами. Формулировка для курса:
|
||||
|
||||
> Kafka на стенде — учебная замена реальному транспорту. Основной путь выгрузки
|
||||
> у Яндекс Метрики — батч: Logs API отдаёт TSV-файл, и данных за сегодня в нём
|
||||
> нет. Поток у Метрики есть только в платном тарифе «Метрика Про» и идёт не
|
||||
> через Kafka, а через Yandex Data Transfer прямо в управляемый ClickHouse, с
|
||||
> задержкой до 15 минут. Kafka в этой схеме не участвует.
|
||||
|
||||
Почему Kafka всё-таки уместна на стенде:
|
||||
|
||||
- Задачи, которые она ставит перед менти, реальные: чтение из потока,
|
||||
контроль смещений, дубли, опоздавшие события, обратное давление. Такие задачи
|
||||
есть в любом продуктовом дата-контуре — просто перед Kafka там стоит свой
|
||||
коллектор, а не Метрика.
|
||||
- Механика «поток + версии одной записи» у Метрики Про (`Sign`, `HitVersion`,
|
||||
`VisitVersion`) ближе к потоковому приёму, чем к «скачал файл раз в сутки».
|
||||
Разговор про `ReplacingMergeTree` и `CollapsingMergeTree`, который стенд уже
|
||||
ведёт, попадает в реальную практику точно.
|
||||
|
||||
Чего делать нельзя: говорить менти «Яндекс отдаёт кликстрим в Kafka». Это
|
||||
неправда.
|
||||
|
||||
## 3. AppMetrica
|
||||
|
||||
Источник: [ресурсы Logs
|
||||
API](https://appmetrica.yandex.ru/docs/ru/mobile-api/logs/endpoints).
|
||||
|
||||
Главное отличие от Метрики: **AppMetrica отдаёт много узких таблиц вместо двух
|
||||
широких.** Каждый вид данных — свой эндпоинт: `events`, `installations`,
|
||||
`sessions_starts`, `ecommerce_events`, `revenue_events`, `ad_revenue_events`,
|
||||
`crashes`, `errors`, `clicks`, `postbacks`, `deeplinks`, `push_tokens`,
|
||||
`profiles_v2`. Формат — CSV или JSON.
|
||||
|
||||
Поле-строка с JSON **есть**: `event_json` — «атрибуты, сериализованные в JSON».
|
||||
То есть у мобильного трекера Яндекса произвольные свойства события лежат
|
||||
единым JSON в одной колонке — не так, как в Метрике с её массивами.
|
||||
|
||||
Метки времени у события — четыре, и это ровно то разделение, которого не
|
||||
хватает Метрике:
|
||||
|
||||
| Поле | Что это |
|
||||
|---|---|
|
||||
| `event_datetime` | Время события, `yyyy-mm-dd hh:mm:ss` |
|
||||
| `event_timestamp` | То же в unix-времени |
|
||||
| `event_receive_datetime` | Время приёма на сервере (расходится с `event_datetime` из-за сети) |
|
||||
| `event_receive_timestamp` | То же в unix-времени |
|
||||
|
||||
Идентификаторы: `appmetrica_device_id`, `installation_id`, `session_id`,
|
||||
`profile_id`. Устройство и гео: `device_manufacturer`, `device_model`,
|
||||
`device_type`, `os_name`, `os_version`, `city`, `country_iso_code`,
|
||||
`google_aid`, `ios_ifa`, `ios_ifv`.
|
||||
|
||||
Потоковые возможности — Data Stream API (см. выше): пятиминутные окна,
|
||||
задержка от 10 минут, хранение 7 дней, до 50 000 запросов в сутки против
|
||||
5 000 в сутки у Logs API.
|
||||
|
||||
## 4. Ecommerce и выручка
|
||||
|
||||
### Как это описывается на сайте
|
||||
|
||||
Источник: [передача данных
|
||||
ecommerce](https://yandex.ru/support/metrica/ru/ecommerce/data). Магазин кладёт
|
||||
в `dataLayer` объект такой формы:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"ecommerce": {
|
||||
"currencyCode": "RUB",
|
||||
"purchase": {
|
||||
"actionField": { "id": "TRX987", "revenue": 12300, "coupon": "SALE10" },
|
||||
"products": [
|
||||
{ "id": "SKU-1", "name": "Кружка", "price": 4100,
|
||||
"brand": "Acme", "category": "Посуда/Кружки",
|
||||
"variant": "белая", "quantity": 3 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Виды действий: `impressions`, `click`, `detail`, `add`, `remove`, `purchase`,
|
||||
`promoView`, `promoClick`. У покупки `actionField.id` обязателен; `revenue`
|
||||
считается автоматически, если его не передать. У товара обязателен `id` или
|
||||
`name`; остальное — `price`, `quantity`, `brand`, `category`, `variant`,
|
||||
`coupon`, `list`, `position`, `discount`.
|
||||
|
||||
### Что из этого попадает в выгрузку
|
||||
|
||||
**Не JSON, а плоские массивы.** Один заказ на 3 товара превращается не в
|
||||
вложенный объект, а в набор массивов одинаковой длины:
|
||||
|
||||
| Поле выгрузки | Тип | Что это |
|
||||
|---|---|---|
|
||||
| `ym:pv:purchaseID` | Array(String) | Идентификатор покупки |
|
||||
| `ym:pv:purchaseRevenue` | Array(Float64) | **Сумма заказа** |
|
||||
| `ym:pv:purchaseCurrency` | Array(String) | Валюта |
|
||||
| `ym:pv:purchaseCoupon` | Array(String) | Промокод на весь заказ |
|
||||
| `ym:pv:purchaseTax`, `purchaseShipping` | Array(String) | Налоги, доставка |
|
||||
| `ym:pv:purchaseProductQuantity` | Array(UInt64) | Число товаров в покупке |
|
||||
| `ym:pv:productID`, `productName`, `productBrand`, `productCategory` | Array(String) | Товар |
|
||||
| `ym:pv:productPrice` | Array(Int64) | Цена товара |
|
||||
| `ym:pv:productQuantity` | Array(UInt64) | Количество |
|
||||
| `ym:pv:productEventType` | Array(String) | `view_item_list`, `click`, `detail`, `add`, `purchase`, `remove` |
|
||||
| `ym:pv:ecommerce` | String | Сырое событие ecommerce (одна строка) |
|
||||
|
||||
Обратите внимание на две вещи. Первая: `purchaseTax` и `purchaseShipping` в
|
||||
хитах имеют тип `Array(String)`, хотя это денежные суммы, — так в документации.
|
||||
Вторая: рядом с разобранными массивами лежит поле `ym:pv:ecommerce` типа
|
||||
`String`. То есть Яндекс отдаёт и разобранное, и сырое.
|
||||
|
||||
На уровне визита к этому добавляются `ym:s:purchaseDateTime`
|
||||
`Array(DateTime)`, `ym:s:purchaseAffiliation`, весь блок
|
||||
`ym:s:purchasedProduct*` (около 20 массивов про купленные товары) и
|
||||
`ym:s:impressions*` (около 18 массивов про показы).
|
||||
|
||||
## 5. Как это кладут в ClickHouse
|
||||
|
||||
Здесь два первоисточника, и оба полезны.
|
||||
|
||||
### 5.1. Примерный датасет в документации ClickHouse
|
||||
|
||||
У ClickHouse корни в Метрике, и в его документации до сих пор лежит
|
||||
обезличенный датасет Метрики: `hits_v1` (8 873 898 строк) и `visits_v1`
|
||||
(1 680 609 строк). Источники:
|
||||
[страница датасета](https://clickhouse.com/docs/getting-started/example-datasets/metrica),
|
||||
DDL получен через MCP Context7 (`/clickhouse/clickhouse-docs`, файл
|
||||
`docs/getting-started/example-datasets/anon_web_analytics_metrica.md`).
|
||||
|
||||
`hits_v1` — примерно 130 плоских колонок:
|
||||
|
||||
```sql
|
||||
CREATE TABLE datasets.hits_v1
|
||||
(
|
||||
WatchID UInt64, JavaEnable UInt8, Title String, GoodEvent Int16,
|
||||
EventTime DateTime, EventDate Date, CounterID UInt32,
|
||||
ClientIP UInt32, ClientIP6 FixedString(16), RegionID UInt32,
|
||||
UserID UInt64, CounterClass Int8, OS UInt8, UserAgent UInt8,
|
||||
URL String, Referer String, URLDomain String, RefererDomain String,
|
||||
IsRobot UInt8, RefererCategories Array(UInt16), URLRegions Array(UInt32),
|
||||
ResolutionWidth UInt16, ResolutionHeight UInt16,
|
||||
ClientTimeZone Int16, ClientEventTime DateTime, UTCEventTime DateTime,
|
||||
Params String, GoalsReached Array(UInt32),
|
||||
UTMSource String, UTMMedium String, UTMCampaign String,
|
||||
UTMContent String, UTMTerm String, FromTag String,
|
||||
HasGCLID UInt8, RefererHash UInt64, URLHash UInt64,
|
||||
CLID UInt32, YCLID UInt64,
|
||||
ParsedParams Nested(Key1 String, Key2 String, Key3 String,
|
||||
Key4 String, Key5 String, ValueDouble Float64),
|
||||
...
|
||||
)
|
||||
ENGINE = MergeTree()
|
||||
PARTITION BY toYYYYMM(EventDate)
|
||||
ORDER BY (CounterID, EventDate, intHash32(UserID))
|
||||
SAMPLE BY intHash32(UserID)
|
||||
```
|
||||
|
||||
Что тут стоит заметить:
|
||||
|
||||
- **Три метки времени:** `EventTime`, `ClientEventTime`, `UTCEventTime`. Плюс
|
||||
`ClientTimeZone`. Во внутренней схеме разделение клиента и сервера есть, хотя
|
||||
в публичную выгрузку хитов оно не попадает.
|
||||
- **Ключ сортировки** — `(CounterID, EventDate, intHash32(UserID))`: сначала
|
||||
сайт, потом дата, потом пользователь. Не идентификатор события. Это ключ под
|
||||
типовые запросы «по сайту за период», а не под точечный поиск.
|
||||
- **Ключ семплирования** `intHash32(UserID)` — чтобы считать по 10% данных и
|
||||
получать корректные метрики по пользователям.
|
||||
- **Nested** вместо параллельных массивов: `ParsedParams Nested(Key1 String,
|
||||
…, ValueDouble Float64)`. В ClickHouse `Nested` — это синтаксический сахар
|
||||
над теми же параллельными массивами: колонка `ParsedParams.Key1` физически и
|
||||
есть `Array(String)`.
|
||||
- Есть `YCLID UInt64` и `CLID UInt32` — метки Яндекс Директа.
|
||||
- Есть хеши (`URLHash`, `RefererHash`, `NormalizedRefererHash`) — внутренняя
|
||||
оптимизация Яндекса под быстрые сравнения.
|
||||
|
||||
`visits_v1` — около 190 колонок и другой движок:
|
||||
|
||||
```sql
|
||||
CREATE TABLE datasets.visits_v1
|
||||
(
|
||||
CounterID UInt32, StartDate Date, Sign Int8, IsNew UInt8,
|
||||
VisitID UInt64, UserID UInt64, StartTime DateTime, Duration UInt32,
|
||||
UTCStartTime DateTime, PageViews Int32, Hits Int32, IsBounce UInt8,
|
||||
...
|
||||
Goals Nested(ID UInt32, Serial UInt32, EventTime DateTime,
|
||||
Price Int64, OrderID String, CurrencyID UInt32),
|
||||
WatchIDs Array(UInt64),
|
||||
TraficSource Nested(ID Int8, SearchEngineID UInt16, AdvEngineID UInt8,
|
||||
PlaceID UInt16, SocialSourceNetworkID UInt8,
|
||||
Domain String, SearchPhrase String,
|
||||
SocialSourcePage String),
|
||||
ParsedParams Nested(Key1 String, ..., ValueDouble Float64),
|
||||
Market Nested(Type UInt8, GoalID UInt32, OrderID String,
|
||||
OrderPrice Int64, PP UInt32, ...,
|
||||
GoodID String, GoodName String,
|
||||
GoodQuantity Int32, GoodPrice Int64),
|
||||
...
|
||||
)
|
||||
ENGINE = CollapsingMergeTree(Sign)
|
||||
PARTITION BY toYYYYMM(StartDate)
|
||||
ORDER BY (CounterID, StartDate, intHash32(UserID), VisitID)
|
||||
SAMPLE BY intHash32(UserID)
|
||||
```
|
||||
|
||||
Здесь `CollapsingMergeTree(Sign)` — тот самый механизм версий визита, о котором
|
||||
пишет справка Метрики Про. Ecommerce лежит в `Nested`-блоке `Market` с полями
|
||||
`GoodID`, `GoodName`, `GoodQuantity`, `GoodPrice`.
|
||||
|
||||
### 5.2. Официальные схемы потоковой выгрузки
|
||||
|
||||
Списки колонок для выгрузки в свой ClickHouse: [поля
|
||||
хитов](https://yandex.ru/support/metrica/ru/pro/hits), [поля
|
||||
визитов](https://yandex.ru/support/metrica/ru/pro/visits).
|
||||
|
||||
Существенное: **в облачной выгрузке имена колонок не в стиле API, а в стиле
|
||||
ClickHouse.** Не `ym:s:visitID`, а `VisitID`. Не `ym:pv:watchID`, а `WatchID`.
|
||||
Префиксы `ym:pv:` и `ym:s:` — это про язык API, в таблице их нет.
|
||||
|
||||
Колонки хитов, которые видит инженер:
|
||||
|
||||
| Колонка | Тип |
|
||||
|---|---|
|
||||
| `WatchID` | UInt64 |
|
||||
| `pageViewID` | UInt32 |
|
||||
| `VisitID` | UInt64 (с 10.10.2025) |
|
||||
| `CounterID` | UInt32 |
|
||||
| `ClientID` | UInt64 |
|
||||
| `CounterUserIDHash` | UInt64 |
|
||||
| `EventDate` | Date |
|
||||
| `UTCEventTime` | DateTime |
|
||||
| `ClientTimeZone` | Int16 |
|
||||
| `Sign` | Int8 — признак статуса записи в инкрементальном логе |
|
||||
| `HitVersion` | UInt32 |
|
||||
| `URL`, `Title` | String |
|
||||
| `GoalsReached` | Array(UInt32) |
|
||||
| `ecommerce` | String |
|
||||
|
||||
Колонок в хитовой выгрузке около 140, в визитной — около 200. В визитной есть
|
||||
`Sign` (Int8) и `VisitVersion` (UInt32).
|
||||
|
||||
Прицельная проверка по хитовой выгрузке: колонок `EventTime`,
|
||||
`ClientEventTime`, `LocalEventTime`, `UserID` и `UserIDHash` там **нет**.
|
||||
Единственная метка времени — `UTCEventTime`, а часовой пояс клиента — отдельным
|
||||
числом `ClientTimeZone`.
|
||||
|
||||
## 6. Сравнение с западными трекерами
|
||||
|
||||
**Snowplow — плоское ядро.** Обогащённое событие — строка TSV из 131 колонки.
|
||||
Таблица `atomic.events` описана как «широкая», и «отдельные поля хранятся в
|
||||
своих колонках». Самоописываемые события и сущности в BigQuery, Snowflake и
|
||||
Databricks добавляются как дополнительные колонки той же таблицы; в Redshift —
|
||||
отдельными таблицами со связью один-к-одному по `event_id`. Источник:
|
||||
[введение в таблицу atomic
|
||||
events](https://docs.snowplow.io/docs/fundamentals/warehouse-tables/).
|
||||
|
||||
**Segment — вложенный JSON.** Событие `track` — это JSON, где `properties` и
|
||||
`context` — вложенные объекты. Верхний уровень: `anonymousId`, `userId`,
|
||||
`event`, `properties`, `context`, `type`, `messageId`, `timestamp`,
|
||||
`originalTimestamp`, `sentAt`, `receivedAt`, `integrations`. Источник:
|
||||
[Spec: Track](https://www.twilio.com/docs/segment/connections/spec/track).
|
||||
|
||||
**Amplitude — вложенный JSON.** В Export API `event_properties`,
|
||||
`user_properties`, `group_properties`, `groups`, `data` — словари. Метки
|
||||
времени: `client_event_time`, `client_upload_time`, `event_time`,
|
||||
`server_upload_time`, `server_received_time`, `processed_time`. Источник:
|
||||
[Export API](https://amplitude.com/docs/apis/analytics/export).
|
||||
|
||||
**Итог сравнения.** Яндекс ближе к Snowplow: плоское широкое ядро, всё
|
||||
переменное — в массивах. Разница в том, что Snowplow добавляет колонки под
|
||||
каждую схему события, а Яндекс держит фиксированный набор массивов
|
||||
(`parsedParamsKey1..10`, `product*`, `purchase*`) и отдельное сырое поле
|
||||
(`params`, `ecommerce`). До Segment и Amplitude, где `properties` — свободный
|
||||
JSON-объект, Яндексу далеко.
|
||||
|
||||
## Вывод: что копировать стенду
|
||||
|
||||
### Форма
|
||||
|
||||
**Плоское широкое событие с массивами.** Одно событие = одна строка. Внутри —
|
||||
плоские колонки. Всё, что бывает «много раз внутри одного события», — набор
|
||||
параллельных массивов одной длины (или `Nested`, что в ClickHouse то же самое).
|
||||
Свободного вложенного JSON вроде `context` или `properties` не делать: в РФ
|
||||
менти его не встретит.
|
||||
|
||||
Стенду стоит воспроизводить именно **хит** — одно действие с полным контекстом.
|
||||
Визит оставить тем, чем он сейчас является: результатом сборки в DDS/DM. Так
|
||||
менти сам делает то, что Метрика делает за него, и понимает, откуда берётся
|
||||
`visitDuration` и `pageViews`.
|
||||
|
||||
Отдельно рекомендуется добавить **одно сырое поле-строку с JSON** — как
|
||||
`ym:pv:ecommerce` у Метрики и `event_json` у AppMetrica. Это даёт честное
|
||||
упражнение «разобрать JSON внутри колонки», которое в бою встречается постоянно.
|
||||
|
||||
### Список полей для широкого события стенда
|
||||
|
||||
Колонка «есть» — про текущий стенд (`sql/ddl/ods/20_ods.sql`,
|
||||
`sql/ddl/dds/30_dds.sql`). «нет» означает: поля у нас сейчас нет.
|
||||
|
||||
| № | Имя | Тип | Откуда взято | Есть у нас |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `WatchID` | UInt64 | `ym:pv:watchID`, `hits_v1.WatchID` | ~ (есть `event_id` UUID, тип другой) |
|
||||
| 2 | `VisitID` | UInt64 | `ym:pv:visitID`, `visits_v1.VisitID` | ~ (есть `click_id` UUID, тип другой) |
|
||||
| 3 | `pageViewID` | UInt32 | `ym:pv:pageViewID` | **нет** |
|
||||
| 4 | `CounterID` | UInt32 | `ym:pv:counterID` | **нет** |
|
||||
| 5 | `ClientID` | UInt64 | `ym:pv:clientID` — анонимный id браузера | **нет** ← важное |
|
||||
| 6 | `CounterUserIDHash` | UInt64 | `ym:pv:counterUserIDHash` | **нет** |
|
||||
| 7 | `UTCEventTime` | DateTime | `Метрика Про, хиты` | ~ (есть `event_ts`, одна метка) |
|
||||
| 8 | `EventDate` | Date | `Метрика Про, хиты` | есть (`event_date`) |
|
||||
| 9 | `ClientTimeZone` | Int16 (минуты) | `ym:pv:clientTimeZone` | **нет** ← важное |
|
||||
| 10 | `ClientEventTime` | DateTime | `hits_v1.ClientEventTime`; у AppMetrica — `event_datetime` против `event_receive_datetime` | **нет** ← важное |
|
||||
| 11 | `URL` | String | `ym:pv:URL` | есть (`page_url`) |
|
||||
| 12 | `Title` | String | `ym:pv:title` | **нет** |
|
||||
| 13 | `Referer` | String | `ym:pv:referer` | есть (`referer_url`) |
|
||||
| 14 | `UTMSource` | String | `ym:pv:UTMSource` | есть |
|
||||
| 15 | `UTMMedium` | String | `ym:pv:UTMMedium` | есть |
|
||||
| 16 | `UTMCampaign` | String | `ym:pv:UTMCampaign` | есть |
|
||||
| 17 | `UTMContent` | String | `ym:pv:UTMContent` | есть |
|
||||
| 18 | `UTMTerm` | String | `ym:pv:UTMTerm` | **нет** |
|
||||
| 19 | `LastTrafficSource` | String | `ym:pv:lastTrafficSource` | ~ (есть `referer_medium`, смысл близкий) |
|
||||
| 20 | `LastSearchEngineRoot` | String | `ym:pv:lastSearchEngineRoot` | **нет** |
|
||||
| 21 | `HasGCLID` | UInt8 | `ym:pv:hasGCLID` | **нет** |
|
||||
| 22 | `YCLID` | UInt64 | `hits_v1.YCLID` (в Logs API не найден) | **нет** |
|
||||
| 23 | `Browser` | String | `ym:pv:browser` | есть (`browser_name`) |
|
||||
| 24 | `BrowserMajorVersion` | UInt16 | `ym:pv:browserMajorVersion` | **нет** |
|
||||
| 25 | `BrowserLanguage` | String | `ym:pv:browserLanguage` | есть (`browser_language`) |
|
||||
| 26 | `OperatingSystem` | String | `ym:pv:operatingSystem` | есть (`os`) |
|
||||
| 27 | `OperatingSystemRoot` | String | `ym:pv:operatingSystemRoot` | есть (`os_name`) |
|
||||
| 28 | `DeviceCategory` | String (1..4) | `ym:pv:deviceCategory` | есть (`device_type`) |
|
||||
| 29 | `MobilePhoneModel` | String | `ym:pv:mobilePhoneModel` | **нет** |
|
||||
| 30 | `ScreenWidth`, `ScreenHeight` | UInt16 | `ym:pv:screenWidth/Height` | **нет** |
|
||||
| 31 | `IPAddress` | String | `ym:pv:ipAddress` | есть (`ip_address`) |
|
||||
| 32 | `RegionCountry` | String (ISO) | `ym:pv:regionCountry` | есть (`geo_country`) |
|
||||
| 33 | `RegionCity` | String | `ym:pv:regionCity` | ~ (есть `geo_region_name`) |
|
||||
| 34 | `RegionCountryID`, `RegionCityID` | UInt32 | `ym:pv:regionCountryID/CityID` | **нет** ← важное |
|
||||
| 35 | `IsPageView` | UInt8 | `ym:pv:isPageView` | **нет** |
|
||||
| 36 | `NotBounce` | UInt8 | `ym:pv:notBounce` | **нет** |
|
||||
| 37 | `HTTPError` | String | `ym:pv:httpError` | **нет** |
|
||||
| 38 | `GoalsReached` | Array(UInt32) | `ym:pv:goalsID`, `hits_v1.GoalsReached` | **нет** ← ключевое |
|
||||
| 39 | `ParsedParams` | Nested(Key1..Key5 String, ValueDouble Float64) | `hits_v1.ParsedParams`; в Logs API — `parsedParamsKey1..10` | **нет** ← ключевое |
|
||||
| 40 | `Params` | String (сырой JSON) | `ym:pv:params` | **нет** |
|
||||
| 41 | `ecommerce` | String (сырой JSON) | `ym:pv:ecommerce`; у AppMetrica — `event_json` | **нет** ← ключевое |
|
||||
| 42 | `purchaseID` | Array(String) | `ym:pv:purchaseID` | **нет** ← ключевое |
|
||||
| 43 | `purchaseRevenue` | Array(Float64) | `ym:pv:purchaseRevenue` — **выручка** | **нет** ← ключевое |
|
||||
| 44 | `purchaseCurrency` | Array(String) | `ym:pv:purchaseCurrency` | **нет** |
|
||||
| 45 | `purchaseCoupon` | Array(String) | `ym:pv:purchaseCoupon` | **нет** |
|
||||
| 46 | `productID` | Array(String) | `ym:pv:productID` | **нет** ← ключевое |
|
||||
| 47 | `productName` | Array(String) | `ym:pv:productName` | **нет** |
|
||||
| 48 | `productCategory` | Array(String) | `ym:pv:productCategory` | **нет** |
|
||||
| 49 | `productPrice` | Array(Int64) | `ym:pv:productPrice` | **нет** ← ключевое |
|
||||
| 50 | `productQuantity` | Array(UInt64) | `ym:pv:productQuantity` | **нет** ← ключевое |
|
||||
| 51 | `productEventType` | Array(String) | `ym:pv:productEventType`: `detail`, `add`, `remove`, `purchase`… | **нет** ← ключевое |
|
||||
| 52 | `Sign` | Int8 | Метрика Про, хиты и визиты | **нет** ← ключевое |
|
||||
| 53 | `HitVersion` | UInt32 | Метрика Про, хиты | **нет** |
|
||||
|
||||
Поля 38–53 — то, чего стенду не хватает сильнее всего: цели, свои параметры,
|
||||
ecommerce с выручкой и механика версий записи. Ровно про это спрашивают на
|
||||
работе в первую очередь, и ровно этого сейчас у нас нет
|
||||
(см. `docs/generator-realism.md`, раздел «Что упрощено»).
|
||||
|
||||
Ключ сортировки для широкой таблицы стенда стоит взять по образцу Метрики:
|
||||
`ORDER BY (CounterID, EventDate, intHash32(ClientID))`, а не по идентификатору
|
||||
события. Это заодно повод объяснить менти, зачем ключ сортировки строится под
|
||||
запросы, а не под уникальность.
|
||||
|
||||
### Чего воспроизводить не стоит
|
||||
|
||||
1. **Полный набор полей.** 140 колонок в хитах и 200 в визитах — это шум.
|
||||
Учебной ценности в 56 массивах нет никакой, а поддерживать их дорого.
|
||||
Хватит 40–50 колонок из таблицы выше.
|
||||
2. **Отдельную сущность «визит» из Яндекса.** Если брать и хиты, и визиты,
|
||||
стенд потеряет главное упражнение — сборку сессий своими руками. Берём
|
||||
только хиты.
|
||||
3. **Полную механику `CollapsingMergeTree` с пересчётом визита.** Колонку
|
||||
`Sign` добавить полезно — это узнаваемо и объясняет `sum(Sign)`. А вот
|
||||
пересчитывать визит и присылать по нему пять версий — сложность, которая
|
||||
съест урок целиком. Достаточно показать `Sign` на хитах.
|
||||
4. **Устаревшие технологии из датасета ClickHouse.** `FlashMajor`,
|
||||
`SilverlightVersion1..4`, `NetMajor` — следы 2013 года. Живой аналог
|
||||
сегодня им не соответствует, копировать нечего.
|
||||
5. **Хеши** (`URLHash`, `RefererHash`, `NormalizedStartURLHash`). Это
|
||||
внутренняя оптимизация Яндекса. Менти без контекста примет их за содержимое.
|
||||
6. **Социально-демографические поля** (`Age`, `Sex`, `Income`, `Interests`,
|
||||
`GeneralInterests`, `Robotness`). Это внутренние оценки Яндекса, вне Яндекса
|
||||
их не получить. Ставить их в генератор — учить менти работать с данными,
|
||||
которых у него не будет.
|
||||
7. **Openstat и поля Яндекс Директа** (`openstatAd`, `openstatCampaign`,
|
||||
`DirectClickOrder`, `DirectBannerGroup`, `DirectPhraseOrCond` и десяток
|
||||
соседних). Openstat — устаревший стандарт метки трафика. Поля Директа
|
||||
осмысленны только при связке аккаунтов. Ставим одну метку `YCLID` и одну
|
||||
`HasGCLID` — этого хватит для разговора об атрибуции.
|
||||
8. **Пять уровней категорий товара** (`productCategoryLevel1..5`) и блоки
|
||||
`purchasedProduct*` (~20 массивов), `impressions*` (~18 массивов),
|
||||
`promotion*`, `offlineCall*`. Механика та же, что у `product*`; повторять её
|
||||
четыре раза — только объём.
|
||||
9. **Префиксы `ym:pv:` и `ym:s:` в именах колонок.** Это язык API, а не имена
|
||||
в базе. Сам Яндекс в облачной выгрузке их не использует.
|
||||
10. **Смену UUID на UInt64 у наших `event_id` и `click_id`.** У Яндекса
|
||||
идентификаторы числовые, но переделывать под это весь стенд — работа без
|
||||
учебной отдачи. Достаточно добавить `ClientID` типа UInt64 — это самое
|
||||
узнаваемое поле Метрики, и его отсутствие у нас заметнее всего.
|
||||
11. **`browser_user_agent` как «поле Метрики».** У Яндекса строки user agent в
|
||||
выгрузке нет — отдаются уже разобранные поля. Само поле на стенде оставить
|
||||
можно (разбор user agent — реальная задача), но нельзя выдавать его за
|
||||
формат Яндекса.
|
||||
12. **`geo_latitude` и `geo_longitude` как «поля Метрики».** Координат в
|
||||
выгрузке Метрики нет. Если они нужны для карт в Superset, надо честно
|
||||
сказать, что это наша добавка.
|
||||
|
||||
## Что осталось неподтверждённым
|
||||
|
||||
- Поле `UserID` (свой идентификатор пользователя со стороны сайта) в списках
|
||||
выгружаемых полей Logs API и в облачной выгрузке хитов найти не удалось.
|
||||
- Поле `yclid` в Logs API не найдено. Колонка `YCLID UInt64` есть в примерном
|
||||
датасете ClickHouse.
|
||||
- Ограничение «не более 1 ГБ на один файл выгрузки» встречается в поиске по
|
||||
документации Метрики, но дословно на официальной странице не подтверждено.
|
||||
- Сколько времени хранится подготовленный лог до удаления — на изученных
|
||||
страницах не сказано; сказано только, что удалять их надо самому, иначе
|
||||
кончится квота 10 ГБ.
|
||||
- Экспорт кликстрима Метрики в Yandex Object Storage или в Yandex Data Streams
|
||||
(сервис с Kafka-совместимым интерфейсом) официальной документацией не
|
||||
подтверждён. Единственный подтверждённый потоковый приёмник — управляемый
|
||||
ClickHouse через Yandex Data Transfer.
|
||||
- Точное число колонок в облачной выгрузке (около 140 для хитов, около 200 для
|
||||
визитов) — оценка по объёму страниц, а не цифра из документации.
|
||||
|
||||
## Источники
|
||||
|
||||
Яндекс Метрика, Logs API:
|
||||
|
||||
- [Введение в Logs API](https://yandex.ru/dev/metrika/ru/logs/)
|
||||
- [Поля хитов](https://yandex.ru/dev/metrika/ru/logs/fields/hits)
|
||||
- [Поля визитов](https://yandex.ru/dev/metrika/ru/logs/fields/visits)
|
||||
- [Блог Метрики: выгружайте сырые данные через Logs API](https://yandex.ru/blog/metrika/vygruzhayte-syrye-dannye-iz-metriki-cherez-logs-api)
|
||||
- [setUserID](https://yandex.com/support/metrica/en/objects/set-user-id.html)
|
||||
|
||||
Яндекс Метрика, выгрузка в облако:
|
||||
|
||||
- [Data Streaming (интеграция с Yandex Cloud)](https://yandex.ru/support/metrica/ru/uploading-data/cloud)
|
||||
- [Метрика Про: как работать с данными](https://yandex.ru/support/metrica/ru/pro/data-work)
|
||||
- [Метрика Про: поля хитов](https://yandex.ru/support/metrica/ru/pro/hits)
|
||||
- [Метрика Про: поля визитов](https://yandex.ru/support/metrica/ru/pro/visits)
|
||||
- [Yandex Cloud: репликация данных Метрики в ClickHouse](https://yandex.cloud/ru/docs/tutorials/dataplatform/metrika-to-clickhouse)
|
||||
|
||||
Ecommerce:
|
||||
|
||||
- [Передача данных ecommerce](https://yandex.ru/support/metrica/ru/ecommerce/data)
|
||||
|
||||
AppMetrica:
|
||||
|
||||
- [Logs API: ресурсы и поля](https://appmetrica.yandex.ru/docs/ru/mobile-api/logs/endpoints)
|
||||
- [Data Stream API: описание](https://appmetrica.yandex.ru/docs/ru/mobile-api/datastream/about)
|
||||
|
||||
ClickHouse:
|
||||
|
||||
- [Примерный датасет Метрики](https://clickhouse.com/docs/getting-started/example-datasets/metrica)
|
||||
- DDL таблиц `hits_v1` и `visits_v1` получен через MCP Context7
|
||||
(`/clickhouse/clickhouse-docs`, файл
|
||||
`docs/getting-started/example-datasets/anon_web_analytics_metrica.md`)
|
||||
|
||||
Западные трекеры:
|
||||
|
||||
- [Snowplow: введение в таблицу atomic events](https://docs.snowplow.io/docs/fundamentals/warehouse-tables/)
|
||||
- [Segment Spec: Track](https://www.twilio.com/docs/segment/connections/spec/track)
|
||||
- [Amplitude Export API](https://amplitude.com/docs/apis/analytics/export)
|
||||
@@ -0,0 +1,585 @@
|
||||
# Боевой реализм стенда (v2): широкое событие, заказы, кластер, анонимность
|
||||
|
||||
Статус: Accepted (2026-07-30). Три помеченных отступления подтверждены
|
||||
владельцем на приёмке: порядок страховочных срезов (раздел 9), `Sign` как
|
||||
колонка без механики (раздел 1.1), `VisitID` как эталон самопроверки (1.2).
|
||||
Дата: 2026-07-30. Тикет: #17 (сборка карты #10).
|
||||
Переехала из v1
|
||||
(https://git.dementev.space/ddmitry/clickstream-ch-kafka-superset-demo/src/branch/main/docs/specs/2026-07-30-stand-v2-realism.md);
|
||||
номера тикетов #NN в тексте — из трекера v1.
|
||||
Источники: резолюции #18 (модель данных), #15 (`purchase` и заказы),
|
||||
#14 (кластер), #13 (цена кластера), #16 (анонимность и склейка); исследование
|
||||
[формата кликстрима Яндекса](../research/2026-07-26-yandex-clickstream-format.md);
|
||||
[docs/generator-realism.md](https://git.dementev.space/ddmitry/clickstream-ch-kafka-superset-demo/src/branch/main/docs/generator-realism.md).
|
||||
|
||||
## Зачем
|
||||
|
||||
Менти должен узнавать в стенде тот кликстрим и тот дата-контур, с которыми
|
||||
столкнётся на работе. Сейчас стенд упрощён в четырёх местах: событие разрезано
|
||||
на четыре топика, есть только просмотры страниц (нет денег), весь трафик
|
||||
идентифицирован по email, ClickHouse — одна нода. Карта #10 приняла четыре
|
||||
решения, которые эти упрощения снимают. Эта спека собирает их в одну целевую
|
||||
картину и оценивает объём исполнения.
|
||||
|
||||
Исполнение — **новый репозиторий**, не переработка этого (решение карты #10
|
||||
от 2026-07-23): v1 замораживается как стабильный стенд для менти, v2 стартует
|
||||
пустым с осознанным первым коммитом — переносим только нужное, генератор
|
||||
переписывается, переиспользуются идеи. Рабочее имя — `clickstream-data-platform`
|
||||
(финальное закрепление — при решении тикета о нише). Карта и открытые тикеты
|
||||
переедут туда после создания. Предусловие — задача «Редизайн пути менти»
|
||||
(#9) — выполнено, задача закрыта.
|
||||
|
||||
## Целевая картина одним взглядом
|
||||
|
||||
- **Одно широкое событие** по образцу Яндекс Метрики: плоское ядро,
|
||||
параллельные массивы, сырое поле `ecommerce`. Таксономия `EventType`:
|
||||
`pageview`, `add_to_cart`, `purchase`. Четыре топика уходят.
|
||||
- **Второй источник — заказы бэкенда**: та же Kafka, но ежедневный полный
|
||||
слепок окна изменяемости, со статусами и JSON-позициями. Одна труба,
|
||||
два режима.
|
||||
- **Каталог товаров** — словарь ClickHouse из CSV в репозитории.
|
||||
- **Сверка** клиентского `purchase` против заказа: четыре конструируемых
|
||||
расхождения плюс опоздание. Деньги в витринах — только по бэкенду.
|
||||
- **Кликстрим анонимный**: у события только `ClientID` (кука). Склейка
|
||||
идентичностей — через мост `purchase`↔заказ; часть покупателей — с двух кук.
|
||||
- **Кластер единственным режимом**: 2 шарда × 1 реплика + clickhouse-keeper,
|
||||
`make up` поднимает сразу кластер. Superset — на ноду 2.
|
||||
|
||||
## 1. Широкое событие кликстрима
|
||||
|
||||
Форма — хит Метрики из облачной выгрузки: одно событие = одна строка,
|
||||
многозначное — в параллельных массивах одной длины, плюс одно сырое
|
||||
JSON-поле `ecommerce`. Сессий в потоке нет — их менти собирает сам в DDS.
|
||||
|
||||
### 1.1 Решения по именам и типам
|
||||
|
||||
- **Имена колонок — как в облачной выгрузке Метрики** (`ClientID`,
|
||||
`UTCEventTime`, `purchaseID`…). Сырой слой хранит имена источника; свои
|
||||
snake_case-имена появляются в DDS/DM. Это учебный пункт: у каждого
|
||||
источника — свой стиль, нормализует его склад, а не трекер.
|
||||
- **Идентификаторы — числовые UInt64** (`WatchID`, `VisitID`, `ClientID`),
|
||||
UUID уходят. Исследование советовало UUID не трогать, но тот совет исходил
|
||||
из цены переделки текущего генератора; v2 пишет генератор заново, цена
|
||||
нулевая, а числовые id — самая узнаваемая черта формата Метрики. Генератор
|
||||
держит значения id ниже 2^53: выше этой границы double-числа в JSON (jq,
|
||||
консоль браузера) искажают id при округлении. Настоящая Метрика так не
|
||||
делает — её id длиннее.
|
||||
- **Убираем наши выдумки**: `geo_latitude`, `geo_longitude` — координат в
|
||||
выгрузке Метрики нет (гео — регион и его числовой id). `browser_user_agent`
|
||||
тоже не берём, но это наш выбор, а не запрет источника: исследование
|
||||
запрещало только выдавать это поле за формат Яндекса, оставить разрешало.
|
||||
Разбор строки user agent — не урок этого стенда.
|
||||
- **`Sign` берём как колонку формата, без механики** (решение владельца на
|
||||
приёмке спеки): генератор всегда пишет `Sign = 1`, исправлений записей не
|
||||
шлёт — движки и запросы не меняются. Сама механика версий
|
||||
(CollapsingMergeTree, пара `HitVersion`) — кандидат на потом, по #15.
|
||||
Честность: комментарий в DDL и абзац в документе о реализме («в бою здесь
|
||||
бывают −1/+1, считают через `sum(Sign)`»); в лекции — крючок про
|
||||
CollapsingMergeTree (частый вопрос на собеседованиях).
|
||||
- **Не берём** `ClientEventTime` (в выгрузке Метрики нет клиентской метки;
|
||||
расхождение часов — тема тумана «грязь»), `Params` (второй сырой JSON не
|
||||
нужен: этот навык уже несут заказы), `LastSearchEngineRoot`, `IsPageView`,
|
||||
`NotBounce`, `HTTPError`, `pageViewID`, `CounterUserIDHash`, Openstat
|
||||
и соцдем-поля (см. «чего не воспроизводить» в исследовании).
|
||||
- **Отступление по типу**: `DeviceCategory` берём как UInt8, у Метрики это
|
||||
String; коды те же (1–4).
|
||||
- **Наша честная добавка** — `EventType`: у Метрики такого поля нет
|
||||
(там `isPageView` + `productEventType`), стенду таксономия нужна явно.
|
||||
|
||||
### 1.2 Состав полей (47 колонок)
|
||||
|
||||
Идентификаторы и время:
|
||||
|
||||
| Колонка | Тип | Комментарий |
|
||||
|---|---|---|
|
||||
| `WatchID` | UInt64 | id события (хита) |
|
||||
| `VisitID` | UInt64 | id визита от генератора — эталон самопроверки лабы сессий («собери сам, потом сравни») |
|
||||
| `ClientID` | UInt64 | анонимный id браузера (кука) — ключ шардирования |
|
||||
| `CounterID` | UInt32 | константа стенда (один сайт) |
|
||||
| `EventDate` | Date | дата события |
|
||||
| `UTCEventTime` | DateTime | единственная метка времени, как у Метрики |
|
||||
| `ClientTimeZone` | Int16 | смещение пояса клиента в минутах |
|
||||
| `EventType` | LowCardinality(String) | `pageview` / `add_to_cart` / `purchase` |
|
||||
| `Sign` | Int8 | всегда 1: колонка формата, механика исправлений не реализована (см. 1.1) |
|
||||
|
||||
Правила резки визитов в генераторе документируются и совпадают с лабной
|
||||
логикой (30-минутный таймаут).
|
||||
|
||||
Страница и атрибуция: `URL`, `Referer`, `Title`, `UTMSource`, `UTMMedium`,
|
||||
`UTMCampaign`, `UTMContent`, `UTMTerm`, `LastTrafficSource`, `HasGCLID`
|
||||
(UInt8), `YCLID` (UInt64) — 11 колонок: все String, кроме `HasGCLID`
|
||||
(UInt8) и `YCLID` (UInt64).
|
||||
|
||||
Браузер, устройство, гео: `Browser`, `BrowserMajorVersion` (UInt16),
|
||||
`BrowserLanguage`, `OperatingSystem`, `OperatingSystemRoot`, `DeviceCategory`
|
||||
(UInt8, коды 1–4 как у Метрики), `MobilePhoneModel`, `ScreenWidth`,
|
||||
`ScreenHeight` (UInt16), `IPAddress`, `RegionCountry`, `RegionCity`,
|
||||
`RegionCountryID`, `RegionCityID` (UInt32) — 14 колонок.
|
||||
|
||||
Массивы и параметры: `GoalsReached` Array(UInt32) (две цели: корзина и
|
||||
покупка — цели в бою дублируют события, это нормально), `ParsedParamsKey1`
|
||||
Array(String) (свои параметры сайта, один уровень, например вариант
|
||||
A/B-теста; Key2..10 не берём).
|
||||
|
||||
Ecommerce (заполнены только у торговых событий):
|
||||
|
||||
| Колонка | Тип |
|
||||
|---|---|
|
||||
| `purchaseID` | Array(String) |
|
||||
| `purchaseRevenue` | Array(Float64) |
|
||||
| `purchaseCurrency` | Array(String) |
|
||||
| `purchaseCoupon` | Array(String) |
|
||||
| `productID`, `productName`, `productCategory` | Array(String) |
|
||||
| `productPrice` | Array(Int64) |
|
||||
| `productQuantity` | Array(UInt64) |
|
||||
| `productEventType` | Array(String) |
|
||||
| `ecommerce` | String — сырой JSON события, как отдаёт Метрика (кандидат будущей лабы: сырое против разобранного) |
|
||||
|
||||
`add_to_cart` несёт массивы `product*` с одним товаром; `purchase` — состав
|
||||
заказа и блок `purchase*`. Выручка у клиента — во Float64, как у Метрики:
|
||||
это не недосмотр, а часть урока о расхождениях (см. раздел 4).
|
||||
|
||||
### 1.3 Ключи и движки
|
||||
|
||||
- Партиции — **по дням** (`PARTITION BY EventDate`): дневная партиция —
|
||||
единица переобработки (решение #14). Отступление от Метрики (там месяц) —
|
||||
зафиксировать комментарием в DDL.
|
||||
- `ORDER BY (CounterID, EventDate, intHash32(ClientID), WatchID)` — ключ под
|
||||
запросы «по сайту за период по посетителю», хвост `WatchID` даёт
|
||||
дедупликацию в ReplacingMergeTree. Точную форму проверить при исполнении.
|
||||
`SAMPLE BY intHash32(ClientID)` — семплирование по тому же выражению;
|
||||
учебный вопрос к лабе: почему `SAMPLE 0.1` не портит uniq-метрики.
|
||||
- В DDL комментарием зафиксировать вырождение ключа как учебный факт:
|
||||
`CounterID` — константа стенда (один сайт), `EventDate` — константа внутри
|
||||
дневной партиции; реальная сортировка идёт по посетителю и событию
|
||||
(`intHash32(ClientID)`, `WatchID`).
|
||||
- Шардирование — **по `cityHash64(ClientID)`, не по сырому `ClientID`**:
|
||||
структурированный числовой id перекашивает остаток по модулю числа шардов,
|
||||
хеш — нет. Сессионизация, склейка идентичностей и uniq-метрики остаются
|
||||
локальными на шарде. У анонимов кука есть — перекоса в NULL нет.
|
||||
- Предупреждение-урок из v1: колонка версии не должна попадать ни в
|
||||
партицию, ни в ключ сортировки ReplacingMergeTree — иначе версии одной
|
||||
строки никогда не окажутся рядом и не склеятся при мерже.
|
||||
|
||||
### 1.4 Схема как контракт
|
||||
|
||||
Одно машинное описание схемы события (python-модуль или YAML) — источник
|
||||
истины: из него выводятся DDL и валидация генератора, а не наоборот. 47
|
||||
колонок повторяются примерно в семи местах (генератор, DDL, SELECT матвью,
|
||||
трансформации, витрины, манифест, доки) — без контракта они расходятся
|
||||
молча. Заодно это учебный артефакт: менти видит на живом примере, что такое
|
||||
«схема как контракт».
|
||||
|
||||
## 2. Заказы бэкенда
|
||||
|
||||
Второй источник и вторая версия правды о покупке. Транспорт — та же Kafka
|
||||
(топик `orders`), но **пачками**: раз в модельный день бэкенд выгружает
|
||||
**полный слепок заказов окна изменяемости K дней**.
|
||||
|
||||
- **K = 7 модельных дней, константа мира** (страховочный срез 3 из #15
|
||||
применён — см. раздел 9). За окном заказ неизменяем, возить его незачем;
|
||||
выручка дня D «дышит» K дней, потом замерзает. Боевой аналог окна есть и у
|
||||
трекеров: лог Метрики «доформировывается» ещё около трёх дней.
|
||||
- Запись слепка — состояние заказа на момент выгрузки, «родной» экспорт
|
||||
бэкенда в snake_case:
|
||||
|
||||
| Поле | Тип | Комментарий |
|
||||
|---|---|---|
|
||||
| `order_id` | String | номер заказа; равен клиентскому `purchaseID` |
|
||||
| `user_id` | UInt64 | пользователь магазина — мост к склейке |
|
||||
| `status` | String | `created` → `paid` → `cancelled` |
|
||||
| `created_at`, `updated_at` | DateTime | |
|
||||
| `items_total`, `discount`, `delivery`, `total` | Decimal(18,2) | деньги бэкенда — в Decimal |
|
||||
| `items` | String | позиции вложенным JSON: `[{sku, qty, price}]` |
|
||||
| `snapshot_date` | Date | дата слепка (день выгрузки) |
|
||||
|
||||
- Приём идемпотентный, но дедуп расщеплён на два слоя:
|
||||
- `ods.order_snapshot` — партиция по `snapshot_date`, **без дедупа**,
|
||||
хранит «как приехало»; идемпотентность повторного прогона — заменой
|
||||
партиции дня слепка, а не ReplacingMergeTree.
|
||||
- Дедуп до последней версии — **argMax** в трансформации при сборке
|
||||
`dds.order`. `dds.order` — единственная дедуплицированная таблица:
|
||||
партиция по дню заказа (`toDate(created_at)`),
|
||||
ReplacingMergeTree(`updated_at`), `ORDER BY order_id` — заказ всегда
|
||||
лежит в одной партиции, дедуп работает.
|
||||
|
||||
Пропущенный день ничего не ломает, следующий слепок самовосстанавливает.
|
||||
- Разбор JSON-позиций — **один раз**, в трансформации ODS → DDS; дальше
|
||||
витрины работают с плоскими массивами `dds.order`: `item_sku`
|
||||
Array(String), `item_qty` Array(UInt64), `item_price` Array(Decimal(18,2))
|
||||
— одной длины, порядок как в JSON. Это единственный носитель навыка
|
||||
«вложенный JSON в ClickHouse» на стенде.
|
||||
- Статусы держим все три: смена `created` → `paid` и есть причина «дыхания»
|
||||
выручки внутри окна; сужение до двух — резервный срез 1.
|
||||
|
||||
## 3. Каталог товаров
|
||||
|
||||
CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `category`,
|
||||
`brand`, `price`) — **словарь ClickHouse** из файла. Тот же файл использует
|
||||
генератор — расхождений нет по построению. Даёт `dictGet` в витринах и
|
||||
разговор о политике обновления словаря. На кластере файл монтируется в обе
|
||||
ноды, словарь создаётся ON CLUSTER.
|
||||
|
||||
## 4. Сверка `purchase` против заказов
|
||||
|
||||
Ключ: клиентский `purchaseID` = `order_id` бэкенда (магазин знает номер
|
||||
заказа на `/confirmation`). У события `purchase` массив `purchaseID` несёт
|
||||
ровно один элемент (одно подтверждение — один заказ), сверка соединяет по
|
||||
`purchaseID[1]`; правило зафиксировать комментарием в SQL сверки.
|
||||
Расхождения — перечислимый список,
|
||||
детерминированный от seed, не хаос:
|
||||
|
||||
| | Расхождение | Механика в генераторе | Ориентир доли |
|
||||
|---|---|---|---|
|
||||
| A | Отмена | заказ дошёл до `cancelled`, `purchase` остался | ~5% заказов |
|
||||
| B | Потерянное событие | заказ есть, `purchase` не доехал | ~3% |
|
||||
| C | Дельта суммы | сверка приведена к сравнимой базе (`items_total`, не `total`); `amount_delta` — только необъяснённый остаток после этого: округления Float64, вероятность в генераторе | ~1–2% |
|
||||
| D | Дубль события | повторный `purchase` от обновления `/confirmation`: новый `WatchID` с тем же `purchaseID` — бизнес-дубль, не технический; дедуп ReplacingMergeTree его не съедает и не должен | ~2% |
|
||||
|
||||
Классы пересекаются — приоритет: `cancelled` > `lost_event` >
|
||||
`duplicate_event` > `amount_delta` > `match`.
|
||||
|
||||
Пятое — **опоздание** — бесплатно даёт формат доставки: часть заказов
|
||||
впервые появляется в слепке D+1/D+2 («вчера не сходилось, сегодня сошлось»),
|
||||
ориентир ~10%. Точные доли фиксируются при пересборке эталонного мира;
|
||||
манифест хранит точные счётчики по каждому классу расхождений (отмены,
|
||||
потери, дубли).
|
||||
|
||||
Не берём: сироту-фрод (`purchase` есть, а заказа не будет никогда) —
|
||||
механически дублирует B.
|
||||
|
||||
Правило стенда: **поведение и атрибуцию считаем по трекеру, деньги — по
|
||||
бэкенду**. Единственное разрешённое исключение — клиентская оценка выручки
|
||||
под именем `declared_*` там, где атрибуция без трекера невозможна (UTM);
|
||||
слово `declared` в имени — сигнал «это заявка клиента, не деньги
|
||||
отчётности». Оно выучивается на конфликте: суммы не сойдутся, менти сам
|
||||
раскопает почему (Float64 против Decimal, промокод, доставка, отмены).
|
||||
|
||||
## 5. Анонимность и склейка идентичностей
|
||||
|
||||
- Email из кликстрима исчезает полностью: у события только `ClientID`.
|
||||
Это честно к Logs API Метрики (UserID не выгружается). Отдельная «доля
|
||||
анонимов» не нужна: мы знаем ровно тех, кто купил, — это сам урок.
|
||||
- **Карта соответствий кука↔пользователь** строится трансформацией из уже
|
||||
существующего моста: `purchase`-событие (`ClientID`, `purchaseID`) ↔ заказ
|
||||
(`order_id`, `user_id`). Ни новых полей, ни нового транспорта.
|
||||
- Форма и место карты: таблица `dds.identity_map`
|
||||
(`client_id` UInt64, `user_id` UInt64, `first_matched_at` DateTime),
|
||||
ReplacingMergeTree, `ORDER BY (client_id, user_id)`, шардирование по
|
||||
`cityHash64(client_id)` — тем же выражением, что события (иначе ко-локации
|
||||
нет); ко-локация делает обогащение витрин локальным;
|
||||
заказы при сборке карты подтягиваются через GLOBAL JOIN.
|
||||
- **N:1**: часть покупателей покупает с двух кук («телефон и ноутбук») —
|
||||
параметр мира; значение фиксирует эта спека: 15%, детерминировано
|
||||
от seed. Ядро лабы:
|
||||
`uniq(посетителей) > uniq(людей)`, менти выводит расхождение сам.
|
||||
Константа мира: каждый двухкуковый покупатель делает минимум по одному
|
||||
заказу с каждой куки — иначе вторая кука не попадает в карту соответствий
|
||||
(она строится только из покупок) и лаба не воспроизводится. Манифест
|
||||
хранит число именно таких пар.
|
||||
- Витрины разводят имена честно: **«посетители»** (`uniq(ClientID)`) и
|
||||
**«известные пользователи»** (после склейки) — оба числа рядом в дашборде.
|
||||
|
||||
## 6. Кластер
|
||||
|
||||
Соседний `clickhouse-learning-cluster` остаётся разминкой при курсе: там
|
||||
концепции, здесь жизнь — забыть ON CLUSTER, получить ошибку, починить.
|
||||
|
||||
Топология и режим — по резолюции #14:
|
||||
|
||||
- **2 шарда × 1 реплика + отдельный clickhouse-keeper**, единственный режим:
|
||||
`make up` поднимает сразу кластер, выключателя нет. Страховка от «слишком
|
||||
сложно» — отсутствие реплик и runbook, а не профиль без кластера.
|
||||
- Движки локальных таблиц — `Replicated*` (макросы `{shard}`/`{replica}`,
|
||||
пути keeper, готовность к будущей реплике). Без HAProxy — балансировать
|
||||
нечего; в доках абзац «в бою здесь LB».
|
||||
- Роли нод: нода 1 — инициатор DDL и подключение Airflow; **Superset — на
|
||||
ноду 2**. Это осознанная ловушка правильных ошибок: забытый ON CLUSTER или
|
||||
VIEW поверх локальной таблицы проявляются в дашборде сами.
|
||||
- **Приём Kafka**: Kafka-таблицы и MV — на обеих нодах, одна consumer group,
|
||||
2 партиции на топик; MV пишут в Distributed-цели. Раскладку решает ключ:
|
||||
события — по `cityHash64(ClientID)` (см. 1.3), заказы —
|
||||
`cityHash64(order_id)`, сырьё STG —
|
||||
`cityHash64(сырой строки)`. Урок: «какая нода читала топик — меняется между
|
||||
прогонами, куда легли данные — нет».
|
||||
- **Приём строгий**: `input_format_skip_unknown_fields = 0`, обязательные
|
||||
поля — без значений по умолчанию. Контракт присутствия: генератор выдаёт
|
||||
**все 47 полей в каждом событии**; «пусто» — пустой массив, пустая строка
|
||||
или 0, а не отсутствие ключа в JSON. Так строгий приём уживается с
|
||||
полями, пустыми по смыслу (ecommerce у `pageview`, UTM у прямого захода). Несовпадение имени поля — громкая
|
||||
ошибка в `*_errors`, а не молчаливые нули: имена CamelCase регистрозависимы,
|
||||
опечатка иначе не падает.
|
||||
- **Политика соединений**: по ключу ко-локации — обычное соединение с
|
||||
комментарием, почему локальный результат корректен; по любому другому ключу
|
||||
— только явный GLOBAL; `NOT IN` — только `GLOBAL NOT IN`. Сверка
|
||||
`purchase`↔заказ —
|
||||
легитимная GLOBAL-витрина (заказы малы).
|
||||
- **Конвейер без TRUNCATE**: поток — append-only в ReplacingMergeTree (дедуп
|
||||
через argMax); батчевая переобработка — по дневным партициям
|
||||
(`DROP/REPLACE PARTITION ON CLUSTER`); `TRUNCATE ... ON CLUSTER` остаётся
|
||||
только в `make reset`. `DROP/REPLACE PARTITION` работает только по
|
||||
**локальным** таблицам ON CLUSTER, не по Distributed; замена через
|
||||
DROP+INSERT неатомарна — дашборд в середине прогона честно моргает (это
|
||||
осознанная цена, не баг).
|
||||
- **Поздние заказы поглощает только ODS** (`ods.order_snapshot` — новая
|
||||
партиция дня слепка, без переделки старого); материализованное ниже —
|
||||
нет. Каждый прогон ETL перестраивает партиции последних K+1 дней у
|
||||
заказозависимых объектов (`dds.order` и производные, `dm.dq_summary`).
|
||||
Сессии перестраиваются только за текущий день: правило мира — сессия
|
||||
режется по границе модельных суток, дневная партиция самодостаточна.
|
||||
- Для ETL-вставок — `distributed_foreground_insert = 1` (раньше называлась
|
||||
`insert_distributed_sync`), иначе проверки видят неполные данные.
|
||||
- Все контрольные суммы и dq-проверки считают через `argMax`/`GROUP BY`/
|
||||
`FINAL` — голый `count()` по ReplacingMergeTree зависит от того, сколько
|
||||
мержей уже прошло.
|
||||
- **Проверки и контрольные суммы — только по Distributed-таблицам**:
|
||||
агрегаты от раскладки не зависят; раскладка по шардам нигде не фиксируется,
|
||||
пошардовые наблюдения — исследовательские, в лабах.
|
||||
|
||||
### Ресурсный бюджет (#10)
|
||||
|
||||
Расчёт на ноутбук менти с 16 ГБ памяти; у кого 8 ГБ — берёт VDS за свой счёт.
|
||||
Полный стенд в покое ≈3,4 ГБ. Топология 2×1 добавляет ≈0,6–0,8 ГБ — влезает
|
||||
свободно. Топология 2×2 добавила бы ≈1,7–1,9 ГБ и упёрлась бы в дефолтный
|
||||
бюджет WSL2 (~8 ГБ) — это второй довод против реплик, рядом с главным
|
||||
(репликационная эксплуатация — отдельный операционный домен). Координатор —
|
||||
clickhouse-keeper, а не ZooKeeper, в том числе из-за этого бюджета.
|
||||
|
||||
## 7. Слои: карта таблиц v2
|
||||
|
||||
| Слой | Объект | Что это |
|
||||
|---|---|---|
|
||||
| Kafka | `hits`, `orders` | два топика, по 2 партиции |
|
||||
| STG | `stg.kafka_hits`, `stg.hits_raw` + MV; то же для orders | сырые строки, Kafka Engine на обеих нодах |
|
||||
| ODS | `ods.event` (+`_errors`) | типизированное широкое событие, ReplacingMergeTree |
|
||||
| ODS | `ods.order_snapshot` (+`_errors`) | слепки заказов как приехали, партиция по `snapshot_date`, без дедупа |
|
||||
| DDS | `dds.session` | сборка сессий из событий (наследник `dds.click`) |
|
||||
| DDS | `dds.v_event` | представление над `ods.event`: snake_case-имена, расшифровка кодов `DeviceCategory`; витрины DM читают его, а не ODS напрямую |
|
||||
| DDS | `dds.order` | единственная дедуплицированная таблица заказа: партиция по дню заказа (`toDate(created_at)`), ReplacingMergeTree(`updated_at`), `ORDER BY order_id`, дедуп до последней версии — argMax в трансформации при сборке |
|
||||
| DDS | `dds.identity_map` | карта кука↔пользователь |
|
||||
| DDS | словарь `products` | каталог из CSV |
|
||||
| DM | витрины `dm.v_*`, `dm.dq_summary` | см. ниже |
|
||||
|
||||
Служебные колонки: `ods.event` и `ods.order_snapshot` получают метку приёма
|
||||
`_ingested_at`; у `ods.event` та же колонка — колонка версии
|
||||
ReplacingMergeTree. Таблицы `stg.*_raw` хранят виртуальные колонки Kafka
|
||||
(`_topic`, `_partition`, `_offset`, `_timestamp`) — без них урок «какая нода
|
||||
читала топик» ненаблюдаем. `stg.hits_raw` дополнительно хранит извлечённый
|
||||
`event_date` — им кормится переобработка дня X (при исчерпании retention
|
||||
Kafka переобработка возможна только из эталонного артефакта).
|
||||
|
||||
`dds.v_event` — первый на стенде пример правила «слой — это контракт, а не
|
||||
обязательно копия данных».
|
||||
|
||||
Событие в DDS не дублируется: склейки четырёх источников больше нет, ODS уже
|
||||
широкий и типизированный; DDS хранит бизнес-сущности (сессия, заказ,
|
||||
идентичность). «Грязные» записи по-прежнему уходят в `*_errors`, не валят
|
||||
пайплайн.
|
||||
|
||||
### Витрины DM
|
||||
|
||||
- **`v_revenue_daily`** (выручка, только от заказов): `report_date`,
|
||||
`product_category` (через `dictGet` каталога + ARRAY JOIN позиций),
|
||||
`orders`, `units`, `revenue`, `aov`. Считается по заказам в статусе
|
||||
`paid`; внутри окна K число дня «дышит».
|
||||
- **`v_purchase_vs_orders`** (сверка): FULL OUTER GLOBAL JOIN по
|
||||
`purchaseID = order_id`; колонки: `order_day`, `order_id`,
|
||||
`declared_revenue` (клиент), `items_total` (бэкенд, сравнимая база — не
|
||||
`total`: промокод и доставка клиенту не видны), `status`, `mismatch_class`
|
||||
(`match` / `cancelled` / `lost_event` / `duplicate_event` / `amount_delta`,
|
||||
в порядке приоритета — классы пересекаются, побеждает более ранний).
|
||||
`match` — большинство строк; `amount_delta` — только необъяснённый остаток
|
||||
после приведения к сравнимой базе (округления Float64, ~1–2% заказов).
|
||||
Строка «`purchase` без заказа» внутри живого окна — опоздание, ждущее
|
||||
слепка, а не расхождение: она получает служебный класс `awaiting_order`
|
||||
(шестое значение `mismatch_class`, вне приоритетов расхождений). После
|
||||
закрытия окна K таких строк не остаётся — сироты исключены построением
|
||||
(раздел 4).
|
||||
- **`v_utm_effectiveness`** — остаётся клиентской (атрибуция по трекеру);
|
||||
счётчики `purchases`/`add_to_carts` оживают из таксономии, добавляется
|
||||
`declared_revenue` по UTM.
|
||||
- **`v_daily_traffic`** — расширяется парой «посетители» / «известные
|
||||
пользователи» (обогащение через `dds.identity_map`, локальное соединение
|
||||
по ключу ко-локации).
|
||||
- `v_events_enriched`, `v_top_pages_daily`, `v_session_overview`,
|
||||
`v_dq_errors_daily` — переезжают на новую модель без смены роли: источник —
|
||||
`dds.v_event`, не `ods.event`.
|
||||
- `dm.dq_summary` переводится с TRUNCATE+INSERT на партиционную замену
|
||||
(политика «без TRUNCATE»).
|
||||
|
||||
Дашборд Superset получает три новых сюжета: выручка по дням и категориям,
|
||||
таблица сверки с классами расхождений, пара посетители/известные.
|
||||
|
||||
Ландшафт итогом: Kafka — единственная труба (кликстрим потоком, заказы
|
||||
пачками), Postgres остаётся только служебной базой Airflow. Смешанность
|
||||
ландшафта выражена режимами и частотами, а не второй трубой.
|
||||
|
||||
## 8. Эталонный мир и манифест
|
||||
|
||||
Пересборка артефакта `data/startup_history/` неизбежна и оплачена решением
|
||||
#18 один раз — все изменения генератора съезжаются в одну пересборку.
|
||||
Манифест расширяется контрольными числами:
|
||||
|
||||
- заказная сторона: заказы и выручка по дням; манифест хранит точные
|
||||
счётчики по каждому классу расхождений (отмены, потери, дубли, дельты сумм) —
|
||||
самопроверка лабы сверки;
|
||||
- идентичность: uniq кук, uniq известных пользователей, число двухкуковых
|
||||
покупателей — лаба склейки получает самопроверку.
|
||||
|
||||
Артефакт вырастет (ecommerce-массивы, заказы) — размер проверить при
|
||||
пересборке. Политика версионирования артефакта здесь не решается (туман
|
||||
карты #10).
|
||||
|
||||
## 9. Оценка объёма исполнения
|
||||
|
||||
v2 стартует пустым, поэтому объём ниже — это новый код, а не правка на
|
||||
месте; v1 служит источником идей и образцов (масштаб оценён по нему).
|
||||
|
||||
| Направление | Что строим | Объём |
|
||||
|---|---|---|
|
||||
| Генератор | с нуля: модель v1 не переносится (другая модель данных, плюс известные проблемы производительности v1); широкое событие, таксономия, анонимность, N:1, заказы слепками, расхождения A–D, каталог; масштаб — ~4–5 тыс. строк с тестами | L |
|
||||
| Инфраструктура | compose: 2 ноды CH + keeper + остальной стенд; конфиги кластера, макросы; make/скрипты | M — ~10–12 файлов |
|
||||
| SQL | 5 DDL-файлов (ON CLUSTER, Replicated*, Distributed) + трансформации событий, заказов, identity, сверки + словарь | L — ~12–15 файлов, главная сложность |
|
||||
| Airflow | DAG'и по образцу v1: etl_pipeline (партиционная переобработка, ожидание дневного батча заказов — сенсор/Datasets), world_init/next_day, helpers | M — ~5–6 файлов |
|
||||
| Superset | датасеты + дашборд с тремя новыми сюжетами | M — 2 файла |
|
||||
| Эталонный мир | сборка артефакта v2, манифест-счётчики, чек-скрипты | M–L |
|
||||
| Мониторинг | Prometheus/Grafana: цели двух нод и keeper | S — 2–4 конфига |
|
||||
| Документация | доки v2 пишутся заново (см. раздел 12) | M, в тех же PR |
|
||||
|
||||
Итого ~80–110 файлов нового репозитория (посчитаны конфиги по нодам,
|
||||
документация, экспорт Superset — дерево YAML, CI и артефакты данных);
|
||||
тяжёлое — генератор и SQL. Это крупный релиз, но он режется на этапы с
|
||||
работающим стендом после каждого. Согласуется с оценкой исследования #13
|
||||
(~15–20 файлов только на кластерную часть).
|
||||
|
||||
### Решение по страховочным срезам (#15)
|
||||
|
||||
- **Срез 3 применён**: окно K — константа мира (7 дней), не параметр.
|
||||
- **Срез 2 применён как порядок, не как отказ**: расхождения A+C входят в
|
||||
этап сверки, B+D — отдельным следующим этапом.
|
||||
- **Срез 1 в резерве**: статусы держим все три (`created`/`paid`/
|
||||
`cancelled`) — на статусе `paid` стоит «дыхание» выручки; сужение до пары
|
||||
`created`/`cancelled` — запасной ход, если генератор заказов окажется дороже
|
||||
ожиданий. Связка: если срез 1 сработает, определение выручки в
|
||||
`v_revenue_daily` придётся сменить с «заказы в статусе `paid`» на «все
|
||||
неотменённые заказы».
|
||||
- **Отступление от порядка #15**: резолюция предписывала резать в порядке
|
||||
1 → 2 → 3, спека применяет 3 и 2, а 1 держит в резерве. Довод: срезы 3 и 2
|
||||
ничего не отнимают у уроков (окно и так одно, расхождения и так вводятся
|
||||
этапами), а срез 1 убирает статус `paid` — вместе с ним ушло бы «дыхание»
|
||||
выручки.
|
||||
|
||||
### Этапы для /to-tickets (черновик)
|
||||
|
||||
0. Рождение v2: создать репозиторий (рабочее имя
|
||||
`clickstream-data-platform`), осознанный первый коммит (скелет доков,
|
||||
AGENTS.md, лицензия), переезд карты #10 и открытых тикетов.
|
||||
1. Каркас стенда: кластерный compose (2×CH + keeper + Kafka, Airflow,
|
||||
Superset, мониторинг), конфиги, `make up`, smoke-проверка ON CLUSTER.
|
||||
2. DDL и генератор (слиты в один этап — DDL проверяется только настоящими
|
||||
данными): базы и таблицы событий ON CLUSTER, приём `hits` обеими нодами;
|
||||
широкое событие, таксономия, анонимность, N:1 (клиентская сторона
|
||||
целиком). В конце этапа фиксируется маленький «зерновой» мир для
|
||||
стабильных приёмок следующих этапов (полная пересборка эталонного мира —
|
||||
отдельный этап 7).
|
||||
3. Заказы и каталог: генератор слепков, STG/ODS/DDS заказа, словарь.
|
||||
4. Трансформации и витрины: сессии, identity_map, выручка, сверка A+C.
|
||||
5. Airflow: `etl_pipeline` (партиционная переобработка, ожидание дневного
|
||||
батча заказов — сенсор/Datasets).
|
||||
6. Расхождения B+D и опоздания; счётчики манифеста.
|
||||
7. Эталонный мир: пересборка артефакта, чек-скрипты.
|
||||
8. Superset-дашборд v2.
|
||||
9. Мониторинг и runbook «keeper упал / DDL повис в очереди».
|
||||
|
||||
Критерий приёмки этапа — честный: `make up` работает и проходят
|
||||
smoke-проверки, а не «дашборд зелёный». Это минимальная планка; свои
|
||||
наблюдаемые критерии каждый этап получает при разбиении в /to-tickets. Документация правится в PR этапа
|
||||
(правило AGENTS.md).
|
||||
|
||||
## 10. Границы: чего не делаем
|
||||
|
||||
- «Грязь» в данных: боты, дубли на транспорте, опоздавшие мобильные батчи,
|
||||
расхождение часов клиент/коллектор — туман карты, вернётся своим тикетом.
|
||||
- Политика версионирования эталонного артефакта — туман карты.
|
||||
- Лабы и курс: v2 — другой стенд, лабы для него пишутся с нуля отдельной
|
||||
работой после этой спеки; редизайн лаб v1 (#7) остаётся в v1 и сюда не
|
||||
переносится. Спека даёт будущим лабам только опорные точки — контрольные
|
||||
числа манифеста (сверка, идентичность). Явное следствие: после этапа 9
|
||||
стенд работает, но учебного пути на нём ещё нет.
|
||||
- Инкрементальный ETL (#8) — свой issue.
|
||||
- Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд.
|
||||
- Полный словарь торговых событий Метрики (detail, remove, impressions),
|
||||
пять уровней категорий, блоки `purchasedProduct*`/`impressions*`.
|
||||
- Механика `Sign`/CollapsingMergeTree — кандидат на потом (дом — поток
|
||||
визитов Метрики Про); сама колонка `Sign` уже в схеме, статикой (см. 1.1).
|
||||
- Событийный лог заказов, CDC/Debezium, HTTP-сервис заказов, шапка+строки,
|
||||
отдельный поток возвратов — отклонены в #15/#18.
|
||||
- Файловые дропы как источник — зона следующего стенда (Lakehouse), у нас
|
||||
остаются теорией; каталог отдельным топиком Kafka — в бою так не делают
|
||||
(оба отклонения — из #18).
|
||||
- Вероятностная склейка, identity graph, кука 1:N («семейный планшет») —
|
||||
тема лекции, не лабы.
|
||||
- Эмуляция `setUserID` и сюжет «логин посреди сессии» через свои параметры —
|
||||
отклонены в #16: нечестно к формату выгрузки Метрики.
|
||||
|
||||
## 11. Проверить при исполнении
|
||||
|
||||
- Поведение соединения двух Distributed-таблиц и `distributed_product_mode` —
|
||||
эмпирически на стенде (хвост #14).
|
||||
- Kafka Engine на двух нодах в одной consumer group: ребаланс партиций между
|
||||
прогонами, отсутствие дублей при штатной работе.
|
||||
- Точная форма `ORDER BY` ODS-таблиц (выражение `intHash32` в ключе
|
||||
ReplacingMergeTree).
|
||||
- Размер артефакта эталонного мира после пересборки.
|
||||
- Спорные API (Airflow Datasets/сенсоры, ClickHouse DDL) — перед кодом
|
||||
сверять через MCP Context7 (правило AGENTS.md).
|
||||
- Генератор: рабочее решение — Python с производительной архитектурой
|
||||
(батчевая генерация вместо посточной, быстрая JSON-сериализация,
|
||||
распараллеливание по модельным дням). Читаемость генератора для менти —
|
||||
не довод при выборе языка: он в любом случае сложнее уровня DE-джуна.
|
||||
До этапа 3 зафиксировать требования производительности (пересборка
|
||||
эталонного мира, живой поток ×60); переход на компилируемый язык
|
||||
(Rust/Go) — только если замеры покажут, что Python приемлемой скорости
|
||||
не даёт.
|
||||
|
||||
## 12. Влияние на документацию
|
||||
|
||||
Состав и структура доков v2 проектируются заново: набор документов v1
|
||||
сложился исторически и не копируется. Какие документы нужны v2 — решение
|
||||
этапа 0. Обязательный минимум по содержанию (не по списку файлов):
|
||||
быстрый старт, архитектура слоёв, операционка с runbook keeper/DDL,
|
||||
словарь терминов (широкое событие, слепок, окно изменяемости,
|
||||
посетители/известные пользователи, ко-локация).
|
||||
|
||||
Документ о реализме (`generator-realism.md`) переезжает в v2 и получает
|
||||
крупное обновление: Kafka — учебная замена батчевого Logs API («настоящий
|
||||
Logs API — это скачанный TSV»); trade-off «инкремент экономнее, слепок
|
||||
надёжнее» + сноска про compacted topic; identity stitching перестаёт быть
|
||||
чистой теорией; «прямое чтение прод-базы — анти-приём, в бою — реплика или
|
||||
выгрузка»; колонка `Sign` без механики — честное ограничение стенда
|
||||
(в бою −1/+1 и `sum(Sign)`); полный словарь торговых событий — теория.
|
||||
|
||||
В v1 при заморозке — указатель на v2 в README (форму решить при рождении
|
||||
v2, этап 0).
|
||||
|
||||
### Опорные точки для будущих лекций и лаб
|
||||
|
||||
Лабы и курс — вне скоупа спеки (см. раздел 10). Список нужен только затем,
|
||||
чтобы хвосты резолюций не потерялись:
|
||||
|
||||
- анти-паттерны ключа шардирования (`rand()`, `toDate`) — как отрицательные
|
||||
примеры;
|
||||
- «как выбирают топологии в бою»: часто 1 шард × N реплик, шардирование — про
|
||||
рост;
|
||||
- сцена «разные consumer groups → дубли»;
|
||||
- словарь регионов из CSV той же машинерией, что каталог товаров (оживляет
|
||||
`RegionCityID`);
|
||||
- лаба сессий: менти сначала собирает сессии сам, и только после — рассказ,
|
||||
что с октября 2025 Метрика отдаёт `VisitID` прямо в хитах; частично
|
||||
синтетическая постановка — осознанный приём;
|
||||
- лекция «`Sign` и CollapsingMergeTree»: почему на стенде `sum(Sign)` =
|
||||
`count()`, а в бою — нет; частый вопрос на собеседованиях;
|
||||
- лекция про идентичность «как в бою»: `setUserID` и first-party id,
|
||||
детерминированная против вероятностной склейки, identity graph,
|
||||
кросс-девайс, CDP — с рамкой «мы склеили через транзакции, потому что трекер
|
||||
user id не отдаёт».
|
||||
Reference in New Issue
Block a user