Дано: интернет-магазин одежды, обуви и аксессуаров и интернет-магазин автозапчастей, две совершенно разные сферы онлайн-покупок со своей уникальной логикой пользовательского опыта, которые обслуживают миллионы пользователей в нескольких странах.
Требуется:
построить умную рекомендательную систему, чтобы при заходе на сайт, руки сами тянулись к заветной кнопке «Купить»;
облегчить пользователю поиск товаров даже с самыми нестандартными запросами;
подружить пользовательские предпочтения с требованиями бизнеса, когда требуется распродать определенный товар.
Ресурсы: команда ИИ из 6 человек, полгода работы, графовая база Neo4j, векторный поиск, генеративный ИИ и безграничное терпение бизнес-команд.
Эта статья о том, как мы это сделали и с какими трудностями столкнулись на своем пути.
Какие бывают рекомендации
Путь пользователя по сайту интернет-магазина может пролегать разными маршрутами. Начинается он, как правило, на главной странице, и уже тут пользователю, который еще может быть не авторизован и не обозначил цели своего визита, надо показать что-то. В этом случае в дело идут популярные товары, сезонный спрос, акционные товары — то есть самые общие рекомендации, а также новинки, знакомство с ассортиментом, товары повышенного спроса, тематические подборки. Разнообразить их можно, например, определив погоду или какие-то значимые события по дате или местоположению пользователя.
Когда пользователь перешел в каталог, показываем общие рекомендации уже применительно к конкретной категории, а уж если он начал смотреть конкретные товары, есть где развернуться дата-сайентисту:
какие товары обычно смотрят или покупают совместно с тем, что сейчас смотрит, покупает или добавил в избранное пользователь;
какие товары похожи по своим характеристикам на текущий;
порекомендовать аксессуары для данного товара;
рекомендации с учетом текущей пользовательской сессии — даже если пользователь не авторизован, но он уже успел что-то посмотреть и поискать, мы можем учесть эту, пусть и короткую, историю в текущих рекомендациях, попробовав по этой истории предугадать цель пользовательского визита.
И наконец, у нас есть авторизованный пользователь и вся история взаимодействия с ним.
В этом случае мы можем:
рекомендовать ему в первую очередь те товары, что он отложил в избранное, но подзабыл;
найти пользователей, которые имеют схожий профиль по купленным товарам, и на основе их совокупности предпочтений сделать рекомендацию текущему пользователю;
сегментировать пользователя по частоте, объему и стоимости покупок и сделать рекомендацию по покупкам схожей аудитории;
строить рекомендации исходя из пользовательского поведения.
Наконец, есть еще один кейс, когда рекомендация нацелена на помощь пользователю в сложном подборе, то есть когда он не уверен, действительно ли ему необходим именно этот товар, так как не имеет достаточной экспертизы (это случаи покупки запчастей к чему-либо, выбор сложной техники, материалов для ремонта). Тогда рекомендация должна быть направлена на то, чтобы определить применимость товаров и показать наиболее подходящие.
Этой теме будет посвящена отдельная статья, а пока спойлер: в таком случае особенно важным становится взаимодействие с пользователем для уточнения его потребностей, и просто показ товаров удобнее заменить интерактивным режимом общения с чат-ботом.
О том, что бизнесу не достаточно иметь волшебную кнопку
Итак, алгоритм рекомендации выбран, возможно, мы даже уже отобрали для показа топ-N товаров. Можно отдавать их на фронт? А вот и нет. Как известно, самым продаваемым товаром в магазине независимо от ассортимента является пакет, но с позиции бизнеса рекомендовать его покупку недопустимо, поэтому поверх найденных взаимосвязей в обязательном порядке накладываются ограничения.
Пришло время поговорить о продактах и категорийных менеджерах интернет-магазинов, которые день за днем неустанно следят за тем, какие именно товары должны быть успешно распроданы в определенный период времени. Ситуации бывают разные — заканчивается сезон и нужно избавиться от остатков на складах, или нужно в первую очередь распродать товар, у которого заканчивается срок реализации, а может быть, магазин закупил большую партию суперкроссовок, и пользователям нужно оперативно об этом сообщить.
Как же сделать так, чтобы пользователь увидел именно то, что мы хотим показать ему в первую очередь, но при этом не отпугнуть его лавиной ненужного спама?
Для этого продакты получили возможность преднастраивать требования к рекомендациям, например, ставить в приоритет товары определенной категории, если известно, что эта категория интересна пользователю. Для этого у нас реализована специальная панель администрирования рекомендаций, которая содержит в себе:
возможность выбора рекомендательных алгоритмов;
возможность установки фильтрации и сортировки рекомендованного товара по дополнительным параметрам, например, показывать товары, начиная с самых популярных или с тех, что продаются с большой скидкой;
проведение A/B тестирования, например, выбрать регион, которому будет показываться рекомендация именно в таком наборе алгоритмов, фильтров и сортировок;
исключение товаров и категорий — никаких пакетов, товаров, которые пользователь уже купил или только что положил в корзину и т. п.;
выбор конкретного места размещения рекомендации на сайте и ее публикация для показа.
В результате продакты получили возможность самостоятельно определять, что когда и как показать пользователю.
Настройки панели прилетают в рекомендательную систему двумя частями. Первая часть включает в себя то, что актуально для всех пользователей, например, выбранные для рекомендации категории. Вторая часть правила приходит непосредственно в момент рекомендации и включает пользовательские особенности — город показа, ID пользователя и просматриваемого товара, дополнительные характеристики, например, VIN-номер автомобиля, если человек ищет автозапчасти. Благодаря второй части алгоритмы определяют ограничения, накладываемые на предрассчитанные данные. Например, алгоритм показа похожих товаров заранее располагает данными о том, какие товары похожи между собой, но именно в момент рекомендации учитывает их наличие рядом с пользователем, вместе с подходящими ему размерами, если вдруг они нам известны.
Почему именно граф
Рекомендательные системы строят разными способами, один из самых известных — это матричная факторизация. На Хабре про нее написано довольно много статей, поэтому не будем углубляться в теорию, отмечу лишь, что цель таких рекомендаций в построении матрицы, содержащей вероятность покупки каждым пользователем определенного товара. Из минусов — когда у нас таких товаров и пользователей много, матрица имеет очень большую размерность, а также не учитывает многие дополнительные факторы.
Мы выбрали графы, потому что у графовых рекомендаций есть ряд преимуществ:
все взаимосвязи заранее построены в графе, поэтому минимизированы операции, требующие расчетов в реальном времени;
графам достаточно небольшого объема данных, благодаря чему упрощается холодный старт рекомендаций для нового пользователя;
доступна визуализация и наглядное объяснение, почему выбрана та или иная позиция товара для рекомендации;
данные легко разделяются по категориям товара, полу пользователя и другим параметрам. В расчете могут участвовать только те данные, которые действительно необходимы;
графы могут учитывать историю, быть динамическими;
взаимосвязи между товарами способны находить то, что действительно покупают вместе, поэтому рекомендательная система порекомендует пользователю не второй крем от загара на основе его покупок, а солнечные очки и пляжные принадлежности;
для особо хитрых алгоритмов можно использовать графовые нейронные сети, подавая туда подграф, связанный с конкретным товаром или пользователем.
В качестве расчетного движка и хранилища данных выбор пал на Neo4j, как на самую популярную графовую базу, богатую функционалом для рекомендаций, имеющую широкое сообщество и бесплатную версию.
Графовый RAG
В идеальном мире все рекомендации можно было бы построить с помощью коллаборативной фильтрации, а в реальном встречаются случаи, которые коллаборативная фильтрация не может покрыть полностью. Ярким примером является продажа автозапчастей. Пользователь приходит с конкретной поломкой автомобиля конкретной марки конкретного года выпуска. Он также хочет иметь возможность выбрать наиболее дешевую запчасть, и готов взять неоригинальную. И для того, чтобы сделать рекомендацию, необходимо не только найти похожий профиль, но и понять, насколько рекомендации профиля подходят к конкретной марке автомобиля, и какие именно неоригинальные запчасти в этом случае применимы, а какие — нет.
Здесь на помощь приходит дополнение другими графами — какие запчасти или бренды покупают для определенных автомобилей и типов ремонта, в каких случаях встречаются возвраты. Использование графа в качестве RAG — внешней базы знаний для генеративного ИИ — позволило не только находить похожие профили, но и учитывать подобные взаимосвязи.
Не графом единым
Граф с помощью взаимосвязей покрывает большинство рекомендательных алгоритмов, но один все-таки потребовал применения дополнительных инструментов — это алгоритм похожих товаров. Не просто товаров, которые покупают как взаимозаменяемые, а обладающих большим набором схожих характеристик. Их можно было бы разместить в свойствах узлов графа, но для ускорения расчетов мы предпочли применение векторного поиска. Для каждого товара были отобраны значимые свойства, а затем на основе векторного сходства каждый товар получил топ-K похожих, и уже эта схожесть перенесена в граф в виде связей.
Трудности на пути к успеху. Динамические запросы
Приступая к работе, мы изначально видели свою работу так: подготовить под каждый алгоритм рекомендаций определенный запрос и использовать его в процессе рекомендаций. Но, когда оказалось, что бизнес хочет иметь гибкий интерфейс для создания сложных рекомендаций, например, первые 3 товара — самых похожих, дальше 3 самых популярных и так далее и все должно происходить без нашего вмешательства в код, нам пришлось серьезно поштурмить над решением. В итоге решили, что запросы в базу должны строиться динамически и реализовали умный внутренний конструктор запросов к базе Neo4j, который понимает параметры правила и собирает в запрос нужные элементы.
У продакта есть возможность собрать панель рекомендаций сразу из нескольких базовых блоков, а еще и наложить на их совокупность дополнительную фильтрацию и сортировку. В этом случае у нас будут отдельные запросы по выбранным алгоритмам и один дополнительный алгоритм, который соберет результат в единую панель с учетом фильтров и сортировки.
И вот собранный алгоритм рекомендаций готов отправиться в базу за извлечением данных, но до того момента, как придет время им воспользоваться, он будет храниться в Redis.
Трудности на пути к успеху. Быстродействие
Рекомендации должны работать с такой скоростью, чтобы успевать реагировать на каждое действие пользователя, пока тот не успел покинуть страницу или проскроллить панель с рекомендацией за пределы видимости. Поэтому нам пришлось потрудиться и над тем, как обеспечить быструю выдачу. Neo4j заявляет в своих преимуществах гораздо более высокую производительность по сравнению с SQL-базами в задачах, когда требуется выполнить множество join. Но более высокая производительность еще не означает, что все будет летать из коробки. Очень важно было правильно построить запросы и связи, и на эту тему нам удалось найти полезные гайды тут и тут.
На скорость влияет то, где расположить свойство — в узле или в связи между узлами. Связи между узлами изначально ищутся быстрее. Но, если нужно дотянуться за данными через несколько связей, например, для конкретного пользователя найти его чеки, товары, которые он покупал в этих чеках, а в этих товарах сделать фильтр по определенной категории или свойству, то бывает удобнее простроить связь от пользователя напрямую к конечной точке или дать узлу пользователя дополнительную характеристику «предпочитаемая категория». При построении связей не стоит увлекаться созданием суперузлов, это узлы с особенно большим количеством связей. Такими могли бы быть узлы пола пользователей — мужской и женский, поделившие между собой связи от всех пользователей магазина. Это антипаттерн, который сильно замедляет работу базы.
P.S. Если вам интересны новости про генеративный ИИ, LLM, мультиагентов, я рассказываю об этом в своем Телеграм канале https://t.me/generative_ai_ru