[Перевод] Искали носки, получили брюки: сравнили шесть способов поиска на одном каталоге

Искали носки, получили брюки: сравнили шесть способов поиска на одном каталоге
13 мин
190
Перевод
Искали носки, получили брюки: сравнили шесть способов поиска на одном каталоге
Проверили полнотекстовый, векторный, гибридный, поиск по фото, геопоиск и AI на 82 524 товарах. Показываем, где они ошибаются, что находят и какой SQL стоит за результатами.
«Мир изменился. Я чувствую это в воде, чувствую в земле, ощущаю в воздухе».
Галадриэль, «Властелин колец: Братство кольца»
С поиском тоже многое изменилось. Долгое время, когда нужно было сделать поиск по каталогу, первым делом думали о словах: как их разобрать, как учесть опечатки и как отсортировать найденное. Эти вопросы никуда не делись. Просто теперь к полнотекстовому поиску добавились векторы, гибридный поиск, поиск по фотографиям и AI, который вместо списка товаров может сразу дать ответ.
Каждый подход звучит убедительно, пока не начинаешь проверять его на конкретных запросах. Вот, например, человек ищет merino wool socks - носки из мериносовой шерсти. Векторный поиск понимает, что речь о мериносовой шерсти, и предлагает брюки, куртку, ещё одни брюки и футболку. Носки появляются только на пятом месте.
Так какой поиск лучше? Мы решили не спорить на словах, а дать разным способам поиска один и тот же каталог и посмотреть, что получится.
Для этого собрали Manticore Search Playground. В нём семь вкладок: можно искать по словам, по смыслу, по фотографии, по координатам, сравнивать выдачи и задавать вопросы AI. Причём под каждой выдачей виден SQL, который её получил, и аналогичный запрос к HTTP JSON API.

