Такая глубина анализа стоит за каждым нашим проектом. Мы публикуем её как подтверждение того, как мы на самом деле работаем — реальные варианты, которые мы взвешивали, цифры, которые их отсеяли, архитектура, пережившая столкновение с реальным каталогом клиента — чтобы процесс был для клиентов таким же прозрачным, как и результат. В этом проекте «высекание из камня» было не про выбор модели — это было про решение, что НЕ строить самим, потому что главная ловушка на поисковом проекте — изобретать инфраструктуру, которая уже существует.
У клиента была витрина ритейлера среднего размера с каталогом, наполняемым несколькими поставщиками — около 120 000 SKU, растущим на несколько тысяч в неделю по мере подключения новых поставщиков. Поиск работал через SQL ILIKE по названиям товаров. Он давал плохие совпадения на всё, кроме точных попаданий по ключевым словам — покупатель, ищущий «тёплые непромокаемые ботинки», не получал ничего, если ни в одном названии товара не встречались буквально все три слова, даже если под другой формулировкой существовал десяток подходящих товаров.
Команда мерчандайзинга из четырёх человек компенсировала это вручную: вела таблицы ручных синонимов поиска и постоянно тушила пожары «почему X не находится по запросу Y». Это не масштабируемый процесс при 3000 новых SKU в неделю.
Запрос звучал так: сделать так, чтобы поиск реально понимал, что имеет в виду покупатель, оставаться быстрым по мере роста каталога, и дать мерчандайзингу возможность настраивать релевантность без заведения тикета в разработку каждый раз. Именно последний пункт — нетехнической команде нужен контроль без деплоев — определил решение по фронтенду не меньше, чем любое решение по бэкенду.
Почему мы не купили просто Algolia: собственный опубликованный прайсинг Algolia тарифицирует по двум измерениям — поисковые запросы и записи — от $0.50 за 1000 дополнительных запросов и $0.40 за 1000 дополнительных записей сверх бесплатного тарифа, а их тариф с AI-функциями (NeuralSearch, персонализация) прыгает до $1.75 за 1000 запросов — рост в 3.5 раза относительно стандартного поиска именно за ту возможность, которая была нужна в этом проекте. При 120 тыс. SKU с еженедельной ротацией каталога и заметном объёме запросов это прогнозируемая, но реальная постоянная стоимость, которая растёт вместе с бизнесом, а не выходит на плато. Это не аргумент против Algolia в целом — для меньшего каталога или команды, которая хочет вообще не владеть поисковой инфраструктурой, это обычно правильный выбор. Это аргумент про соответствие именно этому размеру каталога и траектории его роста.
Почему «генеративные рекомендации», а не просто «ранжированные результаты», исключили готовый ретривер сам по себе: ни Algolia, ни Elasticsearch нативно не генерируют обоснованные рекомендации на естественном языке — оба являются поисковыми движками, возвращающими ранжированные структурированные результаты. Собственные материалы Algolia позиционируют генеративные ответы как отдельный надстроечный слой («Agent Studio»), которому всё равно нужно вызывать внешнюю LLM; документация Elastic описывает тот же паттерн как «подключите нас к своему RAG-пайплайну». Реальное отличие не в том, можно ли подключить Algolia к LLM — а в том, что владение цепочкой retrieval→rerank→generation самостоятельно даёт полный контроль над prompt-инжинирингом, стоимостью за вызов генерации и логикой ранжирования, вместо оплаты чёрного ящика той же идеи через чужой AI-тариф.
Почему не Elasticsearch тоже: векторный поиск Elasticsearch способный, но это вторая система, которую нужно эксплуатировать, патчить и масштабировать независимо от Postgres — для каталога такого размера это операционные накладные расходы за возможности, которые pgvector уже покрывает на этом масштабе (см. §2 ниже). Мы пересмотрим это решение в момент, когда объём запросов или количество векторов перерастёт то, что комфортно тянет один инстанс Postgres.
Собственный сервис поиска на FastAPI. Не потому что managed-поиск плох — а потому что при 120 тыс. SKU и окне в 4 недели, за которое нужно ещё поставить реранкинг и консоль мерчандайзинга, владение пайплайном обошлось дешевле по совокупной стоимости и дало генеративный слой, который клиент реально просил, а не просто лучше ранжированные результаты по ключевым словам.
Почему pgvector справился на этом масштабе: опубликованный бенчмарк HNSW от Supabase (сентябрь 2023) тестировал pgvector на эмбеддингах размерности OpenAI (1536), от ~224 тыс. до 1 миллиона векторов, и показал, что индексация HNSW даёт примерно в 3 раза больше запросов в секунду по сравнению со старым индексом IVFFlat при равной или лучшей точности на этом уровне железа. Наш каталог — 120 тыс. SKU сегодня, растущий, возможно, до 300-400 тыс. в течение пары лет — комфортно укладывается в диапазон, который этот бенчмарк реально тестировал, а не выходит за его пределы.
Почему хранение векторов в том же экземпляре Postgres важнее сырой скорости поиска: реальный риск здесь был не в задержке поиска, а в устаревании данных — векторный индекс, рассинхронизированный с живым каталогом, уверенно поднимет в топ товар, который распродался час назад. Когда эмбеддинги и строки каталога живут в одной базе данных, изменения цены/наличия и семантический ре-эмбеддинг можно рассуждать как одну систему, а не как две системы, которые могут незаметно разойтись. Мы используем паттерн асинхронного триггера с очередью, а не синхронный ре-эмбеддинг на каждой записи, который иначе блокировал бы обновления каталога на время внешнего API-вызова — реальный риск для таблицы, которую фиды поставщиков трогают тысячи раз в неделю.
Деталь, которая реально сэкономила инженерное время: поскольку изменения цены и наличия срабатывают тот же триггер записи, что и настоящее изменение контента (название, описание, категория), мы фильтруем на уровне триггера — ре-эмбеддинг запускается только при изменении семантических полей, а не на каждое колебание цены. Полноценные CDC-инструменты вроде Debezium существуют для команд, которым нужны развязанные, воспроизводимые потоки событий для нескольких потребителей, но одноинстансовый Postgres с фильтрующими триггерами покрывает реальную потребность этого каталога без второй единицы инфраструктуры в эксплуатации.
Postgres с pgvector, синхронизация через паттерн асинхронного триггера с очередью, отфильтрованный только на изменения семантических полей. Отслеживаем два триггера для пересмотра: приближение размера каталога к диапазону многих миллионов векторов, или реальная потребность в CDC-уровне воспроизводимых потоков событий для более чем одной потребляющей системы.
Почему мы выбрали именно текущее поколение модели эмбеддингов OpenAI, а не предыдущее: собственный анонс OpenAI от января 2024 даёт реальные, датированные цифры — text-embedding-3-large набирает 64.6% по среднему бенчмарка MTEB против 61.0% у предыдущей модели ada-002, скромный, но реальный прирост на англоязычном поиске. Более решающая цифра для каталога с разноязычными данными от нескольких поставщиков — MIRACL (мультиязычный retrieval): text-embedding-3-large прыгает до 54.9% против 31.4% у ada-002 — по-настоящему большое улучшение, не маргинальное. Поскольку этот каталог собирается от нескольких поставщиков с непоследовательными (иногда неанглоязычными) описаниями товаров, именно мультиязычная цифра реально повлияла на наше решение.
Почему не self-hosted открытые эмбеддинги: в текущем лидерборде MTEB топовые открытые модели (например, BGE-M3) набирают достаточно близко к моделям OpenAI и Cohere (примерно 63 против 64-65), что сырое качество не является решающим фактором — они реально конкурентоспособны. Реальный компромисс операционный: self-hosting означает владение мощностями GPU-инференса и их масштабированием, мониторингом и отказоустойчивостью; managed API эмбеддингов означает оплату за токен без какой-либо инфраструктуры в эксплуатации. Для окна разработки в 8 недель без выделенной MLOps-функции на стороне клиента именно этот операционный компромисс — а не качество модели — исключил self-hosting здесь.
text-embedding-3-small, не -large — разрыв по MTEB между small и large меньше 3 пунктов, а small стоит примерно в пять раз дешевле large за токен. На масштабе каталога с непрерывным ре-эмбеддингом при обновлениях от поставщиков эта разница в стоимости накапливается; разница в качестве на нашем масштабе не оправдывает её оплату.
Почему одного векторного сходства было недостаточно: bi-encoder ранжирует по тому, насколько близки два эмбеддинга в векторном пространстве — быстро, но это приближение релевантности, а не прямая её оценка. Бенчмарки реранкинга в независимых сравнениях на разных датасетах стабильно показывают улучшение nDCG@10 кросс-энкодерным реранкингом примерно на 5-15 пунктов по сравнению с поиском только по bi-encoder — реальный, достаточно воспроизводимый диапазон, который мы считаем защитимым базовым ожиданием, в отличие от собственного маркетингового заявления Cohere «до 25% лучше на сложных задачах retrieval», которое мы цитируем как вендорский потолок для лучшего случая, а не как среднее, которое можно обещать клиенту.
Почему добавленная задержка того стоила именно здесь: реранкинг означает второй сетевой round-trip с оценкой топ-50-200 кандидатов из векторного поиска перед выдачей финального топ-10 — реальная добавленная задержка, обычно от десятков до пары сотен миллисекунд. Для низкорискового внутреннего инструмента поиска эти накладные расходы часто того не стоят. Для поиска по каталогу e-commerce, где релевантность результатов напрямую влияет на конверсию, мы сделали противоположный выбор: дёшево получить широкий-но-неточный набор векторным поиском, а затем потратить дополнительную задержку на сужение до точного топ-10 — потому что покупатель, не нашедший нужный товар на первом экране результатов, не листает дальше, а уходит.
Cohere Rerank поверх топ-N кандидатов векторного поиска перед выдачей результатов. Это слой, наиболее напрямую ответственный за опубликованный прирост конверсии — retrieval находит правдоподобных кандидатов, а реранкинг делает так, что именно топ результатов оказывается правильным.
Одна этикетка («поиск»), две разные фронтенд-задачи. Виджет, построенный на полноценном фреймворк-рантайме, — неверная форма для стороннего embed независимо от того, какой это фреймворк; каждый виджет на витрине, который мы поставляем, — маленький, предсказуемый, фреймворк-агностичный бандл именно по этой причине.
React зарабатывает своё место на консоли мерчандайзинга по той же причине, что и на любом внутреннем операционном инструменте, который мы строим: зрелые паттерны для плотных по данным дашбордов, никаких ограничений по размеру бандла, и команда, которая, скорее всего, в итоге будет поддерживать этот инструмент, нанимает из самого широкого доступного пула специалистов. Сделать «без деплоя» реально верным утверждением означало представить правила релевантности как данные, которые команда мерчандайзинга редактирует напрямую, а не код, который выходит через спринт.
Скомпилированный, фреймворк-агностичный Web Component для поискового виджета на витрине + React для внутренней консоли релевантности мерчандайзинга.
Ни одна из трёх опубликованных цифр не случайна — каждая является следствием конкретной пары решений выше:
Опубликованные, именованные кейсы вендоров в этой сфере (кейс Algolia про ManoMano: +20% конверсии; Lacoste: +37%; Gémo: коэффициент конверсии ×2.3) ставят +25% нашего клиента ровно в достоверный, ничем не примечательный диапазон результатов «перешли на AI-поиск» — мы не заявляем аномальный результат, мы объясняем, почему попадание в середину этого диапазона потребовало всех пяти решений выше, а не просто «добавить векторную базу».
Стоит сказать прямо, потому что ни одна архитектура не вечна:
Ни один из этих пунктов не является провалом исходного решения — это условия, при которых тот же самый процесс рассуждения, запущенный заново, дал бы другой ответ.
Мы публикуем эту таблицу доверия намеренно. Клиенту полезнее документ «вот в чём мы уверены, а что мы бы перепроверили», чем документ, который выглядит чисто только потому, что неопределённость из него тихо вычистили.