Семантический поиск против полнотекстового: тестим три embedding-модели на 10 000 категорий Ozon
1. Семантический поиск: поиск по смыслу
Идея семантического поиска — представить и документы, и запрос в виде числовых векторов. Такие векторы называют эмбеддингами (embeddings). Близкие по смыслу тексты получают близкие векторы. Для измерения похожести используется косинусное расстояние — метрика от 0 до 1, где 1 означает полную похожесть.
Как это работает
Текст → Embedding-модель → Вектор [0.012, -0.034, 0.071, ...]
(сотни/тысячи измерений)
При индексации каждый документ превращается в вектор и сохраняется в базу. При поиске запрос тоже векторизуется. Затем pgvector находит ближайшие документы по косинусному расстоянию:
SELECT d.id, d.path, d.title,
1 - (v.embedding <=> $1::vector) AS score
FROM documents d
JOIN document_vectors v ON v.document_id = d.id
ORDER BY v.embedding <=> $1::vector
LIMIT $2
Оператор <=> в pgvector — косинусное расстояние. 1 - distance даёт similarity score от 0 до 1.
Особенности pgvector
Расширение pgvector позволяет хранить векторы прямо в PostgreSQL. Для ускорения поиска создаётся IVFFlat-индекс — тип индекса, который разбивает векторы на кластеры:
CREATE INDEX ON document_vectors
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
Параметр lists задаёт число кластеров при построении индекса. Для 10K документов 100 кластеров — разумный выбор. В production с миллионами записей стоит рассмотреть HNSW-индекс (CREATE INDEX ... USING hnsw). Он даёт лучшую recall-точность (долю найденных релевантных результатов), но потребляет больше памяти.
Три модели: кого сравниваем
| Модель | Провайдер | Размерность | Развертывание | Особенности |
|---|---|---|---|---|
| Qwen3-Embedding-0.6B | Alibaba / Qwen | 1024 | Локально, через TEI на GPU | Мультиязычная, компактная, быстрая |
| GigaChat (EmbeddingsGigaR) | Сбер | 2560 | API gigachat.devices.sberbank.ru |
Специально обучена на русском языке |
| OpenAI (text-embedding-3-small) | OpenAI | 1536 | API api.openai.com |
Мультиязычная, широко используется |
Каждая модель генерирует вектор своей размерности, поэтому в базе три отдельные таблицы:
-- Для Qwen (1024 измерения)
embedding vector(1024)
-- Для GigaChat (2560 измерений)
embedding vector(2560)
-- Для OpenAI (1536 измерения)
embedding vector(1536)
2. Полнотекстовый поиск: как работает и где упирается
PostgreSQL предлагает зрелый полнотекстовый поиск из коробки. Его ядро — два типа данных:
tsvector— нормализованное представление документа: слова приводятся к начальной форме (лемматизация), удаляются стоп-слова.tsquery— нормализованное представление запроса в том же формате.
Оператор @@ проверяет совпадение, ts_rank ранжирует результаты по частотности совпавших лексем.
Как это выглядит в коде
Миграция, добавляющая полнотекстовый поиск к существующей таблице documents:
ALTER TABLE documents
ADD COLUMN IF NOT EXISTS search_vector tsvector;
UPDATE documents
SET search_vector = to_tsvector('russian', COALESCE(path, ''))
WHERE search_vector IS NULL;
CREATE INDEX IF NOT EXISTS documents_search_vector_gin_idx
ON documents USING gin (search_vector);
CREATE OR REPLACE FUNCTION documents_search_vector_update()
RETURNS trigger AS $$
BEGIN
NEW.search_vector := to_tsvector('russian', COALESCE(NEW.path, ''));
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER documents_search_vector_update_trigger
BEFORE INSERT OR UPDATE OF path
ON documents
FOR EACH ROW
EXECUTE FUNCTION documents_search_vector_update();
Поисковый запрос:
SELECT id, path, title, LEFT(path, 220) AS snippet,
ts_rank(search_vector, plainto_tsquery('russian', $1)) AS score
FROM documents
WHERE search_vector @@ plainto_tsquery('russian', $1)
ORDER BY score DESC
LIMIT $2
Работает быстро — медиана 1.3 мс на 10K документов. Но у полнотекстового поиска есть фундаментальные ограничения:
- Только совпадение лексем. Запрос
лекарстванайдёт документы со словом «лекарств*» в тексте. Но не найдёт «Аптека» или «БАДы». - Нет понимания синонимов.
велик— это велосипед, но для tsquery просто неизвестное слово. - Нет кросс-языковости.
gaming mouseне найдёт «Игровая мышь». - Нет понимания намерения.
у меня протекает кран— ноль результатов. Слова «протекает» и «кран» не встречаются в названиях категорий сантехники.
3. Архитектура проекта
Проект собран на Next.js + PostgreSQL + pgvector. Docker Compose поднимает pgvector/pgvector:pg18 и фронтенд на node:20-alpine. Qwen3 запускается отдельно через Hugging Face Text Embeddings Inference (TEI) с GPU.
Процесс индексации:
- Импорт: скрипт читает CSV с категориями Ozon и записывает
path(иерархический путь видаЭлектроника / Компьютеры / Ноутбук) иtitleв таблицуdocuments. - Векторизация: для каждого документа три embedding-провайдера параллельно генерируют векторы. Текст, уходящий в модель — это
path, та же строка, которая индексируется вtsvector. - Поиск: при запросе текст одновременно отправляется во все три модели, получает три вектора, по каждому ищет top-K ближайших документов.
4. Эксперимент
Датасет: 10 019 категорий товаров Ozon с иерархическими путями. Примеры:
Электроника / Компьютеры / НоутбукСтроительство и ремонт / Сантехника / СмесительСпорт и отдых / Велосипед / ЭлектровелосипедТовары для животных / Корма и лакомства для кошек и собак
Подготовлено 18 запросов в 5 категориях. Они специально подобраны, чтобы показать разницу между подходами. Каждый запрос прогонялся через все 4 метода: full-text + 3 embedding-модели, top-5 результатов.
5. Результаты
5.1. Синонимы и разговорная лексика
Запросы, где слово из запроса отсутствует в данных, но смысл совпадает.
Запрос: «лекарства»
В данных нет слова «лекарства» в корневых категориях — есть «Аптека».
| Метод | Top-1 результат | Score |
|---|---|---|
| Full-text | (пусто) | – |
| Qwen3 | Канцелярские товары | 0.66 |
| GigaChat | Аптека / Лекарственные средства | 0.92 |
| OpenAI | Товары для взрослых / БДСМ / Плетка | 0.31 |
GigaChat безошибочно связал «лекарства» с аптечными категориями (score 0.92). Qwen3 промахнулся, уведя в канцелярию. OpenAI выдал абсолютно нерелевантный результат.
Запрос: «велик»
Разговорное слово для «велосипед».
| Метод | Top-1 результат | Score |
|---|---|---|
| Full-text | (пусто) | – |
| Qwen3 | Красота и гигиена / Щетка для сухого массажа | 0.55 |
| GigaChat | Спорт и отдых / Велосипед | 0.89 |
| OpenAI | Товары для взрослых / Секс игрушки / Расширитель | 0.24 |
GigaChat — единственная модель, которая «знает», что «велик» = «велосипед». Это прямое следствие обучения на русскоязычных данных, включая разговорную речь.
Запрос: «косметичка»
Слово-омоним: может означать сумку для косметики или специалиста-косметолога.
| Метод | Top-1 результат | Score |
|---|---|---|
| Full-text | Галантерея / Аксессуары / Косметичка | 0.06 |
| Qwen3 | Красота и гигиена / Декоративная косметика | 0.81 |
| GigaChat | Галантерея / Аксессуары / Косметичка | 0.94 |
| OpenAI | Аптека / Эстетическая косметология | 0.63 |
Full-text нашёл точное совпадение, но с низким рангом (0.06). GigaChat нашёл то же самое с score 0.94, плюс подтянул смежные категории (сумки, кошельки, декоративная косметика). Семантический поиск не только находит точное совпадение, но и понимает контекст.
5.2. Ситуационные запросы (intent)
Запросы, описывающие ситуацию, а не товар. Full-text бессилен во всех случаях.
Запрос: «у меня протекает кран»
| Метод | Top-3 результата | Score |
|---|---|---|
| Full-text | (пусто) | – |
| Qwen3 | Запчасть для кулера, Тепловая обработка, Модуль доступа | 0.50, 0.48, 0.45 |
| GigaChat | Сантехника / Смеситель, Сантехника / Сифон сливной, Сантехника / Слив-перелив | 0.79, 0.77, 0.77 |
| OpenAI | Стержень для ручки, Инструмент для развод… | 0.25, 0.24 |
GigaChat понял, что протекающий кран — задача для категории «Сантехника». Все 5 результатов — сантехнические товары. Qwen3 ушёл в бытовую технику. OpenAI выдал канцелярию.
Запрос: «собираюсь в поход»
| Метод | Top-3 результата | Score |
|---|---|---|
| Full-text | (пусто) | – |
| Qwen3 | Охота и стрельба (разные позиции) | ~0.49 |
| GigaChat | Спорт и отдых, Походная аптечка, Набор походной посуды | 0.78, 0.78, 0.77 |
| OpenAI | Тренажеры / Силовая скамья | 0.28 |
GigaChat правильно определил туристическую тематику. Qwen3 уловил направление (спорт и отдых), но ушёл в «охоту и стрельбу».
Запрос: «первый раз завожу кота»
| Метод | Top-3 результата | Score |
|---|---|---|
| Full-text | (пусто) | – |
| Qwen3 | Профиль для светодиодной ленты (один результат) | 0.22 |
| GigaChat | Товары для животных, Когтеточка, Антицарапки | 0.71, 0.71, 0.69 |
| OpenAI | Корма для кошек и собак, Лакомство | 0.31, 0.29 |
GigaChat точно понял: человек заводит кота, и ему нужны когтеточка, наполнитель, сетка-фиксатор для мытья. OpenAI двинулся в правильном направлении (корма), но score низкий. Qwen3 полностью промахнулся.
Запрос: «хочу научиться рисовать»
| Метод | Top-3 результата | Score |
|---|---|---|
| Full-text | (пусто) | – |
| Qwen3 | Набор для рисования, Набор для создания гравюры, Картина по контурам | 0.56, 0.55, 0.50 |
| GigaChat | Набор для рисования, Раскраска, Бумага для рисования | 0.80, 0.80, 0.80 |
| OpenAI | Обучающий плакат, Декоративный элемент | 0.28, 0.26 |
Здесь и Qwen3, и GigaChat показали хорошие результаты. GigaChat точнее — раскраски и бумага для рисования ближе к запросу начинающего, чем гравюра.
5.3. Подарки и события
Запрос: «подарок маме на 8 марта»
| Метод | Top-3 результата | Score |
|---|---|---|
| Full-text | (пусто) | – |
| Qwen3 | Крем для загара, Игрушка-тренажер для дыхания | 0.36, 0.35 |
| GigaChat | Открытка, Букет из игрушек, Пасхальный декор | 0.78, 0.76, 0.75 |
| OpenAI | Брошь ювелирная, Сувенир ювелирный | 0.25, 0.25 |
GigaChat ассоциировал запрос с подарочной тематикой: открытки, букеты, подарочные коробки. OpenAI зацепился за ювелирные украшения — направление не совсем верное, но логичное. Qwen3 выдал случайный шум.
Запрос: «что купить первокласснику»
| Метод | Top-3 результата | Score |
|---|---|---|
| Full-text | (пусто) | – |
| Qwen3 | Детские товары, Неокуб, Пупс | 0.60, 0.58, 0.58 |
| GigaChat | Детские рюкзаки и ранцы, Сумка для сменной обуви, Дневник школьный | 0.84, 0.81, 0.80 |
| OpenAI | Запчасть для р/у моделей, Кубики | 0.31, 0.29 |
GigaChat не просто понял «детские товары», а выбрал именно школьные: ранцы, сменка, дневник, пенал. Впечатляющий уровень семантического понимания.
5.4. Кросс-языковые запросы
Запросы на английском при полностью русскоязычных данных.
Запрос: «gaming mouse»
| Метод | Top-1 результат | Score |
|---|---|---|
| Full-text | (пусто) | – |
| Qwen3 | Электроника / Устройства ручного ввода / Игровая мышь | 0.73 |
| GigaChat | Электроника / Устройства ручного ввода / Игровая мышь | 0.90 |
| OpenAI | Товары для взрослых / Секс игрушки / … | 0.28 |
И Qwen3, и GigaChat точно перевели «gaming mouse» в «Игровая мышь». Qwen3 показал отличную мультиязычность (score 0.73). OpenAI полностью провалился.
Запрос: «DIY tools»
| Метод | Top-3 результата | Score |
|---|---|---|
| Full-text | (пусто) | – |
| Qwen3 | Электропилы, Садовый электроинструмент, Расходники для инструмента | 0.78, 0.76, 0.76 |
| GigaChat | Инструменты для ремонта, Оснастка для инструмента, Набор инструментов | 0.79, 0.79, 0.78 |
| OpenAI | Мелок разметочный, Нож для садового инструмента | 0.38, 0.37 |
Обе модели уверенно определили «DIY tools» как строительные инструменты с сопоставимыми результатами. OpenAI хотя бы зацепился за правильную область (score ~0.37).
Запрос: «smartphone accessories»
| Метод | Top-1 результат | Score |
|---|---|---|
| Full-text | (пусто) | – |
| Qwen3 | Запчасти и инструменты для ремонта смартфонов | 0.76 |
| GigaChat | Смартфоны, планшеты, мобильные телефоны | 0.85 |
| OpenAI | Гаджеты и аксессуары / Умная визитка | 0.42 |
Все три embedding-модели уловили тематику электроники. GigaChat точнее: в его top-5 есть «Чехол для смартфона» и «Шнурок для телефона».
5.5. Абстрактные формулировки
Запрос: «здоровое питание»
| Метод | Top-1 результат | Score |
|---|---|---|
| Full-text | Продукты питания / Программа здорового питания | 0.18 |
| Qwen3 | Продукты питания | 0.79 |
| GigaChat | Продукты питания + Программа здорового питания + Мюсли, Овес | 0.88, 0.87, 0.84 |
| OpenAI | Детское питание | 0.48 |
Full-text нашёл точное совпадение (есть категория «Программа здорового питания»), но с низким рангом. GigaChat дал тот же результат плюс мюсли, овес, суперфуды — контекстуально релевантные категории.
Запрос: «уютный вечер дома»
| Метод | Top-3 результата | Score |
|---|---|---|
| Full-text | (пусто) | – |
| Qwen3 | Дом и сад, Печи, Одноразовая посуда | 0.74, 0.72, 0.71 |
| GigaChat | Дом и сад, Декор и интерьер, Пледы и покрывала, Свечи и подсвечники | 0.76, 0.75, 0.74, 0.74 |
| OpenAI | Товары для взрослых / БДСМ / … | 0.26 |
GigaChat ассоциировал «уютный вечер» с пледами, свечами, декором — именно то, что ожидаешь. Qwen3 пошёл в правильном направлении, но менее точно.
6. Сводная таблица по латентности
| Метод | Медиана (мс) | Среднее (мс) | Мин (мс) | Макс (мс) |
|---|---|---|---|---|
| Full-text (PostgreSQL) | 1.3 | 3.3 | 1.0 | 37.2 |
| Qwen3-Embedding-0.6B (локально) | 22.8 | 21.1 | 9.0 | 55.9 |
| GigaChat API | 168.3 | 201.3 | 150.4 | 645.4 |
| OpenAI API | 274.9 | 360.0 | 250.1 | 1182.3 |
Full-text вне конкуренции по скорости. Среди embedding-моделей Qwen3 на локальном GPU в 8x быстрее GigaChat и в 12x быстрее OpenAI. Это закономерно: локальный инференс (выполнение модели на своих серверах) против сетевого вызова API.
7. Итоговое сравнение моделей
| Критерий | Qwen3-0.6B | GigaChat | OpenAI |
|---|---|---|---|
| Русский язык (синонимы) | Слабо | Отлично | Слабо |
| Разговорная лексика (велик, косметичка) | Не понимает | Понимает | Не понимает |
| Intent-запросы (ситуации) | Частично | Отлично | Слабо |
| Кросс-язык (EN→RU) | Хорошо | Отлично | Слабо |
| Абстрактные запросы | Средне | Хорошо | Слабо |
| Латентность | ~21 мс | ~200 мс | ~360 мс |
| Стоимость | Бесплатно (свой GPU) | По тарифу API | По тарифу API |
| Конфиденциальность | Данные не покидают сервер | Данные уходят в Сбер | Данные уходят в OpenAI |
Почему OpenAI показал слабые результаты?
text-embedding-3-small — общепризнанно качественная модель. Причины низких результатов в этом эксперименте:
- Короткие тексты на русском. Модель обучена преимущественно на английском корпусе. Короткие иерархические пути (
Дом и сад / Свечи и подсвечники) — не тот формат, на котором она максимально эффективна. - Минимум контекста. В отличие от полноценных описаний товаров, только пути категорий — мало текста для извлечения семантики.
- Высокая латентность из-за прокси. Запросы шли через HTTP-прокси. Это добавило задержку и, возможно, повлияло на стабильность.
Для объективной оценки стоит протестировать text-embedding-3-large или другие модели OpenAI на более длинных текстах.
Почему GigaChat лидирует?
Модель EmbeddingsGigaR специально обучена на русскоязычном корпусе. Она «знает»:
- что «велик» = «велосипед»
- что «протекает кран» связано с сантехникой
- что «первоклассник» — про школу
- что «уютный вечер» — это пледы и свечи
Это подтверждает тезис: для задач на конкретном языке локализованные модели работают лучше универсальных.
Роль Qwen3
Qwen3-Embedding-0.6B при всего 600M параметрах и 1024-мерных векторах показала неровные результаты. С одной стороны — отличный кросс-лингвальный поиск (gaming mouse, DIY tools, smartphone accessories) и хорошая работа на отдельных русских запросах (хочу научиться рисовать, ноутбук). С другой — провалы на разговорной лексике и intent-запросах.
Главное преимущество — скорость и автономность: 21 мс на запрос, данные не покидают инфраструктуру. Для production-сценариев, где важны приватность и латентность, это может перевесить разницу в качестве.
8. Когда что использовать
Только полнотекстовый поиск
- Пользователи вводят точные названия товаров
- Критична латентность (< 5 мс)
- Не нужна обработка синонимов и разговорной речи
- Минимальная инфраструктура (только PostgreSQL)
Только семантический поиск
- Запросы в свободной форме («у меня протекает кран»)
- Мультиязычные пользователи
- Поиск по коротким или неструктурированным текстам
Гибридный подход (рекомендация для production)
Лучший вариант — комбинация обоих методов:
- Запустить полнотекстовый и семантический поиск параллельно.
- Если полнотекстовый дал точные совпадения с высоким рангом — поднять их в выдаче.
- Дополнить семантическими результатами для расширения охвата.
Примерная формула: hybrid_score = alpha * fts_score + (1 - alpha) * semantic_score, где alpha подбирается экспериментально (обычно 0.3–0.5).
В PostgreSQL это реализуется одним запросом через UNION + COALESCE + нормализацию рангов.
9. Как воспроизвести эксперимент
Весь код проекта открыт: github.com/borodulin/embeddings-demo. Для запуска:
# Поднять PostgreSQL с pgvector
docker compose up -d postgres
# Установить зависимости и применить миграции
npm install
npm run db:migrate
# Импортировать данные
npm run import:data
# Проиндексировать все три модели
npm run index:vectors
# Запустить бенчмарк
npm run benchmark:search -- --limit 5
Для Qwen3 потребуется GPU и запуск TEI:
docker run --gpus all -p 8080:80 -v ./data:/data \
ghcr.io/huggingface/text-embeddings-inference:cuda-1.9 \
--model-id Qwen/Qwen3-Embedding-0.6B
Заключение
Семантический поиск — не замена полнотекстовому, а принципиально другой инструмент. Полнотекстовый ищет слова, семантический ищет смысл. На эксперименте с 10K категорий Ozon:
- GigaChat показал лучшее качество на русскоязычных запросах, особенно на разговорной лексике и intent-запросах.
- Qwen3-0.6B удивил скоростью (21 мс) и хорошей мультиязычностью, но нестабилен на русском.
- OpenAI разочаровал на данном датасете, хотя на длинных английских текстах это сильная модель.
- Full-text незаменим по скорости (1.3 мс) и точности на буквальных совпадениях.
Для production-поиска на русскоязычном маркетплейсе оптимальная стратегия — гибрид: быстрый полнотекстовый поиск для точных попаданий + семантический (GigaChat или локальная модель) для понимания намерений пользователя.
