Эмбеддинги простыми словами: что это и как работают (с кодом)
Что такое эмбеддинг?
Эмбеддинг - это вектор (список чисел), в который модель превращает текст, изображение или другой объект так, что близкие по смыслу объекты получают близкие векторы. Поэтому смысл можно сравнивать арифметикой: расстоянием или косинусом угла между векторами.
TL;DR
- -Эмбеддинг - вектор из сотен или тысяч чисел, который описывает смысл текста. Близкие по смыслу тексты получают близкие векторы, и их можно сравнивать косинусом.
- -У OpenAI, Voyage, Gemini и Ollama векторы нормализованы до длины 1, поэтому косинус совпадает со скалярным произведением, а евклидово расстояние даёт тот же порядок результатов.
- -Документы и запросы нужно векторизовать одной и той же моделью. Некоторым моделям нужны префиксы: nomic-embed-text ждёт search_query: и search_document:, multilingual-e5 ждёт query: и passage:.
- -Вектор можно укоротить, если модель обучена по схеме Matryoshka (OpenAI v3, Voyage 4, Gemini, Qwen3-Embedding, nomic v1.5). После ручной обрезки вектор нормализуют заново.
- -Для русского текста английская модель не подходит. В замере автора на 10 парах RU/EN nomic-embed-text нашла правильный перевод в 2 случаях из 10, многоязычная bge-m3 во всех 10.
Эмбеддинг - это вектор (список чисел), в который модель превращает текст, изображение или другой объект так, что близкие по смыслу объекты получают близкие векторы. Фразы «кот спит на диване» и «котёнок дремлет на кушетке» не делят ни одного слова, но у хорошей модели их векторы окажутся рядом, а вектор фразы про квартальную выручку уйдёт далеко. Поэтому смысл можно сравнивать арифметикой: расстоянием или косинусом угла между векторами.
На эмбеддингах держатся семантический поиск, RAG, дедупликация, кластеризация и рекомендации. При этом многие ошибки в таких системах происходят не в языковой модели, а на шаге векторизации: документы и запросы закодированы разными моделями, забыт обязательный префикс, длинный текст молча обрезан, русский текст отдан английской модели.
Статья о том, как устроен эмбеддинг на уровне интуиции, как считать близость, как выбрать модель на ноябрь 2026 года и как укоротить вектор без потери качества. В примере на Python векторы считаются локально через Ollama, а цифры сравнения английской и многоязычной модели на русских и английских фразах взяты из замера автора 2026-11-08 на Ollama 0.40.14.
Что такое эмбеддинг
Эмбеддинг-модель принимает текст и возвращает вектор фиксированной длины: 768, 1024, 1536 или 3072 числа, в зависимости от модели. Длина не зависит от того, подали вы одно слово или страницу. Модель обучена так, чтобы тексты с похожим смыслом попадали в соседние точки этого многомерного пространства.
С чем эмбеддинг путают:
- Не сжатие в архивном смысле. Из вектора нельзя восстановить исходный текст. Модель сохраняет то, что нужно для сравнения по смыслу, и теряет формулировки.
- Не перевод. Вектор не текст на другом языке. Многоязычная модель кладёт фразу и её перевод рядом, но сам вектор ни на каком языке не написан.
- Не словарь значений слова. Эмбеддинг не перечисляет значения. Он даёт точку в пространстве, и смысл виден только по тому, какие точки оказались рядом.
Формально эмбеддинг - это отображение: правило, которое ставит объекту в соответствие вектор. Отсюда и название, «вложение» объектов в векторное пространство.
Аналогия: координаты на карте смыслов
На обычной карте у города две координаты, и по ним видно, какие города рядом. Эмбеддинг делает то же для смысла, только координат сотни. Отдельная координата сама по себе ничего не значит: нет оси «про животных» или «про деньги». Значение имеет только взаимное расположение точек. Поэтому вектор одного текста бесполезен, полезно сравнение двух векторов.
От word2vec к эмбеддингам предложений
Массовой идея стала после word2vec: статья Миколова и соавторов Efficient Estimation of Word Representations in Vector Space вышла на arXiv в январе 2013 года. В ней приводится известный пример: вектор «King» минус «Man» плюс «Woman» ближе всего к вектору «Queen».
У word2vec один вектор на слово. Слово «ключ» получает одну точку, будь то ключ от двери или родник. Современные модели векторизуют предложение или абзац целиком и учитывают контекст: «ключ» в двух разных фразах влияет на вектор по-разному. Для поиска это и нужно, потому что люди ищут ответы на вопросы, а не отдельные слова.
Как это работает: вектор, расстояние, косинус
Размерность
Размерность - это количество чисел в векторе. У text-embedding-3-small от OpenAI по умолчанию 1536, у text-embedding-3-large 3072, у bge-m3 1024, у nomic-embed-text 768. Больше измерений обычно значит больше деталей смысла, но и больше места.
Место считается просто. Одно число float32 занимает 4 байта, вектор из 1536 чисел занимает 6 КБ, миллион таких векторов около 6 ГБ без индекса. Поэтому при выборе модели размерность считают вместе с качеством. Voyage, например, отдаёт векторы в int8 и бинарном виде, это по документации уменьшает объём в 4 и в 32 раза соответственно.
Косинус, скалярное произведение и евклидово расстояние
Близость двух векторов меряют тремя способами:
| Мера | Что считает | Когда использовать |
|---|---|---|
| Косинусная близость | Угол между векторами, от -1 до 1 | Значение по умолчанию для текста |
| Скалярное произведение | Угол и длину векторов вместе | Если векторы нормализованы, это тот же косинус, но быстрее |
| Евклидово расстояние | Прямую дистанцию между концами векторов | Для нормализованных векторов даёт тот же порядок, что и косинус |
OpenAI, Voyage и Ollama возвращают векторы, нормализованные до длины 1. Это прямо сказано в документации OpenAI и в гайде по эмбеддингам у Anthropic (про Voyage), а Ollama пишет, что /api/embed отдаёт L2-нормализованные векторы. Для таких векторов косинус равен скалярному произведению, а косинус и евклидово расстояние ранжируют результаты одинаково. Выбор меры в этом случае влияет на скорость, а не на результат.
Нормализация теряется, если вектор обрезать вручную: после этого скалярное произведение уже не равно косинусу.
Где применяются эмбеддинги
- Семантический поиск. Запрос и документы превращаются в векторы, ближайшие документы и есть результат. Работает, потому что близость векторов отражает близость смысла, а не совпадение слов.
- RAG. Тот же поиск, только найденные фрагменты отдаются языковой модели как контекст для ответа.
- Кластеризация. Отзывы, тикеты или статьи группируются по смыслу алгоритмом вроде k-means прямо по векторам.
- Рекомендации. «Похожие статьи» и «похожие товары» ищутся по ближайшим векторам описаний.
- Классификация. Поверх векторов обучают простой классификатор или сравнивают текст с векторами эталонных примеров каждого класса.
- Дедупликация. Пары текстов с очень высоким косинусом чаще всего оказываются дублями или перефразами друг друга.
Пример: считаем близость руками
Пример ниже воспроизводит замер автора: 10 русских фраз и их английские переводы, две модели в Ollama. Для каждой модели считаются три числа: средний косинус фразы и её перевода, средний косинус с чужими переводами (90 несовпадающих пар) и top-1, то есть для скольких русских фраз ближайшим английским вектором оказался их собственный перевод.
Нужна запущенная Ollama и две модели: nomic-embed-text (английская, 274 MB) и bge-m3 (многоязычная, 1,2 GB).
ollama pull nomic-embed-text
ollama pull bge-m3
Код использует только стандартную библиотеку Python. Запрос идёт в нативный эндпоинт /api/embed: поле model и поле input со строкой или массивом строк, в ответе массив embeddings.
import json, math, urllib.request
def embed(model, texts):
req = urllib.request.Request(
"http://localhost:11434/api/embed",
json.dumps({"model": model, "input": texts}).encode(),
{"Content-Type": "application/json"})
return json.load(urllib.request.urlopen(req, timeout=600))["embeddings"]
def cos(a, b):
dot = sum(x * y for x, y in zip(a, b))
return dot / math.sqrt(sum(x * x for x in a) * sum(y * y for y in b))
PAIRS = [("Кот спит на диване.", "The cat is sleeping on the sofa."),
("Поезд опоздал на сорок минут.", "The train was forty minutes late."),
# остальные пары из таблицы ниже
]
Функция cos считает косинус полностью, хотя у Ollama векторы уже нормализованы и хватило бы скалярного произведения. Так код работает с любым источником векторов.
n = len(PAIRS)
for model, prefix in (("nomic-embed-text", "clustering: "), ("bge-m3", "")):
ru = embed(model, [prefix + r for r, _ in PAIRS])
en = embed(model, [prefix + e for _, e in PAIRS])
match = [cos(ru[i], en[i]) for i in range(n)]
other = [cos(ru[i], en[j]) for i in range(n) for j in range(n) if i != j]
top1 = sum(max(range(n), key=lambda j: cos(ru[i], en[j])) == i
for i in range(n))
print(model, len(ru[0]), round(sum(match) / n, 3),
round(sum(other) / len(other), 3), f"top-1 {top1}/{n}")
У nomic-embed-text префикс задачи обязателен по карточке модели. Для сравнения двух текстов между собой подходит clustering: . Для поиска нужна пара search_query: и search_document: . У bge-m3 префиксы не нужны, это прямо сказано в её карточке.
Результат и как его читать
Замер автора 2026-11-08, Ollama 0.40.14 на Mac с Apple Silicon, полный набор из 10 пар:
| Модель (Ollama) | Размерность | Косинус перевода, среднее (мин.) | Косинус с чужой фразой, среднее (макс.) | top-1 RU→EN |
|---|---|---|---|---|
nomic-embed-text v1.5 (английская), префикс clustering: | 768 | 0,587 (0,539) | 0,583 (0,622) | 2/10 |
| bge-m3 (многоязычная) | 1024 | 0,921 (0,821) | 0,465 (0,630) | 10/10 |
У английской модели перевод и случайная фраза почти неотличимы: 0,587 против 0,583. Диапазоны пересекаются: худший перевод (0,539) получил меньший косинус, чем самая похожая чужая пара (0,622), и поиск нашёл правильный перевод только в 2 случаях из 10. У bge-m3 разрыв большой: 0,92 против 0,47, и даже самый слабый перевод (0,821) выше самой похожей чужой фразы (0,630), поэтому и результат 10 из 10.
Косинус по каждой паре:
| Русская фраза | Английский перевод | nomic-embed-text | bge-m3 |
|---|---|---|---|
| Кот спит на диване. | The cat is sleeping on the sofa. | 0,617 | 0,821 |
| Завтра обещают сильный дождь. | Heavy rain is forecast for tomorrow. | 0,577 | 0,944 |
| Сервер перестал отвечать после обновления. | The server stopped responding after the update. | 0,577 | 0,926 |
| Я забыл ключи от машины дома. | I left my car keys at home. | 0,602 | 0,931 |
| Цены на билеты выросли вдвое. | Ticket prices have doubled. | 0,554 | 0,971 |
| Модель плохо справляется с длинными документами. | The model struggles with long documents. | 0,590 | 0,886 |
| Врач посоветовал больше гулять. | The doctor advised walking more. | 0,618 | 0,948 |
| Поезд опоздал на сорок минут. | The train was forty minutes late. | 0,606 | 0,920 |
| Как вернуть деньги за неудачную покупку? | How do I get a refund for a bad purchase? | 0,539 | 0,900 |
| Библиотека закрывается в восемь вечера. | The library closes at eight in the evening. | 0,589 | 0,963 |
Сравнивать надо разрыв внутри одной модели, а не абсолютные числа между моделями. Косинус 0,6 у nomic-embed-text попадает и в переводы, и в чужие пары, а у bge-m3 это уровень несвязанных фраз: все переводы у неё выше 0,82.
Ограничения замера: 10 коротких бытовых фраз, одна пара языков, одна метрика. Это иллюстрация того, что английская модель не годится для русского текста, а не бенчмарк. По ollama show у bge-m3 566,7M параметров и контекст 8192 токена, у nomic-embed-text 137M параметров и контекст 2048 в Ollama.
Как поставить Ollama и выбрать модель под память, разобрано в гайде по Ollama и в гайде по локальным LLM.
Какую модель выбрать (ноябрь 2026)
Цены и характеристики ниже сняты 8 ноября 2026 года с официальных страниц вендоров.
Облачные модели
| Модель | Размерность | Контекст, токенов | Цена за 1M токенов | Языки по документации |
|---|---|---|---|---|
| OpenAI text-embedding-3-small | 1536, меньше через dimensions | 8192 | $0,02 | на странице не указаны |
| OpenAI text-embedding-3-large | 3072, меньше через dimensions | 8192 | $0,13 | на странице не указаны |
| Voyage voyage-4-large | 1024 (256, 512, 2048) | 32 000 | $0,12 | многоязычная |
| Voyage voyage-4 | 1024 (256, 512, 2048) | 32 000 | $0,06 | многоязычная |
| Voyage voyage-4-lite | 1024 (256, 512, 2048) | 32 000 | $0,02 | не заявлено |
| Google gemini-embedding-2 | 3072 (от 128 до 3072) | 8192 | $0,20 текст, $0,10 в Batch | 100+ |
| Cohere embed-v5.0-pro | 2048 (256, 512, 768, 1024, 1536) | 128 000 | на официальной странице не опубликована | 100+ |
OpenAI. В документации по-прежнему text-embedding-3-small и text-embedding-3-large, моделей новее v3 там нет. Размерность уменьшается параметром dimensions.
Voyage. Своей эмбеддинг-модели у Anthropic нет. В документации Claude рекомендуют Voyage AI (принадлежит MongoDB) с оговоркой, что стоит сравнить нескольких вендоров. Ключ теперь выдаёт MongoDB Atlas, эндпоинт https://ai.mongodb.com/v1/embeddings. Первые 200 млн токенов бесплатны по странице биллинга. Для поиска обязателен параметр input_type: к тексту добавляется служебный префикс для запроса или документа.
import numpy as np, voyageai
vo = voyageai.Client() # ключ берётся из переменной окружения VOYAGE_API_KEY
docs = ["The cat is sleeping on the sofa", "Quarterly revenue grew by 12 percent"]
d = vo.embed(docs, model="voyage-4", input_type="document").embeddings
q = vo.embed(["where is the cat?"], model="voyage-4", input_type="query").embeddings[0]
print(np.dot(d, q)) # векторы нормализованы: dot == cosine
Для ключей Atlas нужна библиотека voyageai версии 0.3.7 или новее (pip install -U voyageai). Ключ держат в окружении, а не в коде.
Google. gemini-embedding-2 мультимодальная: текст, изображения, видео, аудио и PDF попадают в одно пространство. Параметра task_type у неё нет, задача пишется прямо в тексте: task: search result | query: ... для запроса, title: ... | text: ... для документа. Предыдущая gemini-embedding-001 работает только с текстом, принимает до 2048 токенов и стоит $0,15 за 1M токенов. Пространства двух моделей несовместимы: при переходе на gemini-embedding-2 всю базу векторизуют заново.
Cohere. Embed 5 есть в двух вариантах, embed-v5.0-pro и embed-v5.0-fast, с одинаковыми размерностями и контекстом 128K. Понимает текст, изображения и смешанный ввод вроде страниц PDF. На официальных страницах Cohere цены Embed 5 на 8 ноября 2026 года не нашлось, сторонние агрегаторы приводят свои цифры, их стоит сверить с прайсом перед расчётом бюджета.
Открытые модели для своего сервера
| Модель | Размер в Ollama | Размерность | Контекст | Лицензия | Префиксы | Языки |
|---|---|---|---|---|---|---|
| nomic-embed-text v1.5 | 274 MB | 768 (Matryoshka до 64) | 8192 в модели, 2048 в Ollama | Apache 2.0 | обязательны | английский |
| bge-m3 | 1,2 GB | 1024 | 8192 | MIT | не нужны | 100+ |
| multilingual-e5-large | не проверялся | 1024 | 512 | MIT | query: / passage: обязательны | около 100 |
| Qwen3-Embedding 0.6B / 4B / 8B | 639 MB / 2,5 GB / 4,7 GB | до 4096, любая от 32 | 32K | Apache 2.0 | инструкции поддерживаются | 100+ и код |
| EmbeddingGemma | 622 MB | 768 (512, 256, 128) | 2048 | Gemma | рекомендуются, task: / title: | 100+ |
BGE-M3 кроме обычного плотного вектора умеет разреженный и multi-vector режим. multilingual-e5-large обрезает текст до 512 токенов, а без префиксов query: и passage: по карточке модели работает хуже. Карточка Qwen3-Embedding-8B заявляет первое место в мультиязычном рейтинге MTEB со средним 70,58 на 5 июня 2025 года: это заявление авторов на дату публикации, а не текущее положение. EmbeddingGemma на 300M параметров используется в официальных примерах Ollama, запросы к ней оформляют так же, как к gemini-embedding-2.
Российские сервисы
Yandex AI Studio. Модели идут парами, для документов и для запросов: text-search-doc и text-search-query с вектором на 256 измерений и вторая версия text-embeddings-v2-doc / text-embeddings-v2-query с размерностью 128, 256, 512 или 768 по документации. Документы векторизуют doc-моделью, запросы query-моделью. По прайсу на 8 ноября 2026 года векторизация стоит 0,0101 ₽ за 1000 токенов с НДС.
GigaChat. Модель Embeddings с окном 512 токенов и вектором 1024 и EmbeddingsGigaR с окном 4096 токенов и вектором 2560. На странице EmbeddingsGigaR на 8 ноября 2026 года все эмбеддинг-модели GigaChat (Embeddings, Embeddings-2, EmbeddingsGigaR, GigaEmbeddings-3B-2025-09) стоят 14 ₽ за 1 млн токенов.
Как читать MTEB и ruMTEB
MTEB - общий лидерборд эмбеддинг-моделей по множеству задач: поиск, кластеризация, классификация, сравнение пар. Места в нём меняются часто, поэтому смотреть стоит не общий рейтинг, а колонку той задачи, которая нужна вам, обычно retrieval. Для русского языка есть ruMTEB (Snegirev et al., NAACL 2025), его задачи входят в тот же лидерборд.
Цифры из лидерборда служат для короткого списка, а не для финального выбора. Финальный выбор делают на своих данных: 50-100 реальных запросов с заранее известными правильными документами и доля случаев, когда нужный документ попал в top-5.
Matryoshka: как укоротить вектор и не потерять качество
Matryoshka Representation Learning (Kusupati et al., 2022) обучает модель так, что самая важная информация собирается в начале вектора. Первые 256 чисел из 768 уже работают как самостоятельный вектор, только чуть грубее, как меньшая матрёшка внутри большой.
Насколько грубее, видно по карточке nomic-embed-text-v1.5, средний балл MTEB при разной длине вектора:
| Размерность | 768 | 512 | 256 | 128 | 64 |
|---|---|---|---|---|---|
| MTEB | 62,28 | 61,96 | 61,04 | 59,34 | 56,10 |
Вектор в три раза короче теряет чуть больше балла, в двенадцать раз короче уже около шести.
Проще всего попросить нужную длину у API: dimensions у OpenAI и в Ollama /api/embed, output_dimension у Voyage, output_dimensionality у Gemini. Если векторы уже лежат в базе, их обрезают сами и нормализуют заново:
import numpy as np
def shorten(v, k):
v = np.array(v)[:k]
return v / np.linalg.norm(v)
Без второй строки обрезанный вектор теряет длину 1, и скалярное произведение перестаёт совпадать с косинусом. gemini-embedding-2 нормализует укороченные векторы сама, gemini-embedding-001 нет: для неё нормализация вручную обязательна.
Обрезать имеет смысл только модели, обученные по этой схеме: OpenAI v3, Voyage 4, Gemini, Cohere Embed 5, Qwen3-Embedding, EmbeddingGemma, nomic-embed-text v1.5. У bge-m3 и multilingual-e5 размерность фиксированная: обрезка их векторов в карточках моделей не предусмотрена, и качество после неё никто не гарантирует.
Типичные ошибки
Разные модели в индексе и в запросе. Векторы разных моделей лежат в разных пространствах, сравнивать их бессмысленно. Это касается и версий одного вендора: gemini-embedding-001 и gemini-embedding-2 несовместимы. Смена модели означает переиндексацию всей базы. Ollama в документации отдельно советует одну модель для индексации и поиска.
Забытые префиксы. nomic-embed-text ждёт search_query: для запроса и search_document: для документа, multilingual-e5 ждёт query: и passage: , EmbeddingGemma и gemini-embedding-2 ждут формат task: ... | query: .... Без префикса модель отдаёт вектор, ошибки нет, но поиск работает хуже. В облачных API ту же роль играет параметр: input_type у Voyage.
Слишком длинный текст. У multilingual-e5 лимит 512 токенов, у EmbeddingGemma 2048, у nomic-embed-text в Ollama 2048. Ollama по умолчанию обрезает вход молча (truncate: true), и хвост документа просто не попадает в вектор. С truncate: false вместо обрезки придёт ошибка. Длинные документы режут на фрагменты до векторизации.
Английская модель на русском тексте. Замер выше показывает, как это выглядит: nomic-embed-text дала переводу и чужой фразе одинаковый косинус, около 0,58. Модель не падает и не предупреждает, она просто перестаёт различать смысл.
Сравнение косинуса между моделями. Порог «похоже, если больше 0,8» привязан к конкретной модели. В замере выше у bge-m3 такой порог отделил бы все переводы от чужих фраз, а у nomic-embed-text не прошёл бы ни один перевод: её максимум 0,618. При смене модели порог подбирают заново на своих данных.
Когда эмбеддинги не нужны
Векторный поиск плохо находит точные строки: артикулы, коды ошибок, номера договоров, имена функций. Вектор передаёт смысл, и векторы «ERR_4012» и «ERR_4013», скорее всего, окажутся рядом. Для таких запросов работает обычный полнотекстовый поиск вроде BM25, а в продакшене оба поиска часто объединяют в гибридный.
Если весь корпус умещается в контекст модели, а запросов немного, документы можно передать в промпт целиком без векторного поиска. Когда хватает длинного контекста, а когда без RAG не обойтись, разобрано в статье Context Engineering vs RAG.
Векторная база тоже нужна не всегда. Тысяча векторов по 1024 числа занимает около 4 МБ, и поиск по ним сводится к одному умножению матрицы на вектор в numpy. База нужна, когда векторов миллионы, нужны фильтры по метаданным и обновления без пересборки. Если проект уже на Postgres, удобный вариант - расширение pgvector, оно есть в Supabase.
С чего начать
- Поставить Ollama и скачать многоязычную модель:
ollama pull bge-m3илиollama pull embeddinggemma. - Прогнать пример из этой статьи на своих текстах и посмотреть, как разводятся похожие и непохожие пары.
- Собрать 50-100 реальных запросов с известными правильными документами и сравнить на них две-три модели из коротких списков выше.
- Выбрать размерность: начать с размерности по умолчанию и уменьшать через Matryoshka, если упираетесь в объём базы.
- Зафиксировать модель, версию и префиксы в коде индексации и поиска в одном месте, чтобы они не разошлись.
Как собрать из эмбеддингов полноценный поиск по документам с разбиением на фрагменты, гибридным поиском и reranking, описано в гайде по RAG-системам.