Эмбеддинги и семантический поиск: практический гайд
28 июня 2026 г. · Команда Hubris · 7 мин чтения
Эмбеддинг — это представление текста в виде вектора чисел, в котором близкие по смыслу фразы получают близкие векторы. Семантический поиск сравнивает такие векторы и находит документы по смыслу, а не по совпадению слов. В этом гайде вы получите векторы через /v1/embeddings, посчитаете косинусную близость на numpy и соберёте рабочий поиск по 20 документам — с оплатой в рублях.
Что такое векторное представление текста
Векторное представление (эмбеддинг) — это список из сотен или тысяч чисел, который модель сопоставляет тексту. Сами числа по отдельности ничего не значат, важно взаимное расположение: тексты с похожим смыслом модель размещает рядом, с разным — далеко друг от друга.
Несколько свойств, которых достаточно для практики:
- «Кот спит на диване» и «Кошка дремлет на софе» окажутся рядом, хотя общих слов у них нет.
- Запрос «как вернуть деньги за заказ» найдёт документ «Политика возвратов» — поиск работает по смыслу, а не по вхождению слов.
- Длина вектора фиксирована: например,
openai/text-embedding-3-smallвсегда возвращает 1536 чисел — и для короткой фразы, и для страницы текста. - Сравнивать можно только векторы одной модели: у разных моделей системы координат не совпадают.
Обучать ничего не нужно — готовые векторы отдаёт обычный HTTP-эндпоинт.
Как получить векторы через /v1/embeddings
Эндпоинт принимает OpenAI-совместимый формат, поэтому подойдёт стандартный openai SDK — меняются только base_url и ключ. Подготовка занимает пару минут:
- Зарегистрируйтесь на hubris.pw/sign-in — вход по коду из письма, пароль не нужен.
- Создайте API-ключ в разделе «Ключи».
- Пополните баланс в разделе «Биллинг» — СБП, карта или счёт для юрлиц.
Подробнее эти шаги разобраны в быстром старте. Дальше — код:
from openai import OpenAI
client = OpenAI(
base_url="https://api.hubris.pw/v1",
api_key="sk-gw-...", # ваш ключ из раздела «Ключи»
)
response = client.embeddings.create(
model="openai/text-embedding-3-small",
input=["Кот спит на диване", "Кошка дремлет на софе", "Трактор пашет поле"],
)
vectors = [item.embedding for item in response.data]
print(len(vectors), len(vectors[0])) # 3 1536
В input можно передать одну строку или сразу пакет до 2048 строк — пакетная отправка на порядок быстрее и экономнее по числу HTTP-запросов, чем отправка по одной. Полное описание параметров — в справке по /v1/embeddings.
Косинусная близость на numpy
Чтобы понять, насколько два текста похожи, считают косинусную близость их векторов — косинус угла между ними. Значение около 1 означает «почти одинаковый смысл», около 0 — «ничего общего».
import numpy as np
def cosine(a, b):
a, b = np.asarray(a), np.asarray(b)
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))
print(cosine(vectors[0], vectors[1])) # ~0.8 — кот и кошка близки
print(cosine(vectors[0], vectors[2])) # ~0.2 — кот и трактор далеки
Абсолютные значения зависят от модели: у одной «похоже» начинается с 0,75, у другой — с 0,5. Поэтому пороги подбирают на своих данных, а для поиска чаще берут не порог, а просто топ-N ближайших документов.
Мини-поиск по 20 документам: полный пример
Соберём поиск по небольшой базе знаний целиком: векторизуем документы один раз, потом для каждого запроса считаем один вектор и сравниваем со всеми.
import numpy as np
from openai import OpenAI
client = OpenAI(base_url="https://api.hubris.pw/v1", api_key="sk-gw-...")
MODEL = "openai/text-embedding-3-small"
docs = [
"Доставка по России занимает от 2 до 7 рабочих дней",
"Возврат товара возможен в течение 30 дней после покупки",
"Оплатить заказ можно картой или по счёту для юридических лиц",
"Гарантия на технику действует один год",
"Самовывоз доступен из 12 городов",
# ... всего 20 документов
]
def embed(texts):
r = client.embeddings.create(model=MODEL, input=texts)
return np.array([item.embedding for item in r.data])
# 1. Индексация: один пакетный запрос на все документы
doc_vectors = embed(docs)
doc_vectors /= np.linalg.norm(doc_vectors, axis=1, keepdims=True)
# 2. Поиск: вектор запроса + умножение матрицы на вектор
def search(query, top_n=3):
q = embed([query])[0]
q /= np.linalg.norm(q)
scores = doc_vectors @ q
for i in np.argsort(scores)[::-1][:top_n]:
print(f"{scores[i]:.3f} {docs[i]}")
search("как вернуть деньги за заказ")
# 0.62 Возврат товара возможен в течение 30 дней после покупки
# 0.34 Оплатить заказ можно картой или по счёту для юридических лиц
# 0.21 Гарантия на технику действует один год
Векторы нормированы заранее, поэтому косинусная близость сводится к умножению матрицы на вектор — и на 20, и на 20 тысячах документов это доли секунды. Сохраните векторы документов рядом с базой (хоть в JSON-файл): пересчитывать их при каждом запросе не нужно.
Когда нужен векторный движок
Связка «numpy + массив в памяти» честно работает до десятков тысяч документов. Специализированное хранилище (pgvector, Qdrant, Milvus) становится нужным, когда:
- документов сотни тысяч и полный перебор перестаёт укладываться в требования по скорости;
- база постоянно меняется и нужно добавлять или удалять векторы без пересборки индекса;
- вместе с поиском нужны фильтры по метаданным («только статьи за 2026 год»);
- поиск — часть боевого сервиса с требованиями к отказоустойчивости.
Если у вас FAQ, база знаний поддержки или документация на сотни записей — векторный движок пока не нужен, пример выше закрывает задачу.
Embedding-модели в каталоге: цены в рублях
У embedding-моделей тарифицируются только входные токены: вы платите за объём текста, который векторизуете, выходных токенов нет. Цены каталога (актуальны на июнь 2026):
| Модель | Контекст | Цена за 1 млн входных токенов |
|---|---|---|
qwen/qwen3-embedding-8b | 32 000 | 1,06 ₽ |
baai/bge-m3 | 8 192 | 1,06 ₽ |
intfloat/multilingual-e5-large | 8 192 | 1,06 ₽ |
openai/text-embedding-3-small | 8 192 | 2,12 ₽ |
mistralai/mistral-embed-2312 | 8 192 | 10,61 ₽ |
openai/text-embedding-3-large | 8 192 | 13,79 ₽ |
google/gemini-embedding-001 | 20 000 | 15,92 ₽ |
Для русскоязычных текстов хорошо показывают себя мультиязычные bge-m3, multilingual-e5-large и qwen3-embedding-8b — около рубля за миллион токенов. Полный список — в подборке embedding-моделей, а все модели платформы — в каталоге моделей; встречаются и варианты с пометкой :free с нулевой ценой за токены.
Частые вопросы
Сколько стоит проиндексировать базу из 1000 документов?
Документ на 2–3 абзаца — это примерно 300 токенов, тысяча документов — около 300 тысяч токенов. На openai/text-embedding-3-small (2,12 ₽ за миллион токенов) такая индексация обойдётся примерно в 64 копейки, на bge-m3 — около 32 копеек. Цены актуальны на июнь 2026.
Подходят ли эмбеддинги для русского языка?
Да. Мультиязычные модели (bge-m3, multilingual-e5-large, qwen3-embedding-8b) обучены в том числе на русском и уверенно сближают похожие по смыслу русские фразы. Модели OpenAI тоже работают с русским, но на узких предметных областях стоит сравнить качество на своих данных.
Можно ли сравнивать векторы разных моделей?
Нет. Каждая модель строит собственное пространство координат, и косинусная близость между векторами разных моделей не имеет смысла. При смене модели пересчитайте векторы всей базы — пакетные запросы делают это быстро и недорого.
Чем семантический поиск отличается от полнотекстового?
Полнотекстовый ищет совпадения слов и их форм, семантический — близость смысла. На практике их часто объединяют: полнотекстовый находит точные термины и артикулы, векторный — перефразированные вопросы. Гибридная схема даёт результат лучше, чем каждый подход по отдельности.
Все модели из статьи доступны в Hubris — единый API, оплата в рублях.