← все статьи
8 мин

Embeddings: как выбрать модель и не пожалеть

Вы строите поиск или RAG-пайплайн. Берёте первую попавшуюся модель эмбеддингов, запускаете — и всё вроде бы работает. А потом оказывается, что русскоязычные запросы дают мусорные результаты, latency убивает UX, или счёт за API в пять раз выше запланированного. Проблема не в векторном хранилище — проблема в выборе модели на старте.

Разберёмся с тем, какие модели существуют, чем они реально отличаются и как выбрать под конкретную задачу.

Зачем вообще нужны эмбеддинги

Эмбеддинги — это плотные числовые векторы, которые кодируют семантику текста. Похожие по смыслу тексты получают похожие векторы — это и есть база для трёх основных применений.

Semantic search. Пользователь пишет «как настроить авторизацию», а в базе есть документ «конфигурация аутентификации JWT». Точное совпадение слов нулевое, но семантически — это одно и то же. BM25 такой запрос завалит, эмбеддинги — нет.

RAG (Retrieval-Augmented Generation). Перед генерацией ответа LLM нужно достать релевантный контекст из базы знаний. Качество эмбеддинга напрямую определяет, насколько релевантный контекст попадёт в промпт — и насколько точным будет ответ модели.

Классификация и кластеризация. Эмбеддинги как признаки для downstream-задач: определение тональности, группировка тикетов, обнаружение дубликатов. Здесь важна не столько топовая точность, сколько стабильность и стоимость на большом объёме.

Что сравниваем: четыре модели

Рассмотрим четыре варианта, которые реально используются в продакшне. Не весь рынок — только то, что проверено на практике.

OpenAI text-embedding-3-small — 1536 измерений по умолчанию (можно сжать до 512), стоимость $0.02 за 1M токенов. Быстрый, удобный API, хороший результат из коробки. Минус: платный, данные уходят в OpenAI, поддержка не-английских языков заметно слабее.

OpenAI text-embedding-3-large — 3072 измерения, $0.13 за 1M токенов. В 6.5 раз дороже small, прирост качества на MTEB — порядка 3–5 пунктов. Оправдан только если у вас уже есть задача, где small явно не дотягивает.

multilingual-e5-large (sentence-transformers) — open-source модель от Microsoft, 1024 измерения, поддержка 100+ языков включая русский, казахский, арабский. Запускается локально — нулевая стоимость per-запрос. На мультиязычных бенчмарках MTEB регулярно обходит OpenAI small. Цена — нужны GPU или CPU-ресурсы на inference.

Cohere embed-v3 — 1024 измерения, $0.10 за 1M токенов (search), специально обученный для retrieval задач. Поддерживает input_type параметр: отдельно для «документов» и «запросов», что даёт асимметричный поиск. Один из лучших результатов на BEIR-бенчмарке.

Метрики: что важно при выборе

Не смотрите только на MTEB Leaderboard — там английские бенчмарки доминируют. Важно понять свой конкретный профиль.

from sentence_transformers import SentenceTransformer
import numpy as np
import time

def benchmark_model(model_name, texts, queries):
    model = SentenceTransformer(model_name)

    # Индексируем документы
    t0 = time.perf_counter()
    doc_embeddings = model.encode(texts, batch_size=64, show_progress_bar=False)
    index_time = time.perf_counter() - t0

    # Тестируем запросы
    query_times = []
    for q in queries:
        t0 = time.perf_counter()
        q_emb = model.encode([q])
        query_times.append(time.perf_counter() - t0)

    # Косинусное сходство
    sims = np.dot(doc_embeddings, q_emb.T).flatten()

    return {
        "index_time_s": round(index_time, 3),
        "avg_query_ms": round(np.mean(query_times) * 1000, 2),
        "dim": doc_embeddings.shape[1],
    }

texts = ["...ваши документы..."]
queries = ["...ваши запросы..."]

result = benchmark_model("intfloat/multilingual-e5-large", texts, queries)
print(result)

Три параметра, на которые смотрим в первую очередь:

Размерность влияет на память и скорость поиска. 3072 у text-embedding-3-large — это в 3 раза больше памяти под индекс по сравнению с 1024. При 10M документов разница ощутимая.

Latency на inference. Для real-time поиска latency запроса должна быть под 50ms. OpenAI API добавляет сетевой round-trip ~100–200ms. Локальные модели на GPU — 5–20ms.

Стоимость. При больших объёмах ($0.02 vs $0.13 vs $0) разница в десятки тысяч долларов в год. Считайте заранее.

Multilingual: когда без него не обойтись

Если ваши данные или пользователи — на русском, казахском или смеси языков, OpenAI text-embedding-3-small заметно деградирует. Проверьте это просто:

import openai
import numpy as np

client = openai.OpenAI()

def embed(text, model="text-embedding-3-small"):
    resp = client.embeddings.create(input=[text], model=model)
    return np.array(resp.data[0].embedding)