Получилось не просто демо с красивыми карточками, а небольшое приложение с открытым кодом. Его можно запустить локально через Docker Compose и посмотреть, как всё сделано. Полнотекстовый, векторный, гибридный поиск и геопоиск используют одну таблицу товаров. Для фотографий мы добавили отдельную таблицу и сервис векторизации, а для AI - CALL CHAT (SQL-команда Manticore)
Ниже покажем конкретные запросы, неожиданные результаты и SQL, который за ними стоит. Все числа по количеству результатов в выдаче и времени работы - за сентябрь 2026 года. После обновления каталога, моделей или самого приложения они могут измениться.
Что именно мы сравниваем
Мы взяли 82 524 товара из ConvApparel - датасета от Google по подбору одежды. В рекомендациях из этих диалогов есть названия, описания, характеристики и фотографии товаров.
На этих данных работают семь вкладок: Full-text, Vector, Hybrid, Image, Geo, Compare и Ask AI. Для каждого запроса Playground показывает запрос и в виде SQL и виде команды curl: их можно выполнить на своей мантикоре.
Почему это важно? Потому что сравнение поисковиков часто сводится к двум разным демо с разными данными. Здесь каталог один. Меняется только способ поиска, и разницу видно сразу.
Для большинства режимов нам хватило одной таблицы:
CREATE TABLE convapparel_products (
id bigint,
item_id text,
title text,
description text,
features text,
image_url text,
category string indexed attribute,
source text,
categories json,
lat float,
lon float,
embedding_vector float_vector knn_type='hnsw' hnsw_similarity='COSINE'
model_name='onnx-models/all-MiniLM-L12-v2-onnx' from='title,description,features'
) min_infix_len='2'
Что тут важно и что стоит иметь в виду, если вы сами делаете что-то с Manticore:
min_infix_len='2'позволяет искать части слов длиной от двух символов - это пригодится для подсказок и автодополненияmodel_nameвместе сfrom='title,description,features'включает автоматическую векторизацию. Добавляете товар - Manticore рассчитывает вектор по его текстовым полям. Ищете по смыслу - запрос преобразуется в вектор той же моделью. Для этого не нужно поднимать ещё один сервис.
С фотографиями будет иначе, но до них доберёмся попозже.
1. Полнотекстовый поиск: две опечатки - и поиск ничего не находит
Начнём с обычного полнотекстового поиска. Допустим, покупатель хотел написать leather jacket, но ввёл lether jaket. Две опечатки в двух словах - ничего необычного для интернет-магазина.
Если отключить Typo tolerance, Playground вернёт ноль товаров.
Включаем исправление опечаток - и получаем 1 027 товаров. В начале списка, как и ожидалось, кожаные куртки.
Вот запрос, который выполняется при включённой опции:
SELECT id, title, description, features, category, image_url, weight() AS text_score
FROM convapparel_products
WHERE MATCH('lether jaket')
LIMIT 12
OPTION fuzzy=1
FACET categories ORDER BY COUNT(*) DESC
Что тут происходит? MATCH() ищет по текстовым полям. weight() показывает оценку релевантности. По умолчанию используется proximity_bm25: на результат влияют и BM25, и расстояние между словами.
Но главное в этом примере - OPTION fuzzy=1. Эта опция позволяет находить слова с похожим написанием. Убрали её - получили ноль. Вернули - получили кожаные куртки.
Есть два нюанса по user experience, о которых стоит помнить, когда добавляешь такой поиск в приложение.
Покажите пользователю, что запрос был исправлен. Playground вызывает
CALL QSUGGESTи пишет Showing results for leather jacket. Если поисковик однажды неправильно угадает слово, пользователь хотя бы поймёт, откуда взялась странная выдача.В режиме исправления опечаток не предлагайте пользователям использовать весь синтаксис полнотекста Manticore. Люди вводят обычные фразы, а не специально составленные выражения для
MATCH(). В режиме поиска с опечатками символы операторов экранируются (кроме кавычек для фразового поиска).
Нашлось 1 027 товаров, а по категориям - 1 127. Как так?
В конце того же SQL есть FACET categories. Он возвращает не только товары, но и разбивку результатов по категориям. Для нашего запроса получается:
Outerwear - 960, Footwear - 58, Tops - 55, Bottoms - 54.
Складываем. Получается 1 127, хотя всего найдено 1 027.
Нет, это не ошибка в арифметике. Один товар может относиться к нескольким категориям, поэтому при подсчёте по категориям он встречается несколько раз, а в общем числе товаров - один.
Это считается так:
Храним категории в JSON-массиве. У одного товара, например, могут быть сразу
["outerwear", "tops"].Для фильтрации используем
categories IN ('outerwear', 'tops'): товар подходит, если у него есть хотя бы одна из выбранных категорий.Общее количество товаров получаем отдельным
COUNT(*), чтобы не складывать значения фасетов и не считать один товар несколько раз.
Почему автодополнение не должно предлагать print на запрос int
Есть ещё одна привычная вещь, которая часто раздражает, если её неправильно настроить: подсказки в поисковой строке.
Откройте вкладку Full-text, выключите Search as you type (иначе выдача перекроет подсказки) и наберите linen sh. Ещё до того, как вы закончите слово, появятся linen shirt, linen shoes и linen shorts.
За это отвечает всего один запрос:
CALL AUTOCOMPLETE('linen sh', 'convapparel_products', 0 AS fuzziness, 1 AS append, 0 AS prepend)
Но сами параметры мы выбрали не случайно.
Передаём весь введённый текст. Для
linen shManticore сохраняетlinenи дополняет только последнее слово —sh. Каждая строка ответа уже готовая подсказка.Не ищем совпадения посреди слова. При
0 AS prependк запросу слева не добавляется*. Иначе наintсистема может предложитьprint: технически совпадение есть, но человеку, который начал вводить слово, оно вряд ли нужно.Не исправляем опечатки прямо в подсказках. Параметр
0 AS fuzzinessозначает, что система продолжает именно введённое слово. Исправление опечаток происходит уже при поиске, и там пользователь видит, что изменилось.
Все настройки CALL AUTOCOMPLETE есть в документации.
2. Векторный поиск понимает запрос, даже если вы не знаете название товара
Теперь другая задача и другой тип поиска. Человек пишет не название вещи, а то, зачем она ему нужна: outfit for a job interview. То есть хочет подобрать одежду для собеседования.
Полнотекстовому поиску здесь непросто: он в общем случае соединяет слова через AND (можно и иначе конечно, но по дефолту так), и товаров, в которых одновременно встречаются outfit, job и interview, нет. Получаем ноль результатов.
А на вкладке Vector появляются деловые костюмы и комплекты с пиджаками. Хотя пользователь не писал ни business suit, ни blazer.
SELECT id, title, description, features, category, image_url, knn_dist() AS distance
FROM convapparel_products
WHERE knn(embedding_vector, 100, 'outfit for a job interview')
LIMIT 12
Заметьте, что в этом SQL нет готового вектора - передаётся обычный текст. Manticore преобразует его в вектор той же моделью, которой обработаны товары, и через индекс HNSW ищет ближайшие. Модель связывает слова из запроса с описаниями вроде business suit, office и blazer.
Иногда это помогает даже с опечатками. Например, warm waterprof jacket возвращает водонепроницаемые куртки, хотя никакого специального исправления опечаток здесь не включали.
Казалось бы, отлично. Но вернёмся к носкам из начала статьи.
По запросу merino wool socks первые четыре результата - брюки из мериносовой шерсти, куртка, ещё одни брюки и футболка. Носки только пятые.
Векторный поиск уловил merino wool, но слову socks не придал достаточного значения. Точно так же он может слишком свободно отнестись к цвету, размеру или конкретному типу товара. Смысл похожий, а товар всё-таки не тот.
3. Гибридный поиск: если можно искать и по словам, и по смыслу, зачем выбирать?
После примера с носками возникает закономерный вопрос: почему не объединить оба подхода?
Именно это делает гибридный поиск. Откройте merino wool socks во вкладке Hybrid. Теперь носки на первом месте, а дальше - другие товары из мериносовой шерсти.
SQL выглядит так:
SELECT id, title, description, features, category, image_url,
weight() AS text_score, knn_dist() AS distance
FROM convapparel_products
WHERE MATCH('merino wool socks')
AND knn(embedding_vector, 100, 'merino wool socks')
LIMIT 12
OPTION fuzzy=1, fusion_method='rrf'
Manticore выполняет полнотекстовый и векторный поиск, а затем объединяет их результаты через Reciprocal Rank Fusion (RRF). Проще говоря, учитывает место товара в обоих списках. Если он стоит высоко и там, и там, это помогает ему подняться в общей выдаче.
В интерфейсе это видно по меткам Words, Meaning и Words + meaning. Они показывают, какой из способов нашёл конкретный товар.
Для случая, когда обе части используют один и тот же текст, есть и короткая запись - hybrid_match(). Она сама запускает полнотекстовый поиск, преобразует запрос в вектор, выполняет KNN и объединяет выдачи через RRF.
Ещё один пример — bicycle shoes. Велотуфли MTB и Unisex Bike Shoes находятся и по словам, и по смыслу. Векторный поиск добавляет Cycling Shoes, в которых вообще не встречается слово bicycle. Но есть и неожиданный результат: на первом месте оказываются классические оксфорды Paul Parkman, не имеющие отношения к велоспорту. Просто у них носок типа bicycle toe. Из-за этого совпадения они возглавляют полнотекстовую выдачу, а поскольку векторный поиск тоже их находит, гибридный ставит их первыми.
А для warm waterprof jacket полнотекстовая часть исправляет опечатку, пока векторная помогает подобрать тёплые водонепроницаемые куртки. Оба способа полезны, просто по-разному.
Сравнили три выдачи рядом - разница стала очевиднее
Чтобы не переключаться туда-сюда, мы сделали вкладку Compare. Она запускает запрос сразу тремя способами - Full-text, Vector и Hybrid - и показывает результаты в соседних колонках.
Если один товар встретился в нескольких выдачах, Playground это отмечает. Навели курсор на карточку - подсветились все её появления. Иногда полнотекстовый и векторный поиск действительно находят одно и то же:
Но интереснее случаи, когда они расходятся.
Запрос outfit for a job interview: слева у полнотекстового поиска пусто, а векторный и гибридный предлагают костюмы и комплекты с пиджаками.
Запрос merino wool socks: полнотекстовый находит носки, векторный увлекается брюками и куртками, а гибридный ставит носки первыми.
Для такого сравнения лучше всего брать не придуманные нами фразы, а запросы, которые вводят ваши пользователи. Если хотите просто посмотреть, как это работает, начните с bicycle shoes, lether jaket или steel toe boots.
Похожие товары: можно обойтись даже без текста запроса
Допустим, посетитель уже открыл кожаную куртку и хочет посмотреть что-то похожее. Ему не нужно снова вводить слова в поисковую строку.
В карточках Playground для этого есть два блока: Similar by description и Similar by photo.
Для поиска похожих товаров по описанию выполняется такой SQL:
SELECT id, title, description, features, category, image_url, knn_dist() AS distance
FROM convapparel_products
WHERE knn(embedding_vector, 8, 2388085963546244483)
LIMIT 8
Здесь число в knn() - это id товара, а не сам вектор. Manticore находит сохранённый вектор товара и ищет по нему ближайшие. Приложению не нужно сначала вытаскивать вектор из базы, а потом отправлять его обратно.
Для похожих фотографий принцип тот же. Только векторы изображений лежат в отдельной таблице.
4. Поиск по фотографиям
Пробовали описать в магазинной карточке конкретную клетчатую рубашку? Название и характеристики конечно несут какую-то информацю о ткани и размере, но не всегда помогут представить рисунок и оттенок. Здесь полезнее смотреть на сами фотографии.
На вкладке Image можно ввести red plaid flannel shirt - или вообще загрузить, перетащить либо вставить картинку, по которой хочется найти похожую вещь.
В демо, используя специализированную модель, мы преобразуем и текст, и фотографии в сопоставимые векторы. Поэтому описание красной клетчатой рубашки и её изображение оказываются рядом в многомерном пространстве при векторном поиске.
В отличие от текстового векторного поиска, для изображений используется отдельный сервис векторизации. Сначала он рассчитывает вектор запроса - в публичном Playground это обычно занимает 50–250 мс, и время показывается как turned into a vector in … ms. Затем Manticore ищет ближайшие векторы фотографий и получает из основной таблицы соответствующие товары.
SELECT id, knn_dist() AS distance
FROM convapparel_product_images
WHERE knn(image_vector, 100, (-0.0021, -0.01, 0.0782, …))
LIMIT 100;
SELECT id, title, description, features, category, image_url
FROM convapparel_products
WHERE id IN (3698494869106650404, 3557146672156430291, 1902002584948931003, …)
LIMIT 100
Здесь мы сократили SQL для наглядности: в настоящем запросе передаются все 512 чисел вектора и 100 айдишек товаров.
В реализации есть два решения, которые могут пригодиться и в вашем приложении:
Поиск по изображениям не зависит от основного каталога. Для текста мы в данном случае использовали MiniLM, а для фотографий - модель Fashion CLIP. Векторы изображений храним в отдельной таблице, поэтому можем заменить модель или пересчитать все векторы фотографий, не затрагивая товары и их текстовый поиск.
Не индексируем одинаковые картинки-заглушки. Если вы вдруг сами кравлите, то понимаете, что стандартная заглушка No image available ничего не говорит о товаре, даже если встречается тысячу раз. Заглушки лучше вообще не индексировать. Поэтому в нашем демо из 82524 товаров векторов изображений нет ну 0.3%.
5. Геопоиск: товары на карте
С координатами была одна проблемка: в исходном ConvApparel их нет. Чтобы показать геопоиск, мы присвоили товарам случайные координаты в районе Нью-Йорка просто, чтобы показать сам механизм работы геопоиска.
Во вкладке Geo можно перетащить точку по карте, изменить радиус и тут же увидеть, каким стал SQL.
SELECT id, title, description, features, category, image_url, lat, lon,
GEODIST(40.712800, -74.006000, lat, lon, {in=degrees, out=km}) AS distance_km
FROM convapparel_products
WHERE distance_km <= 10
ORDER BY id ASC
LIMIT 100
GEODIST() считает расстояние. Его результат, если нужно, можно вывести в SELECT, использовать в фильтре или сортировке.
В нашем демо точки сортируются по id, чтобы они распределялись по всей выбранной области. Для реальной задачи типа “покажи ближайшие магазины” логичнее ORDER BY distance_km ASC: тогда первыми окажутся ближайшие объекты.
6. AI-поиск: умный совет дороже золота
А если покупатель вообще не хочет разбираться в фильтрах и списках? Допустим, он спрашивает: What should I wear to a summer wedding on the beach? - что надеть на летнюю свадьбу на пляже?
На вкладке Ask AI можно задать этот вопрос и получить ответ, основанный на товарах из каталога.
В Manticore для этого есть CALL CHAT:
CALL CHAT('What should I wear to a summer wedding on the beach?',
'convapparel_products', 'shopping_assistant', '', 'embedding_vector')
CALL CHAT работает с таблицей товаров и векторным индексом. Для ответа система может сначала найти подходящие товары и передать языковой модели сведения о них. В нашем случае это поля title, description и features. Playground добавляет к ответу ссылки на найденные карточки.
Диалог можно продолжить. Например, сначала спросить I need warm boots for walking the dog in snow, а следом - Which of those are waterproof? Контекст предыдущего вопроса сохраняется, и не приходится заново объяснять, о каких ботинках речь.
Что стоит учесть:
Показывайте товары, на которые ссылается AI. Если он уверенно советует «дышащее платье», пусть человек сможет открыть карточку и проверить, действительно ли это написано в описании.
Не ждите, что языковая модель исправит все ошибки поиска. Сначала подходящие товары должны попасть в контекст. Если нужная вещь не нашлась на этом этапе, AI не сможет надёжно учесть её в рекомендации. Соответственно влияет и качество модели и настройки полнотекстового поиска.
И ещё скорость. В публичном Playground ответ AI обычно занимает 5–15 секунд. Это тоже зависит много от чего, но в первую очередь от LLM-модели и провайдера LLM (или вашего железа, если вы запускаете всё локально, что тоже вполне возможно с Manticore). О настройке чата и оценке качества поиска подробнее писали в отдельной статье.
А если мне нужен не SQL, а HTTP API?
Manticore говорит на двух языках: SQL и JSON. Про SQL было приведено много примеров выше, для любителей JSON через HTTP в демо помимо SQL те же примеры приведены в виде команды curl. Она обращается к HTTP JSON API и выполняет тот же поиск, что показан в SQL.
Например, для запроса с опечатками команда выглядит так:
curl -X POST localhost:9308/search \
-H 'Content-Type: application/json' \
-d '{
"table": "convapparel_products",
"query": {"query_string": "lether jaket"},
"options": {"fuzzy": true},
"limit": 12,
"_source": ["title", "description", "features", "category", "image_url"],
"aggs": {"categories": {"terms": {"field": "categories"}, "sort": [{"count(*)": {"order": "desc"}}]}}
}'
Если нужная таблица уже загружена в Manticore, команда вернёт те же товары и разбивку по категориям.
Кстати, возвращаясь к теме того, что по умолчанию полнотекст объединяет все слова, которой коснулись выше: здесь есть неочевидная разница между двумя вариантами JSON-запроса. query_string, как и SQL MATCH(), по умолчанию соединяет слова через AND. А JSON-запрос match - через OR. Если переписываете запрос из одного формата в другой, об этом лучше помнить.
Всё можете запустить у себя локально
Playground открыт на GitHub. Чтобы запустить его локально, достаточно Docker Compose:
git clone https://mnt.cr/go/KMnCwP
cd demo-search-playground
cp .env.example .env
./scripts/init_manticore.sh
docker compose up --build app embed
После запуска откройте https://mnt.cr/go/3j4ju6
В репозитории уже лежат рассчитанные векторы товаров, поэтому заново считать их при установке не нужно. Если хотите попробовать Ask AI, заранее добавьте ключ OpenRouter в OPENROUTER_API_KEY файла .env. Для остальных вкладок ключ не требуется.
При первом запуске сервис изображений скачивает модель Fashion CLIP размером около 600 МБ. Пока она загружается, другие режимы уже работают. А если захотите выполнить любой SQL со страницы вручную, используйте docker compose exec manticore mysql.
Что попробовать в Playground - короткая шпаргалка
Возможность | В Manticore | Попробовать |
|---|---|---|
Полнотекстовый поиск с сортировкой по релевантности |
| |
Устойчивость к опечаткам |
| |
Группировка по категориям |
| |
Автодополнение |
| На вкладке Full-text введите |
Векторный поиск с автоматической векторизацией |
| |
Гибридный поиск |
| |
Похожие товары |
| Откройте любой товар |
Поиск по изображениям |
| red plaid flannel shirt или загрузите фото |
Геопоиск |
| |
AI-поиск |
| |
JSON API | эндпоинт | Команда |
Публичный Playground работает на небольшом сервере. Большинство запросов, кроме AI-чата, выполняется примерно за 5–100 мс.
На этих примерах видно, почему нет смысла выбирать один способ поиска на все случаи. Полнотекстовый хорош, когда важны сами слова; векторный помогает, когда пользователь описывает задачу; гибридный сочетает оба подхода. Для фотографий и координат нужны свои способы поиска, а AI пригодится, когда вместо списка товаров хочется получить готовый ответ.
Но это не значит, что наши примеры исчерпывают все возможные ошибки. Наверняка существует запрос, с которым не справятся ни полнотекстовый, ни векторный, ни гибридный поиск.
Сможете такой придумать?
Откройте Playground и попробуйте. Если найдёте особенно неудачный запрос, создайте issue в GitHub или напишите нам в Slack. А если хотите собрать похожий поиск в своём приложении, код Playground можно взять за основу.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.