Алгоритмы ранжирования в Elasticsearch: настройка и кастомизация scoring модели

Kate

Administrator
Команда форума
Сегодня рассмотрим одну очень важную вещь в Elasticsearch — алгоритмы ранжирования. Благодаря которой запрос возвращает релевантные результаты, а не кучу бессмысленных данных. Но что, если стандартные настройки больше не устраивают? Как сделать так, чтобы поиск находил только самое важное и нужное?

Нет волшебной кнопки «Сделать поиск идеальным», но есть способы управлять моделью scoring.

BM25​

Для начала освежим базовые знания. BM25 — это улучшенная версия классической модели TF-IDF. BM25 считается более умной, поскольку учитывает не только частоту термина в документе, но и его длину. Это позволяет алгоритму лучше справляться с длинными и короткими документами.

Основные параметры BM25 — это k1 и b. Их настройка влияет на то, как алгоритм оценивает релевантность документа относительно поискового запроса.

Параметры k1 и b​

Параметр k1: интенсивность роста влияния термина

Этот параметр отвечает за то, насколько сильно будет увеличиваться оценка документа при добавлении в него новых вхождений термина. По умолчанию значение k1 равно 1.2, что является компромиссным вариантом для большинства случаев.

Например, если есть документ с большим количеством повторений термина, при значении k1=1.2 каждый новый повтор термина будет влиять на итоговый _score, но не линейно. При увеличении k1 влияние термина на результат будет расти более агрессивно. Если же k1 равно 0, то частота термина не будет учитываться вообще.

Пример настройки индекса с измененным k1:

PUT /my-index
{
"settings": {
"index": {
"similarity": {
"my_bm25": {
"type": "BM25",
"k1": 1.8, // Увеличиваем значение k1 для более агрессивного учета частоты термина
"b": 0.75 // Параметр b оставим по умолчанию
}
}
}
}
}
Когда стоит увеличить k1:

Если документы содержат термины, которые встречаются часто и хочется усилить их влияние на результат или же при работе с длинными документами, где важно учитывать каждый дополнительный повтор термина.

Когда стоит уменьшить k1:

Если документы короткие и ключевые слова встречаются редко. В этом случае низкое значение k1 сделает каждое вхождение более весомым.

Параметр b: нормализация по длине документа

Параметр b отвечает за то, насколько сильно будет учитываться длина документа при оценке его релевантности. Значение по умолчанию — 0.75. Т.е длина документа влияет на результат, но не слишком агрессивно.

Пример:

PUT /my-index
{
"settings": {
"index": {
"similarity": {
"my_bm25": {
"type": "BM25",
"k1": 1.2,
"b": 0.5 // Уменьшаем влияние длины документа
}
}
}
}
}
Когда стоит увеличить b:

Если много длинных документов, и нужно, чтобы они не «перевешивали» короткие документы с релевантными терминами.

Когда длина документа важна для точности поиска (например, длинные тексты могут содержать много «шума»).

Когда стоит уменьшить b:

Если документы примерно одинаковой длины, тогда лучше уменьшить влияние длины и сделать акцент на других факторах (частота термина, популярность).

Как все это влияет на результаты поиска​

Теперь посмотрим, как эти параметры влияют на результат. Допустим, есть два документа:

  1. Документ 1: «Elasticsearch Elasticsearch Elasticsearch — все о поиске.»
  2. Документ 2: «Elasticsearch — это классный поисковый движок.»
При стандартных настройках k1=1.2, b=0.75 Document 1 будет иметь больший _score, так как он содержит больше вхождений термина «Elasticsearch». Но если уменьшить k1, то частота термина перестанет так сильно влиять на итоговый результат, и Document 2 может оказаться выше в выдаче, если он релевантен по другим параметрам.

Пример запроса с использованием новой модели:

GET /my-index/_search
{
"query": {
"match": {
"title": "Elasticsearch"
}
}
}
BM25 — мощная вещь для улучшения поисковой релевантности в Elasticsearch, но его нужно уметь правильно настраивать.

