Зачем превращать текст в числа
Компьютер не понимает слова — он работает с числами. Чтобы сравнивать тексты по смыслу, нужен способ отобразить любой текст в числовой объект, пригодный для математического сравнения.
Наивный подход — посчитать, сколько раз каждое слово встречается в тексте (TF-IDF, bag-of-words). Это работает для поиска по ключевым словам, но полностью теряет смысл: «установить» и «инсталлировать» — это одно и то же, но у них разные числа в таком представлении.
«установка FastAPI»«установка FastAPI»Embedding решает задачу иначе: каждый текст превращается в точку в многомерном пространстве так, что семантически близкие тексты оказываются геометрически близки. Запрос «установка FastAPI» и документ «FastAPI installation guide» находятся в похожих точках пространства — их можно сравнить математически.
Теория: от слов к пространству смыслов
Гипотеза дистрибутивности
Всё начинается с наблюдения лингвиста Джона Фёрта (1957):
Возьмём слова «врач», «доктор», «хирург». Они появляются в текстах рядом с похожими словами: больница, пациент, лечение, операция. Значит, эти слова означают нечто близкое, даже если мы никогда явно не программировали это знание.
Именно это используют нейросети для создания embeddings: модель обучается на миллиардах текстов и самостоятельно обнаруживает, какие слова и фразы появляются в похожих контекстах. Никаких словарей, никаких правил — только статистика совместной встречаемости.
Путь от Word2Vec к контекстным моделям
Идея векторных представлений прошла несколько поколений:
2013 — Word2Vec (Google)
Каждое слово = один фиксированный вектор.
«bank» всегда один и тот же вектор — нельзя различить
«river bank» и «bank account».
Плюс: работает быстро, небольшой размер.
2018 — BERT (Google), контекстные embeddings
Каждый токен получает вектор, зависящий от контекста.
«bank» в «river bank» → один вектор.
«bank» в «bank account» → другой вектор.
Весь текст представлен набором контекстных векторов.
2019–2020 — Sentence-BERT, E5, BGE — sentence embeddings
Модели специально обучены давать один вектор
на целое предложение/абзац через contrastive learning:
похожие тексты принудительно притягиваются в пространстве,
непохожие — отталкиваются.
2022–2024 — text-embedding-3, Cohere embed-v3
Большие модели с тысячами измерений. Поддержка 100+ языков,
матрёшечные embeddings (можно урезать размерность без переобучения).
Для RAG используются модели последнего поколения — sentence/document embeddings. Они дают один вектор на произвольный текст (не только на слово), и специально обучены на задачу семантического поиска.
Как устроена embedding-модель
Посмотрим, что происходит внутри, когда мы передаём текст в embedding-модель на основе трансформера.
Transformer обрабатывает каждый токен в контексте всех остальных (механизм self-attention). После 12–24 таких слоёв каждый токен «знает» о всём окружении. Mean pooling усредняет все токены в один вектор — это и есть embedding документа.
Свойства векторного пространства
Векторное пространство embeddings имеет несколько замечательных свойств, которые делают его полезным для поиска.
sim("Python документация", "рецепт торта") = 0.08
Кластер SQL: запрос, таблица, индекс, JOIN...
+ v(«женщина») ≈ v(«королева»)
v(«Россия») − v(«Москва»)
+ v(«Берлин») ≈ v(«Германия»)
⟹ dot(a, b) = cos(θ) = cosine_sim(a, b)
Косинусное сходство: главная метрика близости
Для сравнения embeddings используют косинусное сходство — косинус угла между двумя векторами. Оно измеряет направление, а не длину вектора, что важно для текстов разной длины.
cos(θ) = (A · B) / (|A| × |B|) где · — скалярное произведение
Интерпретация значений:
cos = 1.0 → векторы сонаправлены → идентичный смысл
cos = 0.8–1 → очень похожие тексты → «тот же раздел»
cos = 0.5–0.8 → схожая тема → «та же предметная область»
cos = 0.0–0.5 → слабая связь
cos < 0 → теоретически «противоположные», на практике редко
На практике для нормализованных векторов:
cosine_sim(a, b) = dot(a, b) = sum(a[i] * b[i] for i in range(dim))
— быстрая операция, реализована в любой векторной БД
Размерность и нормализация
Embedding — это вектор из N чисел типа float32.
Размерность N называют числом измерений или dim.
Получение embeddings на практике
OpenAI Embeddings API
import numpy as np
from openai import AsyncOpenAI
client = AsyncOpenAI() # OPENAI_API_KEY из переменной окружения
async def embed_one(text: str, model: str = "text-embedding-3-small") -> np.ndarray:
"""Эмбеддинг одного текста. Возвращает нормализованный вектор."""
response = await client.embeddings.create(input=text, model=model)
vector = np.array(response.data[0].embedding, dtype=np.float32)
# API уже возвращает нормализованные векторы, но явная нормализация не помешает
return vector / np.linalg.norm(vector)
async def embed_batch(
texts: list[str],
model: str = "text-embedding-3-small",
batch_size: int = 100,
) -> np.ndarray:
"""
Batch-эмбеддинг списка текстов.
Разбиваем на батчи: API принимает до 2048 входов, но 100 — надёжный лимит.
Возвращает матрицу (N, dim).
"""
all_vectors: list[list[float]] = []
for i in range(0, len(texts), batch_size):
batch = texts[i : i + batch_size]
response = await client.embeddings.create(input=batch, model=model)
# Ответы возвращаются в том же порядке, что входы
batch_vectors = [item.embedding for item in sorted(response.data, key=lambda x: x.index)]
all_vectors.extend(batch_vectors)
matrix = np.array(all_vectors, dtype=np.float32)
# Нормализуем все строки сразу
norms = np.linalg.norm(matrix, axis=1, keepdims=True)
return matrix / norms
# Пример
import asyncio
async def main():
texts = [
"Как установить FastAPI?",
"FastAPI installation guide", # другой язык, похожий смысл
"Рецепт торта Наполеон", # другая тема
"Как НЕ нужно устанавливать FastAPI", # с отрицанием!
]
vectors = await embed_batch(texts)
print(f"Размерность: {vectors.shape}") # (4, 1536)
# Сравниваем с первым запросом
query = vectors[0]
for i, (text, vec) in enumerate(zip(texts[1:], vectors[1:]), 1):
sim = float(np.dot(query, vec)) # dot = cosine sim для нормализованных
print(f"sim(query, text[{i}]) = {sim:.3f} ← «{text[:40]}»")
# Ожидаемый вывод:
# sim(query, text[1]) = 0.921 ← «FastAPI installation guide»
# sim(query, text[2]) = 0.124 ← «Рецепт торта Наполеон»
# sim(query, text[3]) = 0.873 ← «Как НЕ нужно устанавливать FastAPI» ← !
asyncio.run(main())
Локальные модели: sentence-transformers
"""
sentence-transformers — библиотека для локальных embedding-моделей.
Поддерживает CPU и GPU, возвращает numpy-массивы.
pip install sentence-transformers
"""
import numpy as np
from sentence_transformers import SentenceTransformer
# Модель загружается один раз и переиспользуется
# multilingual-e5-large: 1.2 ГБ, хорошее качество для RU/EN
model = SentenceTransformer("intfloat/multilingual-e5-large")
def embed_local(
texts: list[str],
batch_size: int = 32,
prefix: str = "passage: ",
) -> np.ndarray:
"""
Эмбеддинг текстов локальной моделью.
E5-модели требуют префиксы:
"query: " — для поисковых запросов
"passage: " — для индексируемых документов
Это важно: при неправильном префиксе качество снижается на 5-15%.
"""
prefixed = [prefix + t for t in texts]
return model.encode(
prefixed,
batch_size=batch_size,
normalize_embeddings=True, # вернёт векторы длиной 1
show_progress_bar=len(texts) > 100,
)
def embed_query(query: str) -> np.ndarray:
return embed_local([query], prefix="query: ")[0]
def embed_documents(docs: list[str]) -> np.ndarray:
return embed_local(docs, prefix="passage: ")
# Пример: поиск ближайшего документа
documents = [
"FastAPI — современный веб-фреймворк для Python",
"Установка FastAPI: pip install fastapi uvicorn",
"Django — полнофункциональный веб-фреймворк",
"PostgreSQL — реляционная база данных",
"Машинное обучение с PyTorch",
]
doc_vectors = embed_documents(documents)
query_vector = embed_query("как установить FastAPI?")
# Cosine similarity = dot product для нормализованных
similarities = doc_vectors @ query_vector # матричное умножение (N,)
ranked = sorted(enumerate(similarities), key=lambda x: -x[1])
print("Результаты поиска:")
for idx, score in ranked:
print(f" {score:.3f} {documents[idx]}")
# Ожидаемый вывод:
# 0.912 Установка FastAPI: pip install fastapi uvicorn ← top-1
# 0.874 FastAPI — современный веб-фреймворк для Python
# 0.731 Django — полнофункциональный веб-фреймворк
# 0.312 PostgreSQL — реляционная база данных
# 0.219 Машинное обучение с PyTorch
Что embedding улавливает — и что нет
a = b vs b = aРоль embeddings в RAG-системе
В RAG embeddings используются дважды — и каждый раз для одной задачи: найти ближайшие точки в векторном пространстве.
ИНДЕКСИРОВАНИЕ (один раз):
Документ → chunking → [чанк1, чанк2, чанк3, ...]
↓ embed_documents()
Векторы: [v1, v2, v3, ...] → сохраняем в векторную БД
Ключ: векторы хранятся рядом с исходным текстом чанка
RETRIEVAL (каждый запрос):
Запрос пользователя → embed_query() → вектор запроса q
↓
Векторная БД: найти k ближайших векторов к q
Метод: Approximate Nearest Neighbor (ANN) — быстро, O(log N)
↓
Возвращаем тексты чанков с наибольшим cosine_sim(chunk, query)
↓
LLM: prompt = "Контекст: {чанки}\nВопрос: {запрос}"
КЛЮЧЕВОЕ ТРЕБОВАНИЕ:
Документы и запросы должны эмбеддироваться ОДНОЙ И ТОЙ ЖЕ моделью.
Векторные пространства разных моделей несовместимы:
нельзя индексировать через E5 и искать через OpenAI.
Визуализация пространства: UMAP
1 536-мерное пространство нельзя нарисовать. Но его можно спроецировать в 2D через UMAP или t-SNE — с частичным сохранением структуры кластеров. Это полезно для отладки: убедиться, что тексты одной темы кластеризуются.
"""
Визуализация embedding-пространства через UMAP.
pip install umap-learn matplotlib
"""
import asyncio
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import umap
async def visualize_embeddings():
# Тестовые тексты с метками тематики
data = [
# Python
("Python — язык программирования", "python"),
("pip install пакет", "python"),
("def функция(аргументы):", "python"),
("FastAPI веб-фреймворк", "python"),
# Базы данных
("SQL запрос SELECT * FROM", "database"),
("PostgreSQL база данных", "database"),
("индекс в базе данных", "database"),
("JOIN таблиц в SQL", "database"),
# ML
("обучение нейронной сети", "ml"),
("функция потерь loss", "ml"),
("градиентный спуск оптимизация", "ml"),
("точность модели accuracy", "ml"),
# История
("Пётр I реформы России", "history"),
("Французская революция 1789", "history"),
("Второй мировой войны", "history"),
("история древнего Рима", "history"),
]
texts, labels = zip(*data)
vectors = await embed_batch(list(texts))
# Снижаем размерность до 2D
reducer = umap.UMAP(n_components=2, random_state=42, n_neighbors=5)
points_2d = reducer.fit_transform(vectors)
# Рисуем
colors = {"python": "#7c6fff", "database": "#60a5fa", "ml": "#4ade80", "history": "#fbbf24"}
fig, ax = plt.subplots(figsize=(10, 8), facecolor="#0d1117")
ax.set_facecolor("#0d1117")
for (x, y), text, label in zip(points_2d, texts, labels):
color = colors[label]
ax.scatter(x, y, color=color, s=80, alpha=0.8, zorder=3)
ax.annotate(text[:25], (x, y), fontsize=7, color=color,
xytext=(4, 4), textcoords="offset points", alpha=0.9)
# Легенда
patches = [mpatches.Patch(color=c, label=l) for l, c in colors.items()]
ax.legend(handles=patches, facecolor="#1a1a2e", edgecolor="#374151", labelcolor="white")
ax.set_title("Embedding space (UMAP 2D)", color="white", pad=15)
ax.tick_params(colors="#374151")
for spine in ax.spines.values():
spine.set_edgecolor("#374151")
plt.tight_layout()
plt.savefig("embedding_space.png", dpi=150, bbox_inches="tight", facecolor="#0d1117")
print("Сохранено: embedding_space.png")
asyncio.run(visualize_embeddings())
Типичные ошибки
"query: " для запросов и
"passage: " для документов. Без них качество падает
на 5–15% даже при технически правильном коде.
Шпаргалка
- Выберите модель: для старта —
text-embedding-3-small(API) илиmultilingual-e5-small(локально) - Документы:
embed_batch(chunks)→ матрица (N, dim) - Запросы:
embed_one(query)→ вектор (dim,) - Нормализуйте оба:
v / np.linalg.norm(v) - Сходство:
np.dot(query_vec, doc_vec)→ число от 0 до 1 - Помните: одна модель для документов и запросов
ВЫБОР МОДЕЛИ:
Критерий Рекомендация
─────────────────────────────────────────────────────────
Прототип / старт быстро multilingual-e5-small (бесплатно, 384d)
Production, RU+EN text-embedding-3-small ($0.02/1M токенов)
Максимальное качество text-embedding-3-large ($0.13/1M токенов)
Только локально, качество multilingual-e5-large / BGE-M3
Длинные документы (>512t) text-embedding-3 или BGE-M3 (8192 токенов)
ТИПИЧНЫЕ ПОРОГИ СХОДСТВА (text-embedding-3-small):
≥ 0.90 → почти идентичные тексты (дублированный контент)
0.75–0.90 → очень близкая тема (top-k для RAG)
0.50–0.75 → схожая предметная область
< 0.50 → слабая связь (фильтровать при retrieval)
ПАМЯТЬ (одна коллекция, 100k документов):
dim=384 → 100k × 384 × 4 байта ≈ 147 МБ
dim=1536 → 100k × 1536 × 4 байта ≈ 590 МБ
dim=3072 → 100k × 3072 × 4 байта ≈ 1.2 ГБ
Практические задания
-
Негативный эксперимент с отрицанием.
Составьте 10 пар предложений, где одно — утверждение, второе —
его прямое отрицание. Например: «Redis быстрый» / «Redis медленный»,
«установка прошла успешно» / «установка завершилась с ошибкой».
Вычислите cosine_sim для каждой пары через
text-embedding-3-smallиmultilingual-e5-large. Есть ли разница между моделями? Какая минимальная схожесть получилась? - Поиск дубликатов. Возьмите коллекцию из 50–100 текстовых документов (например, новостные заголовки или статьи вики). Эмбеддируйте все. Для каждого документа найдите 3 ближайших соседа. Отфильтруйте пары с cosine_sim > 0.92 — это кандидаты в дубликаты. Вручную проверьте: действительно ли похожи?
-
Сравнение моделей. Возьмите одну коллекцию документов
(минимум 20 штук разных тем) и 5 поисковых запросов. Проранжируйте
результаты тремя моделями:
text-embedding-3-small,multilingual-e5-small,multilingual-e5-large. Вручную оцените top-3 для каждого запроса по шкале 0–2 (0 = нерелевантно, 2 = точное попадание). Подсчитайте MAP@3. Какая модель победила?