4.1 · Эмбеддинги руками на numpy
Неделя 4 · RAG · ⏱ ~4 ч · Теория, Практика
Зачем это AI-инженеру
Заголовок раздела «Зачем это AI-инженеру»С этой главы начинается второй проект — ассистент поддержки, который отвечает покупателям «Полки» по базе знаний магазина. Чтобы найти в базе нужный кусок, мало искать по словам: «можно сдать вскрытый чай?» и «вскрытая упаковка не принимается» почти не совпадают по словам, но говорят об одном. Такой поиск «по смыслу» строится на эмбеддингах.
Эмбеддинги лежат под всем, что ищет «по смыслу»: RAG (retrieval-augmented generation — модель отвечает по найденным кускам документов; вся схема — в следующей главе), семантический поиск, дедупликация, кластеризация обращений в поддержку, рекомендации. На собеседовании почти гарантированно спросят «что такое эмбеддинг», «почему косинус, а не евклидово расстояние» и «как выбрать модель эмбеддингов». Если один раз посчитать сходство руками на numpy, векторная БД перестаёт быть магией: это просто быстрый способ сделать то же самое на миллионе строк.
Что такое эмбеддинг
Заголовок раздела «Что такое эмбеддинг»Эмбеддинг — это массив чисел фиксированной длины (например, 384, 768, 1536 или 3072 чисел), который модель выдаёт для куска текста. Модель обучена так, чтобы тексты с похожим смыслом получали близкие векторы, а с разным — далёкие. «Оформить возврат товара» и «return an item for a refund» окажутся рядом, хотя общих слов у них нет. «Cash on delivery» и «наложенный платёж» — тоже рядом, если модель мультиязычная.
Аналогия из магазина: представим, что у каждого товара есть «профиль» из сотни метрик (цена, вес, сезонность, кто и вместе с чем его покупает…). Два товара с похожими профилями продаются похоже. Эмбеддинг — такой же профиль, только для смысла текста, и метрики не названы: модель сама их выучила, отдельные координаты не интерпретируются.
Важные свойства:
- Длина вектора фиксирована для модели и не зависит от длины текста. Фраза из трёх слов и абзац дают векторы одной размерности.
- Векторы разных моделей несравнимы. Нельзя положить в индекс векторы от модели A и искать по ним вектором от модели B — даже при одинаковой размерности. Сменили модель — переиндексируем всё.
- У модели есть лимит входа (в токенах). Текст длиннее лимита обрезается или вызывает ошибку — это одна из причин, зачем нужен chunking — нарезка документов на куски (гл. 4.3).
- Эмбеддинг сжимает. Длинный текст про пять тем превращается в «среднее» по этим темам и плохо находится по каждой из них. Отсюда правило: чанк — про одну мысль.
Немного линейной алгебры (ровно столько, сколько нужно)
Заголовок раздела «Немного линейной алгебры (ровно столько, сколько нужно)»«Близкие векторы» нужно уметь измерить числом: тогда поиск сводится к тому, чтобы посчитать близость вопроса к каждому тексту и взять лучшие. Для этого хватает трёх формул.
Вектор a = [a1, a2, ..., an].
Скалярное произведение (dot product): a·b = a1*b1 + a2*b2 + ... + an*bn. Большое положительное — векторы «смотрят» в одну сторону.
Норма (длина): ||a|| = sqrt(a·a).
Косинусное сходство: cos(a, b) = (a·b) / (||a|| * ||b||). Это косинус угла между векторами, от −1 до 1. Оно не зависит от длины векторов, только от направления. На практике у текстовых эмбеддингов значения обычно лежат в узком диапазоне (например, 0.2–0.9), и абсолютное число мало что значит: важен порядок (кто ближе), а не «0.8 — это хорошо».
Почему нормализованные векторы удобны. Если заранее поделить каждый вектор на его норму (длина станет 1), то cos(a, b) = a·b. Косинус превращается в одно скалярное произведение, а поиск по всей базе — в одно матричное умножение:
scores = M @ q # M: (N, d) matrix of normalized docs, q: (d,) normalized queryМногие API уже отдают нормализованные векторы (OpenAI пишет об этом в гайде по эмбеддингам), а в Sentence Transformers есть флаг normalize_embeddings=True. Всё равно стоит проверить: np.linalg.norm(v) должно быть ≈ 1.0.
Ещё одно следствие: для нормализованных векторов евклидово расстояние и косинус дают одинаковый порядок (||a−b||² = 2 − 2·cos). Поэтому выбор метрики в векторной БД для нормализованных векторов почти ничего не меняет — но метрика индекса должна совпадать с той, под которую обучена модель (см. карточку модели).
numpy за 5 минут
Заголовок раздела «numpy за 5 минут»Считать эти формулы циклом по каждому вектору можно, но медленно. В Python для такой арифметики есть numpy. numpy — это типизированные массивы (как Float32Array в JavaScript или float[] в Java), над которыми операции выполняются целиком, без циклов в Python. Цикл for по 100 000 векторам в Python медленный; M @ q — быстрый, потому что работает на C.
import numpy as np
a = np.array([1.0, 2.0, 3.0], dtype=np.float32)M = np.random.rand(5, 3).astype(np.float32) # 5 vectors of dim 3
M.shape # (5, 3) — rows x columnsa @ a # dot product -> scalarnp.linalg.norm(a) # vector lengthM @ a # (5,) — dot of every row with anorms = np.linalg.norm(M, axis=1, keepdims=True) # (5, 1)M_normed = M / norms # broadcasting: each row divided by its own normscores = M_normed @ (a / np.linalg.norm(a)) # cosine of every row with anp.argsort(-scores)[:5] # indices of top-5 largest scoresКлючевые понятия: shape (размерности), axis (вдоль какой оси считать), broadcasting (автоматическое «растягивание» массивов разной формы при арифметике), argsort (индексы сортировки). Если формы запутались — печатаем .shape после каждой строки, как отладочный вывод типа в привычном языке.
Два способа получить эмбеддинги
Заголовок раздела «Два способа получить эмбеддинги»Математика и инструмент есть; осталось получить сами векторы. Их выдаёт модель эмбеддингов, и запустить её можно двумя способами.
1. Через API (OpenAI, Voyage, Cohere, Google и др.). Плюсы: высокое качество, ничего не ставить. Минусы: платно за токены, данные уходят наружу, сеть, rate limits. Иллюстративный вызов в стиле официального SDK:
# Illustrative: check model names and params in the current OpenAI docsfrom openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY from the environmentresp = client.embeddings.create( model="text-embedding-3-small", input=["можно ли вернуть вскрытый чай", "warranty for the Light desk lamp"], # batch of texts)vectors = [item.embedding for item in resp.data]Отправляем батчами (список строк в одном запросе), а не по одной строке — быстрее и меньше шансов упереться в rate limit. У моделей text-embedding-3-* есть параметр dimensions, который укорачивает вектор: меньше места, чуть ниже качество. У Anthropic своей модели эмбеддингов нет — в их документации рекомендован партнёр Voyage AI; это нормально, генерация и эмбеддинги часто от разных провайдеров.
2. Локально через Sentence Transformers. Модель скачивается с Hugging Face один раз и работает на нашей машине (CPU, видеокарта NVIDIA через cuda или Apple Silicon через mps). Бесплатно, офлайн, данные никуда не уходят.
# Illustrative: pick a concrete model from sbert.net / MTEBfrom sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")vecs = model.encode(texts, normalize_embeddings=True, batch_size=32) # numpy array (N, d)Нюанс: некоторые модели (семейства E5, BGE и др.) требуют префиксов — например, "query: ..." для запроса и "passage: ..." для документа. Без них качество заметно падает. Карточку модели на Hugging Face читаем всегда.
Как выбрать модель: MTEB
Заголовок раздела «Как выбрать модель: MTEB»Моделей эмбеддингов десятки, и для обоих способов встаёт вопрос, какую взять. Общий ориентир — публичный бенчмарк.
MTEB (Massive Text Embedding Benchmark) — рейтинг моделей по десяткам задач: retrieval, классификация, кластеризация, на разных языках. Как читать:
| Что смотреть | Почему |
|---|---|
| Колонка Retrieval, а не общий средний балл | Нам нужен поиск, а не классификация |
| Язык (multilingual / Russian) | Покупатели спрашивают по-русски, а инструкции производителей к товарам часто на английском — нужна кросс-язычность |
| Размер модели и размерность | 7B-модель не поедет на ноутбуке быстро; 3072 измерения = больше места |
| Max tokens | Сколько текста модель «видит» |
| Лицензия | Для портфолио неважно, для продукта важно |
Лидерборд — ориентир, не истина: модели могут быть «подогнаны» под бенчмарк. Финальное решение — только собственный мини-eval на своих данных (его мы и сделаем в практике).
Поиск по смыслу без БД — это просто
Заголовок раздела «Поиск по смыслу без БД — это просто»Соберём всё вместе: модель превращает тексты и вопрос в векторы, нормализация сводит косинус к скалярному произведению, numpy считает его для всех текстов разом.
тексты ──embed──► матрица M (N×d), нормализованавопрос ──embed──► вектор q (d), нормализованscores = M @ q ──► argsort ──► топ-k текстовЭто и есть «точный поиск» (brute force, flat index). Для 100 или даже 100 000 векторов он работает за миллисекунды. Векторные БД (гл. 4.4) нужны для хранения, метаданных, фильтров, обновлений и приближённого поиска (ANN: находит почти ближайших соседей, не сравнивая запрос со всеми векторами) на миллионах.
DeepSeek и Ollama: как это сделать у нас
Заголовок раздела «DeepSeek и Ollama: как это сделать у нас»Это та глава, где разница между провайдерами важнее всего: у DeepSeek нет эндпоинта эмбеддингов. На ноябрь 2026 (проверено 13.11.2026 по API Reference DeepSeek) в разделе API Reference есть Chat Completions, Responses, Messages (Anthropic-формат), FIM Completion, список моделей, баланс, файлы и встроенные инструменты (поиск, чтение URL, OCR, речь), а эмбеддингов нет. Значит, OpenAI(base_url="https://api.deepseek.com").embeddings.create(...) работать не будет — генерацию берём у DeepSeek, а эмбеддинги делаем локально. Это обычная ситуация: генерация и эмбеддинги часто от разных поставщиков (у Anthropic, как мы уже видели, своей модели эмбеддингов тоже нет).
Основной путь в этой книге — два локальных варианта, и оба бесплатные и приватные:
- Ollama — локальный сервер моделей на
http://localhost:11434. Эмбеддинги отдаёт нативный эндпоинтPOST /api/embedи OpenAI-совместимыйPOST /v1/embeddings. - Sentence Transformers — модель грузится прямо в наш Python-процесс (то, что уже описано выше в «Теории»).
Установка (по правилам безопасности)
Заголовок раздела «Установка (по правилам безопасности)»- Ollama ставим только с официального сайта ollama.com/download (там же инструкции для Linux и Windows). По документации для macOS нужна macOS 14 Sonoma или новее; на Apple Silicon работают CPU и GPU, модели лежат в
~/.ollamaи занимают гигабайты. На macOS перед первым запуском приложения проверяем подпись:codesign -dv /Applications/Ollama.app(кто подписал) иspctl -a -vv /Applications/Ollama.app(нотаризация Apple). - Модели берём из официальной библиотеки (
ollama pull <имя>), а модели Sentence Transformers — только из официальных организаций на Hugging Face (BAAI/...,sentence-transformers/...,intfloat/...и т. п.). - Версии библиотек фиксируем. На момент проверки на PyPI были
ollama==0.6.3(релиз 29.09.2026) иsentence-transformers==6.3.0. Ставим конкретную версию, а не «последнюю», и не берём релиз в день его выхода.
Какую модель взять
Заголовок раздела «Какую модель взять»На странице embedding-моделей Ollama есть, среди прочих, bge-m3, embeddinggemma, qwen3-embedding, nomic-embed-text, mxbai-embed-large, snowflake-arctic-embed2, paraphrase-multilingual. Для русских вопросов к английским текстам (инструкции производителей) нужна мультиязычная модель. Разумный старт — bge-m3:
bge-m3 |
|
|---|---|
| Где | ollama.com/library/bge-m3 (скачивание ~1.2 GB) и huggingface.co/BAAI/bge-m3 |
| Размерность | 1024 (по карточке на Hugging Face) |
| Лимит входа | 8192 токена |
| Языки | 100+ |
Префиксы query:/passage: |
Не нужны (карточка прямо пишет, что инструкции к запросам больше не требуются) |
| Лицензия | MIT |
Альтернативы для сравнения: embeddinggemma (300M параметров, скачивание ~622 MB, 100+ языков, но окно всего 2K токенов, нужна Ollama ≥ 0.11.10) и qwen3-embedding:0.6b (~639 MB, окно 32K, размерность можно задавать). Перед использованием читаем карточку модели: префиксы и инструкции у разных семейств разные.
Код: Ollama (нативная библиотека)
Заголовок раздела «Код: Ollama (нативная библиотека)»# Illustrative: pip install ollama==0.6.3 ; ollama pull bge-m3import numpy as npimport ollama
texts = ["можно ли вернуть вскрытый чай", "warranty for the Light desk lamp"]res = ollama.embed(model="bge-m3", input=texts) # a list -> one vector per textM = np.array(res["embeddings"], dtype=np.float32) # shape (2, 1024)print(M.shape, np.linalg.norm(M, axis=1)) # norms should be ~1.0Что важно знать про /api/embed (по справке API):
inputпринимает строку или список строк — батч одним запросом.- Векторы приходят уже L2-нормализованными (длина каждого — 1) (страница про эмбеддинги). Нормы всё равно проверяем.
truncateпо умолчаниюtrue: текст длиннее окна модели молча обрезается. Сtruncate=Falseвместо тихой обрезки получим ошибку — для отладки chunking (глава 4.3) это то, что нужно.dimensions— укоротить вектор (если модель это поддерживает),keep_alive— сколько держать модель в памяти между запросами.- В ответе есть
prompt_eval_count(сколько токенов обработано) иload_duration(сколько ушло на загрузку модели — первый вызов медленный).
Код: Ollama через OpenAI SDK
Заголовок раздела «Код: Ollama через OpenAI SDK»Тот же клиент, что для OpenAI, только base_url другой. Удобно, когда провайдера хочется менять одной переменной окружения.
# Illustrative: Ollama's OpenAI-compatible endpoint; the api_key is required by the SDK but ignoredfrom openai import OpenAI
local = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")resp = local.embeddings.create(model="bge-m3", input=texts)vectors = [item.embedding for item in resp.data]По странице OpenAI-совместимости /v1/embeddings поддерживает model, input (строки и списки строк, но не массивы токенов), encoding_format и dimensions; поле user не поддерживается.
Код: та же модель через Sentence Transformers
Заголовок раздела «Код: та же модель через Sentence Transformers»# Illustrative: model card https://huggingface.co/BAAI/bge-m3from sentence_transformers import SentenceTransformer
st = SentenceTransformer("BAAI/bge-m3") # downloads once from Hugging FaceV = st.encode(texts, normalize_embeddings=True) # numpy (N, 1024)Одна и та же «bge-m3» в Ollama и в Sentence Transformers — это разные сборки и разные рантаймы (у Ollama модель упакована в свой формат). Не считаем их векторы взаимозаменяемыми, пока не проверим: считаем косинус между векторами одной фразы из обоих бэкендов. Правило «один индекс — одна модель» распространяется и на бэкенд: храним в метаданных не только имя модели, но и откуда она (ollama:bge-m3 или st:BAAI/bge-m3).
Сравнение провайдеров для этой главы
Заголовок раздела «Сравнение провайдеров для этой главы»| Возможность | Claude (Anthropic) | OpenAI | DeepSeek | Ollama (локально) |
|---|---|---|---|---|
| Эндпоинт эмбеддингов | Нет; в документации рекомендован партнёр Voyage AI | Есть, /v1/embeddings, модели text-embedding-3-* |
Нет (на 11.2026) | Есть: /api/embed и /v1/embeddings |
| Укоротить вектор | — | dimensions |
— | dimensions (зависит от модели) |
| Нормализация | — | Векторы нормализованы (по гайду OpenAI) | — | /api/embed отдаёт L2-нормализованные |
| Стоимость | Отдельный счёт у Voyage | За токены | — | Бесплатно, считает наша машина |
| Куда уходят тексты | В облако Voyage | В облако OpenAI | — | Никуда, всё локально |
| Обходной путь | Ollama / Sentence Transformers | — | Ollama / Sentence Transformers | — |
Для полноты: OpenRouter (глава 2.4 · OpenRouter: один API ко многим моделям) тоже даёт эмбеддинги через POST https://openrouter.ai/api/v1/embeddings (в документации примеры openai/text-embedding-3-small и другие) — это вариант «облачные эмбеддинги без отдельного аккаунта OpenAI». Бесплатные модели там могут обучаться на входных данных, поэтому годятся только для выдуманных или публичных текстов.
Что использовать в практике этой главы
Заголовок раздела «Что использовать в практике этой главы»- Основной путь: Ollama (
bge-m3) и Sentence Transformers — два локальных бэкенда, которые мы сравниваем между собой. Ключ API не нужен вообще. - Для сравнения (по желанию): OpenAI
text-embedding-3-small, если ключ уже есть, — увидим, сколько даёт платная облачная модель на наших фразах. - Память. Эмбеддинг-модели маленькие, и даже на машине с 8 GB RAM проблем с ними нет:
bge-m3~1.2 GB,embeddinggemma~622 MB,qwen3-embedding:0.6b~639 MB (размеры скачивания по библиотеке Ollama; в памяти примерно столько же плюс рабочий буфер). Но в сравнении бэкендов одна модель живёт в Ollama, а другая в нашем Python-процессе (Sentence Transformers), и вместе они занимают вдвое больше. Прогоняем бэкенды по очереди, а после прогона выгружаем модель Ollama:ollama psпоказывает загруженные модели,ollama stop <модель>выгружает. Крупные эмбеддинг-модели (qwen3-embedding:4b/8b) при 8 GB RAM не берём; на 16 GB и больше их уже можно попробовать. - DeepSeek в этой главе не участвует — эмбеддингов у него нет. Он появится как генератор ответа в главах 5.1 и 5.3.
- Безопасность: фразы в этой главе выдуманные, а база знаний магазина публичная. Когда дело дойдёт до данных покупателей (сообщения в поддержку, имена, телефоны, номера заказов, база магазина), эмбеддинги и генерация для них — только Ollama, локально: эти тексты не уходят ни в DeepSeek (серверы в Китае), ни в другие облака.
Маршрут изучения
Заголовок раздела «Маршрут изучения»- Открываем NumPy quickstart → разделы «The Basics», «Basic Operations», «Indexing, Slicing and Iterating», «Broadcasting rules» (≈40 мин). Обращаем внимание на
shape,axisи на то, что*— поэлементное умножение, а@— матричное. Набираем примеры руками в REPL. - OpenAI: Embeddings guide → читаем целиком (≈25 мин). Выносим: какие модели есть сейчас, их размерность и цена (меняются — смотрим на странице), параметр
dimensions, замечание о нормализации векторов, раздел FAQ про выбор метрики. - Sentence Transformers → Quickstart и раздел про Semantic Search; бегло — список Pretrained Models (≈30 мин). Выносим:
model.encode(...),normalize_embeddings, утилитуutil.cos_sim, разницу между bi-encoder (эмбеддинги) и cross-encoder (понадобится в гл. 5.2). - MTEB Leaderboard → фильтруем по задаче Retrieval и мультиязычности (≈20 мин). Выписываем 2–3 кандидата небольшого размера (до ~600M параметров), у которых есть карточка с инструкцией по префиксам. Открываем карточку выбранной модели и читаем, как её правильно вызывать.
- Повторяем линейную алгебру из раздела «Теория» на бумаге: считаем косинус для
[1,0]и[1,1]руками (≈10 мин). Ответ: ≈0.707. - Ollama: Embeddings и справка
/api/embed(≈20 мин). Выносим: батч через список вinput, нормализацию,truncate(по умолчаниюtrue— тихая обрезка),dimensions,keep_alive, поля ответаprompt_eval_countиload_duration. - Ollama: OpenAI compatibility → раздел
/v1/embeddings(≈10 мин). Выносим, какие поля поддерживаются, — чтобы один и тот же код на OpenAI SDK ходил и в OpenAI, и в Ollama. - Карточки ollama.com/library/bge-m3 и huggingface.co/BAAI/bge-m3 (≈15 мин). Выносим размерность, лимит входа, нужны ли префиксы, лицензию. Сравниваем с 1–2 кандидатами из своего списка MTEB.
- API Reference DeepSeek → пробегаемся по списку эндпоинтов в боковом меню (≈5 мин). Убеждаемся сами, что эндпоинта эмбеддингов нет (на ноябрь 2026), — и запоминаем, как такие вещи проверять: по списку эндпоинтов в официальной справке, а не по чужим статьям.
На что обратить внимание
Заголовок раздела «На что обратить внимание»- Смешивание моделей. Индекс построен одной моделью, запрос эмбеддится другой — поиск выдаёт мусор без всяких ошибок. Имя модели храним рядом с векторами.
- Забытые префиксы у E5/BGE-подобных моделей — тихая потеря качества.
- Ненормализованные векторы + dot product = поиск, который предпочитает «длинные» векторы, а не похожие. Нормализуем сами и проверяем нормы.
- Абсолютные пороги сходства («если cos > 0.8, то релевантно») переносятся между моделями плохо. Порог подбираем на своих данных или используем top-k.
- float64 vs float32. numpy по умолчанию создаёт float64 — вдвое больше памяти. Для эмбеддингов хватает float32.
- Стоимость и лимиты API. Не эмбеддим одно и то же дважды: кэшируем результат на диск (
np.save), ключ — хэш текста + имя модели. - Ключи.
OPENAI_API_KEY— только из окружения или.env, а.env— в.gitignore. - Короткие фразы против длинных документов. Эмбеддинг вопроса покупателя и эмбеддинг абзаца из базы знаний живут в немного разных «регионах». Это нормально и отчасти решается в гл. 5.1 (HyDE).
Практика
Заголовок раздела «Практика»Проект для недель 4–5 — папка projects/02-support-kb-rag/: ассистент поддержки, который отвечает покупателям по базе знаний магазина. Самой базы ещё нет (её страницы появятся в гл. 4.3), поэтому здесь тренируемся на ста коротких фразах в projects/02-support-kb-rag/playground/: получаем эмбеддинги двумя локальными бэкендами, пишем поиск на numpy и сравниваем модели.
- Окружение.
uv init(uv из главы 1.3), черезuv addставим (версии фиксируетuv.lock)numpy,ollama,sentence-transformers,python-dotenv, (опционально)openai,scikit-learn,matplotlib. Сразу добавляем.envи.venv/в.gitignore. Если делаем шаг «для сравнения» с OpenAI — ключOPENAI_API_KEYтолько в.env. - Ollama. Ставим приложение с ollama.com (на macOS — проверка
codesign -dvиspctl -a -vvдо первого запуска), затемollama pull bge-m3. Проверка:ollama listпоказывает модель, аpython -c "import ollama; print(len(ollama.embed(model='bge-m3', input='test')['embeddings'][0]))"печатает размерность. - Данные. Файл
phrases.txt: 100 фраз о магазине, по одной на строку, смесь русского и английского — вопросы покупателей и строки из базы знаний и инструкций к товарам. Группы по смыслу (по 8–12 фраз): доставка, возврат, гарантия, оплата, промокоды, статус заказа, уход за товаром и инструкции, шум (отзывы ни о чём, приветствия). Пример: «сколько идёт посылка в Казань», «opened tea and coffee cannot be returned», «где мой заказ, трек не обновляется», «do not use the lamp without the shade». Это выдуманные фразы — никаких реальных сообщений покупателей. - Эмбеддинги через Ollama (основной путь).
embed_ollama.py: читает фразы, эмбеддит одним-двумя батчами черезollama.embed(model="bge-m3", input=[...]), сохраняетemb_ollama.npyиmeta.json(бэкенд, модель, размерность, дата). Повторный запуск не пересчитывает, если файл есть.load_durationпервого вызова выводится отдельно от остального времени. - Эмбеддинги через Sentence Transformers.
embed_local.py: то же с другой мультиязычной моделью из нашего списка MTEB (или сBAAI/bge-m3, чтобы сравнить бэкенды),normalize_embeddings=True→emb_local.npy. Префиксы — если модель их требует. - (Для сравнения, по желанию) Эмбеддинги через облачный API.
embed_api.py: OpenAItext-embedding-3-small→emb_api.npy. Повторный запуск не ходит в API, если файл есть. - Проверка DeepSeek. Одной строкой в
compare.mdзаписываем, что нашли в API Reference DeepSeek про эмбеддинги и почему этот провайдер в таблице сравнения отсутствует. Ключ DeepSeek для этого не нужен — запросы не делаем, хватит документации. - Поиск руками.
search.py, ≤ 50 строк, без векторной БД и безutil.cos_sim:
import numpy as np
def normalize(M: np.ndarray) -> np.ndarray: # TODO: divide each row by its L2 norm (use axis=1, keepdims=True) ...
def top_k(M: np.ndarray, q: np.ndarray, k: int = 5) -> list[tuple[int, float]]: # TODO: scores = M @ q, return k (index, score) pairs sorted by score desc ...Проверяем, что np.allclose(np.linalg.norm(normalize(M), axis=1), 1.0). Запуск: python search.py --backend ollama "можно ли вернуть вскрытый чай" печатает 5 фраз со скорами (--backend local — Sentence Transformers, --backend api — облачная модель, если её подключали). Запрос эмбеддим тем же бэкендом и моделью, что и фразы.
- Сравнение моделей. 10 запросов, для каждого заранее записываем, какие фразы «правильные». Таблица в
compare.md: запрос → сколько правильных в топ-5 у Ollama-модели, у Sentence Transformers и (если подключали) у облачной API-модели, плюс время эмбеддинга 100 фраз (у Ollama — отдельно с загрузкой модели и без). Обязательно включаем 2–3 запроса на русском к английским фразам (вопрос покупателя к инструкции производителя) и наоборот. - (Опционально) 2D-картинка. PCA из
sklearn.decompositionдо 2 измерений, scatter, цвет = группа. Ожидаемо: группы образуют облака, но с перекрытием — PCA теряет много информации, это иллюстрация, а не метрика.
Ожидаемый результат: все модели находят «очевидных» соседей; различия проявятся на кросс-язычных запросах и на жаргоне («наложка», «ПВЗ», «трек»).
Готово, когда
Заголовок раздела «Готово, когда»-
search.pyна ≤ 50 строк ищет по смыслу через numpy, без векторной БД. - Есть
emb_ollama.npy,emb_local.npy(и, по желанию,emb_api.npy) и таблица сравнения на 10 запросах с выводом в 2–3 предложения: какую модель и какой бэкенд берём для проекта и почему. - Можем объяснить, почему эмбеддинги в этом проекте делаются локально, а не у DeepSeek.
- Можем объяснить, почему для нормализованных векторов косинус равен скалярному произведению.
- Ключ не попал в git (
git statusне показывает.env).
Тест для самопроверки
Заголовок раздела «Тест для самопроверки»Вопрос 1
Что такое эмбеддинг одним предложением?
Вектор фиксированной длины, который модель выдаёт для текста так, что похожие по смыслу тексты получают близкие векторы.
Вопрос 2
Даны векторы a = [3, 4] и b = [6, 8]. Чему равно косинусное сходство?
b) 1.0. b = 2a, векторы смотрят в одну сторону; косинус не зависит от длины.
Вопрос 3
Почему для нормализованных векторов поиск сводится к M @ q?
Потому что cos(a,b) = a·b / (||a||·||b||), а при единичных нормах знаменатель равен 1; умножение матрицы на вектор считает скалярное произведение со всеми строками сразу.
Вопрос 4
Можно ли искать вектором от модели text-embedding-3-small по индексу, построенному моделью из Sentence Transformers той же размерности?
c) нет. Пространства разных моделей не связаны: совпадение размерности случайно, близость в них ничего не значит.
Вопрос 5
В каком случае евклидово расстояние и косинус дают одинаковый порядок результатов?
Когда векторы нормализованы: ||a−b||² = 2 − 2·cos(a,b), то есть расстояние монотонно убывает с ростом косинуса.
Вопрос 6
Что в MTEB важнее всего смотреть для задачи RAG по базе знаний магазина и почему?
Метрики Retrieval и поддержку нужных языков (русский вопрос покупателя → английская инструкция производителя), плюс размер модели и лимит токенов. Общий средний балл смешивает задачи, которые здесь не нужны.
Вопрос 7
Что не так в этом коде?
M = np.load("emb.npy") # raw vectors from a local modelq = model.encode("return policy")best = np.argsort(M @ q)[:5]Три ошибки: векторы не нормализованы (dot product ≠ косинус); argsort сортирует по возрастанию, поэтому [:5] — пять худших, нужно np.argsort(-scores)[:5]; не проверено, нужны ли модели префиксы для запроса.
Вопрос 8
Зачем некоторым моделям префиксы query: / passage:?
Модель обучалась с разными префиксами для вопросов и документов, чтобы лучше сопоставлять короткие запросы с длинными пассажами; без префиксов вход не похож на обучающие данные и качество падает.
Вопрос 9
Модель эмбеддингов сменили. Что нужно сделать с уже построенным индексом?
Переэмбеддить все документы новой моделью и перестроить индекс; хранить имя модели рядом с векторами, чтобы такие несовпадения ловились.
Вопрос 10
Почему длинный текст про пять разных тем плохо находится по каждой из них?
Эмбеддинг — одно «сжатое» представление всего текста, он усредняет темы, и вектор оказывается между ними, не близко ни к одной.
Вопрос 11
Какой тип данных numpy разумно использовать для хранения эмбеддингов и почему?
c) float32: вдвое меньше памяти, чем float64, без заметной потери качества поиска; int8 — это уже квантизация, отдельная оптимизация с потерями.
Вопрос 12
В коде написано OpenAI(base_url="https://api.deepseek.com", api_key=...).embeddings.create(model=..., input=texts). Что произойдёт и как правильно получить эмбеддинги в стеке «DeepSeek + Ollama»?
Ошибка: у DeepSeek нет эндпоинта эмбеддингов (на ноябрь 2026 его нет в API Reference). DeepSeek используется для генерации, а эмбеддинги делаются локально — ollama.embed(...), /v1/embeddings у Ollama или Sentence Transformers.
Вопрос 13
Что делает Ollama с текстом длиннее окна модели при вызове /api/embed по умолчанию, и как превратить это в явную ошибку?
b). По умолчанию truncate=true, и лишнее молча отрезается — кусок текста просто не участвует в поиске. С truncate=False слишком длинный вход вызывает ошибку, и о проблеме chunking мы узнаём сразу.
Вопрос 14
Индекс построен через Sentence Transformers (BAAI/bge-m3), а запросы решили эмбеддить через Ollama (bge-m3) — «модель же та же». Что нужно сделать перед этим?
Проверить, что векторы совпадают (косинус между векторами одних и тех же фраз из обоих бэкендов близок к 1), а надёжнее — эмбеддить индекс и запросы одним бэкендом. Сборки и рантаймы разные, поэтому в метаданных хранят и модель, и бэкенд.
Что дальше
Заголовок раздела «Что дальше»В 4.2 · Как устроен RAG: обзор архитектуры увидим, куда встраивается этот поиск в полной цепочке RAG, а в 4.3 · Chunking — как нарезать базу знаний магазина, чтобы эмбеддинги работали.