Искали носки, получили брюки: сравнили шесть способов поиска на одном каталоге
Проверили полнотекстовый, векторный, гибридный, поиск по фото, геопоиск и 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 можно взять за основу.
Автор: ManticoreSearch


