⚠️ Эта страница автоматически переведена, и перевод может быть несовершенным.
blog-post

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

author image
View as markdown

«Мир изменился. Я чувствую это в воде, чувствую в земле, ощущаю в воздухе».

  • Галадриэль, «Властелин колец: Братство кольца»

С поиском тоже многое изменилось. Долгое время, когда нужно было сделать поиск по каталогу, первым делом думали о словах: как их разобрать, как учесть опечатки и как отсортировать найденное. Эти вопросы никуда не делись. Просто теперь к полнотекстовому поиску добавились векторы, гибридный поиск, поиск по фотографиям и AI, который вместо списка товаров может сразу дать ответ.

Каждый подход звучит убедительно, пока не начинаешь проверять его на конкретных запросах. Вот, например, человек ищет merino wool socks - носки из мериносовой шерсти. Векторный поиск понимает, что речь о мериносовой шерсти, и предлагает брюки, куртку, ещё одни брюки и футболку. Носки появляются только на пятом месте.

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

Для этого собрали Manticore Search Playground . В нём семь вкладок: можно искать по словам, по смыслу, по фотографии, по координатам, сравнивать выдачи и задавать вопросы AI. Причём под каждой выдачей виден SQL, который её получил, и аналогичный запрос к HTTP JSON API.

Manticore Search Playground

Получилось не просто демо с красивыми карточками, а небольшое приложение с открытым кодом . Его можно запустить локально через 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 вернёт ноль товаров.

Поиск без учёта опечаток на запрос lether jaket

Включаем исправление опечаток - и получаем 1 027 товаров. В начале списка, как и ожидалось, кожаные куртки.

Полнотекстовый поиск по lether jaket показывает результаты для leather jacket, количество товаров по категориям, SQL и команду curl

Вот запрос, который выполняется при включённой опции:

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.

Когда сумма результатов по категориям больше общего числа найденных товаров

Нет, это не ошибка в арифметике. Один товар может относиться к нескольким категориям, поэтому при подсчёте по категориям он встречается несколько раз, а в общем числе товаров - один.

Это считается так:

  1. Храним категории в JSON-массиве. У одного товара, например, могут быть сразу ["outerwear", "tops"].
  2. Для фильтрации используем categories IN ('outerwear', 'tops'): товар подходит, если у него есть хотя бы одна из выбранных категорий.
  3. Общее количество товаров получаем отдельным COUNT(*), чтобы не складывать значения фасетов и не считать один товар несколько раз.

Почему автодополнение не должно предлагать print на запрос int

Есть ещё одна привычная вещь, которая часто раздражает, если её неправильно настроить: подсказки в поисковой строке.

Откройте вкладку Full-text , выключите Search as you type (иначе выдача перекроет подсказки) и наберите linen sh. Ещё до того, как вы закончите слово, появятся linen shirt, linen shoes и linen shorts.

Подсказки автодополнения для linen sh

За это отвечает всего один запрос:

CALL AUTOCOMPLETE('linen sh', 'convapparel_products', 0 AS fuzziness, 1 AS append, 0 AS prepend)

Но сами параметры мы выбрали не случайно.

  • Передаём весь введённый текст. Для linen sh Manticore сохраняет 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 : слева у полнотекстового поиска пусто, а векторный и гибридный предлагают костюмы и комплекты с пиджаками.

Сравнение запроса outfit for a job interview: полнотекстовый поиск ничего не находит, а векторный и гибридный возвращают костюмы

Запрос merino wool socks : полнотекстовый находит носки, векторный увлекается брюками и куртками, а гибридный ставит носки первыми.

Сравнение запроса 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 - или вообще загрузить, перетащить либо вставить картинку, по которой хочется найти похожую вещь.

Поиск по изображениям для 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 можно задать этот вопрос и получить ответ, основанный на товарах из каталога.

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://github.com/manticoresoftware/demo-search-playground
cd demo-search-playground
cp .env.example .env
./scripts/init_manticore.sh
docker compose up --build app embed

После запуска откройте http://127.0.0.1:8000

В репозитории уже лежат рассчитанные векторы товаров, поэтому заново считать их при установке не нужно. Если хотите попробовать Ask AI, заранее добавьте ключ OpenRouter в OPENROUTER_API_KEY файла .env. Для остальных вкладок ключ не требуется.

При первом запуске сервис изображений скачивает модель Fashion CLIP размером около 600 МБ. Пока она загружается, другие режимы уже работают. А если захотите выполнить любой SQL со страницы вручную, используйте docker compose exec manticore mysql.

Что попробовать в Playground - короткая шпаргалка

ВозможностьВ ManticoreПопробовать
Полнотекстовый поиск с сортировкой по релевантностиMATCH(), weight()levis 501
Устойчивость к опечаткамOPTION fuzzy=1, CALL QSUGGESTlether jaket
Группировка по категориямFACETlether jaket
АвтодополнениеCALL AUTOCOMPLETEНа вкладке Full-text введите linen sh
Векторный поиск с автоматической векторизациейknn() с текстовым запросомoutfit for a job interview
Гибридный поискMATCH() + knn(), fusion_method='rrf'merino wool socks
Похожие товарыknn() с id документаОткройте любой товар
Поиск по изображениямknn() по векторам фотографийred plaid flannel shirt или загрузите фото
ГеопоискGEODIST()вкладка Geo
AI-поискCALL CHATsummer wedding on the beach
JSON APIэндпоинт /searchКоманда curl под любым результатом

Публичный Playground работает на небольшом сервере. Большинство запросов, кроме AI-чата, выполняется примерно за 5–100 мс.

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

Но это не значит, что наши примеры исчерпывают все возможные ошибки. Наверняка существует запрос, с которым не справятся ни полнотекстовый, ни векторный, ни гибридный поиск.

Сможете такой придумать?

Challenge accepted: найдите запрос, с которым не справятся все три подхода к поиску

Откройте Playground и попробуйте. Если найдёте особенно неудачный запрос, создайте issue в GitHub или напишите нам в Slack . А если хотите собрать похожий поиск в своём приложении, код Playground можно взять за основу.

Установить Manticore Search

Установите Manticore Search одной командой в Linux или macOS:

curl https://manticoresearch.com | sh

Для расширенных вариантов установки см. полное руководство по установке и документацию .

Установить Manticore Search