Переходим к следующему пункту статьи — пользовательские модели ранжирования с помощью Function Score и Painless.

Пользовательские модели ранжирования с помощью Function Score и Painless​

Ранжирование — это душа поиска. Когда пользователь вводит запрос, он ожидает увидеть релевантные результаты, а не случайные документы. Но что делать, если обычного ранжирования на основе BM25 или TF-IDF недостаточно? Например, нужно учитывать не только вхождения термина в документе, но и такие метрики, как популярность товара, количество просмотров статьи или влияние времени с момента последнего обновления.

Function Score Query и Painless Script дают возможность модифицировать расчет _score и создавать кастомные модели, подходящие именно под случай.

Function Score​

Function Score Query — это запрос, который позволяет изменять итоговый _score документа, применяя к результатам поиска различные функции.

Пример запроса на основе популярности товара:

GET /products/_search
{
"query": {
"function_score": {
"query": {
"match": {
"description": "laptop"
}
},
"field_value_factor": {
"field": "popularity", // поле, содержащее популярность товара
"factor": 1.2, // множитель
"modifier": "log1p" // тип модификации значения
}
}
}
}
Этот запрос не просто ищет товары по слову «laptop», но и учитывает значение поля popularity для расчета _score. Мы умножаем популярность на фактор 1.2 и применяем логарифмическую модификацию через log1p, чтобы уменьшить влияние больших значений.

Когда использовать Function Score:

Когда есть поле, отражающее популярность документа (например, количество продаж товара или просмотров статьи), и нужно, чтобы это влияло на поисковую выдачу.

Painless​

Иногда просто функции недостаточно. Нужно что-то большее, чтобы учесть динамические изменения, более сложные вычисления или условные зависимости. Тут на помощь приходит Painless Script — встроенный скриптовый язык в Elasticsearch, который позволяет внедрять логику прямо в запросы.

Допустим, нужно учитывать временной decay (например, как долго документ был актуален). В этом случае хорош будет именно Painless Script:

GET /articles/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "Elasticsearch"
}
},
"script_score": {
"script": {
"source": "doc['popularity'].value / Math.log(1 + (params.now - doc['publish_date'].value))",
"params": {
"now": "2024-09-21T00:00:00"
}
}
}
}
}
}
Комбинируем popularity с временным decay, где более старые статьи будут получать меньше веса, даже если они популярны. Функция Math.log помогает контролировать влияние времени на итоговый _score.

Еще один сценарий — нужно учесть, как долго товар или статья остаются актуальными, и со временем их значимость должна снижаться. Для этого есть готовые decay-функции, такие как exp, gauss и linear.

Пример с функцией exp:

GET /articles/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "AI"
}
},
"exp": {
"publish_date": {
"origin": "now",
"scale": "10d", // каждые 10 дней значимость уменьшается
"offset": "5d", // первые 5 дней документ не теряет значимость
"decay": 0.5 // каждое десятидневное окно снижает значимость на 50%
}
}
}
}
}
Функция exp работает как экспоненциальное уменьшение веса документа в зависимости от даты публикации. Таким образом, свежие статьи будут ранжироваться выше, а старые постепенно теряют свои позиции.

Комбинирование функций и скриптов​

Самая большая сила Function Score и Painless Script — это возможность комбинировать их для создания сложных моделей ранжирования.

Пример комбинирования веса по популярности и decay по времени:

GET /articles/_search
{
"query": {
"function_score": {
"query": {
"match": {
"content": "Elasticsearch"
}
},
"functions": [
{
"field_value_factor": {
"field": "popularity",
"factor": 1.5,
"modifier": "sqrt"
}
},
{
"exp": {
"publish_date": {
"origin": "now",
"scale": "30d",
"offset": "7d",
"decay": 0.6
}
}
}
],
"score_mode": "multiply", // комбинируем результаты функций через умножение
"boost_mode": "sum" // добавляем boost от основной query
}
}
}
Здесь мы комбинируем два фактора: популярность (через field_value_factor) и актуальность (через exp decay), и затем умножаем результаты обеих функций для расчета окончательного _score.

