← Все статьи

Семантический поиск против полнотекстового: тестим три embedding-модели на 10 000 категорий Ozon

Семантический поиск против полнотекстового: тестим три 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.6BAlibaba / Qwen1024Локально, через TEI на GPUМультиязычная, компактная, быстрая
GigaChat (EmbeddingsGigaR)Сбер2560API gigachat.devices.sberbank.ruСпециально обучена на русском языке
OpenAI (text-embedding-3-small)OpenAI1536API 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 документов. Но у полнотекстового поиска есть фундаментальные ограничения:

  1. Только совпадение лексем. Запрос лекарства найдёт документы со словом «лекарств*» в тексте. Но не найдёт «Аптека» или «БАДы».
  2. Нет понимания синонимов. велик — это велосипед, но для tsquery просто неизвестное слово.
  3. Нет кросс-языковости. gaming mouse не найдёт «Игровая мышь».
  4. Нет понимания намерения. у меня протекает кран — ноль результатов. Слова «протекает» и «кран» не встречаются в названиях категорий сантехники.

3. Архитектура проекта

Проект собран на Next.js + PostgreSQL + pgvector. Docker Compose поднимает pgvector/pgvector:pg18 и фронтенд на node:20-alpine. Qwen3 запускается отдельно через Hugging Face Text Embeddings Inference (TEI) с GPU.

Процесс индексации:

  1. Импорт: скрипт читает CSV с категориями Ozon и записывает path (иерархический путь вида Электроника / Компьютеры / Ноутбук) и title в таблицу documents.
  2. Векторизация: для каждого документа три embedding-провайдера параллельно генерируют векторы. Текст, уходящий в модель — это path, та же строка, которая индексируется в tsvector.
  3. Поиск: при запросе текст одновременно отправляется во все три модели, получает три вектора, по каждому ищет 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.33.31.037.2
Qwen3-Embedding-0.6B (локально)22.821.19.055.9
GigaChat API168.3201.3150.4645.4
OpenAI API274.9360.0250.11182.3

Full-text вне конкуренции по скорости. Среди embedding-моделей Qwen3 на локальном GPU в 8x быстрее GigaChat и в 12x быстрее OpenAI. Это закономерно: локальный инференс (выполнение модели на своих серверах) против сетевого вызова API.


7. Итоговое сравнение моделей

КритерийQwen3-0.6BGigaChatOpenAI
Русский язык (синонимы)СлабоОтличноСлабо
Разговорная лексика (велик, косметичка)Не понимаетПонимаетНе понимает
Intent-запросы (ситуации)ЧастичноОтличноСлабо
Кросс-язык (EN→RU)ХорошоОтличноСлабо
Абстрактные запросыСреднеХорошоСлабо
Латентность~21 мс~200 мс~360 мс
СтоимостьБесплатно (свой GPU)По тарифу APIПо тарифу API
КонфиденциальностьДанные не покидают серверДанные уходят в СберДанные уходят в OpenAI

Почему OpenAI показал слабые результаты?

text-embedding-3-small — общепризнанно качественная модель. Причины низких результатов в этом эксперименте:

  1. Короткие тексты на русском. Модель обучена преимущественно на английском корпусе. Короткие иерархические пути (Дом и сад / Свечи и подсвечники) — не тот формат, на котором она максимально эффективна.
  2. Минимум контекста. В отличие от полноценных описаний товаров, только пути категорий — мало текста для извлечения семантики.
  3. Высокая латентность из-за прокси. Запросы шли через 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)

Лучший вариант — комбинация обоих методов:

  1. Запустить полнотекстовый и семантический поиск параллельно.
  2. Если полнотекстовый дал точные совпадения с высоким рангом — поднять их в выдаче.
  3. Дополнить семантическими результатами для расширения охвата.

Примерная формула: 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 или локальная модель) для понимания намерений пользователя.