Данная статья является гайдом по использованию кастомных агрегаторов в Spark SQL API. Она “выросла” из моих заметок, которые я делал себе с начала работы со Spark. Сейчас, по мере накопления опыта, мне все это кажется уж слишком наивным и простым, но в свое время мне это показалось чертовски удобным/изящным/заслуживающим внимания, поэтому и решил опубликовать, тем более на Хабре про это еще вроде не писали.  Статья ориентирована в первую очередь на тех, кто только начинает работать со Spark, поэтому и помечена как “tutorial”. Если же вы опытный и у вас есть какие-либо интересные кейсы по использованию кастомных агрегаторов - делитесь в комментариях! Ниже мы будем говорить о user-defined aggregations functions (UDAF) org.apache.spark.sql.expressions.Aggregator, которые могут быть использованы для DataSet’ов с целью агрегации группы элементов в одно значение каким-угодно-пользователю образом. Давайте сначала разберем пример из официальной документации, реализующий простое суммирование. Case class “Data” нам нужен потому, что UDAF используются для DataSet’ов.  Для создания своего агрегатора нам надо определить 6 функций с предопределенным названием: zero - оно же нулевое, или начальное значение; должна удовлетворять требованию: “нечто” + zero = “нечто” reduсe - функция reduсe, выполняющая нашу агрегацию в буфер merge - функция для мёржа буферов finish - финишная обработка для получения целевого значения; в конкретном примере ее по сути нет, но часто бывает полезной (увидим в примерах ниже) два энкодера для буфера и выходного значения Обратите внимание, что по сути агрегация производится в 3 этапа: сначала в буферы потом идет агрегация буферов потом финальная обработка и поэтому, даже если вам не нужен какой-либо этап, все равно надо определять данные функции. С другой стороны, вряд ли вы будете использовать UDAF для простого суммирования, а для сложных агрегаций такое разбиение на этапы бывает очень полезным. Также следует обратить внимание, что необходимо при создании агрегатора определить типы входного, буферного, и выходного значения. Входным типом обычно является case class для элементов DataSet’а Давайте рассмотрим учебный “а-ля word count”-пример, реализованный на агрегаторах. Пусть у нас есть простой CSV-файл с твитами пользователей: Определим для каждого пользователя самое употребялемое им слово. Естественно задачу можно решить многими способами, но мы в учебных целях покажем, как ее решить с помощью UDAF. Обратите внимание на указание типов <IN,BUF,OUT>: входной тип у нашего агрегатора - case class Tweet, описываюший наши данные для буфера мы используем тип Map[String, Int] ну и выходной тип - просто String, так как возвращается самое популярное слово. В итоге получаем такой результат: Рассмотрим другой, более сложный пример, который на мой взгляд лучше может показать удобство UDAF. Пусть у нас есть кликстримы пользователей примерно в таком виде: То есть для каждого пользователя мы отслеживаем его действия - заход на сайт, поиск продукта, просмотр деталей продукта, оформление заказа, закрытие сайта и т.п. Причем для определенных действий у нас могут быть дополнительные данные, помещенные в поле attributes:  для первичного входа это, например рекламная кампания (campaign_id) и канал привлечения (channel_id) при оформлении заказа генерируется purchase_id, благодаря которому можем связать данный лид с данными из других систем.  Для оценки эффективности компаний и каналов нас могут попросить сделать выгрузку в примерно таком виде, на основании которого простыми группировками аналитики отдела маркетинга смогут ответить на различные вопросы по оценке рекламных компаний, каналов продвижения и т.п.  Здесь у нас стоит 2 задачи: нам надо проанализировать поток логов и “наиграть” sessionId “вытащить” из JSON-подобных текстовых полей нужные нам атрибуты в колонки, причем первые два содержатся в атрибутах с eventType = “app_open”, а последний - в eventType = “purchase” попутно преобразовать все к простой структуре, где одной sessionId соответствует 1 строка данных Первая задача хоть и не относится к теме статьи, но затронем ее немного. Один из вариантов ее решения - применить оконную функцию. Идея в следующем: Группируем по userId и сортируем по eventTime Помечаем все eventType = “app_open” (то есть запись, которую ) Суммируем нарастающим итогом с применением оконной функции в сортировке по eventTime, получаем номер сессии пользователя Уникальный идентификатор всей сессии можно сгенерировать, например конкатенируя userId и номер сессии В коде это будет выглядеть примерно так Итого, мы имеем на текущий момент такой DataFrame А напомним, что наша задача - получить структуру со следующей схемой, где одной сессии соответствует одна строка. Налицо необходимость агрегации по sessionId и какой-то хитрой обработке колонки mapAttributes. Как и хотели выше, сделаем это с помощью UDAF, который будет “собирать” данные колонки mapAttributes в одну Map Данные успешно агрегированы! Теперь уже дело техники - вытащить данные в разные колонки: Просто? Вроде просто! Но несмотря на это, я все же сторонник подхода: “Если задача может быть решена стандартными средствами, то лучше решать это стандартными средствами”, поэтому не переусердствуйте) Надеюсь, данный туториал будет кому-нибудь полезен :-) А вы используете UDAF? Поделитесь примерами в комментариях. Архитектор и инженер данных