Обзор модуляЧасть IV · ~10 ч · Сложность: (продвинутый) · Пререквизиты: Модуль 1, 6, 9
Все предыдущие модели релевантности — булева, векторная tf-idf, BM25 (Модуль 6), обучаемое ранжирование на ручных признаках (Модуль 9) — роднит одно: они оперируют поверхностным совпадением слов. Если запрос «как лечить простуду», а документ написан про «терапию ОРВИ», лексические модели увидят ноль общих значимых терминов и поставят документу почти нулевой текстовый скор, хотя по смыслу это идеальный ответ. Это проблема лексического разрыва (vocabulary mismatch): одно и то же значение выражают разными словами, а одно и то же слово значит разное в разных контекстах («лук» — растение или оружие). Нейросетевой поиск (neural IR) — это семейство методов, которые сопоставляют запрос и документ в пространстве смыслов, а не строк, и тем самым закрывают этот разрыв.
Ключевой объект модуля — эмбеддинг (embedding): плотный вектор фиксированной размерности (например, 768), который нейросеть-энкодер сопоставляет тексту так, что близкие по смыслу тексты получают близкие векторы. Поиск превращается в геометрию: индексируем документы как точки в d-мерном пространстве, запрос — тоже точка, релевантность ≈ близость (косинус или скалярное произведение). На этом стоит вся глава 10.1. Но наивная реализация — «прогнать нейросеть на паре (запрос, документ) для каждого из миллиардов документов» — невозможна по стоимости. Поэтому в сквозном конвейере «обход → индекс → факторы → ранжирование → выдача → постобработка → измерение» нейропоиск распадается надвое. Документные эмбеддинги вычисляются офлайн, при индексации, и материализуются прямо в индекс (глава 10.2, прямая связь с Модулем 4): это самая дорогая часть, и она делается один раз на документ. Близость запроса к документам считается в рантайме (глава 10.3, связь с каскадом ранжирования Модуля 12): запрос пришёл — мы кодируем только его и ищем ближайшие векторы. Это и есть центральная инженерная идея модуля, которой посвящена глава 10.4: «половина скалярного произведения» — документную половину считаем заранее, запросную и само произведение — на лету.
После модуля вы сможете: объяснить разницу между bi-encoder (две независимые башни, dual-tower) и cross-encoder (запрос и документ кодируются совместно) и понять, почему первый годится для индекса, а второй — только для переранжирования горстки кандидатов; устроить приближённый поиск ближайших соседей (ANN) структурами HNSW и IVF/PQ, чтобы не сравнивать запрос со всеми векторами; применить дистилляцию (distillation) — перенос «знания» тяжёлого cross-encoder в лёгкий bi-encoder; и спроектировать гибридный поиск, где лексический сигнал (BM25) и векторный объединяются в один ранжированный список.
Интуиция. Лексический поиск спрашивает: «встречаются ли в документе те же слова, что в запросе?» Нейропоиск спрашивает: «находится ли документ в той же области смысла, что и запрос?» Первый точен на редких терминах и опечатках-как-есть, второй силён на перефразировках и синонимии. Поэтому в проде их почти всегда комбинируют, а не противопоставляют.
Как читать по трекамВнимание. Нейропоиск не отменяет BM25. На точных совпадениях, артикулах, кодах ошибок, именах собственных и редких терминах лексика часто бьёт вектора, потому что эмбеддинг «размывает» точные строки. Грамотная система — гибрид, а не замена.
- Студент CS — обязательно всё. Ядро — 10.1 (архитектуры энкодеров) и 10.2 (геометрия ANN, HNSW, IVF/PQ). Прорешайте обе лабы. Глава 10.4 — концептуальный стержень, его надо понять до конца.
- Инженер поиска/ML — обязательно всё. Особое внимание — инженерным заметкам про материализацию эмбеддингов в индекс (связь с Модулем 4), про параметры HNSW (M, efSearch) и IVF/PQ (nlist, nprobe, число суб-квантизаторов), про переиндексацию при смене модели и про место ANN в каскаде L0–L1 (Модуль 12).
- SEO-специалист — обязательно SEO-врезки во всех главах. Главное к усвоению: вектора ранжируют по смыслу, поэтому переспам ключевыми словами теряет силу, а ясная, тематически связная подача текста — выигрывает. Формулы и устройство ANN — обзорно.
- Смешанный/руководитель — Обзор, интуиции, заблуждения, глава 10.4 целиком и Итоги. Запомните принцип «документ кодируем офлайн, близость — онлайн» и идею гибрида.
- 10.1. Эмбеддинги документов и запросов; bi-encoder vs cross-encoder; dual-tower (продвинутый)
- 10.2. Офлайн-эмбеддинги документа при индексации; ANN-поиск, HNSW, IVF/PQ (продвинутый)
- 10.3. Рантайм-близость query↔doc; дистилляция тяжёлых моделей в лёгкие (продвинутый)
- 10.4. «Половина скалярного произведения»: что считать офлайн (документ), что онлайн (близость) (средний)
Цели обучения
После главы студент сможет:
- Объяснить, что такое плотный эмбеддинг текста и чем он отличается от разреженного tf-idf/BM25-представления.
- Сформулировать, как близость в векторном пространстве (косинус / скалярное произведение) выражает семантическую релевантность.
- Сравнить bi-encoder (dual-tower) и cross-encoder по архитектуре, выразительности и вычислительной стоимости.
- Обосновать, почему только bi-encoder допускает офлайн-индексацию документов, а cross-encoder применим лишь к переранжированию кандидатов.
- Объяснить, как обучают bi-encoder контрастной функцией потерь с негативами.
Что такое эмбеддинг
Эмбеддинг (embedding) текста — это отображение E: текст → R^d, переводящее произвольный фрагмент (запрос, абзац, документ) в плотный вектор фиксированной длины d (типично d ∈ {256, 384, 768, 1024}). Сеть-энкодер обучается так, чтобы семантически близкие тексты давали близкие векторы, а далёкие — далёкие. Близость измеряют скалярным произведением или косинусом:
Код: Выделить всё
cos(q, d) = (q · d) / (|q| · |d|)
Интуиция. tf-idf/BM25 представляют документ разреженным вектором размером со словарь (миллионы измерений, почти все нули): измерение = слово. Два текста близки, только если делят буквально одни и те же слова. Эмбеддинг — плотный вектор из сотен чисел, где измерения — это абстрактные «направления смысла», а не конкретные слова. Поэтому «простуда» и «ОРВИ» оказываются рядом, хотя в лексическом пространстве они ортогональны.
Как из токенов получается один векторПример. Запрос «недорогой отель у моря». Документ A: «бюджетная гостиница на первой береговой линии». Документ B: «дорогая яхта в открытом океане». Лексически A почти не пересекается с запросом (другие слова), B делит «море/океан»-окрестность. В пространстве эмбеддингов A окажется ближе к запросу: «недорогой ≈ бюджетная», «отель ≈ гостиница», «у моря ≈ береговая линия» — все эти пары близки по смыслу, а «дорогая яхта» уезжает в сторону.
Современный энкодер — это трансформер (базовые понятия про эмбеддинги слов и self-attention предполагаются известными). Он принимает последовательность токенов и выдаёт по вектору на каждый токен (контекстные представления). Чтобы получить один вектор на весь текст, применяют пулинг (pooling):
- CLS-пулинг — берут представление специального служебного токена, который сеть учится использовать как «сводку».
- Mean-пулинг (усреднение) — усредняют токенные векторы; на практике часто устойчивее.
Две архитектуры: bi-encoder и cross-encoder
Есть принципиально разные способы получить оценку релевантности пары (запрос q, документ d) нейросетью.
Cross-encoder (совместное кодирование). Запрос и документ склеивают в один вход [q || d] и прогоняют через трансформер целиком. Self-attention позволяет каждому слову запроса «смотреть» на каждое слово документа на всех слоях — это богатейшее взаимодействие. На выходе — одно число: оценка релевантности.
Код: Выделить всё
q ──┐
├──► [ один трансформер: q⇄d взаимодействуют ] ──► score(q,d)
d ──┘
Код: Выделить всё
q ──► [ энкодер-запрос ] ──► vec(q) ─┐
├──► score = vec(q) · vec(d)
d ──► [ энкодер-документ ] ──► vec(d) ─┘
Почему архитектура решает всё для индексацииИнтуиция. Cross-encoder читает запрос и документ вместе, как экзаменатор, держащий перед глазами и вопрос, и ответ — и оттого точен. Bi-encoder читает их по отдельности и сводит каждый к точке-«адресу» в пространстве смыслов; сравнение адресов дёшево, но информации в одной точке меньше, чем в совместном чтении.
Это — главный водораздел модуля. Сравним стоимость поиска по корпусу из N документов.
Код: Выделить всё
Свойство | Cross-encoder | Bi-encoder (dual-tower)
------------------------------------+-------------------------------------+----------------------------------
Кодирование | совместное [q‖d] | раздельное q и d
Взаимодействие q⇄d | глубокое, на всех слоях | только финальное q·d
Точность (качество) | выше | ниже
Можно ли посчитать vec(d) заранее? | нет — нужен q | да — d не зависит от q
Прогонов сети на запрос | N (по разу на каждый документ) | 1 (только сам запрос)
Где применим | переранжирование топ-K кандидатов | первичный отбор по всему индексу
У bi-encoder vec(d) зависит только от документа. Поэтому его можно вычислить один раз при индексации и положить в индекс (глава 10.2). В рантайме на запрос — ровно один прогон сети (кодируем q) плюс дешёвые скалярные произведения. Это и есть «половина скалярного произведения» из главы 10.4.
Как обучают bi-encoder: контрастная потеря и негативыИнженерная заметка. На практике их совмещают в каскаде (Модуль 12): bi-encoder + ANN дёшево достаёт топ-K кандидатов (скажем, 1000) из всего индекса (этап L0–L1), затем cross-encoder, который дорог, но точен, переранжирует только эти K на верхних уровнях (L2–L3). Получаем и охват, и точность. Cross-encoder никогда не запускают по всему корпусу.
Bi-encoder надо научить тянуть релевантные пары (q, d⁺) ближе, а нерелевантные (q, d⁻) — дальше. Стандартный приём — контрастная функция потерь (contrastive loss), частный случай — InfoNCE / softmax по списку кандидатов:
Код: Выделить всё
L = − log exp(q·d⁺ / τ)
────────────────────────────────
exp(q·d⁺/τ) + Σ_j exp(q·d⁻_j / τ)
- Случайные (in-batch) негативы — другие документы из того же батча; дёшево, но слишком «лёгкие».
- Жёсткие негативы (hard negatives) — документы, которые лексически или поверхностно похожи на релевантный, но на деле нерелевантны. Их часто добывают самим BM25 или предыдущей версией модели. Именно жёсткие негативы учат модель тонким различиям.
Внимание. Bi-encoder, обученный только на случайных негативах, в проде «всплывает мусором» — документами, которые «вообще про ту же тему», но не отвечают на запрос. Грамотный майнинг жёстких негативов — половина успеха.
Частые заблужденияSEO-врезка. Поскольку релевантность считается по смыслу всего фрагмента, а не по плотности ключа, выигрывает текст с ясной, связной формулировкой темы и явными ответами на типичные формулировки запроса. Перечисление синонимов «для роботов» теряет смысл: близкие по смыслу слова и так лягут рядом в пространстве, а несвязный «винегрет» из ключей даёт «размазанный» эмбеддинг, далёкий от любого конкретного запроса.
Заблуждение. «Эмбеддинги поняли смысл, лексический поиск больше не нужен.» Нет. На точных строках (артикулы WX-7740, коды, имена, редкие термины) эмбеддинг усредняет и теряет точность, а BM25 попадает точно. Прод почти всегда гибрид (глава 10.3).
Заблуждение. «Bi-encoder и cross-encoder — это просто две реализации одного и того же.» Нет: разница архитектурная и определяет всё. Только bi-encoder даёт офлайн-вектор документа и потому масштабируется на весь индекс; cross-encoder — лишь финальный переранжировщик горстки кандидатов.
Лаба / практикаЗаблуждение. «Больше размерность вектора — всегда лучше.» Нет: рост d увеличивает память индекса и стоимость ANN линейно, а прирост качества выходит на плато. Размерность — компромисс «качество ↔ стоимость», а не «чем больше, тем лучше».
Цель: на пальцах ощутить разницу bi- vs cross-encoder и роль негативов. Время ~60 мин.
- Возьмите готовую предобученную модель-bi-encoder (любая открытая sentence-эмбеддинг-модель) и мини-корпус из 20–30 коротких документов на 3–4 темы, где есть перефразировки (запрос и релевантный документ почти не делят слов).
- Закодируйте все документы (vec(d)), нормируйте к единичной длине. Для 5 запросов закодируйте vec(q) и отсортируйте документы по q·d.
- Параллельно посчитайте BM25 (из Модуля 6) на тех же запросах. Сравните топ-3 двух методов на запросах-перефразировках и на запросе с редким точным термином/кодом.
- (Если есть cross-encoder) переранжируйте топ-10 bi-encoder’а cross-encoder’ом и сравните порядок.
Критерий «сделано»: для каждого запроса выписано, какой метод выиграл и почему (синонимия vs точное совпадение); объяснено, почему cross-encoder нельзя было применить ко всему корпусу сразу.
Контрольные вопросы
- Чем плотный эмбеддинг принципиально отличается от разреженного tf-idf-вектора? Какое измерение что означает в каждом случае?
- Почему при нормировке к единичной длине косинус равен скалярному произведению?
- Опишите, как из последовательности токенных векторов получают один вектор текста. Что такое CLS- и mean-пулинг?
- Сформулируйте, почему vec(d) можно посчитать офлайн для bi-encoder, но нельзя — для cross-encoder.
- Сколько прогонов тяжёлой сети на один запрос требует cross-encoder при поиске по N документам, а сколько — bi-encoder? Почему?
- Что такое жёсткие негативы и зачем они нужны при обучении bi-encoder?
- Почему в проде bi- и cross-encoder обычно сочетают в каскаде, а не выбирают один?
- Приведите пример запроса, на котором BM25 побьёт чистый векторный поиск, и объясните механизм.
Цели обучения
После главы студент сможет:
- Объяснить, как и когда документные эмбеддинги вычисляются и материализуются в индекс (связь с Модулем 4).
- Сформулировать задачу поиска ближайших соседей (k-NN) и объяснить, почему точный перебор не масштабируется.
- Описать идею приближённого поиска (ANN) и компромисс «полнота ↔ скорость ↔ память».
- Объяснить устройство HNSW (графовый индекс) и роль параметров M и efSearch.
- Объяснить устройство IVF (инвертированные ячейки) и PQ (product quantization), роль nlist, nprobe и сжатия.
Документный эмбеддинг живёт в индексе
Из главы 10.1: vec(d) зависит только от документа. Поэтому его место — в этапе индексации (Модуль 4), рядом с обратным индексом. Конвейер документа дополняется шагом:
Код: Выделить всё
обход → нормализация/каноникализация → разбиение на токены/поля
→ построение постингов (обратный индекс, Модуль 4)
→ ЭНКОДЕР bi-encoder: текст документа → vec(d) ∈ R^d ◄── НОВЫЙ ШАГ
→ запись vec(d) в векторный индекс (ANN-структуру)
Инженерная заметка. Документ часто длиннее окна энкодера. Стандартно его режут на пассажи (passages) — абзацы/окна по N токенов — и кодируют каждый. Тогда «единица поиска» — пассаж, а скор документа агрегируется по его пассажам (например, max по пассажам). Это удваивает важность разбиения: один документ → много векторов в индексе.
Внимание. Векторный индекс привязан к конкретной модели-энкодеру. Векторы разных версий модели несопоставимы (живут в разных пространствах). Поэтому смена модели = полная переиндексация всех документов (заново прогнать энкодер на всём корпусе) — дорогая операция, которую планируют заранее. Это прямое продолжение темы перестроения индекса из Модуля 4.
Задача: ближайшие соседи и почему перебор не работаетSEO-врезка. Поскольку индексируется именно текст пассажей, важно, чтобы каждый смысловой блок страницы был самодостаточен: пассаж, выдранный из контекста, должен оставаться понятным. Контент, смысл которого «размазан» по странице и зависит от соседних блоков, даёт слабые пассажные эмбеддинги.
Запрос закодирован в vec(q). Нужно найти k документов с наибольшим q·d (ближайших соседей, k-NN). Наивно — посчитать q·d для всех N векторов и взять топ-k. Стоимость — O(N·d) на запрос.
ANN: приближённый поиск ближайших соседейПример. N = 10⁹ векторов, d = 768, float32. Один запрос — это ~7.7·10¹¹ умножений-сложений. Даже на быстром железе это сотни миллисекунд–секунды на запрос и терабайты памяти под векторы (10⁹·768·4 ≈ 3 ТБ). При тысячах запросов в секунду — невозможно. Нужен индекс по векторам.
ANN (Approximate Nearest Neighbors) жертвует точностью ради скорости: возвращает «почти ближайших» соседей, не перебирая всё. Ключевая метрика качества — recall@k: какая доля истинных k ближайших соседей реально найдена. ANN — это треугольник компромиссов:
Код: Выделить всё
скорость (QPS / латентность)
/\
/ \
/ \
полнота /______\ память
(recall@k) (размер индекса)
HNSW — иерархический навигируемый граф малого мира
HNSW (Hierarchical Navigable Small World) строит над векторами многослойный граф: вершины — векторы документов, рёбра соединяют близкие вершины. Слоёв несколько: верхние — разреженные (дальние «магистральные» прыжки), нижние — плотные (точная навигация). Поиск — это жадный спуск:
Код: Выделить всё
слой 2 (редкий): o───────────o вход сверху, большие прыжки
\ /
слой 1: o────o───o────o───o прыжки поменьше
| | | | |
слой 0 (все): o─o─o─o─o─o─o─o─o─o─o точная окрестность
Параметры:
- M — число рёбер на вершину (степень графа). Больше M → плотнее граф → выше recall, но больше памяти и дольше построение.
- efConstruction — ширина поиска при построении графа (качество рёбер).
- efSearch — ширина «фронта» поиска в рантайме (сколько кандидатов держим в очереди). Больше efSearch → выше recall, но медленнее. Это главный рантайм-рычаг «recall ↔ латентность».
Интуиция. HNSW — как авиаперелёт с пересадками: сначала дальний рейс между хабами (верхний слой), потом региональный, потом местный автобус (слой 0). Не объезжаешь все города — прыгаешь по уменьшающимся масштабам к цели.
IVF — инвертированные ячейки (грубая кластеризация)Инженерная заметка. HNSW даёт отличный recall при низкой латентности, но хранит полные векторы в памяти (граф + float32-векторы) — это дорого по RAM на больших N. Вставка новых точек поддерживается, но массовое удаление/обновление графа неудобно — ещё один аргумент за периодическую полную переиндексацию.
IVF (Inverted File) сначала кластеризует все векторы на nlist ячеек (например, k-means; центр ячейки — центроид). Каждый документ приписан к ближайшему центроиду. Поиск:
- Сравнить q только с nlist центроидами (дёшево).
- Выбрать nprobe ближайших ячеек.
- Перебрать векторы только внутри этих ячеек.
- nlist — число ячеек (грубость разбиения).
- nprobe — сколько ячеек просматриваем на запрос. nprobe = 1 — быстро, но можно промахнуться мимо ячейки с истинным соседом (низкий recall); больше nprobe → выше recall, но медленнее.
PQ — product quantization (сжатие векторов)Внимание. Главный риск IVF — граничный эффект: истинный сосед лежит в соседней ячейке, которую мы не просмотрели. Лечится ростом nprobe. Это прямой аналог компромисса efSearch в HNSW.
IVF сокращает число сравнений, но каждый вектор всё ещё d чисел по 4 байта. PQ (Product Quantization) сжимает сам вектор: делит его на m подвекторов, в каждом подпространстве обучает маленький кодбук (например, 256 центроидов = 1 байт) и заменяет подвектор номером ближайшего центроида.
Код: Выделить всё
исходный вектор (d=768, float32 = 3072 байт)
│ делим на m=96 подвекторов по 8 чисел
▼
[c1][c2]...[c96] каждый ci — 1 байт (номер из 256) → 96 байт
сжатие ≈ в 32 раза
Инженерная заметка. IVF-PQ — выбор для очень больших корпусов, где полные векторы не влезают в RAM: сжатие в десятки раз делает индекс на диске/в памяти реалистичным ценой небольшой потери recall. HNSW — выбор, когда RAM позволяет и нужен максимальный recall при низкой латентности. Часто их комбинируют: HNSW поверх PQ-сжатых векторов.
Код: Выделить всё
| HNSW | IVF | IVF-PQ
----------------------+--------------------------------------+-------------------------------+-----------------------------------
Идея | граф близости | кластеры-ячейки | ячейки + сжатие векторов
Что сокращает | число обходимых вершин | число сравниваемых векторов | + размер каждого вектора
Память | высокая (полные векторы в RAM) | средняя | низкая (сжатие)
Главный рычаг recall | efSearch (M) | nprobe (nlist) | nprobe + параметры PQ
Когда выбирать | RAM есть, нужен max recall/latency | большой корпус | гигантский корпус, RAM в дефиците
Заблуждение. «ANN находит точно тех же соседей, что перебор, только быстрее.» Нет: ANN приближённый — он может пропустить истинного соседа. Качество меряют recall@k и сознательно крутят его параметрами против скорости/памяти.
Заблуждение. «Раз есть векторный индекс, обратный индекс (Модуль 4) больше не нужен.» Нет: они сосуществуют. Лексический (обратный) индекс нужен для гибрида, точных совпадений и фильтров; векторный — для семантики (глава 10.3).
Лаба / практикаЗаблуждение. «Можно дообучить/заменить модель и индекс продолжит работать.» Нет: новые векторы несопоставимы со старыми. Смена энкодера требует полной переиндексации корпуса.
Цель: прочувствовать компромисс recall ↔ скорость в ANN. Время ~70 мин.
- Возьмите 50–100 тыс. эмбеддингов (готовый датасет или сгенерируйте энкодером из лабы 10.1).
- Постройте эталон: для 100 случайных запросов найдите истинных 10 ближайших соседей полным перебором (q·d по всем).
- Постройте HNSW-индекс. Прогоните те же запросы при efSearch ∈ {16, 64, 256}. Для каждого значения замерьте recall@10 (доля совпадения с эталоном) и среднюю латентность.
- Постройте IVF (или IVF-PQ) с фиксированным nlist, варьируйте nprobe ∈ {1, 8, 32}. Замерьте те же метрики.
- Постройте таблицу/график recall@10 vs латентность для обоих индексов.
Критерий «сделано»: для каждого индекса выбрана рабочая точка (recall@10 ≥ 0.95 при минимальной латентности) и объяснено, какой параметр за что отвечает.
Контрольные вопросы
- На каком этапе конвейера и почему именно там вычисляется vec(d)? Где он хранится?
- Зачем длинные документы режут на пассажи и как тогда считают скор документа?
- Почему смена модели-энкодера требует полной переиндексации?
- Оцените стоимость точного k-NN перебором для N=10⁹, d=768 и объясните, почему он нежизнеспособен.
- Что измеряет recall@k и почему он — главная метрика качества ANN?
- Опишите жадный спуск в HNSW. За что отвечают M и efSearch?
- Как работает IVF? Что такое граничный эффект и как nprobe на него влияет?
- Зачем нужен PQ поверх IVF и какой ресурс он экономит? Чем платим?
- Когда вы предпочтёте HNSW, а когда IVF-PQ? Сформулируйте через треугольник компромиссов.
Цели обучения
После главы студент сможет:
- Описать рантайм-путь запроса: кодирование q → ANN-поиск → (опц.) переранжирование cross-encoder’ом.
- Объяснить, где в каскаде ранжирования (Модуль 12) живут векторный отбор и нейропереранжирование.
- Спроектировать гибридный поиск: объединение лексического (BM25) и векторного списков (взвешивание скоров, RRF).
- Объяснить дистилляцию (distillation): перенос знания тяжёлого cross-encoder-«учителя» в лёгкий bi-encoder-«ученика».
- Обосновать, зачем нужна дистилляция: качество cross-encoder при стоимости bi-encoder.
Рантайм-путь запроса
При индексации (глава 10.2) вся тяжёлая работа над документами уже сделана. В рантайме на запрос остаётся:
Код: Выделить всё
запрос q
│ 1. ЭНКОДЕР bi-encoder: q → vec(q) (ОДИН прогон сети)
▼
2. ANN-поиск по векторному индексу (HNSW / IVF-PQ)
│ → топ-K кандидатов по близости q·d
▼
3. (параллельно) BM25 по обратному индексу → топ-K' лексических кандидатов
▼
4. СЛИЯНИЕ списков (гибрид) → объединённые кандидаты
▼
5. (опц.) ПЕРЕРАНЖИРОВАНИЕ cross-encoder’ом топ-K'' (точно, но дорого)
▼
6. постранжирование, выдача (Модули 16, 12–13)
Гибридный поиск: лексика + вектораИнженерная заметка. Это и есть каскад ранжирования Модуля 12. Дешёвый и широкий векторный/лексический отбор — нижние уровни L0–L1 (тысячи кандидатов). Дорогой cross-encoder работает на L2–L3 только над десятками–сотнями выживших. Стоимость сети распределена: офлайн — документы (один раз), онлайн — только запрос (один прогон) + переранжирование горстки.
Из глав 10.1–10.2: лексика точна на словах, вектора — на смысле. Гибридный поиск (hybrid search) объединяет оба сигнала, чтобы взять сильные стороны каждого. Два кандидата дают два списка; их надо слить. Два основных способа:
1. Линейная комбинация скоров. Нормируем оба скора и складываем с весом:
Код: Выделить всё
score_hybrid(d) = α · score_BM25(d) + (1 − α) · score_vec(d)
2. Reciprocal Rank Fusion (RRF). Слияние по рангам, а не по скорам — устойчиво к несопоставимым шкалам:
Код: Выделить всё
score_RRF(d) = Σ_списки 1 / (k + rank_список(d)), k ≈ 60
Интуиция. Гибрид — это «второе мнение». Если документ в топе и у лексики, и у векторов — он почти наверняка релевантен. Если только у одного — это либо точное совпадение по редкому слову (лексика), либо перефразировка (вектора); оба случая ценны, и RRF их не теряет.
Пример. Запрос «ошибка E-204 при оплате». BM25 точно цепляет страницу с кодом E-204 (вектор бы «размыл» код). Векторный поиск находит страницу «сбой транзакции на этапе платежа» без кода, но по смыслу. Гибрид (RRF) выдаёт обе наверх; чистый вектор потерял бы первую, чистая лексика — вторую.
Дистилляция: качество учителя при стоимости ученикаSEO-врезка. Гибрид означает: побеждает контент, который и содержит точные термины запроса (для лексики), и связно раскрывает их смысл (для векторов). Ставка только на «нашпиговать ключи» проигрывает векторной половине; ставка только на «лить воду вокруг темы» без точных формулировок проигрывает лексической. Нужно и то, и другое.
Дилемма модуля: cross-encoder точнее, bi-encoder дешевле и индексируем. Хочется качество первого при стоимости второго. Решение — дистилляция знаний (knowledge distillation).
Идея: тяжёлый, точный учитель (teacher) — обычно cross-encoder — размечает обучающие пары (q, d) своими оценками релевантности (мягкими, непрерывными). Лёгкий ученик (student) — bi-encoder — обучается воспроизводить оценки учителя, а не только бинарные метки «релевантно/нет».
Код: Выделить всё
cross-encoder (учитель) ──► score_T(q,d) ◄ богатый «мягкий» сигнал
│
▼ ученик подгоняет q·d под score_T
bi-encoder (ученик) ──► q·d ≈ score_T(q,d)
Эффект: дистиллированный bi-encoder заметно превосходит bi-encoder, обученный с нуля на бинарных метках, и приближается к учителю по качеству — сохраняя дешёвую офлайн-индексируемую архитектуру dual-tower. Поэтому дистилляция — стандартный приём прод-нейропоиска.Интуиция. Бинарная метка говорит ученику только «да/нет». Учитель-cross-encoder говорит «этот релевантен на 0.9, тот на 0.6, а вон тот на 0.05» — градации, которые несут куда больше сигнала о том, что значит «более релевантно». Ученик впитывает эти градации и потому учится тоньше, чем на голых метках.
Инженерная заметка. Дистилляция часто идёт итеративно и в связке с майнингом жёстких негативов (глава 10.1): текущий ученик достаёт кандидатов → учитель их переоценивает → ученик дообучается на этих оценках → повторить. Каждый цикл подтягивает ученика ближе к учителю.
Частые заблужденияЗаблуждение по ходу. «Дистилляция — это сжатие cross-encoder в меньший cross-encoder.» В контексте поиска ключевое — смена архитектуры: из неиндексируемого cross-encoder в индексируемый bi-encoder. Это не просто «уменьшить», а «переложить в форму, пригодную для офлайн-индекса».
Заблуждение. «Гибрид — это просто запустить два поиска и показать оба.» Нет: суть в корректном слиянии. Складывать сырые BM25 и косинус нельзя (разные шкалы) — нужна нормировка или ранговое слияние (RRF).
Заблуждение. «Дистиллированный ученик так же хорош, как учитель.» Обычно нет — он приближается, но обычно чуть слабее. Ценность в том, что он дёшев и индексируем, тогда как учитель — нет.
Лаба / практикаЗаблуждение. «Cross-encoder можно поставить первым уровнем, если железо мощное.» Нет: его стоимость растёт линейно с числом кандидатов, а первый уровень видит весь индекс. Cross-encoder — всегда переранжировщик горстки, не отборщик.
Цель: построить и оценить гибридный поиск. Время ~60 мин.
- На корпусе из лабы 10.1 получите для 10 запросов два ранжированных списка: BM25 (Модуль 6) и векторный (bi-encoder + поиск из 10.2).
- Слейте их RRF (k=60) и отдельно — линейной комбинацией с min-max-нормировкой при α ∈ {0.3, 0.5, 0.7}.
- Если есть размеченные релевантности (хотя бы вручную для 10 запросов) — посчитайте nDCG@10 (Модуль 19) для: чистого BM25, чистого вектора, RRF и линейного слияния.
- Найдите запросы, где гибрид строго лучше обоих одиночных методов, и объясните механизм (точный код vs перефразировка).
Критерий «сделано»: приведена сводная таблица метрик и минимум 2 запроса-иллюстрации, где гибрид выигрывает за счёт взаимодополнения лексики и векторов.
Контрольные вопросы
- Опишите рантайм-путь запроса по шагам. Сколько прогонов тяжёлой сети приходится на один запрос и почему так мало?
- Где в каскаде L0–L3 (Модуль 12) находятся векторный отбор и cross-encoder-переранжирование? Почему именно там?
- Почему нельзя просто сложить score_BM25 и cos(q,d)? Как это лечат?
- Объясните RRF. Почему он устойчив к разным шкалам скоров?
- Приведите запрос, на котором гибрид строго превосходит и чистую лексику, и чистый вектор.
- Что такое дистилляция в нейропоиске? Кто учитель, кто ученик и что именно перенимается?
- Почему «мягкие» оценки учителя информативнее бинарных меток релевантности?
- Почему в контексте поиска дистилляция — это смена архитектуры, а не просто уменьшение сети?
Цели обучения
После главы студент сможет:
- Сформулировать принцип «половины скалярного произведения»: документную часть — офлайн, запросную и само произведение — онлайн.
- Объяснить, почему именно факторизуемость score = f(q) · g(d) делает нейропоиск масштабируемым.
- Связать офлайн-часть с индексацией (Модуль 4), а онлайн-часть — с каскадом ранжирования (Модуль 12).
- Распознавать, какие модели факторизуемы (bi-encoder), а какие нет (cross-encoder), и почему это определяет их место в системе.
Это — концептуальный замок модуля, собирающий главы 10.1–10.3 в одну идею. Скалярное произведение score = vec(q) · vec(d) симметрично, но в инженерном смысле его две «половины» радикально разной стоимости и разного времени жизни.
Принцип факторизации
Релевантность bi-encoder факторизуется: её можно записать как произведение двух функций, каждая зависит только от своего аргумента:
Код: Выделить всё
score(q, d) = f(q) · g(d)
└┬─┘ └┬─┘
зависит зависит
только только
от запроса от документа
Раскладка по времени и стоимостиИнтуиция. Представьте магазин. Можно для каждого покупателя заново обмерять и описывать каждый товар (cross-encoder — пересчёт всего на каждый запрос). А можно один раз снабдить каждый товар «паспортом» — координатами на полке смыслов (g(d), офлайн), а когда приходит покупатель — описать только его потребность теми же координатами (f(q), онлайн) и подвести к ближайшей полке. Вторая схема и есть «половина скалярного произведения»: тяжёлую инвентаризацию делаем заранее, на запрос — лёгкое сопоставление.
Код: Выделить всё
Часть | Что это | Когда считается | Как часто | Стоимость на единицу | Модуль
---------------+------------------------+--------------------------+---------------------+----------------------------+--------
g(d) = vec(d) | вектор документа | офлайн, при индексации | 1 раз на документ | дорого (тяжёлый энкодер) | 4
хранение g(d) | векторный ANN-индекс | офлайн | — | память/диск | 4
f(q) = vec(q) | вектор запроса | онлайн, на запрос | 1 раз на запрос | дорого, но один прогон | 12
f(q)·g(d) | близость | онлайн | ANN по индексу | дёшево (+ANN) | 12
Почему cross-encoder сюда не вписывается
У cross-encoder score(q,d) не факторизуется: [q‖d] идёт в сеть совместно, и q, d взаимодействуют на всех слоях. Нет такого g(d), которое можно посчитать без q. Поэтому:
- его нельзя материализовать в индекс (нет офлайн-вектора документа);
- его стоимость на запрос ∝ числу оцениваемых документов;
- его место — только переранжирование уже отобранной горстки кандидатов (L2–L3).
Код: Выделить всё
ФАКТОРИЗУЕМ (bi-encoder): score = f(q) · g(d) → g(d) офлайн → индексируем → отбор по всему корпусу
НЕ ФАКТОРИЗУЕМ (cross-enc.): score = h(q, d) → нет g(d) → не индексируем → только rerank топ-K
Внимание. Любой приём, который «подмешивает запрос в документную часть» (например, документный вектор, зависящий от запроса), ломает факторизацию и убивает офлайн-индексируемость. Если архитектуру тянет в сторону взаимодействия q⇄d на этапе документа — это уже cross-encoder со всеми его ограничениями на масштаб.
Инженерная заметка. Поздневзаимодействующие (late-interaction) модели — компромисс: документ хранят множеством токенных векторов (всё ещё офлайн, факторизация по токенам сохраняется), а взаимодействие с запросом — отложенное и дешёвое (max-sim по токенам в рантайме). Точнее одного-вектора bi-encoder, но дороже по памяти индекса. Это «полторы половины»: чуть больше онлайн-взаимодействия ценой большего офлайн-индекса.
Частые заблужденияSEO-врезка. Принцип объясняет, почему «обмануть на лету» нельзя: документная половина зафиксирована при индексации. Влиять можно только на то, что закодировано в g(d) — то есть на ясный, тематически сфокусированный смысл текста на момент индексации, а не на сиюминутную подгонку под конкретный запрос.
Заблуждение. «Раз скалярное произведение симметрично, безразлично, что считать первым.» Математически — да; инженерно — нет. Документную половину считают офлайн один раз (её миллиарды), запросную — онлайн на каждый запрос. Асимметрия частоты и времени — весь смысл приёма.
Лаба / практикаЗаблуждение. «Можно взять качество cross-encoder и просто закэшировать его выходы как векторы документов.» Нельзя: у cross-encoder нет документного вектора в отрыве от запроса — кэшировать нечего. Перенос его качества в индексируемую форму — это дистилляция в bi-encoder (глава 10.3), а не кэш.
Цель: измерить экономию от факторизации. Время ~40 мин.
- Возьмите корпус из 10 тыс. пассажей и 100 запросов. Реализуйте две схемы оценки близости.
- Схема A (факторизуемая, bi-encoder): один раз закодируйте все пассажи (g(d), засеките время — это «офлайн»). Затем на каждый запрос: один прогон f(q) + скалярные произведения. Засеките суммарное онлайн-время на 100 запросов и число прогонов сети.
- Схема B (имитация нефакторизуемой): запретите кэш g(d) — на каждый запрос пересчитывайте «совместное» представление каждой пары (можно сымитировать повторным прогоном энкодера на склейке для подвыборки пассажей, чтобы не ждать вечность). Экстраполируйте число прогонов на полный корпус.
- Сведите: число прогонов сети и время для A и B; во сколько раз A дешевле.
Критерий «сделано»: в отчёте явно показано, какая часть в A — офлайн, какая — онлайн, и сформулировано, почему B не масштабируется на реальный корпус.
Контрольные вопросы
- Запишите факторизацию score(q,d) = f(q)·g(d) и объясните, почему g(d) можно посчитать офлайн.
- Почему «половина скалярного произведения» даёт экономию именно за счёт асимметрии частоты документов и запросов?
- Заполните по памяти: что в нейропоиске считается офлайн (и в каком модуле живёт), что — онлайн (и в каком модуле).
- Почему cross-encoder не факторизуется и что из этого следует для его места в системе?
- Что произойдёт с масштабируемостью, если документный вектор сделать зависящим от запроса?
- Чем late-interaction-модели отличаются по балансу офлайн/онлайн от обычного bi-encoder?
- Почему нельзя «закэшировать» cross-encoder в виде документных векторов, и как тогда переносят его качество в индекс?
- Нейропоиск сопоставляет смыслы, а не строки — плотные эмбеддинги закрывают лексический разрыв (синонимия, перефразировки), где BM25 бессилен.
- Архитектура решает всё. Bi-encoder (dual-tower) кодирует запрос и документ раздельно и факторизуем → индексируем; cross-encoder кодирует их совместно, точнее, но не факторизуем → только переранжирование горстки кандидатов.
- Документные эмбеддинги материализуются в индекс при индексации (Модуль 4): vec(d) зависит только от документа, считается один раз офлайн; смена модели = полная переиндексация.
- Близость к запросу считается в рантайме (Модуль 12): на запрос — один прогон энкодера vec(q) + ANN-поиск; тяжёлая сеть никогда не гоняется «корпус × запросы» раз.
- Точный k-NN не масштабируется — нужен ANN. HNSW (графовый, max recall при наличии RAM; рычаг efSearch/M) и IVF/PQ (ячейки + сжатие, для гигантских корпусов; рычаг nprobe/nlist). Качество меряют recall@k в треугольнике «полнота ↔ скорость ↔ память».
- Гибридный поиск обязателен. Лексика точна на словах/кодах, вектора — на смысле; их сливают нормированной линейной комбинацией или ранговым RRF. Гибрид ≥ любого одиночного метода.
- Дистилляция даёт качество cross-encoder при стоимости bi-encoder: тяжёлый учитель размечает пары мягкими оценками, лёгкий индексируемый ученик их воспроизводит; часто итеративно с майнингом жёстких негативов.
- «Половина скалярного произведения» — стержень модуля: score = f(q)·g(d); документную половину g(d) считаем офлайн (Модуль 4), запросную f(q) и произведение — онлайн (Модуль 12). Вся масштабируемость нейропоиска — в этой факторизации.
- Эмбеддинг (embedding) — плотный вектор фиксированной размерности, представляющий текст в пространстве смыслов; близкие по смыслу тексты дают близкие векторы.
- Лексический разрыв (vocabulary mismatch) — несовпадение слов запроса и документа при совпадении смысла; главная проблема, которую решает нейропоиск.
- Bi-encoder / dual-tower — архитектура с раздельным кодированием запроса и документа двумя башнями; релевантность = скалярное произведение векторов; факторизуема и индексируема.
- Cross-encoder — архитектура с совместным кодированием склейки [q‖d]; точнее, но не факторизуема; применяется только для переранжирования.
- Пулинг (pooling) — сведение токенных векторов в один вектор текста (CLS-пулинг, mean-пулинг).
- Контрастная потеря (contrastive loss / InfoNCE) — функция обучения bi-encoder, тянущая релевантные пары ближе, нерелевантные — дальше.
- Жёсткие негативы (hard negatives) — поверхностно похожие, но нерелевантные документы; учат модель тонким различиям.
- k-NN / ANN — поиск k ближайших соседей; ANN (Approximate NN) — его приближённая, масштабируемая версия.
- recall@k — доля истинных k ближайших соседей, найденных ANN; главная метрика качества ANN.
- HNSW — иерархический навигируемый граф малого мира; графовый ANN-индекс; параметры M, efConstruction, efSearch.
- IVF (inverted file) — ANN на кластерах-ячейках; параметры nlist (число ячеек), nprobe (просматриваемых ячеек).
- PQ (product quantization) — сжатие вектора покусочным квантованием по кодбукам; экономит память; обычно поверх IVF (IVF-PQ).
- Гибридный поиск (hybrid search) — объединение лексического (BM25) и векторного списков (нормированная линейная комбинация или RRF).
- RRF (reciprocal rank fusion) — ранговое слияние списков, устойчивое к разным шкалам скоров.
- Дистилляция (knowledge distillation) — перенос знания тяжёлого учителя (cross-encoder) в лёгкого ученика (bi-encoder) через мягкие оценки.
- Факторизация score = f(q)·g(d) — представимость релевантности произведением функций запроса и документа порознь; основа офлайн-индексируемости.
- Late interaction (позднее взаимодействие) — компромисс: документ хранится множеством токенных векторов (офлайн), взаимодействие с запросом отложено и дёшево (max-sim).
- Опирается на: Модуль 1 (векторная модель, понятие близости и релевантности); Модуль 6 (BM25 как лексический базлайн и вторая половина гибрида); Модуль 9 (обучение ранжированию — связь с обучаемыми моделями релевантности).
- Прямо продолжает: Модуль 4 (индексация) — документные эмбеддинги материализуются в векторный индекс рядом с обратным; переиндексация при смене модели.
- Питает дальше: Модуль 12 (каскад ранжирования) — векторный/лексический отбор на L0–L1, cross-encoder-переранжирование на L2–L3; рантайм-кодирование запроса.
- Тесно связан: Модуль 5 (понимание запроса — что именно кодировать в vec(q)); Модуль 16/13 (постранжирование и выдача — что происходит после нейроотбора); Модуль 19 (оценка качества: nDCG/recall@k, подбор α, efSearch, nprobe); Модуль 16 (антиспам — устойчивость векторных сигналов к манипуляциям); Модуль 20 (прикладное SEO под семантический и гибридный поиск).
- Классические работы по плотному поиску (dense retrieval) и архитектуре dual-tower / bi-encoder для первичного отбора.
- Обзоры по cross-encoder-переранжированию и каскадным схемам «отбор → переранжирование».
- Литература по приближённому поиску ближайших соседей: графовые методы (HNSW), инвертированные ячейки (IVF), квантизация векторов (PQ, IVF-PQ) — устройство, параметры, компромиссы recall/latency/memory.
- Работы по обучению плотных энкодеров: контрастные функции потерь, майнинг жёстких негативов, дистилляция знания из cross-encoder в bi-encoder.
- Исследования по гибридному поиску и слиянию ранжированных списков (нормировка скоров, reciprocal rank fusion).
- Обзоры по late-interaction-моделям (многовекторное представление документа, max-sim) как компромиссу между bi- и cross-encoder.