L3 Открытый урок · Стажёр

L3.6. Эмбеддинги и семантический поиск

Косинусная близость, откуда брать эмбеддинги, минимальный поисковик на NumPy и чего эмбеддинги не умеют: коды, отрицания, длинные тексты.

Объём
5 уроков · 10 часов
Компетенции
EMB-01·A, EMB-02·A, RAG-01·F

От ключевых слов к смыслу

Клиент спрашивает: «вода после фильтра пахнет странно». В базе есть статья «Посторонний запах воды: причины и замена картриджа». Общих слов почти нет, поиск по ключевым словам её не найдёт. Эмбеддинги (L2.3) переводят текст в вектор так, что тексты близкие по смыслу оказываются рядом.

Косинусная близость

Мерой близости служит косинус угла между векторами: 1 — направлены одинаково, 0 — не связаны.

import numpy as np

def cosine(a: np.ndarray, b: np.ndarray) -> float:
    return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))

a, b = np.array([0.2, 0.9, 0.1]), np.array([0.25, 0.8, 0.0])
print(round(cosine(a, b), 3))   # 0.991 — почти одно и то же направление

Если векторы заранее нормализованы (длина 1), косинус — это просто скалярное произведение, а поиск по всей базе — одно умножение матрицы на вектор.

Откуда брать эмбеддинги

Вариант Плюсы Минусы
API провайдера эмбеддингов качество, не нужно железо данные уходят наружу, плата за токены
Открытая модель локально (sentence-transformers) бесплатно, данные остаются у вас нужен CPU/GPU, выбор модели под язык

У Anthropic нет собственного API эмбеддингов: в связке с Claude используют сторонние API или открытые модели. Модель выбирают под язык и домен: модель, обученная на английском, хуже работает с русским. Проверяйте на своих запросах (практикум ниже), а не по рейтингам.

Правило совместимости: запросы и документы кодируются одной и той же моделью. Векторы разных моделей несравнимы, даже если у них одинаковая размерность. Сменили модель — переиндексируйте весь корпус.

Минимальный поисковик

import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("<модель, выбранная под русский язык>")

def build_index(texts: list[str]) -> np.ndarray:
    return model.encode(texts, normalize_embeddings=True)       # матрица N × d

def search(query: str, texts: list[str], index: np.ndarray, k: int = 5) -> list[tuple[str, float]]:
    q = model.encode([query], normalize_embeddings=True)[0]
    scores = index @ q                                          # косинус со всеми сразу
    top = np.argsort(-scores)[:k]
    return [(texts[i], float(scores[i])) for i in top]

Индекс сохраните на диск (np.save) вместе с тем, какой моделью и из какой версии корпуса он построен. Иначе через неделю вы не поймёте, почему поиск «сломался».

Что эмбеддинги делают плохо

  • Точные идентификаторы. «Картридж АК-3/2025» и «АК-3/2024» для эмбеддинга почти одно и то же. Коды, артикулы, имена лучше находит лексический поиск (BM25). Гибридный поиск соединяет оба канала — L5.1.
  • Отрицание. «Фильтр, который не требует замены картриджа» близок к «фильтр с заменой картриджа».
  • Длинные тексты. Вектор документа на 20 страниц — «среднее по больнице». Документы режут на фрагменты (чанки) и ищут по ним: на L3 — простая нарезка по абзацам, на L4.6 — стратегии чанкинга.

Поиск ≠ RAG

Поиск возвращает фрагменты. RAG (retrieval-augmented generation) подаёт найденные фрагменты модели, и она формулирует ответ с цитатами. На L3 вы строите только первую половину (RAG-01·F) и измеряете её отдельно. Это правильный порядок: если поиск не находит нужный фрагмент, генерация не поможет.

Практикум: поиск по корпусу

Над корпусом из L3.5 (нарезка по абзацам, не длиннее ~300 слов): индекс, функция search, CLI python search.py "запрос" -k 5 с выводом doc_id, оценки и начала фрагмента. Сравните две модели эмбеддингов на 10 своих запросах: какая чаще ставит нужный документ на первое место? Найдите и запишите 2 запроса, где поиск ошибается, и объясните почему (идентификатор, отрицание, синоним).

Диагностика уровня

С какого уровня начать?

Оставьте контакты — свяжемся, зададим несколько вопросов об опыте и целях и подскажем уровень входа: с нуля, с середины программы или через Fast Track для разработчиков.

Или напишите: hello@superint.ru