hubris
Эмбеддинги и семантический поиск: практический гайд

Эмбеддинги и семантический поиск: практический гайд

28 июня 2026 г. · Команда Hubris · 7 мин чтения

Эмбеддинг — это представление текста в виде вектора чисел, в котором близкие по смыслу фразы получают близкие векторы. Семантический поиск сравнивает такие векторы и находит документы по смыслу, а не по совпадению слов. В этом гайде вы получите векторы через /v1/embeddings, посчитаете косинусную близость на numpy и соберёте рабочий поиск по 20 документам — с оплатой в рублях.

Что такое векторное представление текста

Слова распадаются на частицы и превращаются в координатные оси векторов

Векторное представление (эмбеддинг) — это список из сотен или тысяч чисел, который модель сопоставляет тексту. Сами числа по отдельности ничего не значат, важно взаимное расположение: тексты с похожим смыслом модель размещает рядом, с разным — далеко друг от друга.

Несколько свойств, которых достаточно для практики:

  • «Кот спит на диване» и «Кошка дремлет на софе» окажутся рядом, хотя общих слов у них нет.
  • Запрос «как вернуть деньги за заказ» найдёт документ «Политика возвратов» — поиск работает по смыслу, а не по вхождению слов.
  • Длина вектора фиксирована: например, openai/text-embedding-3-small всегда возвращает 1536 чисел — и для короткой фразы, и для страницы текста.
  • Сравнивать можно только векторы одной модели: у разных моделей системы координат не совпадают.

Обучать ничего не нужно — готовые векторы отдаёт обычный HTTP-эндпоинт.

Как получить векторы через /v1/embeddings

Эндпоинт принимает OpenAI-совместимый формат, поэтому подойдёт стандартный openai SDK — меняются только base_url и ключ. Подготовка занимает пару минут:

  1. Зарегистрируйтесь на hubris.pw/sign-in — вход по коду из письма, пароль не нужен.
  2. Создайте API-ключ в разделе «Ключи».
  3. Пополните баланс в разделе «Биллинг» — СБП, карта или счёт для юрлиц.

Подробнее эти шаги разобраны в быстром старте. Дальше — код:

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-8b32 0001,06 ₽
baai/bge-m38 1921,06 ₽
intfloat/multilingual-e5-large8 1921,06 ₽
openai/text-embedding-3-small8 1922,12 ₽
mistralai/mistral-embed-23128 19210,61 ₽
openai/text-embedding-3-large8 19213,79 ₽
google/gemini-embedding-00120 00015,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, оплата в рублях.