Опыт персонализации интернет-магазина на примере динамической рекомендации

    Привет, Хабр!

    Поделюсь опытом о том, как собрали собственную систему персонализации на базе «знаний» о потенциальном покупателе.

    image

    Единственное чем отличалось наше решение от классических — это использование комбинированной связки ряда решений и удовлетворял списку требований:

    • сервис должен был работать сразу на N сайтах
    • динамическая сегментация аудитории
    • Коллаборативная фильтрация для целей прогнозирования в разных состояниях сегментов аудитории
    • предварительно сгенеренная статика в виде рекомендованного контента + динамический подмес товаров на основе анализа кликстрима
    • изменение контента, практически в реал-тайме, из оперативной памяти, с учетом динамических коэффициентов

    Об этом подробнее :) И о тех граблях, которые помогали нам меняться стек в лучшую сторону.

    Предыстория


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

    У каждого сайта своя команда маркетологов, свои счетчики GA и Я.Метрики. Анализируя собственную аудиторию, коллеги маркетологи настраивали контент сайта, учитывая потребности посетителей. Естественно, аудитории этих сайтов пересекались, но так как на тот момент единого счетчика не существовало, то и результаты анализа были локальными.

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

    Почему не Google Analytics?

    Сэмплирование, отсутствие возможности вытянуть все о пользователе с его цепочкой ходов от внешней площадке, через кучу наших сайтов, с детализацией что смотрел и т.п. Да, по части задач GA хороший инструмент, но когда ты хочешь получить данные в реал-тайме и сразу принять решение о том, какой контент показать посетителю, то… нет такого решения у гиганта аналитики. Танцы с бубнами клиентского ID для передачи между сайтами — не наш вариант.
    Ставить единый счетчик для всех сайтов, не совсем корректное «политическое» решение, да и сразу бы попадали в ограничение free версии.

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

    1. Фиксировать каждого уникального пользователя, не зависимо от того, на каком сайте группы он находится. Это необходимо было для создания единого профиля клиента в который бы писались все его данные с привязкой к каждому сайту.
    2. Отслеживание длинной цепочки переходов между всеми сайтами группы в рамках сессии. Это было необходимо для выявления: интересов пользователей; что смотрели; что покупали; что положили в корзину, но не купили; какие товары разных сайтов могли быть «товарами заменителями» в процессе покупки и т.д.
    3. Отслеживание рекламных активностей всех маркетологов (в каждой команде сайта) для последующего анализа. Это было необходимо для: обогащения профиля каждого посетителя; выявления оптимальных рекламных кампаний с привязкой к товару; выявление эффективных рекламных каналов с привязкой к товару или кампании, и т.д. список очень большой.

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

    Хвала ситуации, я аналитик + разработчик + маркетолог + управленец + у меня под рукой был доступ ко всему, что оцифровано в холдинге. У меня не было на старте технического задания, делал для себя для решения обычных задач по анализу данных.

    Технический момент:


    • в качестве единой системы хранения данных использовали MongoDB
    • сборка данных в реал-тайме на базе javascript
    • локальный обмен данными между сайтами на базе rabbitmq

    Пока все, как у всех… Но тут началось

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

    Всё получилось замечательно. Анализировали всё, но без фанатизма. Модель увеличивалась в размерах и как это обычно бывает — наступил час, когда захотелось большего.

    Технический момент 2:


    • Репликация MongoDB помогала жить, а вот от шардирования сразу отказались. Этот момент не прошел по ряду внутренних аспектов. Если коллекцию кликстрима можно было раскидать по шардам, то коллекцию профайлов пользователей уже нет. Результаты агрегированных запросов с разных шардов собирать для части отчетов можно было, но скорость выполнения оставляла желать лучшего. Без шардирования работало лучше и стабильнее
    • Сборка данных в реал-тайме на базе javascript — тут диктатором была связка CORS+HTTPS. Кейс, когда один пользователь приходил на один из сайтов группы, а наш сервис его «авторизовал» сразу на мультидоменной зоне (напоминаю, самостоятельных отдельных сайтов на тот момент было 5) это было технологически красиво и загадочно для коллег маркетологов, которые теперь могли видеть всю цепочку хитов сразу по всем сайтам.
    • Модельку рекомендательного сервиса написали на Python. Но обучение занимало длительное время. Файл модели в несколько десятков Гб.
    • Рекомендательный сервис стандартно работал по собственному API, но узким местом стал ответ сервера, а точнее время получения результата. Чем больше данных, там больше модель, чем больше модель, тем медленнее она выдавала результат. В ответе нужно было учесть множество факторов, которые менялись каждый час (остатки товаров на складе, пользовательские характеристики и мода, всяческие скидки и т.д.)

    Спустя несколько месяцев, качество работы API перешло все вменяемые границы «качества». По части пользователей скорость ответа перевалила отметку в 400ms. Контентные блоки собирались медленно, сайт стал заметно подтуплять. Коллекции MongoDB исчислялись десятками миллионов записей…

    Наступило время что-то менять


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

    Что на что меняли:

    • Clickhouse на MongoDB
    • далее MongoDB на MongoDB + Tarantool
    • EVE на Flask
    • далее Flask на Falcon
    • отдельный фейл был с промисами в js и с авторизацией юзера на всех доменах. Менять логику не стали, победил рефакторинг

    Почему не API метрики?

    Поначалу я как раз смотрел в сторону готовых решений от Яндекса, но это было не долго. Это хорошо когда в работе один сайт. а когда их n, и данные хочется обрабатывать сразу, то тут не до танцев с бубнами.

    Почему MongoDB?

    Характеристики товаров постоянно добавлялись, часть из них увы не всегда была представлена.
    Использование агрегированных запросов — очень хорошо ложилось в формат локального технологического стиля работы команды. Плодить таблицы в классических SQL не хотелось.

    Достаточно часто модифицировали типы и варианты данных, которые хранили и обрабатывали.
    По началу я считал, что в качестве основы для сервиса буду использовать clickhouse от Яндекс, но потом отказался от этой идеи, но и clickhouse был в нашей обойме стеков.

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

    Во время машинного обучения первый раз в htop увидел 100% загрузку сразу 12 ядер и полный своп на продуктивном сервере. Zabbix активно информировал о том, что MongoDB за 10 минут уже дважды менял мастера в реплике. Всем хотелось стабильности и предсказуемого состояния.

    Наступило время что-то менять 2.0


    Число пользователей увеличивалось. Число клиентов — аналогично. По каждому кто когда-либо был на любом из сайтов мы накопили персональный профайл. Аудитория постоянных посетителей частично была сформирована.

    Что умели делать? Да любой не стандартный отчет для аналитики + контентного разнообразия:

    • Выбрать всех пользователей, которые пришли по рекламной кампании А, которая была в прошлом квартале, из них найти тех, кто интересовался товарами из рубрикатора N, далее исключить тех, кто покупал только по акциям, исключить тех, кто положил любой товар в корзину и покинул сайт. По ним сделать сортировку в порядке убывания дохода магазина, если бы эти пользователи сейчас пришли на сайт магазина и купили Z товар. Как-то так, и с другими перламутровыми пуговицами...
    • Проанализировать входящий трафик, для пользователей с utm меткой Y сформировать контентное предложение, но при этом ДО момента сборки определить пользователя, исключить тех, кто был на прошлой неделе но был на сайте S (один из группы сайтов холдинга) и интересовался товаром Q — для таких сформировать контент с сортировкой по критериям x,y,z
    • Банально, найти тех, кто часто посещает сайт А, иногда бывает на сайте Б (посещали определенный раздел), и у которых средний чек в интернет-магазине более N рублей

    На самом деле это было не в формате «ненормального программирования», хотелось что-то иное. Иное к нам и пришло. В момент проведения рекламных кампаний интернет-магазин загибался от нагрузки, что ух говорить о наших API-шках, которые ловили эту нагрузку «стоя рядом».

    Решения, которые были приняты вовремя


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

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

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

    Под каждый кластер мы заранее формировали контентные блоки и записывали в память. Тут на сцену вышел Tarantool во всей своей красе. Ранее мы его использовали для хранения быстрых данных, которые потом использовались в машинном обучении. Это было оптимальное решение, дабы не тараканить MongoDB которая и так была занята иными задачами. В спейсах Tarantool хранил данные о товарах, данные пользователей (необходимое знание о покупателе).

    Грубо говоря, мы сегодня вечером «готовили» контент под каждый кластер аудиторий, которые возможно посетят сайт завтра. Пользователь приходил, мы быстро определяли знаем ли мы о нем что-либо, и если ответ положительный — в Nginx улетал нужный пакет контента. Отдельно, для NoName юзеров был собран дефолтный кластер со своим контентом.

    Postprocessing для персонализации


    Мы знали, что есть простые юзеры, а есть те, по которым у нас целая картотека знаний. Все это добро лежало в Tarantool и апдейтилось в реальном режиме времени.

    В момент сборки страницы мы знали всю историю покупок и брошенных корзин каждого посетителя (если он ранее был нашим клиентом), определяли его кластерную принадлежность, модуль кликстрима давал знания об источнике трафика, мы знали о его прежних и сиюминутных интересах. Выстраивая налету массив ТОП50 из ранее просмотренных товарах (на любом из сайтов группы) мы «определяли моду» и подмешивали в контент «вкус» тех товаров, тематика которых чаще всего фонила в ТОП50. Этот простой анализ последних просмотренных товаров давал реальный профит. Кластерный контент, мы обогащали персональным.

    Результат нашего опыта


    1. Мы получили ускорение процесса формирования персонального контента в N раз
    2. Снизили нагрузку на серверах в 15 раз
    3. Можем формировать практически любой контент персонально, учитывая множество хотелок маркетологов, особенностей товарного представления и множества исключений, учитывая данные из профиля юзера и евентов, которые происходят на сайте в данный момент — и все это за ~25ms
    4. Конверсия для таких блоков не опускается ниже 5,6% — юзеры охотно покупают то, что им ближе по потребностям именно сейчас
    5. Скорость загрузки страницы идеальная — убрали тот контент, который «мимо» кластера на >67% что правильно
    6. Получили инструмент, который по задачам маркетологов не только дает ответ «что было ранее», но и помогает сформировать контент на ближайшее время фрагментарно, учитывая интересы и потребности потенциальных покупателей
    7. В профиль каждого покупателя добавили информацию от DMP, теперь можем делать кластеризацию в том числе по соцдему, интересам, уровню дохода и прочим сладостям
    8. Наш рекомендательный сервис стал лучше, заказов в магазине больше

    Какой с этого прок?


    Получили новый опыт, протестировали ряд гипотез, к которым не знали ранее как подступиться, не используем сторонние платные решения по рекомендательному сервису, который не учитывал все наши особенности и работал на одном домене. Команда получила хороший и интересный кейс, с которым успешно справилась.

    Аппетиты растут… сейчас тестируем новую логику сборки контента. Хотим собирать страницы под рекламные кампании, рассылки и прочую внешнюю активность. В планах — перенос логики сборки конфигов из манульного режима, в сторону machine learning. Сайты будут жить своей жизнью, а мы в сторонке «с попкорном» будем наблюдать за эволюцией представления контента сайта, основанного на мнении AI.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 4

      +1
      Выбрать всех пользователей, которые пришли по рекламной кампании А, которая была в прошлом квартале, из них найти тех, кто интересовался товарами из рубрикатора N, далее исключить тех, кто покупал только по акциям, исключить тех, кто положил любой товар в корзину и покинул сайт. По ним сделать сортировку в порядке убывания дохода магазина, если бы эти пользователи сейчас пришли на сайт магазина и купили Z товар. 

      Интересно, что использовали, чтобы такие вещи считать?

        0
        Я же в статье указал, используется mongodb
        0
        Очень глубоко и фанатично. Обожаю такой подход.
        Пробовал сделать такое не в екомерс и получил неудачу, потому как заказ может быть заявкой, а может быть звонком. Только через год после того, как закрыли проект появились варианты «слушать» звонки и выдергивать слова, чтобы составлять карту интересов пользователя.

        Показали бы немного внутрянки, как это выглядит.
        Не думали завернуть и продавать подход?)
          0
          уточните, что из «внутрянки» интересует? Там по факту много разных модулей с разным функционалом. По поводу продать — все обсуждается

        Only users with full accounts can post comments. Log in, please.