Всем привет! В данной статье хочу поделится результатам R&D о применении  LLM и графов в поиске товаров по текстовому запросу юзера. Данная идея появилась при разработке Retrivier модуля RAG - системы, осуществлявшей поиск по документам. Были высокие требования к качеству ретривера, при этом латенси в 10-20 секунд являлось приемлемым, что позволило применять вызов нескольких тяжелых модулей. Стало интересно, применима ли получившаяся архитектура для поиска не только документов, но и товаров.

Ожидания от алгоритма поиска

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

  • Простые запросы из одного - двух слов( джинсы, зеленое платье)

  • Семантический поиск(юзер хочет свитер - выдача содержит не только свитеры, но и свитшоты)

  • Запросы содержащие отрицания(платье не из хлопка, рубашка без кармана, майка без логотипа)

  • Запросы содержащие эпитеты(эффектное платье, стильная сумка)

  • Запросы имеющие структуру(длинное платье с короткими рукавами и пр. )

  • Запросы с несколькими сущностями(белый топ и синие джинсы)

  • Запросы без конкретной сущности, но с указанием назначения(обувь для спорта и пр, здесь поисковая система расширяется до AI - ассистента)

  • Запросы с указанием цены или бренда(сумку Kors, пиджак дешевле 20к)

  • Запросы с ссылкой на публичную личность(сумку как у Dua Lipa, и пр.)

Данный набор покрывает большинство пожеланий, которые юзер может вместить в запрос длиной в одно - два предложения.

Чем полезны графы в поиске товаров

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

graph2graph score
graph2graph score

Предположим, юзер хочет найти майку, и в каталоге есть товар имеющий текстовое описание "джинсы Levi's, хорошо сочетаются с топом". Поиск по ключевым словам выдаст ложно положительный результат, т.к. формально слово "майка" присутствует в описании. Но для графового поиска, который учитывает не только сущности, но и связи, запрос и товар не являются похожими, т.к. в графе запроса юзера узел "майка" связана с узлом "основной обьект"(технический узел), а в описании товара узел "майка" не связан с аналогичным узлом своего графа, что отражается на значении graph2graph сходства.

graph2graph score
graph2graph score

Для запроса "джинсы", скор принимает высокое значение, отражающее семантическое сходтство запроса и товара.

LLM Graph Search конвейер

 Блок-схема конвейера поиска
 Блок-схема конвейера поиска

