- BrainTools - https://www.braintools.ru -
Идея семантического поиска: представить и документы, и запрос в виде числовых векторов (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 [1] позволяет хранить векторы прямо в 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-точность за счет большего потребления памяти [2].
Для эксперимента я выбрал три модели с разными характеристиками:
|
Модель |
Провайдер |
Размерность |
Развертывание |
Особенности |
|---|---|---|---|---|
|
Qwen3-Embedding-0.6B |
Alibaba / Qwen |
1024 |
Локально, через TEI [3] на 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)
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 не найдет «Игровая мышь».
Нет понимания намерения. у меня протекает кран — ноль результатов, потому что слова «протекает» и «кран» не встречаются в названиях категорий сантехники.
Проект собран на 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 ближайших документов.
Датасет: 10 019 категорий товаров Ozon с иерархическими путями. Примеры:
Электроника / Компьютеры / Ноутбук
Строительство и ремонт / Сантехника / Смеситель
Спорт и отдых / Велосипед / Электровелосипед
Товары для животных / Корма и лакомства для кошек и собак
Я подготовил 18 запросов в 5 категориях, специально подобранных так, чтобы показать разницу между подходами. Каждый запрос прогонялся через все 4 метода: full-text + 3 embedding-модели, top-5 результатов.
Запросы, где слово из запроса отсутствует в данных, но смысл совпадает.
В данных нет слова «лекарства» в корневых категориях — есть «Аптека».
|
Метод |
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 — единственная модель, которая «знает», что «велик» = «велосипед». Это прямое следствие обучения [4] на русскоязычных данных, включая разговорную речь.
Слово-омоним: может означать сумку для косметики или специалиста-косметолога.
|
Метод |
Top-1 результат |
Score |
|---|---|---|
|
Full-text |
Галантерея / Аксессуары / Косметичка |
0.06 |
|
Qwen3 |
Красота и гигиена / Декоративная косметика |
0.81 |
|
GigaChat |
Галантерея / Аксессуары / Косметичка |
0.94 |
|
OpenAI |
Аптека / Эстетическая косметология |
0.63 |
Full-text нашел точное совпадение, но с низким рангом (0.06). GigaChat нашел то же самое с score 0.94, плюс подтянул смежные категории (сумки, кошельки, декоративная косметика). Это показывает, что семантический поиск не только находит точное совпадение, но и понимает контекст.
Запросы, описывающие ситуацию, а не товар. 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 точнее — раскраски и бумага для рисования ближе к запросу начинающего, чем гравюра.
|
Метод |
Top-3 результата |
Score |
|---|---|---|
|
Full-text |
(пусто) |
— |
|
Qwen3 |
Крем для загара, Игрушка-тренажер для дыхания [5] |
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 не просто понял «детские товары», а выбрал именно школьные: ранцы, сменка, дневник, пенал. Это впечатляющий уровень семантического понимания.
Запросы на английском при полностью русскоязычных данных.
|
Метод |
Top-1 результат |
Score |
|---|---|---|
|
Full-text |
(пусто) |
— |
|
Qwen3 |
Электроника / Устройства ручного ввода / Игровая мышь |
0.73 |
|
GigaChat |
Электроника / Устройства ручного ввода / Игровая мышь |
0.90 |
|
OpenAI |
Товары для взрослых / Секс игрушки / … |
0.28 |
И Qwen3, и GigaChat точно перевели «gaming mouse» в «Игровая мышь». Qwen3 здесь показал отличную мультиязычность (score 0.73). OpenAI на этом запросе полностью провалился.
|
Метод |
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).
|
Метод |
Top-1 результат |
Score |
|---|---|---|
|
Full-text |
(пусто) |
— |
|
Qwen3 |
Запчасти и инструменты для ремонта смартфонов |
0.76 |
|
GigaChat |
Смартфоны, планшеты, мобильные телефоны |
0.85 |
|
OpenAI |
Гаджеты и аксессуары / Умная визитка |
0.42 |
Все три embedding-модели уловили тематику электроники. GigaChat точнее: в его top-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 пошел в правильном направлении, но менее точно.
|
Метод |
Медиана (мс) |
Среднее (мс) |
Мин (мс) |
Макс (мс) |
|---|---|---|---|---|
|
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.
|
Критерий |
Qwen3-0.6B |
GigaChat |
OpenAI |
|---|---|---|---|
|
Русский язык (синонимы) |
Слабо |
Отлично |
Слабо |
|
Разговорная лексика (велик, косметичка) |
Не понимает |
Понимает |
Не понимает |
|
Intent-запросы (ситуации) |
Частично |
Отлично |
Слабо |
|
Кросс-язык (EN->RU) |
Хорошо |
Отлично |
Слабо |
|
Абстрактные запросы |
Средне |
Хорошо |
Слабо |
|
Латентность |
~21 мс |
~200 мс |
~360 мс |
|
Стоимость |
Бесплатно (свой GPU) |
По тарифу API |
По тарифу API |
|
Конфиденциальность |
Данные не покидают сервер |
Данные уходят в Сбер |
Данные уходят в OpenAI |
Важно оговориться: text-embedding-3-small — это общепризнанно качественная модель. Вероятная причина низких результатов в нашем эксперименте:
Короткие тексты на русском. Модель обучена преимущественно на английском корпусе. Короткие иерархические пути (Дом и сад / Свечи и подсвечники) — не тот формат, на котором она максимально эффективна.
Отсутствие контекста. В отличие от полноценных описаний товаров, у нас только пути категорий — минимум текста для извлечения семантики.
Высокая латентность из-за прокси. Запросы шли через HTTP-прокси, что добавило задержку и, возможно, повлияло на стабильность.
Для объективной оценки стоит протестировать text-embedding-3-large или другие модели OpenAI на более длинных текстах.
GigaChat (модель EmbeddingsGigaR) специально обучена на русскоязычном корпусе. Она «знает»:
что «велик» = «велосипед»
что «протекает кран» связано с сантехникой
что «первоклассник» — это про школу
что «уютный вечер» — это пледы и свечи
Это подтверждает тезис: для задач на конкретном языке локализованные модели работают лучше универсальных.
Qwen3-Embedding-0.6B при всего 600M параметрах и 1024-мерных векторах показала неровные результаты: отличный кросс-лингвальный поиск (gaming mouse, DIY tools, smartphone accessories), хорошая работа на некоторых русских запросах (хочу научиться рисовать, ноутбук), но провалы на разговорной лексике и intent-запросах.
Ее главное преимущество — скорость и автономность: 21 мс на запрос, данные не покидают инфраструктуру. Для production-сценариев, где важна приватность и латентность, это может перевесить разницу в качестве.
Пользователи вводят точные названия товаров
Критична латентность (< 5 мс)
Не нужна обработка синонимов и разговорной речи
Минимальная инфраструктура (только PostgreSQL)
Запросы в свободной форме («у меня протекает кран»)
Мультиязычные пользователи
Поиск по к��ротким или неструктурированным текстам
Лучший вариант — комбинация обоих методов:
Запустить полнотекстовый и семантический поиск параллельно.
Если полнотекстовый дал точные совпадения с высоким рангом — поднять их в выдаче.
Дополнить семантическими результатами для расширения охвата.
Примерная формула: hybrid_score = alpha * fts_score + (1 - alpha) * semantic_score, где alpha подбирается экспериментально (обычно 0.3–0.5).
В PostgreSQL это можно реализовать одним запросом через UNION + COALESCE + нормализацию рангов.
Весь код проекта открыт: github.com/borodulin/embeddings-demo [6]. Для запуска:
# Поднять 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
Источник [7]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/27116
URLs in this post:
[1] pgvector: https://github.com/pgvector/pgvector
[2] памяти: http://www.braintools.ru/article/4140
[3] TEI: https://github.com/huggingface/text-embeddings-inference
[4] обучения: http://www.braintools.ru/article/5125
[5] дыхания: http://www.braintools.ru/article/4500
[6] github.com/borodulin/embeddings-demo: https://github.com/borodulin/embeddings-demo
[7] Источник: https://habr.com/ru/articles/1010200/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1010200
Нажмите здесь для печати.