Векторний пошук для e-commerce: чому ключові слова не працюють при масштабі і що будувати натомість
Ми перебудовували пошук з нуля на каталогах від 30 000 SKU до понад двох мільйонів — патерн скрізь однаковий: пошук за ключовими словами тримається на малих обсягах, потім розсипається так, що важко поставити діагноз. Запити повертають нуль результатів за товарами, які точно є. Синоніми не враховуються. Користувач пише "кросівки для бігу" і отримує туфлі, бо слово "взуття" зустрічається в описі. Це не проблема конфігурації. Це фундаментальне обмеження інвертованого індексу — і векторний пошук тут правильний інструмент, а не срібна куля, яку можна просто вставити без міркувань.
Гібридний пайплайн пошуку: один і той самий запит іде двома шляхами одночасно — ключовим (BM25/інвертований індекс) і векторним (модель ембедингів → наближений індекс найближчих сусідів) — потім обидва результати зливаються і проходять через reranker до того, як потрапити до користувача. Розподіл відбувається на етапі запиту; побудова індексу — окремий офлайн-процес.
Що насправді робить BM25 і де ламається
BM25 — функція ранжування під капотом Elasticsearch, OpenSearch і більшості Solr-установок — оцінює документи за частотою терміна і зворотною частотою документа. Терміни, що часто зустрічаються в документі і рідко — у корпусі, отримують високу вагу. Елегантний алгоритм із 30 роками виробничого використання за плечима, і для багатьох завдань це саме те, що потрібно.
Проблема проявляється при масштабі. Каталог із 50 000+ SKU накопичує довгохвостові запити — нішеві назви, абревіатури брендів, розмовні терміни, помилки — яких індекс ніколи не бачив як буквальні рядки. BM25 не може вивести, що "NB 574" і "New Balance 574" описують одну й ту саму кросівку. Він не знає, що "зарядник" і "зарядний кабель" пов'язані. Це суто лексичний матчинг: якщо токен не в індексі, він нічого не додає до оцінки.
Найчастіші режими відмови: сторінки нульових результатів за запитами, які мають повертати товари (сліпота до синонімів); нерелевантні товари, бо вони ділять один високочастотний токен із запитом (невідповідність наміру); і результати, що технічно коректні, але кладуть найбільш релевантну позицію на третю сторінку через різницю термінології. Всі три вбивають конверсію, і всі три погіршуються в міру зростання каталогу. Магазин із 5 000 товарів може заткнути це агресивними словниками синонімів. На 200 000 товарів цей підхід стає некерованим.
Що насправді робить векторний пошук
Векторний пошук не матчить токени. Він конвертує і запити, і документи в щільні числові вектори — зазвичай від 384 до 1536 вимірів залежно від моделі — і знаходить документи, чиї вектори близькі до вектора запиту в цьому багатовимірному просторі. "Близько" — це косинусна подібність вище певного порогу, або практично: top-K найближчих сусідів.
Ембединги беруться з мовної моделі, навченої на величезних масивах тексту, а це означає, що модель уже вивчила: "кросівки" і "бігове взуття" знаходяться поруч у просторі векторів, "NB 574" відображається на бренд New Balance, а "зарядний кабель" пов'язаний із "USB-C зарядником". Це семантичне розуміння дістається практично безкоштовно — без підтримки словників синонімів.
На практиці: попередньо обчислюєте ембединги для кожного товару в каталозі — назва, опис, атрибути, шлях категорії — конкатенуєте або пулюєте їх, зберігаєте результуючий вектор в індексі для пошуку найближчих сусідів. На етапі запиту — ембедите пошуковий запит тією самою моделлю, потім запитуєте індекс. При прогрітому індексі все це працює за одиниці мілісекунд. Що не безкоштовно: вибір правильної моделі ембедингів під ваш домен, актуалізація ембедингів при зміні каталогу і інфраструктура для обслуговування ANN-індексу з малою затримкою.
Гібридний пошук: чому майже завжди потрібні обидва
Де чистий векторний пошук дає збій
Чистий векторний пошук має реальні слабкі місця в комерції. Точний пошук — одне з них. Якщо користувач шукає конкретний артикул — "WD-40 300011" або "SKU-RF2291-BLK" — BM25 знаходить його тривіально. Векторна модель може не знайти: вона навчена на природній мові, а не на каталожних кодах, і ембединг SKU — по суті шум. Отримуєте ситуації, коли точний запит повертає семантично схожі, але неправильні результати.
Рідкісні власні назви — інша велика діра. Маловідомі торгові марки, нішеві номери моделей, назви товарів, що є у вашому каталозі, але слабко представлені в навчальних даних моделі — у всіх них будуть низькоякісні ембединги. Модель не знає, що означає "Ridgid 18V", якщо бачила це лише кілька разів під час попереднього навчання.
Поєднання BM25 і вектора через reciprocal rank fusion
Стандартний підхід — запустити обидва ретривери паралельно і змержити списки результатів. Reciprocal rank fusion (RRF) — стратегія злиття, до якої ми звертаємось першою: для кожного документа обчислюємо оцінку на основі його рангу в кожному списку — 1/(k + rank), де k зазвичай 60 — і підсумовуємо. Стійкий до різниці в масштабі оцінок BM25 і косинусної подібності, не вимагає підбору ваг під кожен тип запиту, добре працює на широкому діапазоні конфігурацій каталогів. Ми порівнювали його з підходами лінійної комбінації — RRF стабільно виходить вперед або на одному рівні при значно меншій чутливості до параметрів.
Відносна вага і маршрутизація запитів
Одне покращення, яке варто реалізувати: маршрутизуйте різні типи запитів на різні суміші. Запити, що схожі на точні ідентифікатори (буквено-цифрові зі специфічними патернами), ідуть у режим із переважанням BM25. Запити на природній мові — повні речення, розмовні формулювання, питання — сильно зміщуються в бік вектора. Для цього не потрібен складний класифікатор; кілька regex-правил і евристики за довжиною запиту дають 80% результату. Решту — налаштування за реальними логами запитів.
Reranker стоїть між ретривалом і підсумковим списком результатів. Cross-encoder бачить повну пару запит-документ, що повільніше, але набагато точніше, ніж косинусна подібність ембедингів сама по собі. Бізнес-правила (маржа, залишки, промоакції) застосовуються як фінальний множник поверх семантичної оцінки.
Reranking: де релевантність зустрічається з бізнес-логікою
Ретривал — BM25 і ANN разом — дає набір кандидатів, зазвичай top-50 до 200 документів. Reranking переоцінює цей набір більш затратною моделлю і застосовує зверху бізнес-правила. Це обчислювально реалізовано, бо ви оцінюєте десятки результатів, а не скануєте мільйони.
- Cross-encoder rerankers — моделі на кшталт BGE-Reranker або Cohere Rerank бачать повну конкатеновану пару запит-документ, а не окремі ембединги. Повільніше, але набагато точніше: модель може міркувати про зв'язок конкретного запиту з конкретним текстом документа, а не лише про усереднену семантичну близькість.
- Персоналізаційні сигнали — кліковість, конверсія та оцінки афінітету з рекомендаційної системи можна підмішати як мультиплікативні або адитивні фактори поверх семантичної оцінки.
- Інвентар і наявність — товари, яких немає в наявності, треба прибирати або опускати в хвіст. Нуль залишків = майже нульовий ранг, незалежно від семантичної релевантності. Ми бачили магазини, що направляли трафік із високим наміром на OOS-товари, бо релевантність ігнорувала залишки.
- Маржа і промо-прапорці — якщо товар акційний або несе вищу маржу, трохи підняти його — законне бізнес-правило. Тримайте це тонко, щоб не перебивати справжню релевантність, інакше підірвете довіру.
- Свіжість — новинки часто виграють від раннього бусту трафіку, але цей сигнал затухає в міру накопичення даних кліків і покупок, і товар починає ранжуватись за власними заслугами.
Порядок важливий: спочатку ретривал (швидкий, широкий), потім reranking кандидатів (повільніше, точніше), потім бізнес-правила останніми (детерміновані оверрайди). Змішувати бізнес-правила у фазу ретривалу спокусливо, але це створює кошмар при налагодженні.
Проблема холодного старту для нових товарів
У нових товарів немає даних кліків, історії покупок, відгуків. Для систем на основі колаборативної фільтрації це відома проблема з відомими рішеннями. Для пошуку — інша: товар одразу з'явиться у векторному пошуку (з першого дня у нього є ембединг), але у нього немає поведінкового сигналу для бусту в reranker. Свіжі товари можуть ранжуватись нижче застарілих, менш релевантних позицій просто тому, що ті накопичили дані кліків.
Ми вирішуємо це через вікно свіжості: нові товари отримують статичний буст оцінки на перші 7-14 днів у каталозі, відкалібрований так, щоб не підняти зовсім нерелевантні позиції наверх, але дати по-справжньому релевантним новинкам чесний шанс на видимість. Після закінчення вікна товар ранжується суто на основі семантичної релевантності і поведінкового сигналу.
Збагачення каталогу теж важливе. Товари з тонким контентом — лише назва і SKU, без опису — дають слабкі ембединги. Перед індексацією ми робимо легкий прохід збагачення: підтягуємо доступні атрибути з PIM, генеруємо короткий опис, якщо його немає, додаємо хлібні крихти категорій і синоніми тегів. Одне це відчутно покращує recall за новими товарами. Якість контенту у ваших даних про товари прямо пропорційна якості пошукових ембедингів.
Затримка і інфраструктура: між чим ви реально обираєте
ANN-індекси — графи Hierarchical Navigable Small World (HNSW) стали стандартом — обмінюють recall на швидкість. Точний пошук найближчих сусідів по мільйону векторів надто повільний для інтерактивного пошуку; HNSW дає 95-99% recall при мілісекундних затримках, досліджуючи структуру графу замість повного сканування. Цей компроміс майже завжди виправданий при комерційних масштабах.
За варіантами інфраструктури: pgvector — розумний вибір для каталогів до ~500k векторів, коли хочете мінімізувати операційну складність і вже працюєте з Postgres. До мільйонів векторів і високого QPS без read-реплік і агресивного налаштування індексу не дотягне. Qdrant і Weaviate — спеціалізовані векторні бази даних із нормальними HNSW, фільтрацією за метаданими і горизонтальним масштабуванням — їхні операційні накладні виправдані після ~200k товарів або ~100 запитів на секунду. Elasticsearch із dense_vector — хороший вибір, якщо вже працюєте з ES і хочете уникнути нового сервісу; реалізація HNSW зріла, гібридний шлях BM25+вектор добре задокументований. Хостингові сервіси (Pinecone, Zilliz) знімають інфраструктурні турботи, але додають вартість за вектор і за запит, яка швидко набігає на великих каталогах.
Цільова затримка: результати пошуку менше 200 мс на p95, не більше 400 мс на p99. Цей бюджет має покрити ембединг запиту, обидва ретривери, злиття і reranking. Якщо додаєте cross-encoder reranker, очікуйте 80-120 мс на CPU — варто прогрівати модель заздалегідь і розглянути GPU-інференс для високонавантажених магазинів.
Що будувати першим: версія 80/20
Перш ніж торкатися архітектури індексу, виправте шар розуміння запитів. Приберіть стоп-слова (крім значущих: "без" у "біле плаття без рукавів" важливо). Виправте очевидні помилки — простий spell checker на основі редакційної відстані ловить більшість із них. Розгорніть абревіатури і нормалізуйте назви брендів. Визначте намір: це навігаційний запит ("магазин Nike"), продуктовий ("білі Nike Air Force 1 розмір 43") або інформаційний ("як чистити замшу")? Маршрутизуйте їх по-різному. Ми бачили, як покращення препроцесингу запитів самотужки знижували частку нульових результатів з 8% до менше 2% — без змін в індексі.
Коли розуміння запитів стабілізується, додайте векторний пошук як паралельний шлях до існуючого keyword-пошуку. Не замінюйте BM25 — доповнюйте. Виміряйте NDCG і MRR на тестовій вибірці запитів із відомими хорошими результатами до і після. Відстежуйте конверсію на пошуковий клік, а не просто CTR: CTR може зростати при падінні конверсії, якщо ви показуєте нерелевантні результати, що виглядають релевантно. Проведіть нормальний A/B-тест — holdout 10-20% трафіку — перш ніж повністю переходити. Останнє, у що інвестувати, — reranker. Почніть із RRF плюс базові бізнес-правила. Cross-encoder додавайте, коли базовий гібридний ретривал устаканиться і обсяг запитів достатній (5 000+ пошуків на день) для точного вимірювання приросту.
Типові помилки, яких варто уникати
Дрейф ембедингів — недооцінений режим відмови. Ваша модель ембедингів — це знімок. Якщо ви індексували каталог за допомогою text-embedding-ada-002 у 2024-му, а запитуєте через text-embedding-3-large у 2026-му — вектори несумісні. Оберіть модель і тримайтеся її, або будуйте пайплайн для повторного ембединга всього каталогу при апгрейді. Те саме стосується дообучених моделей: дообучіть на доменних даних, переіндексуйте все, потім деплойте. Не змішуйте.
Одномовні моделі на багатомовних каталогах створюють більше проблем, ніж команди очікують. Модель, навчена переважно на англійській, дає погані ембединги для французьких, німецьких або арабських описів товарів. Для багатомовних магазинів використовуйте багатомовні моделі (multilingual-e5-large, LaBSE або paraphrase-multilingual-mpnet-base-v2) і тестуйте якість ретривалу окремо для кожної мови. Розрив у якості між мовою, на якій модель інтенсивно навчалась, і тією, на якій ні, може бути разючим.
Переінжиніринг індексу до виправлення препроцесингу запитів — мабуть, найпоширеніша помилка, яку ми бачимо. Команди тижнями порівнюють Weaviate, Qdrant і Pinecone, поки їхній spell checker видає сміття на вході. Індекс стоїть після всієї іншої логіки — сміття на вході, сміття на виході. Спочатку виправте вхід, виміряйте, де реально падає recall, і лише потім інвестуйте в індексний шар, що вирішує саме ці провали.
Наступний крок
Працюєте над складною commerce-системою?
Ми допомагаємо інженерним командам проєктувати, будувати та масштабувати високонавантажені платформи — з чітким процесом та передбачуваними строками.
Поговорімо