Алгоритм поиска состоит из следующих шагов:

  • Dynamic prompts optimization - анализ запроса юзера и формирование промпта для Text2Graph блока. Данный шаг необходим для оценки сложности запроса и присутствия в запросе пожеланий к бренду и цене(джинсы Levi's до 12к). Это, а также выделенные из запроса сущности позволяют эффективно сформировать промпт на следующем этапе(Text2Graph блок). Оценка сложности запроса позволяет сэкономить время и избежать ненужных трат токенов. Если запрос короткий(джинсы, синяя куртка и тп.), поиск по ключевым словам или простой векторный поиск отработают быстрее, и по качеству вероятно не хуже, поэтому целесообразно использовать их вместо LLM Graph поиска. Логически данный шаг можно обьеденить с Text2Graph блоком, но имеет смысл разделять доменно-зависимый шаг(цена и бренд присутствуют в поиске товаров, но отсутствуют например в поиске по документам, где также можно применять графовый поиск), и последующий Text2Graph блок.

  • Text2Graph преобразование - используя LLM, конструируем Knowledge graph. На вход поступает запрос юзера и оптимизированные на предыдущем шаге промты. Производится аналитика графа, быть так, что он состоит из двух или нескольких отдельных подграфов(например если юзер хочет несколько товаров, и их следует обрабатывать отдельно). Данный шаг почти полностью доменно - независимый, при адаптации алгоритма под другие данные(например книги вместо одежды, или текстовые документы), почти все изменения производятся на предыдущем шаге.

  • Graph search - модуль графового поиска. Для каждого подграфа выделенного в предыдущем пункте, вызываем графовый поиск. На вход подается граф запроса, поиск производится среди графов товаров, на выходе индексы top n товаров, чьи графовые представления наиболее близки к графу запроса. Похоже на векторный поиск, но вместо векторов - графы.

  • LLM - реранкер. Тяжелый модуль, использующий результаты с предыдущего шага в качестве кандидатов и осуществляющий переранжирование. В отдельных кейсах существенно добавляет в качестве.

  • Оценка полученного результата. Получаем результат(top n наиболее релевантных товаров). В отличие от поиска по эмбедингам, результаты графового поиска содержат подробную информацию, что мы нашли, а что нет. Например мы можем понять, что нашли нужный юзеру тип товара, но слегка(или не слегка) не попали в цвет. Или наоборот. Здесь мы можем оценить релевантность результатов поиска. Если релевантных товаров не найдено, можно откатиться на шаг назад и перезапустить реранкер для большего числа кандидатов(либо откатиться в самое начало). Идея данного шага - уметь оценивать качество результатов поиска перед показом юзеру, и иметь возможность вызывать дополнительные шаги в случае необходимости.

Примеры

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

Поиск по запросу включающему отрицания

LLM GSearch: запрос содержащий отрицание
LLM GSearch: запрос содержащий отрицание

Запрос юзера: элегантное длинное платье без рукавов.

Запрос юзера преобразовывается в граф, т.е. набор узлов и связей. Можно рассматривать как Knowledge Graph. Кроме явных сущностей , атрибутов, и связей, граф позволяет задавать свойства узлов, отражающие роль (узел является главным товаром, желаемым/нежелаемым атрибутом и т.п.). В примере выше "платье" является желаемым узлом, в отличие от узла "рукава".

Запрос имеющий структуру

LLM GSearch: запрос имеющий сложную структуру, два противоположных атрибута(короткий - длинный), относящихся к разным сущностям(платье и рукава)
LLM GSearch: запрос имеющий сложную структуру, два противоположных атрибута(короткий - длинный), относящихся к разным сущностям(платье и рукава)

На мой взгляд, это наиболее интересное свойство графового поиска — способность корректно отрабатывать запросы, имеющие сложную структуру, где присутствует несколько иерархически связанных сущностей, при этом каждая сущность может иметь свои свойства. Практически полезным является то, что реализованный в общем виде алгоритм поиска не имеет принципиальных ограничений на глубину иерархии. В приведённом примере глубина графа невелика (платье — рукава — характеристика длины — плюс ещё два "технических" узла, не отображённых на графе, но необходимых для корректной работы поиска), но в теории структура может быть сложнее. Для поиска товаров, возможно, это не сильно нужно, но есть мысль применить графовый поиск в продвинутом поиске документов, где могут быть как запросы «найди все заявки за последние полгода, выполненные исполнителем, на которого сегодня пришла жалоба, где статус отличен от "Выполнено полностью" или закрытые через более чем 5 дней…», так и более сложные. Впрочем, чем сложнее структура (запрос пользователя, описание документа или товара), тем выше вероятность того, что на этапе text2graph соберется некорректный граф.

Несколько товаров в одном запросе

LLM GSearch: несколько товаров в одном запросе
LLM GSearch: несколько товаров в одном запросе

В предыдущем примере сущности были связаны друг с другом (рукав является частью платья). Возможна другая ситуация, например запрос «белые джинсы и синяя сумка». Здесь после шага text2graph мы понимаем, что граф запроса представляет собой два отдельных, не связанных друг с другом подграфа. В этом случае GSearch вызывается два раза — для каждого подграфа отдельно.

Строго говоря, это не единственный вариант. Отправив в GSearch без разделения оба подграфа (пиджак и брюки) вместе, в теории на запрос «найди синий пиджак и синие брюки» можно получить синий костюм, содержащий и синий пиджак, и синие брюки. Это будет ровно то, что хотел пользователь, хотя с точки зрения graph2graph-сходства результат будет неидеален, поскольку запрос пользователя не содержит слова "костюм" и граф запроса также не будет содержать данный узел.

Это нежелательное развитие событий, так как перед показом результатов поиска пользователю итоговый результат оценивается, и если скор топа (взвешенное graph2graph similarity + оценка реранкера) ниже заданного, алгоритм ошибочно решит, что то, что хотел пользователь, не удалось найти. Этот сценарий предусматривает повторный запуск алгоритма с другими гиперпараметрами (или откат на несколько шагов назад) либо выдачу "поиск не дал результата" — чего не хотелось бы.

Этот момент надо доработать, чтоб подобные кейсы отрабатывались полностью корректно, в том числе по graph2graph скор.

Пожелания к товару в произвольной форме

LLM GSearch: пожелания к товару в произвольной форме
LLM GSearch: пожелания к товару в произвольной форме

Обычно запрос юзера содержит конкретные характеристики товара(тип, цвет и пр.), но часть запросов содержит информацию о назначении товара, либо сценарии использования.

LLM GSearch: поиск по пожеланию к товару
LLM GSearch: поиск по пожеланию к товару

Можно искать товары только по назначению, не указывая тип товара явно. Запрос также преобразуется в граф, дополняется "техническими" узлами, для соответствия Knowledge Graph head - relation - tail.

Бренда и цена

Knowledge Graph
Knowledge Graph

Представление данных в виде графа позволяет не выносить фильтрацию по бренду и цене в отдельные галочки в интерфейсе, а брать их напрямую из текстового запроса юзера. Кроме конкретных цифр, можно отрабатывать пожелания к цене в текстовом виде(дешево/дорого/экономно), через статистику семантически близких товаров. Но учитывая, сколько жалоб идет на исчезновение физических кнопок в автомобилях, при появлении в них сенсорных экранов, возможно не нужно убирать фильтрацию в GUI, а дать юзеру выбор.

Перспективы развития, Deep Research и мультимодальные модели

Маршрутизация
Маршрутизация

Графовый поиск умеет создавать разницу в сложных кейсах, но в случае простого запроса он избыточен. Алгоритм содержит несколько шагов, вызовы LLM, и занимает 10-15 секунд. Сейчас есть понимание, что не во всех случаях необходимо вызывать реранкер, и доработав этот момент + идеально векторизовав каталог, можно снизить латенси до 2-3 секунд. Но в любом случае, есть смысл для простых запросов использовать более простые методы. Если запрос состоит из одного - двух слов, нет смысла строить граф.

Deep research

Deep research
Deep research

Данный модуль можно описать как "тяжелый реранкер после реранкера". Deep research содержит несколько экспериментальных идей, требующих значительного времени и ресурсов, содержит нелинейную логику, мультимодальные CLIP модели и VLM - вызовы. Данный модуль использует всю доступную информацию(текстовое описание товара, изображения). Это значительно увеличивает время работы и задействованные ресурсы, но добавляет качество в сложных кейсах. Время работы - до 1 минуты, против 15 секунд у стандартного LLM GSearch вызова. Хорошо работает с формой(в примере - в топ выдачи попадает не то, к чему можно применить слово "круглое", а самое круглое, как мяч), может учесть пожелания к шрифту и пр. В идеале, часть идей которые сейчас входят в Deep research шаг, можно реализовать на этапе векторизации каталога, и тогда в вызове отдельного тяжелого шага не будет необходимости. Но то что выглядит избыточно для поиска товаров, может найти применение при реализации поиска в другой задаче, например юридического AI - ассистента, где латенси в одну минуту допустим, а требования к качеству максимальные.

Текущее состояние

В данный момент работа над алгоритмом продолжается, есть что дорабатывать и по качеству, и по времени работы, и по снижению числа багов(некорректно преобразованных в граф запросах/товарах/документах). Есть желание сделать алгоритм максимально доменно независимым, чтоб его можно было применять и для товаров и для документов, и в других случаях, с минимальной адаптацией(но она точно потребуется). Пока выглядит так, что наиболее органично он будет смотреться в корпоративных AI - ассистентах и RAG - системах, где нагрузка ниже чем на сайте/приложении интернет-магазина, требования к качеству и сложности отрабатываемых запросах наиболее высокие, а время работы ~15 секунд вполне допустимо. Но возможно, что после доработки всех этапов алгоритма поиска, с хорошо настроенной маршрутизацией(простые запросы - в стандартный поиск, сложные - в граф), плюс наложив бизнес логику, например применение для клиентов с большими средними чеками, алгоритм можно будет успешно применять в массовых сервисах.