# Проверяем кросс-языковое сходство
en = embed("database connection timeout error")
ru = embed("ошибка подключения к базе данных — превышен таймаут")
kk = embed("деректер қорына қосылу қатесі")

cos = lambda a, b: np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

print(f"EN–RU similarity: {cos(en, ru):.3f}")
print(f"EN–KK similarity: {cos(en, kk):.3f}")
print(f"RU–KK similarity: {cos(ru, kk):.3f}")
# text-embedding-3-small: EN-RU ~0.55, RU-KK ~0.40
# multilingual-e5-large:  EN-RU ~0.82, RU-KK ~0.74

Разрыв в кросс-языковом сходстве — это не абстрактная метрика. Это то, как часто ваш поиск будет находить релевантный русский документ по английскому запросу (и наоборот). Для мультиязычного продукта multilingual-e5-large — очевидный выбор.

Хранение векторов: pgvector, Qdrant, Pinecone

Модель выбрана — теперь нужно где-то хранить и искать. Три популярных варианта с разными trade-offs.

pgvector — расширение для PostgreSQL. Если у вас уже есть Postgres, это нулевой overhead на инфраструктуру. Поддерживает IVFFlat и HNSW индексы, фильтрацию по метаданным через стандартный SQL. Ограничение: при >5M векторов начинает проседать по QPS.

-- Подключаем расширение
CREATE EXTENSION IF NOT EXISTS vector;

-- Таблица с эмбеддингами
CREATE TABLE documents (
    id        BIGSERIAL PRIMARY KEY,
    content   TEXT,
    source    TEXT,
    embedding vector(1024)
);

-- HNSW индекс для быстрого ANN-поиска
CREATE INDEX ON documents
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

-- Поиск ближайших соседей
SELECT id, content, 1 - (embedding <=> $1) AS score
FROM documents
ORDER BY embedding <=> $1
LIMIT 10;

Qdrant — специализированный векторный движок, написанный на Rust. Нативная поддержка фильтрации по payload, sparse+dense гибридный поиск, хорошая горизонтальная масштабируемость. Self-hosted или managed cloud. Для большинства production RAG-пайплайнов — оптимальный выбор.

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct

client = QdrantClient(url="http://localhost:6333")

# Создаём коллекцию
client.create_collection(
    collection_name="docs",
    vectors_config=VectorParams(size=1024, distance=Distance.COSINE),
)

# Загружаем векторы
points = [
    PointStruct(
        id=i,
        vector=embedding.tolist(),
        payload={"content": text, "source": source}
    )
    for i, (embedding, text, source) in enumerate(data)
]
client.upsert(collection_name="docs", points=points)

# Поиск с фильтром по метаданным
from qdrant_client.models import Filter, FieldCondition, MatchValue

results = client.search(
    collection_name="docs",
    query_vector=query_embedding.tolist(),
    query_filter=Filter(
        must=[FieldCondition(key="source", match=MatchValue(value="internal_wiki"))]
    ),
    limit=5,
)

Pinecone — fully managed, простой API, быстрый старт. Хорошо подходит для прототипов и команд без DevOps-ресурсов. Минус: стоимость растёт нелинейно с объёмом, данные у провайдера, меньше контроля над конфигурацией индекса.

Итого: как выбирать

Простой алгоритм принятия решения:

# Псевдокод выбора модели эмбеддингов

def choose_embedding_model(use_case):
    if use_case.languages != ["en"]:
        # Мультиязычные данные — open-source выигрывает
        if use_case.can_host_model:
            return "intfloat/multilingual-e5-large"  # лучшее качество, бесплатно
        else:
            return "cohere/embed-multilingual-v3.0"  # managed, но дорого

    if use_case.scale == "prototype":
        return "openai/text-embedding-3-small"  # быстрый старт

    if use_case.retrieval_quality == "critical":
        # Сравните small vs large на своих данных
        # Разница ~3-5 пп на MTEB, но стоимость в 6.5x
        return benchmark_both_and_decide()

    # Default для англоязычного продакшна
    return "openai/text-embedding-3-small"

Для хранения: начните с pgvector, если уже используете PostgreSQL и объём <5M документов. При росте или необходимости гибридного поиска — мигрируйте на Qdrant. Pinecone — только если нет времени на инфраструктуру и бюджет это позволяет.

Одна рекомендация, которую игнорируют чаще всего: тестируйте модели на своих данных, не на бенчмарках. MTEB — это хорошая стартовая точка, но ваши русско-казахские тексты с доменной терминологией — это не Wikipedia. Напишите тест с 50–100 query-document парами с оценкой relevance, и прогоните все кандидаты. Это 2–3 часа работы, которые сэкономят недели переделки.

Выбор модели эмбеддингов — это не разовое решение. Это архитектурный выбор, который определяет качество поиска, стоимость инфраструктуры и сложность поддержки на годы вперёд. Потратьте день на бенчмарк — сэкономите месяц на рефакторинг.

← все статьи Следующая →Монолит → микросервисы: когда не надо