Обзор модуляЧасть III · ~8 ч · Сложность: (средний) · Пререквизиты: Модуль 1, 4
Текстовая релевантность — это попытка ответить на вопрос «насколько хорошо слова документа объясняют слова запроса», опираясь только на сам текст, без ссылочного графа, поведения пользователей и машинного обучения. Это самый старый и самый понятный класс факторов ранжирования, и одновременно — фундамент, поверх которого надстраивается всё остальное. В сквозном конвейере «обход → индекс → факторы → ранжирование → выдача → постобработка → измерение» этот модуль живёт на стыке индекса (Модуль 4) и факторов (Модуль 8): мы уже умеем находить документы, содержащие термины запроса, и теперь учимся присваивать им числовую оценку соответствия.
В Модуле 1 вы познакомились с булевой и векторной моделями и общей идеей tf-idf. Здесь мы делаем шаг к промышленным методам: разбираем вероятностную модель BM25 и её строгий вывод, расширяем её на документы со структурой (BM25F), затем выходим за пределы «мешка слов» — учитываем покрытие запроса, близость и порядок слов (proximity), фразовость, зональные веса полей и, наконец, внешние тексты-источники (якорный текст и тексты ссылающихся страниц). Именно эти сигналы образуют признаки нижнего уровня каскада ранжирования (L0–L1, Модуль 12), которые потом подаются в обучаемые модели (Модуль 9) и нейропоиск (Модуль 10).
После модуля вы сможете: вывести формулу BM25 из вероятностных соображений и руками посчитать скор для мини-корпуса; объяснить, как k1 и b управляют насыщением и нормировкой длины; настроить веса полей в BM25F; реализовать оценку близости слов и фраз; и грамотно отделить «честные» текстовые сигналы от того, что выглядит как переспам и ловится антиспамом (Модуль 16).
Как читать по трекамИнтуиция. Текстовая релевантность отвечает на вопрос «о том ли этот документ?», а не «хорош ли он?». Авторитетность, свежесть, удобство — это другие модули. Здесь мы измеряем именно тематическое совпадение текста с запросом.
- Студент CS — обязательно всё. Главы 6.1 (вывод BM25) и 6.2 (модели близости) — ядро теории IR. Прорешайте обе лабы руками.
- Инженер поиска/ML — обязательно всё, особенно инженерные заметки про предвычисление IDF, позиционные постинги и стоимость proximity. Эти признаки вы будете считать в L0–L1.
- SEO-специалист — обязательно SEO-врезки во всех главах и главы 6.3–6.4 целиком (title, заголовки, якоря). Вывод формулы в 6.1 — обзорно, но прочитайте раздел про насыщение и переспам.
- Смешанный/руководитель — Обзор модуля, интуиции, заблуждения и итоги. Формулы можно пролистать, но запомните роль k1, b и идею насыщения.
- 6.1. BM25/BM25F и насыщение частоты термина, роль k1 и b (средний)
- 6.2. Покрытие запроса, близость слов (proximity), порядок и фразовость (средний)
- 6.3. Зональная релевантность и веса полей (title/тело/URL/заголовки) (средний)
- 6.4. Якорный текст и тексты-источники как сигнал релевантности (средний)
Цели обучения
После главы студент сможет:
- Объяснить, почему вклад частоты термина должен насыщаться, а не расти линейно.
- Вывести формулу BM25 из вероятностной модели релевантности и из соображений насыщения.
- Объяснить роль и предельные случаи параметров k1 (насыщение) и b (нормировка длины).
- Руками посчитать BM25 для небольшого корпуса.
- Обобщить формулу до BM25F для документов с несколькими полями.
Базовая идея текстового ранжирования: документ d тем релевантнее запросу q, чем чаще в нём встречаются слова запроса — но с двумя поправками. Первая: слово должно быть редким (частые слова вроде «и», «как» почти ничего не различают). Вторая: вклад частоты должен насыщаться (выходить на плато). BM25 — это формула, которая аккуратно совмещает обе поправки и добавляет третью: поправку на длину документа.
Шаг 1. Почему частота должна насыщаться
Линейная функция tf (как в наивном tf-idf) этого не даёт: 50 вхождений она оценивает в 50 раз весомее одного. Нам нужна вогнутая, ограниченная сверху функция от tf. Простейшее семейство с нужным поведением:Интуиция. Если слово «ипотека» встретилось в документе 1 раз против 0 — это огромный скачок смысла: документ вообще «про это». 50 раз против 49 — почти ничего нового: и так понятно, что документ про ипотеку. Вклад частоты должен резко расти у нуля и выходить на плато при больших значениях.
Код: Выделить всё
saturation(tf) = tf / (tf + k1)
В BM25 используют слегка нормированный вариант, чтобы коэффициент при насыщенном tf был интерпретируем:Пример. Пусть k1 = 1.2. Тогда saturation(1) = 1/2.2 ≈ 0.45, saturation(2) ≈ 0.63, saturation(5) ≈ 0.81, saturation(50) ≈ 0.98. Переход от 1 к 2 даёт +0.18, а от 5 к 50 — всего +0.17 при росте частоты в 10 раз. Это и есть насыщение.
Код: Выделить всё
tf · (k1 + 1)
─────────────
tf + k1
Шаг 2. Поправка на длину документа
Введём нормировку длины. Пусть |d| — длина документа (число токенов), avgdl — средняя длина документа в корпусе. Заменим в знаменателе насыщения k1 на k1 · B, гдеИнтуиция. В длинном документе любое слово встречается чаще просто потому, что слов много, а не потому, что документ «более про это». Длину надо штрафовать — но не всегда одинаково.
Код: Выделить всё
B = (1 − b) + b · |d| / avgdl, 0 ≤ b ≤ 1
- b = 0: B = 1 всегда, длина игнорируется полностью.
- b = 1: полная нормировка, вклад делится пропорционально длине.
- типичное значение b ≈ 0.75: частичная нормировка.
Шаг 3. IDF — вес редкости термина (вероятностный вывод)Внимание. Полная нормировка (b = 1) не всегда хороша: бывают честно длинные релевантные документы (подробная статья). b ≈ 0.75 — компромисс, найденный эмпирически, но его стоит подбирать под корпус (см. лабу и Модуль 19).
Теперь — почему редкие слова важнее. BM25 происходит из вероятностной модели релевантности (Probabilistic Relevance Framework). Кратко идея вывода.
Ранжируем документы по шансу быть релевантными. По теореме Байеса логарифм отношения шансов «релевантен / не релевантен» при независимости терминов раскладывается в сумму по терминам запроса. Для каждого термина t вклад в лог-шанс — это вес релевантности Робертсона–Спарк Джонс (RSJ):
Код: Выделить всё
w(t) = log[ (r + 0.5)(N − n − R + r + 0.5) / ((n − r + 0.5)(R − r + 0.5)) ]
В типичном веб-поиске разметки нет: считаем R = 0, r = 0 (ничего не знаем заранее). Подстановка даёт IDF в форме BM25:
Код: Выделить всё
N − n + 0.5
IDF(t) = log ───────────
n + 0.5
Шаг 4. Полная формула BM25 (сборка)Интуиция. IDF тем больше, чем меньше документов содержит термин. «Сглаживание» +0.5 страхует от деления на ноль и от отрицательных всплесков. Эта форма может стать слегка отрицательной для сверхчастых терминов (n > N/2) — на практике часто берут max(0, IDF) или вариант log((N − n + 0.5)/(n + 0.5) + 1), который всегда положителен.
Соединяя три части — вес редкости (IDF), насыщенную частоту и нормировку длины — получаем итоговый скор документа d для запроса q:
Код: Выделить всё
tf(t,d) · (k1 + 1)
BM25(d, q) = Σ IDF(t) · ───────────────────────────────────────
t ∈ q tf(t,d) + k1 · (1 − b + b · |d| / avgdl)
- tf(t,d) — частота термина t в документе d;
- IDF(t) — вес редкости термина (формула выше);
- |d| — длина документа в токенах; avgdl — средняя длина по корпусу;
- k1 — параметр насыщения (типично 1.2…2.0);
- b — параметр нормировки длины (типично 0.75).
Предельные случаи (проверка понимания формулы)Заметка про повторы термина в запросе. Если термин встречается в запросе несколько раз, добавляют множитель qtf·(k3+1)/(qtf+k3) — насыщение по частоте в запросе. Для коротких веб-запросов им обычно пренебрегают (qtf = 1).
Код: Выделить всё
Случай | Что происходит | Почему
-----------------------+-------------------------------+------------------------------------------------+-----+--------
b = 0 | длина документа не влияет | B = 1, нормировка отключена
b = 1 | максимальный штраф за длину | B = | d | /avgdl
k1 → 0 | важен только факт вхождения | насыщение мгновенное: tf любой → вклад ≈ IDF
k1 → ∞ | насыщения почти нет | функция ≈ линейна по tf (как сырой tf-idf)
n мал (редкий термин) | большой вклад | высокий IDF
n ≈ N (стоп-слово) | вклад ≈ 0 или отрицателен | IDF ≈ 0
Корпус из N = 4 документов. Запрос q = {ипотека, ставка}. Берём k1 = 1.2, b = 0.75.
Код: Выделить всё
Док | Текст (упрощённо, токены) | | d | | tf(ипотека) | tf(ставка)
-----+-----------------------------------------------------+-----+-----+-----+---------------+------------
D1 | ипотека ставка ставка банк | 4 | 1 | 2
D2 | ипотека ипотека ипотека условия дом кредит ставка | 7 | 3 | 1
D3 | дом дом ремонт | 3 | 0 | 0
D4 | ставка налог | 2 | 0 | 1
Документная частота (df):
- ипотека встречается в D1, D2 → n = 2.
- ставка встречается в D1, D2, D4 → n = 3.
- IDF(ипотека) = ln((4 − 2 + 0.5)/(2 + 0.5)) = ln(2.5/2.5) = ln(1) = 0.0
- IDF(ставка) = ln((4 − 3 + 0.5)/(3 + 0.5)) = ln(1.5/3.5) = ln(0.4286) ≈ −0.847
Насыщенная частота с нормировкой. Обозначим S(tf, |d|) = tf·(k1+1) / (tf + k1·(1 − b + b·|d|/avgdl)), где k1 = 1.2, b = 0.75.Внимание. На таком крошечном корпусе оба термина встречаются больше чем в половине документов, поэтому «сырой» IDF близок к нулю или отрицателен — это артефакт малого N. Чтобы пример был содержательным, используем неотрицательный вариант IDF(t) = ln(1 + (N − n + 0.5)/(n + 0.5)):
- IDF(ипотека) = ln(1 + 1.0) = ln(2.0) ≈ 0.693
- IDF(ставка) = ln(1 + 0.4286) = ln(1.4286) ≈ 0.357
Множитель длины B(|d|) = 0.25 + 0.75·|d|/4:
- B(4) = 0.25 + 0.75·1.0 = 1.000
- B(7) = 0.25 + 0.75·1.75 = 0.25 + 1.3125 = 1.5625
- B(2) = 0.25 + 0.75·0.5 = 0.625
- термин ипотека, tf=1: S = 1·2.2 / (1 + 1.2) = 2.2/2.2 = 1.000 → вклад 0.693·1.000 = 0.693
- термин ставка, tf=2: S = 2·2.2 / (2 + 1.2) = 4.4/3.2 = 1.375 → вклад 0.357·1.375 = 0.491
- BM25(D1) = 0.693 + 0.491 = 1.184
- ипотека, tf=3: S = 3·2.2 / (3 + 1.875) = 6.6/4.875 = 1.354 → вклад 0.693·1.354 = 0.938
- ставка, tf=1: S = 1·2.2 / (1 + 1.875) = 2.2/2.875 = 0.765 → вклад 0.357·0.765 = 0.273
- BM25(D2) = 0.938 + 0.273 = 1.211
Документ D4 (|d|=2, B=0.625, k1·B = 0.75):
- ипотека, tf=0: вклад 0
- ставка, tf=1: S = 1·2.2 / (1 + 0.75) = 2.2/1.75 = 1.257 → вклад 0.357·1.257 = 0.449
- BM25(D4) = 0.449
BM25F — версия для документов со структуройРазбор. D2 обошёл D1, несмотря на то что в нём оба слова и он длиннее: три вхождения «ипотека» (редкий, более весомый термин) перевесили, хотя насыщение и штраф за длину частично это съели. Обратите внимание: разница D2 и D1 крошечная (0.027) — если поднять b, штраф за длину у D2 (длина 7 против среднего 4) усилится, и порядок может перевернуться. Это иллюстрирует, как b управляет балансом.
Реальный документ — не плоский «мешок слов», а набор полей (зон): title, тело, URL, заголовки H1…H6, мета-описание, якорный текст входящих ссылок (Глава 6.4). Вхождение в title должно весить больше, чем в подвале страницы.
Наивный подход — посчитать BM25 по каждому полю и взвешенно сложить скоры — работает плохо: насыщение применяется к каждому полю отдельно, и одно вхождение в title + одно в теле дают непропорционально много. Правильный приём BM25F — сначала собрать взвешенную псевдочастоту по всем полям, и только потом применить насыщение один раз.
Пусть у документа поля f с весами w_f, длинами |d_f|, средними длинами поля avgdl_f и собственными параметрами нормировки b_f. Тогда:
Код: Выделить всё
w_f · tf(t, f)
tf~(t,d) = Σ ─────────────────────────────────
f 1 − b_f + b_f · |d_f| / avgdl_f
Код: Выделить всё
tf~(t,d)
BM25F(d,q) = Σ IDF(t) · ─────────────────
t∈q tf~(t,d) + k1
Интуиция. BM25F нормирует длину внутри каждого поля (короткий title не штрафуется по меркам длинного тела), затем складывает вклады полей с весами w_f, и только в самом конце «гасит» суммарную частоту насыщением. Так одно сильное вхождение в title повышает скор ощутимо, но два десятка повторов в title уже почти не помогают.
Инженерная заметка. IDF в BM25F обычно считают по всему документу (по объединению полей), а не по каждому полю — иначе df для одного и того же термина был бы несогласован между зонами. Хранить нужно tf по каждому полю отдельно (позиционно-зональный постинг, Модуль 4) и |d_f| по каждому полю. Веса w_f и параметры b_f подбираются как гиперпараметры (Модуль 19) либо обучаются как признаки в LTR (Модуль 9).
Частые заблужденияSEO-врезка. Из BM25F следуют три практических вывода. (1) Title важен — вес поля title в реальных системах выше тела, поэтому ключевой смысл запроса должен быть в заголовке страницы. (2) Насыщение реально — добавить ключевое слово 1–2 раза в важные зоны полезно, набивать его 30 раз бессмысленно (плато) и опасно (антиспам, Модуль 16). (3) Нормировка длины означает, что «разбавление» ключевого слова тоннами нерелевантного текста снижает его вес — релевантный плотный текст выигрывает у «простыни ради объёма», но и переоптимизированная короткая страница ловится фильтрами. Золотая середина — естественная плотность.
Заблуждение. «Чем чаще ключевое слово в тексте, тем выше позиция.» Неверно: вклад частоты насыщается (функция tf/(tf+k1·B) выходит на плато), а сверх некоторого порога переспам распознаётся антиспам-правилами (Модуль 16) и может понизить документ. Линейного выигрыша от частоты не существует уже на уровне базовой формулы.
Заблуждение. «BM25 — это просто tf-idf.» Нет: tf-idf линеен по частоте и обычно не нормирует длину разумно; BM25 добавляет насыщение (k1), управляемую нормировку длины (b) и вероятностно обоснованный IDF. Это качественно другая, эмпирически гораздо более сильная модель.
Лаба / практикаЗаблуждение. «Чтобы усилить поле title, надо считать BM25 по title и прибавить к BM25 по телу.» Это и есть наивная ошибка, которую решает BM25F: складывать надо частоты до насыщения, а не готовые скоры, иначе насыщение работает некорректно.
Задача. Реализуйте BM25 и BM25F поверх обратного индекса и исследуйте параметры. Время ~60 мин.
Входные данные. Мини-корпус из 6 коротких документов (5–15 слов), у каждого есть поле title и поле body. Запрос из 2–3 слов.
Шаги.
- Постройте обратный индекс: для каждого термина — список (doc_id, tf_title, tf_body); посчитайте df, |d_title|, |d_body|, avgdl_title, avgdl_body.
- Реализуйте IDF(t) (неотрицательный вариант) и функцию bm25(doc, query, k1, b).
- Посчитайте ранжирование при k1=1.2, b=0.75. Сверьте с эталоном (дайте порядок руками для одного запроса).
- Эксперимент с b: прогоните b ∈ {0, 0.5, 0.75, 1.0} и зафиксируйте, как меняется позиция самого длинного документа. Объясните.
- Эксперимент с k1: прогоните k1 ∈ {0.1, 1.2, 5, 50} для документа, где ключевое слово повторено 5 раз. Покажите, как растёт вклад и где наступает плато.
- Реализуйте BM25F с весами w_title = 3, w_body = 1. Покажите, что документ, где запрос только в title, поднимается выше документа с тем же словом только в body.
Контрольные вопросы
- Почему вклад частоты термина в BM25 должен быть вогнутой ограниченной функцией, а не линейной? Приведите числовой пример насыщения.
- Что произойдёт со скором при b = 0? А при b = 1? Когда b = 1 навредит?
- Какому поведению соответствуют предельные случаи k1 → 0 и k1 → ∞?
- Откуда в формуле IDF берётся слагаемое +0.5 и почему «сырой» вариант может быть отрицательным?
- Почему в BM25F насыщение применяют один раз к сумме взвешенных частот, а не к каждому полю отдельно?
- У вас два документа с одинаковым tf ключевого слова, но один вдвое длиннее. Как и почему изменится их относительный скор при b = 0.75?
- Как из вероятностной модели релевантности получается, что вклады терминов складываются? Какое допущение за этим стоит?
- Почему набивание ключевого слова в 30 раз почти не повышает BM25, но может понизить документ в целом?
Цели обучения
После главы студент сможет:
- Отличить «мешок слов» от моделей, учитывающих взаимное расположение терминов.
- Объяснить и формализовать покрытие запроса (сколько терминов вообще нашлось).
- Реализовать оценку близости (proximity) через минимальное окно и через расстояния между парами.
- Объяснить, чем фразовое совпадение и совпадение порядка сильнее разрозненного.
- Оценить вычислительную стоимость proximity и понять, где её считают в каскаде.
BM25 — это «мешок слов» (bag of words): он не знает, стоят ли слова запроса рядом или разбросаны по разным абзацам. Но для пользователя «дешёвый ремонт квартиры» и документ, где «дешёвый» в шапке, «ремонт» в середине, а «квартиры» в подвале — это не то же самое, что фраза «дешёвый ремонт квартиры» в одном предложении. Эта глава — про сигналы, которые BM25 игнорирует.
Покрытие запроса (query coverage)
Формально покрытие — доля (или число) уникальных терминов запроса, присутствующих в документе:Интуиция. Документ, в котором нашлись все слова запроса, почти всегда лучше документа, где нашлась только половина — даже если у второго эти слова очень частые. Полнота совпадения важнее интенсивности.
Код: Выделить всё
coverage(d, q) = |{t ∈ q : tf(t,d) > 0}| / |q|
- Жёсткое требование (AND-семантика): документ обязан содержать все термины (или все «обязательные» после анализа запроса, Модуль 5). Документы с неполным покрытием либо отсекаются, либо уходят в хвост.
- Мягкое (OR с бонусом): допускаем неполное покрытие, но добавляем монотонно растущий бонус за число найденных терминов.
- Опциональные термины: после понимания запроса (Модуль 5) часть слов помечается как необязательная (стоп-слова, синонимы), и покрытие считается только по «ядру».
Близость слов (proximity)Инженерная заметка. Покрытие тесно связано с отбором кандидатов на уровне L0 (Модуль 12): часто индекс сначала достаёт документы по пересечению постингов (AND), и только если их мало — расширяет до OR. Это и реализация семантики покрытия, и оптимизация: пересечение коротких списков дешевле.
Даже при полном покрытии важно, насколько кучно расположены термины. Для этого нужен позиционный индекс (Модуль 4): постинг хранит не только (doc_id, tf), но и список позиций каждого вхождения.
Две распространённые формализации близости.
(а) Минимальное покрывающее окно (span). Найдём кратчайший отрезок текста, содержащий хотя бы по одному вхождению каждого термина запроса. Чем короче окно — тем выше близость.
Код: Выделить всё
span(d, q) = min длина окна [i, j], содержащего все термины запроса
proximity_score ∝ 1 / (1 + span − |q|) // span − |q| = «лишние» слова внутри окна
(б) Сумма по парам (pairwise distance). Для каждой пары терминов запроса берём минимальное расстояние между их ближайшими вхождениями и агрегируем затухающей функцией:Пример. Запрос {кофе, доставка}. Текст: «… свежий кофе с быстрой доставкой …». Позиции: кофе=10, доставка=13. Минимальное окно [10,13] длиной 4, лишних слов 4 − 2 = 2. Бонус ∝ 1/3. В другом документе «кофе доставка» подряд: окно длиной 2, лишних 0, бонус ∝ 1 — втрое выше.
Код: Выделить всё
prox(d,q) = Σ f(mindist(ti, tj)), где f(x) = 1/x² или exp(−x/σ)
i<j
Порядок и фразовостьИнженерная заметка. Один из практичных приёмов — встроить близость прямо в BM25-подобную формулу через «псевдо-tf пар»: рядом стоящая пара терминов трактуется как дополнительное «вхождение» виртуального би-граммного термина, к которому применяется то же насыщение. Это даёт сигнал близости в той же шкале, что и основной скор, и легко комбинируется.
Иерархия силы текстового совпадения (от слабого к сильному):Интуиция. «Ремонт квартир» и «квартир ремонт» — близки по смыслу, но для многих запросов порядок несёт смысл («стол для кухни» ≠ «кухня для стола»). Точная фраза — самый сильный текстовый сигнал соответствия.
Код: Выделить всё
Уровень | Что совпало | Сигнал
--------------+----------------------------------+------------------------
Разрозненно | термины есть, но далеко | базовый BM25
Кучно | термины в одном окне | + proximity
По порядку | термины идут в порядке запроса | + ordered-proximity
Точная фраза | термины подряд, как в запросе | + phrase match (макс.)
Код: Выделить всё
phrase(t1 t2 t3) встречается, если ∃ p:
p ∈ positions(t1, d)
p+1 ∈ positions(t2, d)
p+2 ∈ positions(t3, d)
Заметка о стоп-словах и стемминге. Перед проверкой фразы текст проходит ту же нормализацию, что и запрос (Модуль 5): стемминг/лемматизация и, возможно, удаление стоп-слов. Поэтому «ремонт квартир» и «ремонта квартиры» обычно матчатся как фраза. С удалением стоп-слов аккуратнее: «группа крови» без стоп-слов может схлопнуться, поэтому стоп-слова в фразах часто сохраняют как «дырки» в позиционной проверке.
Как это собирается в общий текстовый скорИнженерная заметка (стоимость). Proximity и фразы дороже BM25: нужны позиционные списки (тяжелее по памяти и I/O) и попарная/оконная обработка позиций. Поэтому их почти никогда не считают на всём корпусе. Типичная схема каскада (Модуль 12): L0 быстро отбирает кандидатов по AND/BM25 без позиций → L1 досчитывает proximity/phrase только для top-K кандидатов → L2+ подаёт это как признаки в обучаемую модель. Позиционные данные читаются лениво только для выживших документов.
На практике proximity/phrase редко используют как единственный балл — это набор признаков, подаваемых в модель ранжирования (Модуль 9):
- bm25_body, bm25_title, …
- coverage (доля найденных терминов),
- min_span, ordered_span,
- exact_phrase_hits (число точных вхождений фразы запроса),
- pairwise_proximity.
Частые заблуждения
Заблуждение. «BM25 уже учитывает, что слова рядом.» Нет: BM25 — мешок слов, он видит только частоты, но не позиции. Близость и фразы — отдельные сигналы поверх позиционного индекса.
Заблуждение. «Точная фраза в запросе означает, что система ищет только точное совпадение строки.» В современном поиске даже фразовые совпадения проходят через нормализацию (стемминг, синонимы); «точная фраза» — это сильный бонус, а не обязательно жёсткий строковый фильтр (кроме явного оператора кавычек).
Лаба / практикаЗаблуждение. «Чем ближе слова, тем линейно лучше.» Близость, как и частота, насыщается: соседство даёт большой бонус, но «ещё на один токен ближе» при и так малом расстоянии почти ничего не меняет; функция затухания обычно нелинейна.
Задача. Реализуйте позиционный индекс и три сигнала близости. Время ~50 мин.
Входные данные. 5 документов, каждый — список токенов с позициями. Запрос из 3 слов, присутствующий во всех документах с разной «кучностью» (в одном — точной фразой, в другом — вразброс).
Шаги.
- Постройте позиционный индекс: термин → {doc_id: [позиции]}.
- Реализуйте coverage(d, q).
- Реализуйте min_span(d, q) — минимальное окно, содержащее все термины (алгоритм: указатели по отсортированному слиянию позиций или скользящее окно). Верните длину окна.
- Реализуйте exact_phrase(d, q) через сдвиговое пересечение позиций; верните число вхождений фразы.
- Постройте итоговый учебный скор: 0.5·bm25_norm + 0.3·(1/(1+span−|q|)) + 0.2·phrase_hits и отранжируйте.
- Покажите документ, который при чистом BM25 был не первым, но поднялся в топ за счёт точной фразы.
Контрольные вопросы
- Чем покрытие запроса отличается от частоты термина и почему его выделяют в отдельный сигнал?
- Как устроен алгоритм минимального покрывающего окна и какова его сложность от числа вхождений?
- Почему функция близости обычно затухает нелинейно (например, 1/x²)?
- Как по позиционному индексу проверить точную фразу из 3 слов?
- Чем «совпадение по порядку» сильнее «совпадения в окне», но слабее «точной фразы»?
- Почему proximity/phrase обычно считают только на верхних K кандидатах, а не на всём корпусе?
- Как стемминг и стоп-слова влияют на проверку фразовости и какие тут подводные камни?
Цели обучения
После главы студент сможет:
- Перечислить основные зоны (поля) документа и объяснить, почему их веса различаются.
- Объяснить разницу между полем (field) и зоной (zone) и как они представлены в индексе.
- Оценить, как зональные веса встраиваются в BM25F и в признаки LTR.
- Понимать риски ручного назначения весов и почему их предпочтительно обучать.
Документ структурирован: вхождение термина в title несёт больше сигнала о теме страницы, чем то же вхождение в навигационном меню или подвале. Зональная (полевая) релевантность — это учёт того, где в документе находится совпадение.
Какие бывают зоны и почему они весят по-разному
Код: Выделить всё
Зона | Почему важна | Типичный относительный вес
-------------------------------+-------------------------------------------------+-----------------------------
title (заголовок страницы) | Автор кратко формулирует тему; виден в выдаче | очень высокий
URL / slug | Структурный сигнал темы (/remont-kvartir/) | высокий, но осторожно
Заголовки H1–H3 | Структура содержания, подтемы | высокий
Основной текст (body) | Полное содержание | базовый
Якорный текст входящих ссылок | Как документ описывают другие (Глава 6.4) | высокий, внешний
Мета-описание / alt | Вспомогательный текст | низкий
Меню/подвал/реклама («хром») | Шум, повторяется на всех страницах | минимальный или исключается
Поле vs зонаИнтуиция. Зона — это «голос с разным авторитетом». title — сам автор, кратко и ответственно. Якорь — голос других сайтов (часто честнее автора). Подвал — повторяющийся шум, который почти ничего не говорит про конкретную страницу.
- Поле (field) — отдельный логический атрибут с собственным словарём/постингами: title, body, url, anchors. Запросы могут адресоваться к полю (title:ремонт).
- Зона (zone) — разметка участков внутри одного текстового потока (например, «этот фрагмент тела — заголовок H2»), хранится как диапазоны позиций.
Как зоны входят в скорИнженерная заметка. На практике комбинируют: индексируют несколько полей и внутри тела помечают зоны. В позиционном постинге к каждой позиции можно приписать флаг зоны/поля. При скоринге вес берётся по зоне вхождения. Это удваивает-утраивает размер постингов, поэтому зональность — компромисс между точностью сигнала и стоимостью индекса (Модуль 4).
Вариант 1 — взвешенная сумма полевых скоров (наивный):
Код: Выделить всё
score(d,q) = Σ_f w_f · BM25_f(d,q)
Вариант 2 — BM25F (предпочтительный): взвешиваем частоты до насыщения (формула из 6.1). Это «правильная» зональная модель.
Вариант 3 — зоны как признаки LTR: каждому полю — свой признак (bm25_title, bm25_body, coverage_title, …), а веса обучает модель ранжирования (Модуль 9). Сегодня это доминирующий подход: ручные веса не успевают за разнообразием запросов.
Особый случай: URL и хлебные крошкиЗаблуждение. «Достаточно вынести веса полей в конфиг и подобрать руками.» Для пары полей — может быть. Но качество зависит от типа запроса (навигационный vs информационный), языка, вертикали — единый ручной набор весов почти всегда проигрывает обученному. Ручные веса хороши как базлайн и для интерпретируемости, не как финал.
URL — слабый, но дешёвый и устойчивый к накрутке сигнал: термин в пути (/dostavka-cvetov/) подтверждает тему. Но: домен и общие сегменты (www, index, ru) шумят, а длинные «ключевые» URL легко набиваются спамерами. Поэтому вес URL умеренный, а сегменты часто токенизируют и чистят.
Частые заблужденияSEO-врезка. Практические следствия зональной модели:
- Title — самый недооценённый рычаг. Один точный заголовок, отражающий запрос пользователя, влияет сильнее, чем десяток повторов в теле. Не дублируйте title на всех страницах — уникальный title на страницу.
- H1/H2 структурируют тему — выносите подтемы в заголовки, это и сигнал, и польза для читателя.
- URL — слабый бонус, не повод городить /kupit-deshevo-bystro-nedorogo-remont-kvartir-moskva/: переспам в URL не усиливает, а похож на манипуляцию (Модуль 16).
- Подвал/меню почти не считаются — ключевые слова в подвале на всех страницах не дают зонального веса (это «хром», шум).
- Манипуляция весом полей (скрытый title-spam, набивка alt) — классический объект антиспама. Сильные зоны = сильный соблазн накрутки = пристальный надзор фильтров.
Заблуждение. «Если поднять вес title, всегда станет лучше.» Слишком высокий вес title ломает информационные запросы, где ответ в теле, и поощряет title-спам. Веса — это баланс, который надо измерять (Модуль 19), а не «крутить вверх».
Заблуждение. «Ключевые слова в подвале и меню помогают, они же на странице.» Повторяющийся на всех страницах «хром» обычно детектируется (шаблонные блоки) и почти не вносит зонального веса; иногда исключается из индексации тела вовсе.
Лаба / практикаЗаблуждение. «BM25F и сумма BM25 по полям — одно и то же.» Нет: разница в том, где применяется насыщение (до или после суммирования по полям), и это меняет результат, особенно при вхождениях в нескольких зонах.
Задача. Сравните наивную сумму полевых BM25 с BM25F на документах со структурой. Время ~45 мин.
Входные данные. 5 документов с полями title, body, url. Запрос из 2 слов. Среди документов: (A) слово только в title; (B) слово 10 раз в body; (C) слово по разу в title и body.
Шаги.
- Проиндексируйте по полям; посчитайте tf по каждому полю, длины и avgdl по полю.
- Реализуйте наивный Σ w_f·BM25_f и BM25F (веса title:3, body:1, url:1).
- Сравните ранжирования двух методов; обратите внимание на документ C (вхождения в двух полях).
- Покажите эффект двойного насыщения: на какой позиции C в наивном методе и в BM25F?
- Поэкспериментируйте: при каком весе title документ A (только title) обходит документ B (10 раз в body)? Объясните в терминах насыщения.
Контрольные вопросы
- Почему вхождение термина в title весит больше, чем в подвале страницы?
- В чём разница между «полем» и «зоной» с точки зрения индекса?
- Почему наивная взвешенная сумма полевых BM25 хуже BM25F?
- Почему вес URL держат умеренным, несмотря на то что URL — структурный сигнал?
- Почему ручную настройку весов полей предпочитают заменять обучаемыми весами (LTR)?
- Какие зоны документа чаще всего исключают из текстового скоринга и почему?
- Чем сильные зоны (title, anchor) опасны с точки зрения антиспама?
Цели обучения
После главы студент сможет:
- Объяснить, почему якорный текст входящих ссылок — особенно сильный текстовый сигнал.
- Описать, как якоря агрегируются в виртуальное поле документа.
- Перечислить тексты-источники помимо якоря (окружение ссылки, текст редиректов, заголовки-источники).
- Объяснить уязвимость якорного сигнала к манипуляции и базовые защиты.
До сих пор мы оценивали релевантность по тексту самого документа. Но мощнейший текстовый сигнал часто лежит снаружи: то, как документ описывают другие страницы, ссылаясь на него.
Якорный текст (anchor text)
Якорный текст — это видимый текст гиперссылки, ведущей на документ. Если сотни страниц ссылаются на некий ресурс словами «прогноз погоды», то почти наверняка документ про прогноз погоды — даже если на самой странице слова «прогноз погоды» написаны мелко или картинкой.
Почему якорь так силён:Интуиция. Якорь — это краудсорсинговое название документа, данное независимыми авторами. Автор страницы может хитрить с собственным title; сотни внешних авторов независимо — гораздо более честный коллективный голос о том, «про что эта страница».
- Внешняя точка зрения — труднее накрутить в одиночку (нужны другие сайты).
- Краткость и точность — люди подписывают ссылки коротко и по делу, как мини-запросы.
- Покрытие «немого» контента — описывает документы, чей собственный текст беден (PDF, картинки, формы, главные страницы).
- Совпадение с языком запросов — якоря формулируются как пользователи формулируют запросы.
Якорный текст принадлежит не самой странице, а множеству ссылающихся страниц. При индексации все якоря, ведущие на документ d, собираются в виртуальное поле anchors(d):
Код: Выделить всё
anchors(d) = мультимножество якорных фраз всех входящих ссылок на d
- Дедупликация по источнику-сайту: тысячи ссылок с одного домена считают как один (или логарифмически затухающий) голос, а не как тысячи. Голоса агрегируют по уникальным доменам/хостам, а не по страницам.
- Насыщение по якорной частоте: как и везде, tf якоря насыщается (та же логика BM25).
- Взвешивание источника качеством/авторитетом: якорь с авторитетного домена весит больше (связь с Модулем 7 — ссылочный граф).
- Ограничение длины якоря: сверхдлинные «якоря-абзацы» обрезают или штрафуют (часто это спам).
Другие тексты-источникиИнженерная заметка. Сбор якорей — отдельный офлайн-проход по графу ссылок (после краулинга, Модуль 2, и каноникализации, Модуль 3 — якорь приписывается каноническому URL цели). Это требует инвертировать граф (для каждого d — кто на него ссылается и каким текстом) и устойчивости к редиректам и дублям. Якорное поле часто крупное и обновляется отдельным циклом от тела документа.
Якорь — главный, но не единственный «внешний» текст о документе:
Код: Выделить всё
Источник | Что это | Сигнал
-------------------------------------------------------------+------------------------------------------------------+--------------------------------------------------------
Якорный текст | текст ссылки на d | сильный, внешний
Окружение ссылки (link context) | предложение/абзац вокруг ссылки | дополняет якорь, особенно для общих якорей вроде «тут»
Текст при редиректе | старый URL/якоря, ведшие на перенаправленный адрес | переносится на цель
Заголовки и текст карточек-цитирований | как документ упоминают без прямой ссылки | слабее, но полезно для «немых» документов
Текст из структурированных описаний (каталоги, справочники) | сторонние описания | вспомогательный
Манипуляция и защитаЗаблуждение. «Якорь "нажмите здесь" бесполезен.» Сам якорь — да, но окружающий текст (link context) часто несёт тему: «полный прогноз погоды [нажмите здесь]». Системы учитывают контекст ссылки именно из-за обилия неинформативных якорей.
Якорь силён — значит, его атакуют. Координированная простановка ссылок с одинаковым коммерческим якорем («купить X дёшево») с тысяч помоечных сайтов — классический спам.
Базовые защиты (подробно — Модуль 16):
- агрегация по доменам/владельцам, а не по ссылкам (массовая накрутка с фермы сайтов схлопывается в немного голосов);
- учёт авторитета источника (Модуль 7): голос мусорного сайта почти ничего не весит;
- аномалия распределения якорей: естественный профиль якорей разнообразен (бренд, URL, «тут», вариации), а у накрученного — неестественно однородный коммерческий якорь; резкая однородность — признак манипуляции;
- временна́я аномалия: всплеск одинаковых якорей за короткий срок подозрителен.
Частые заблужденияSEO-врезка. Что из этого следует честному оптимизатору:
- Якорный текст важен — естественные ссылки с осмысленным якорем помогают, особенно для страниц с бедным собственным текстом.
- Не управляйте якорями искусственно. Покупка ссылок с одинаковым «денежным» якорем — самый детектируемый паттерн (однородность профиля + низкокачественные доноры). Это путь под фильтр (Модуль 16), а не в топ.
- Естественный профиль разнообразен — реальные люди ссылаются по-разному (бренд, URL, описательно). Однородность — красный флаг для антиспама.
- Внутренние ссылки (со своего сайта) — тоже якоря: осмысленные анкоры внутренней перелинковки помогают индексации и тематизации, и они в вашей власти без риска.
- Помните про связь с Модулем 16: чем сильнее сигнал, тем строже фильтры на нём. Любая попытка «накрутить» сильный текстовый сигнал (title-spam, anchor-spam, набивка) — первый кандидат на санкции.
Заблуждение. «Якорный текст — это часть текста самой страницы.» Нет: якорь принадлежит ссылающимся страницам и собирается в виртуальное поле цели при офлайн-обработке графа ссылок.
Заблуждение. «Больше ссылок с нужным якорем — линейно лучше.» Голоса агрегируют по доменам и насыщают; тысяча ссылок с одной фермы ≈ один слабый голос, а однородный коммерческий якорь скорее навредит.
Лаба / практикаЗаблуждение. «Якорь работает, только если на странице нет нужных слов.» Якорь усиливает сигнал даже для богатых текстом страниц, но особенно ценен для «немых» документов (картинки, PDF, лендинги без текста).
Задача. Постройте виртуальное поле якорей и оцените влияние агрегации по доменам. Время ~45 мин.
Входные данные. Мини-граф: 8 страниц-источников (с указанием их домена и «авторитета» 0–1), каждая ссылается на одну из 3 целевых страниц с заданным якорным текстом. Запрос из 2 слов.
Шаги.
- Соберите для каждой цели anchors(target) = список (домен, авторитет, якорный_текст).
- Постройте две версии якорного tf: (a) наивную — считая каждую ссылку; (b) с дедупликацией по домену (один домен = один голос, например, максимум или лог-затухание).
- Посчитайте якорный BM25 для обеих версий и сравните ранжирование целей.
- Подмешайте «ферму»: 5 ссылок с одного мусорного домена (авторитет 0.05) с одинаковым коммерческим якорем на одну цель. Покажите, что наивная версия её незаслуженно поднимает, а версия с дедупликацией + взвешиванием авторитетом — нет.
- Включите якорное поле в BM25F тела+title и покажите, как «немой» документ (пустое тело, но хорошие якоря) попадает в топ.
Контрольные вопросы
- Почему якорный текст считается более «честным» сигналом темы, чем собственный title документа?
- Как и почему якоря агрегируют по доменам/владельцам, а не по отдельным ссылкам?
- Зачем учитывать окружение ссылки (link context), а не только сам якорь?
- Как якорный текст помогает ранжировать «немые» документы (PDF, изображения)?
- На каком этапе конвейера собирается якорное поле и при чём здесь каноникализация (Модуль 3)?
- Какие признаки выдают накрученный профиль якорей?
- Почему сила якорного сигнала автоматически означает усиленный надзор антиспама (Модуль 16)?
- Текстовая релевантность измеряет тему, а не качество — «о том ли документ», отдельно от авторитета, свежести и поведения.
- BM25 = редкость (IDF) × насыщенная частота × нормировка длины. Вклад частоты вогнут и ограничен — линейного выигрыша от повторов нет.
- k1 управляет насыщением, b — нормировкой длины. Их предельные случаи (k1→0, k1→∞, b=0, b=1) надо понимать наизусть.
- BM25F — правильная зональная модель: взвешивать частоты полей до единого насыщения, а не складывать готовые полевые скоры.
- Мешок слов недостаточен: покрытие запроса, близость (proximity), порядок и точная фраза — сильные дополнительные сигналы, требующие позиционного индекса.
- Зоны весят по-разному: title/H1/anchor — сильные, body — базовый, «хром» (меню/подвал) — шум; веса лучше обучать (LTR), чем крутить вручную.
- Якорный текст — внешний краудсорсинговый «голос» о теме, агрегируется в виртуальное поле по доменам с поправкой на авторитет.
- Чем сильнее текстовый сигнал, тем строже антиспам на нём — переспам, title-spam и anchor-spam детектируются и наказываются (Модуль 16).
- Насыщение частоты (term frequency saturation) — вогнутое, ограниченное сверху преобразование tf, гасящее выгоду от множества повторов.
- k1 — параметр BM25, задающий скорость насыщения частоты.
- b — параметр BM25 нормировки на длину документа (0 — выкл, 1 — полная).
- avgdl — средняя длина документа в корпусе, база нормировки длины.
- IDF (inverse document frequency) — вес редкости термина; в BM25 выводится из вероятностной модели релевантности.
- BM25F — расширение BM25 на документы с несколькими полями: частоты полей взвешиваются и нормируются до общего насыщения.
- Зона / поле (zone / field) — структурный участок/атрибут документа (title, body, URL, заголовки) со своим весом в скоринге.
- Покрытие запроса (query coverage) — доля уникальных терминов запроса, присутствующих в документе.
- Близость (proximity) — мера взаимной близости терминов запроса в документе; считается по позиционному индексу.
- Минимальное покрывающее окно (span) — кратчайший отрезок текста, содержащий все термины запроса.
- Фразовое совпадение (phrase match) — наличие терминов запроса подряд в порядке запроса; сильнейший текстовый сигнал.
- Якорный текст (anchor text) — видимый текст ссылки; собирается в виртуальное поле документа-цели.
- Окружение ссылки (link context) — текст вокруг гиперссылки, дополняющий якорь.
- Опирается на: Модуль 1 (булева/векторная модели, базовый tf-idf, понятие релевантности); Модуль 4 (обратный и позиционный индекс, постинги, поля/зоны, стоимость хранения позиций).
- Использует результаты: Модуль 5 (понимание запроса: какие термины обязательны, синонимы, стемминг — влияет на покрытие и фразовость); Модуль 3 (каноникализация — куда приписывать якоря); Модуль 7 (ссылочный граф — авторитет источника якоря).
- Питает дальше: Модуль 8 (текстовые сигналы как класс факторов); Модуль 9 (bm25_*, proximity, coverage, phrase — признаки LTR); Модуль 10 (нейропоиск часто комбинируется с BM25 как сильным базлайном/первым уровнем); Модуль 12 (где именно в каскаде L0–L2 считаются дешёвый BM25 и дорогие proximity/phrase).
- Тесно связан: Модуль 16 (антиспам: переспам, title-spam, anchor-spam — обратная сторона сильных текстовых сигналов); Модуль 19 (как измерять и подбирать k1, b, веса полей); Модуль 20 (прикладное SEO).
- Классические работы по вероятностной модели релевантности и выводу BM25 (Probabilistic Relevance Framework, веса Робертсона–Спарк Джонс).
- Обзоры по BM25F и зональному/полевому ранжированию структурированных документов.
- Литература по моделям близости и фразовому поиску (минимальные покрывающие окна, term-dependence модели, марковские случайные поля для IR).
- Исследования по якорному тексту как сигналу и его агрегации; работы об устойчивости текстовых сигналов к манипуляциям (мост к антиспаму).
- Обзоры по настройке гиперпараметров ранжирования и оценке текстовой релевантности (мост к Модулю 19).