Поиск, который находит артикул

Hybrid search that finds the exact part number

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

Middle 5 шагов разбора · 3 развилки · 4 поломки из прода

Вводная

Каталог из 8 млн документов: карточки товаров, инструкции, статьи поддержки.

Запросы короткие, в среднем 3,4 слова, у трети — опечатки.

Отдельный класс запросов — точные строки: артикул «BX-4471», код ошибки «E-22», название модели.

После перехода на векторный поиск жалобы: «ищу артикул, а он не находится».

Требования и ограничения
ЧтоЗначениеОткуда
Документов8 млнрастёт примерно на 3 % в месяц
Запросов600 RPS в пике
Бюджет ответа200 мс по p99включая переранжирование
Recall@50 отбора≥ 0,95на эталонной разметке в 2000 запросов
Точные совпаденияне терятьартикул обязан быть в top-3

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

Разбор по шагам

Понять, что именно сломалось

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

Общая метрика поиска после перехода на даже выросла. Разбиение по типам запросов показывает другое: на описательных запросах вырос на 8 пунктов, на запросах-идентификаторах упал на 40. Средняя цифра это скрывала, потому что идентификаторов около 12 % трафика.

На практике

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

Причина понятна из устройства модели. обучался сближать тексты по смыслу; «BX-4471» и «BX-4478» по смыслу неразличимы — это два кода одного вида. Токенизатор к тому же режет их на куски, общие для обоих. Расстояние между ними в пространстве меньше, чем разрешение задачи.

Собрать две выдачи вместо одной

Лексический индекс возвращается не как запасной путь, а как равноправная половина.

BM25 находит точные редкие строки именно потому, что редкая строка даёт высокий IDF. поиск находит перефразирование. Ни один из них не покрывает второго, поэтому обе выдачи считаются параллельно и объединяются.

Объединять по «сырым» баллам нельзя: у BM25 они не ограничены сверху и зависят от длины документа, у косинусной близости лежат в отрезке. Нормировать их друг к другу — значит подбирать коэффициент, который поплывёт при следующем переиндексировании. Практичнее объединение по рангам.

Обозначения
  • номер или количество: индекс шага, число соседей, кластеров или позиций
  • суммирование по всем перечисленным элементам
Reciprocal rank fusion: k около 60, ranks — позиции документа в каждой выдаче.

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

Переранжировать верхушку

Пятьдесят кандидатов можно прочитать внимательно, восемь миллионов — нет.

Би-энкодер кодирует запрос и документ независимо: только так и можно построить индекс заранее. Цена — модель никогда не видит их вместе. Кросс-энкодер читает пару целиком и ранжирует заметно точнее, но требует прогона на каждую пару.

ЭтапКандидатовСтоимостьЧто даёт
BM25 + 8 млн → 200~25 мс@200 = 0,97
RRF200 → 50< 1 мсустойчивый порядок без
Кросс-энкодер50~90 мс на +11 пунктов @10
На практике

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

Гарантировать точное совпадение

Статистика ранжирования не даёт гарантий, а требование сформулировано как гарантия.

«Артикул обязан быть в top-3» — это не метрика, а инвариант. Ранжирующая модель, даже очень хорошая, обеспечить его не может: она оптимизирует средний порядок. Поэтому поверх ставится правило.

  1. Распознать в запросе идентификатор: регулярное выражение по формату артикулов и кодов, плюс словарь известных префиксов.
  2. Сделать точный поиск по нормализованному полю идентификатора — обычный индекс базы, миллисекунды.
  3. Если совпадение найдено, поставить документ первым, минуя ранжирование, и пометить результат как точное совпадение.
  4. Остальную выдачу оставить как есть: пользователь мог ошибиться в коде, и похожие результаты ему пригодятся.

Правило покрывает узкий класс запросов и потому безопасно. Соблазн решить это обучением — дообучить на парах «артикул — товар» — работает хуже: гарантии всё равно нет, а переобучать придётся при каждом изменении номенклатуры.

Мерить по классам запросов

Одна метрика на всех однажды уже скрыла поломку.

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

Отдельно измеряется отбора — до переранжирования. Это единственный способ отличить «ранжировщик поставил не туда» от «кандидат вообще не был найден»: во втором случае улучшать ранжировщик бессмысленно.

Развилки и выбор

На собеседовании ценится не сам выбор, а объяснение, почему отклонены остальные варианты.

Как объединять две выдачи?

Reciprocal rank fusion по рангам

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

Что отклонено и почему
  • Взвешенная сумма нормированных балловКоэффициент нужно подбирать заново после каждого переиндексирования и при смене эмбеддинг-модели — вечный источник тихой деградации.
  • Обучаемая модель слиянияТребует разметки и добавляет ещё одну модель в контур ради задачи, которую формула решает почти так же.

Чем закрыть точные совпадения?

Правило поверх ранжирования

Требование сформулировано как гарантия, а ранжирующая модель гарантий не даёт. Класс запросов узкий и хорошо распознаётся, поэтому правило не разрастётся.

Что отклонено и почему
  • Дообучить эмбеддинги на артикулахГарантии не даёт, а переобучать нужно при каждом пополнении номенклатуры.
  • Поднять вес BM25 для коротких запросовПомогает в среднем, но именно в среднем: конкретный артикул по-прежнему может не попасть в top-3.

Сколько кандидатов отдавать кросс-энкодеру?

50

Замер показал: на 100 бюджет p99 не выдерживается, на 25 теряется около двух пунктов NDCG@10.

Что отклонено и почему
  • 200 — все кандидаты после слиянияЗадержка вырастает вчетверо и выходит за 200 мс уже на p50.
  • 10 — только верхушкаКросс-энкодеру нечего исправлять: ошибки отбора чаще всего лежат на позициях 10–40.

Что ломается в проде

Индексы расходятся

Лексический и векторный индексы обновляются разными процессами. Документ, удалённый из одного и оставшийся в другом, всплывает в выдаче как «битая» карточка. Лечится общим журналом изменений и проверкой согласованности по счётчику документов.

Кросс-энкодер становится узким местом при всплеске

На пике 600 RPS модель на GPU держит очередь, и p99 уезжает. Нужен предохранитель: при превышении порога очереди переранжирование отключается и выдача отдаётся по RRF. Хуже по качеству, но в бюджете.

Эмбеддинги устарели, а индекс нет

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

Правило точного совпадения ловит лишнее

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

По чему видно, что система здорова

МетрикаЗначениеЗачем
NDCG@10, описательныецель ≥ 0,71
NDCG@10, идентификаторыцель ≥ 0,93срез, который и сломался
Recall@200 отбора≥ 0,97меряется отдельно от ранжирования
Задержка p99≤ 200 мсс переранжированием
Доля отключений переранжирования< 1 % запросовпредохранитель по очереди

Что спрашивают по этому кейсу

  1. Почему нельзя просто взять эмбеддинг-модель получше?
  2. Как вы объединяете две выдачи и почему не взвешенной суммой баллов?
  3. Где вы поставите гарантию точного совпадения и почему не в модели?
  4. Как измерить, что упало именно ранжирование, а не отбор?
  5. Что произойдёт при смене модели эмбеддингов и в каком порядке вы будете выкатывать?

Проверить себя: тест «NLP», тест «Генеративный ИИ и LLM», тест «Рекомендательные системы». Открытые задачи там же — под тестом.