Привет, Хабр!

Сегодня рассмотрим одну очень важную вещь в 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 с умом, тестируйте на каждом шаге, и ваш поиск будет не только быстрым, но и максимально точным.

Подробнее про инфраструктуру приложений вы можете узнать в рамках онлайн-курсов от практикующих экспертов отрасли. Подробности в каталоге.

Комментарии (0)