Пишите запросы, кастомизируйте ранжирование, и пусть ваш Elasticsearch найдет именно то, что нужно!

Теперь переходим к настройке через Field Boosting и мультиполя.

Настройка через Field Boosting и мультиполя​

Как мы знаем, Elasticsearch использует _score для определения релевантности документа запросу. Стандартная модель ранжирования (например, BM25) уже помогает учитывать частоту термина, длину документа и другие факторы. Но что, если нужно сказать поисковой системе: «Поле A более важно, чем поле B»? Вот тут помогает Field Boosting.

Boosting позволяет изменять вес полей или термов, управляя тем, как сильно то или иное поле влияет на общий _score. Это можно сделать во время индексации или во время запроса.

Boosting на уровне запроса​

Boosting во время запроса — это наиболее простой способ кастомизировать поиск под конкретные нужды. Например, если пользователь ищет продукт, можно сделать так, чтобы название продукта имело больший вес, чем его описание.

Пример запроса с boosting для поля title:

GET /products/_search
{
"query": {
"multi_match": {
"query": "laptop",
"fields": ["title^3", "description^1"]
}
}
}
В этом запросе поле title имеет boost 3, а поле description — 1 (дефолтное значение). Это значит, что документы, в которых термин «laptop» встречается в title, будут ранжироваться выше, чем те, в которых термин есть только в description.

Работа с мультиполями​

Elasticsearch позволяет выполнять поиск сразу по нескольким полям. Boosting можно настроить для каждого из этих полей отдельно.

Пример с несколькими полями:

GET /articles/_search
{
"query": {
"multi_match": {
"query": "Elasticsearch",
"fields": ["title^4", "summary^2", "content^1"]
}
}
}
Здесь термин ищется сразу в трех полях: title, summary и content. Но при этом заголовок имеет наибольший вес, краткое описание — средний, а основное содержание — наименьший. Это классическая схема для блогов и новостных сайтов, где заголовки и краткие описания часто более важны для поиска, чем сам текст статьи.

Как работает multi_match:

  1. «best_fields» — по умолчанию, если термин найден в одном поле, то только оно влияет на _score.
  2. «most_fields» — чем больше полей содержат термин, тем выше _score.
  3. «cross_fields» — поле обрабатывается как единый индекс.

Индексационный Boosting​

Хотя Elasticsearch рекомендует использовать query-time boosting, также существует возможность задать вес полей на этапе индексации. Это может быть полезно, если есть предопределенная структура, где значение какого-то поля должно всегда быть приоритетным.

Пример индексационного boosting (с версии 5.0 рекомендуется использовать query-time boosting):

PUT /my-index
{
"mappings": {
"properties": {
"title": {
"type": "text",
"boost": 2 // увеличиваем значение title на этапе индексации
},
"content": {
"type": "text"
}
}
}
}
Такой способ менее гибкий, поскольку требует реиндексации данных при изменении приоритетов.

Настройка через Function Score​

Для тех, кто хочет быть на глубине айсберга, существует Function Score Query. Это инструмент, позволяющий не только бустить поля, но и применять более сложные метрики — например, можно использовать значения полей для динамического изменения _score.

Пример Function Score для бустинга популярности продукта:

GET /products/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "laptop",
"fields": ["title^2", "description^1"]
}
},
"field_value_factor": {
"field": "popularity",
"factor": 1.5,
"modifier": "log1p"
}
}
}
}
В этом запросе мы учитываем не только title и description, но и популярность продукта, которая бустится по логарифмической шкале.

Не переборщите с boosting. Увеличение веса поля до абсурдных значений (например, title^1000) может привести к тому, что результаты поиска будут слишком узкими и неадекватными. Найдите баланс между релевантностью и широтой охвата.


Заключение​

Чтобы добиться максимального результата, важно не только правильно настроить алгоритмы, но и тестировать их на реальных данных, следя за тем, чтобы изменения действительно улучшали поисковую систему. Подходите к настройке Elasticsearch с умом, тестируйте на каждом шаге, и ваш поиск будет не только быстрым, но и максимально точным.

 
Сверху