- BrainTools - https://www.braintools.ru -
Проверили полнотекстовый, векторный, гибридный, поиск по фото, геопоиск и AI на 82 524 товарах. Показываем, где они ошибаются, что находят и какой SQL стоит за результатами.
«Мир изменился. Я чувствую это в воде, чувствую в земле, ощущаю в воздухе».
Галадриэль, «Властелин колец: Братство кольца»
С поиском тоже многое изменилось. Долгое время, когда нужно было сделать поиск по каталогу, первым делом думали о словах: как их разобрать, как учесть опечатки и как отсортировать найденное. Эти вопросы никуда не делись. Просто теперь к полнотекстовому поиску добавились векторы, гибридный поиск, поиск по фотографиям и AI, который вместо списка товаров может сразу дать ответ.
Каждый подход звучит убедительно, пока не начинаешь проверять его на конкретных запросах. Вот, например, человек ищет merino wool socks – носки из мериносовой шерсти. Векторный поиск понимает, что речь о мериносовой шерсти, и предлагает брюки, куртку, ещё одни брюки и футболку. Носки появляются только на пятом месте.
Так какой поиск лучше? Мы решили не спорить на словах, а дать разным способам поиска один и тот же каталог и посмотреть, что получится.
Для этого собрали Manticore Search Playground [1]. В нём семь вкладок: можно искать по словам, по смыслу, по фотографии, по координатам, сравнивать выдачи и задавать вопросы AI. Причём под каждой выдачей виден SQL, который её получил, и аналогичный запрос к HTTP JSON API.
Получилось не просто демо с красивыми карточками, а небольшое приложение с открытым кодом [2]. Его можно запустить локально через 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' включает автоматическую векторизацию [3]. Добавляете товар – Manticore рассчитывает вектор по его текстовым полям. Ищете по смыслу – запрос преобразуется в вектор той же моделью. Для этого не нужно поднимать ещё один сервис.
С фотографиями будет иначе, но до них доберёмся попозже.
Начнём с обычного полнотекстового поиска. Допустим, покупатель хотел написать leather jacket, но ввёл lether jaket [4]. Две опечатки в двух словах – ничего необычного для интернет-магазина.
Если отключить 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 [5]. Эта опция позволяет находить слова с похожим написанием. Убрали её – получили ноль. Вернули – получили кожаные куртки.
Есть два нюанса по user experience, о которых стоит помнить, когда добавляешь такой поиск в приложение.
Покажите пользователю, что запрос был исправлен. Playground вызывает CALL QSUGGEST [6] и пишет Showing results for leather jacket. Если поисковик однажды неправильно угадает слово, пользователь хотя бы поймёт, откуда взялась странная выдача.
В режиме исправления опечаток не предлагайте пользователям использовать весь синтаксис полнотекста Manticore. Люди вводят обычные фразы, а не специально составленные выражения для MATCH(). В режиме поиска с опечатками символы операторов экранируются (кроме кавычек для фразового поиска).
В конце того же SQL есть FACET categories. Он возвращает не только товары, но и разбивку результатов по категориям [7]. Для нашего запроса получается:
Outerwear – 960, Footwear – 58, Tops – 55, Bottoms – 54.
Складываем. Получается 1 127, хотя всего найдено 1 027.
Нет, это не ошибка [8] в арифметике. Один товар может относиться к нескольким категориям, поэтому при подсчёте по категориям он встречается несколько раз, а в общем числе товаров – один.
Это считается так:
Храним категории в JSON-массиве. У одного товара, например, могут быть сразу ["outerwear", "tops"].
Для фильтрации используем categories IN ('outerwear', 'tops'): товар подходит, если у него есть хотя бы одна из выбранных категорий.
Общее количество товаров получаем отдельным COUNT(*), чтобы не складывать значения фасетов и не считать один товар несколько раз.
Есть ещё одна привычная вещь, которая часто раздражает, если её неправильно настроить: подсказки в поисковой строке.
Откройте вкладку Full-text [9], выключите 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 sh Manticore сохраняет linen и дополняет только последнее слово — sh. Каждая строка ответа уже готовая подсказка.
Не ищем совпадения посреди слова. При 0 AS prepend к запросу слева не добавляется *. Иначе на int система может предложить print: технически совпадение есть, но человеку, который начал вводить слово, оно вряд ли нужно.
Не исправляем опечатки прямо в подсказках. Параметр 0 AS fuzziness означает, что система продолжает именно введённое слово. Исправление опечаток происходит уже при поиске, и там пользователь видит, что изменилось.
Все настройки CALL AUTOCOMPLETE [10] есть в документации.
Теперь другая задача и другой тип поиска. Человек пишет не название вещи, а то, зачем она ему нужна: outfit for a job interview [11]. То есть хочет подобрать одежду для собеседования.
Полнотекстовому поиску здесь непросто: он в общем случае соединяет слова через 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 [12] ищет ближайшие. Модель связывает слова из запроса с описаниями вроде business suit, office и blazer.
Иногда это помогает даже с опечатками. Например, warm waterprof jacket [13] возвращает водонепроницаемые куртки, хотя никакого специального исправления опечаток здесь не включали.
Казалось бы, отлично. Но вернёмся к носкам из начала статьи.
По запросу merino wool socks [14] первые четыре результата – брюки из мериносовой шерсти, куртка, ещё одни брюки и футболка. Носки только пятые.
Векторный поиск уловил merino wool, но слову socks не придал достаточного значения. Точно так же он может слишком свободно отнестись к цвету, размеру или конкретному типу товара. Смысл похожий, а товар всё-таки не тот.
После примера с носками возникает закономерный вопрос: почему не объединить оба подхода?
Именно это делает гибридный поиск. Откройте merino wool socks во вкладке Hybrid [15]. Теперь носки на первом месте, а дальше – другие товары из мериносовой шерсти.
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) [16]. Проще говоря, учитывает место товара в обоих списках. Если он стоит высоко и там, и там, это помогает ему подняться в общей выдаче.
В интерфейсе это видно по меткам Words, Meaning и Words + meaning. Они показывают, какой из способов нашёл конкретный товар.
Для случая, когда обе части используют один и тот же текст, есть и короткая запись – hybrid_match() [17]. Она сама запускает полнотекстовый поиск, преобразует запрос в вектор, выполняет KNN и объединяет выдачи через RRF.
Ещё один пример — bicycle shoes [18]. Велотуфли MTB и Unisex Bike Shoes находятся и по словам, и по смыслу. Векторный поиск добавляет Cycling Shoes, в которых вообще не встречается слово bicycle. Но есть и неожиданный результат: на первом месте оказываются классические оксфорды Paul Parkman, не имеющие отношения к велоспорту. Просто у них носок типа bicycle toe. Из-за этого совпадения они возглавляют полнотекстовую выдачу, а поскольку векторный поиск тоже их находит, гибридный ставит их первыми.
А для warm waterprof jacket [19] полнотекстовая часть исправляет опечатку, пока векторная помогает подобрать тёплые водонепроницаемые куртки. Оба способа полезны, просто по-разному.
Чтобы не переключаться туда-сюда, мы сделали вкладку Compare. Она запускает запрос сразу тремя способами – Full-text, Vector и Hybrid – и показывает результаты в соседних колонках.
Если один товар встретился в нескольких выдачах, Playground это отмечает. Навели курсор на карточку – подсветились все её появления. Иногда полнотекстовый и векторный поиск действительно находят одно и то же:
Но интереснее случаи, когда они расходятся.
Запрос outfit for a job interview [20]: слева у полнотекстового поиска пусто, а векторный и гибридный предлагают костюмы и комплекты с пиджаками.
Запрос merino wool socks [21]: полнотекстовый находит носки, векторный увлекается брюками и куртками, а гибридный ставит носки первыми.
Для такого сравнения лучше всего брать не придуманные нами фразы, а запросы, которые вводят ваши пользователи. Если хотите просто посмотреть, как это работает, начните с bicycle shoes [22], lether jaket [23] или steel toe boots [24].
Допустим, посетитель уже открыл кожаную куртку и хочет посмотреть что-то похожее. Ему не нужно снова вводить слова в поисковую строку.
В карточках 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 товара [25], а не сам вектор. Manticore находит сохранённый вектор товара и ищет по нему ближайшие. Приложению не нужно сначала вытаскивать вектор из базы, а потом отправлять его обратно.
Для похожих фотографий принцип тот же. Только векторы изображений лежат в отдельной таблице.
Пробовали описать в магазинной карточке конкретную клетчатую рубашку? Название и характеристики конечно несут какую-то информацю о ткани и размере, но не всегда помогут представить рисунок и оттенок. Здесь полезнее смотреть на сами фотографии.
На вкладке Image можно ввести red plaid flannel shirt [26] – или вообще загрузить, перетащить либо вставить картинку, по которой хочется найти похожую вещь.
В демо, используя специализированную модель, мы преобразуем и текст, и фотографии в сопоставимые векторы. Поэтому описание красной клетчатой рубашки и её изображение оказываются рядом в многомерном пространстве при векторном поиске.
В отличие от текстового векторного поиска, для изображений используется отдельный сервис векторизации. Сначала он рассчитывает вектор запроса – в публичном 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%.
С координатами была одна проблемка: в исходном ConvApparel их нет. Чтобы показать геопоиск, мы присвоили товарам случайные координаты в районе Нью-Йорка просто, чтобы показать сам механизм работы геопоиска.
Во вкладке Geo [27] можно перетащить точку по карте, изменить радиус и тут же увидеть, каким стал 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() [28] считает расстояние. Его результат, если нужно, можно вывести в SELECT, использовать в фильтре или сортировке.
В нашем демо точки сортируются по id, чтобы они распределялись по всей выбранной области. Для реальной задачи типа “покажи ближайшие магазины” логичнее ORDER BY distance_km ASC: тогда первыми окажутся ближайшие объекты.
А если покупатель вообще не хочет разбираться в фильтрах и списках? Допустим, он спрашивает: What should I wear to a summer wedding on the beach? [29] – что надеть на летнюю свадьбу на пляже?
На вкладке 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 [30] работает с таблицей товаров и векторным индексом. Для ответа система может сначала найти подходящие товары и передать языковой модели сведения о них. В нашем случае это поля 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). О настройке чата и оценке качества поиска подробнее писали в отдельной статье [31].
Manticore говорит на двух языках: SQL и JSON. Про SQL было приведено много примеров выше, для любителей JSON через HTTP в демо помимо SQL те же примеры приведены в виде команды curl. Она обращается к HTTP JSON API [32] и выполняет тот же поиск, что показан в 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 [2]. Чтобы запустить его локально, достаточно 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 [33]
В репозитории уже лежат рассчитанные векторы товаров, поэтому заново считать их при установке не нужно. Если хотите попробовать Ask AI, заранее добавьте ключ OpenRouter в OPENROUTER_API_KEY файла .env. Для остальных вкладок ключ не требуется.
При первом запуске сервис изображений скачивает модель Fashion CLIP размером около 600 МБ. Пока она загружается, другие режимы уже работают. А если захотите выполнить любой SQL со страницы вручную, используйте docker compose exec manticore mysql.
|
Возможность |
В Manticore |
Попробовать |
|---|---|---|
|
Полнотекстовый поиск с сортировкой по релевантности |
|
levis 501 [34] |
|
Устойчивость к опечаткам |
|
lether jaket [4] |
|
Группировка по категориям |
|
lether jaket [4] |
|
Автодополнение |
|
На вкладке Full-text введите |
|
Векторный поиск с автоматической векторизацией |
|
|
|
Гибридный поиск |
|
merino wool socks [15] |
|
Похожие товары |
|
Откройте любой товар |
|
Поиск по изображениям |
|
red plaid flannel shirt [26] или загрузите фото |
|
Геопоиск |
|
вкладка Geo [27] |
|
AI-поиск |
|
|
|
JSON API |
эндпоинт |
Команда |
Публичный Playground работает на небольшом сервере. Большинство запросов, кроме AI-чата, выполняется примерно за 5–100 мс.
На этих примерах видно, почему нет смысла выбирать один способ поиска на все случаи. Полнотекстовый хорош, когда важны сами слова; векторный помогает, когда пользователь описывает задачу; гибридный сочетает оба подхода. Для фотографий и координат нужны свои способы поиска, а AI пригодится, когда вместо списка товаров хочется получить готовый ответ.
Но это не значит, что наши примеры исчерпывают все возможные ошибки. Наверняка существует запрос, с которым не справятся ни полнотекстовый, ни векторный, ни гибридный поиск.
Сможете такой придумать?
Откройте Playground [1] и попробуйте. Если найдёте особенно неудачный запрос, создайте issue в GitHub [35] или напишите нам в Slack [36]. А если хотите собрать похожий поиск в своём приложении, код Playground [2] можно взять за основу.
Автор: ManticoreSearch
Источник [37]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/37403
URLs in this post:
[1] Manticore Search Playground: https://mnt.cr/go/d1kqPO
[2] небольшое приложение с открытым кодом: https://mnt.cr/go/KMnCwP
[3] автоматическую векторизацию: https://mnt.cr/go/jIgnc2
[4] lether jaket: https://mnt.cr/go/zSoivk
[5] OPTION fuzzy=1: https://mnt.cr/go/1KqEaR
[6] CALL QSUGGEST: https://mnt.cr/go/UW34bi
[7] разбивку результатов по категориям: https://mnt.cr/go/fSnXNU
[8] ошибка: http://www.braintools.ru/article/4192
[9] Full-text: https://mnt.cr/go/SG4pPx
[10] CALL AUTOCOMPLETE: https://mnt.cr/go/OpAi9w
[11] outfit for a job interview: https://mnt.cr/go/rPhZ7R
[12] индекс HNSW: https://mnt.cr/go/PvTpcG
[13] warm waterprof jacket: https://mnt.cr/go/LoRKCN
[14] merino wool socks: https://mnt.cr/go/ngTtC7
[15] merino wool socks во вкладке Hybrid: https://mnt.cr/go/1SchCs
[16] Reciprocal Rank Fusion (RRF): https://mnt.cr/go/GIJa8N
[17] hybrid_match(): https://mnt.cr/go/awEWgu
[18] bicycle shoes: https://mnt.cr/go/zRufN0
[19] warm waterprof jacket: https://mnt.cr/go/A9Fe84
[20] outfit for a job interview: https://mnt.cr/go/U0PbJI
[21] merino wool socks: https://mnt.cr/go/vfiKc4
[22] bicycle shoes: https://mnt.cr/go/eMQsGF
[23] lether jaket: https://mnt.cr/go/4PXuRr
[24] steel toe boots: https://mnt.cr/go/BOXGjC
[25] id товара: https://mnt.cr/go/1OH00T
[26] red plaid flannel shirt: https://mnt.cr/go/j6Tznd
[27] Geo: https://mnt.cr/go/poWuQG
[28] GEODIST(): https://mnt.cr/go/IDNhQf
[29] What should I wear to a summer wedding on the beach?: https://mnt.cr/go/HVDzvE
[30] CALL CHAT: https://mnt.cr/go/3qLBbb
[31] отдельной статье: https://mnt.cr/go/krKAx8
[32] HTTP JSON API: https://mnt.cr/go/b3luFe
[33] https://mnt.cr/go/3j4ju6: https://mnt.cr/go/3j4ju6
[34] levis 501: https://mnt.cr/go/PSLxoj
[35] создайте issue в GitHub: https://mnt.cr/go/TjT1ly
[36] Slack: https://mnt.cr/go/rYwh3v
[37] Источник: https://habr.com/ru/articles/1098336/?utm_campaign=1098336&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.