1. Семантический поиск: поиск по смыслу
Идея семантического поиска: представить и документы, и запрос в виде числовых векторов (embeddings) в едином пространстве. Близкие по смыслу тексты будут иметь близкие векторы. Для измерения близости используется косинусное расстояние.
Как это работает
Текст → 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 |
Специально обучена на русском языке |
|
OpenAI (text-embedding-3-small) |
OpenAI |
1536 |
API |
Мультиязычная, широко используется |
Каждая модель генерирует вектор своей размерности, поэтому в базе три отдельные таблицы с векторами:
-- Для 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 с 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 |
Профиль для светодиодной ленты (один результат, score 0.22) |
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» как строительные инструменты. Qwen3 и GigaChat показали сопоставимые результаты. 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, что объяснимо: локальный инференс vs сетевой вызов 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 лидирует?
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 или локальная модель) для понимания намерений пользователя.
Автор: kotafey
- Запись добавлена: 14.03.2026 в 12:40
- Оставлено в
Советуем прочесть:
- Ozon планирует создать ИИ-ассистента для помощи в поиске товаров
- Рынок векторных баз под угрозой, Amazon встроил поиск по embedding-вектору прямо в S3
- Google представила Gemini Embedding 2 — нейросеть для поиска и сопоставления текста, изображений, видео и аудио
- Пользователи «ВКонтакте» смогут покупать товары из видео в ленте соцсети
- Найди 10 отличий, или Сравниваем редакции договоров с помощью ИИ
- Векторный поиск внутри PostgreSQL: что умеет и где может пригодиться pgvector
- Fine-tune Qwen3 Embeddings для классификации категорий товаров
- pg_auto_embeddings — считаем эмбеддинги для текста прямо в Postgres, без экстеншенов
- Умный поиск по API, или NLP против функционального поиска
- Встроенный поиск по документации в KodaCode. Сравниваем с Context7


