Эта статья — первая в серии, в которой мы рассказываем о том, как работает оценка релевантности в Manticore Search. Мы начинаем с основы релевантности: частоты термина и специфичности.
Простое введение в оценивание
Поисковые движки, такие как Sphinx/Manticore Search, берут тексты из документов, разбивают их на токены и позволяют выполнять поиск по набору токенов/слов в этой коллекции.
В самом простом сценарии эта коллекция содержит токены/слова (в том, что мы называем словарём индекса) и список документов, где они были найдены, а также поля документов, в которых они находятся.
Поиск сначала определяет, существует ли одно из входных слов в коллекции, и извлекает список документов, который затем объединяется со списками других слов, формируя список документов, содержащих все слова из запроса.
Если по этому критерию найдено более одного документа, следующий шаг — различить их. Какие из этих документов более релевантны заданному запросу?
Первый простой способ различения — подсчитать количество вхождений каждого входного слова в документ. Больше вхождений значит более релевантное слово? Это зависит от контекста.
Некоторые слова могут встречаться в документе много раз — это может указывать на их важность для данного документа, но то же слово может встречаться во многих других документах коллекции, что ставит вопрос, является ли оно просто распространённым словом и следует ли уменьшить его значимость.
Если мы считаем вхождения (frequency) слова в документ, мы захотим подавить возможный эффект этих распространённых слов. Для этого мы добавляем счётчик документов, содержащих слово — это "term frequency", и ещё один счётчик, показывающий, в скольких документах (по сравнению с общим числом) слово найдено — обратное значение называется обратной частотой документа (IDF) и измеряет специфичность терма.
Вместе эти два показателя образуют TF-IDF, который показывает, насколько слово важно относительно документа в коллекции. На основе IDF мы также можем вычислить другую широко используемую функцию ранжирования — BM25.
BM25 и TF-IDF
Как эти факторы реализованы и могут использоваться в Manticore Search?
Для начала существует несколько встроенных ранжировщиков (формул оценки), которые можно использовать сразу. Но самым важным является ранжировщик expression, который позволяет получать доступ к этим факторам и создавать пользовательские формулы.
Мы упоминали ранее функцию ранжирования BM25, которая в Manticore включена в ранжировщик по умолчанию (proximity_bm25) и также может использоваться как фактор уровня документа в выражениях ранжирования.
Мы не будем вдаваться в детали BM25, так как существует множество научных статей по этой теме, а сосредоточимся на реализации в Manticore.
Важно отметить, что фактор bm25 назван несколько некорректно, так как это не традиционная реализация BM25, а скорее оценка, эквивалентная BM25(K1=1.2,B=0) — в некоторых работах её называют BM15.
Вы всё ещё можете использовать классический BM25 и даже настраивать параметры k1 и b с помощью ранжировочной функции bm25a(k1,b).
Более продвинутая версия — bm25f(k1, b, {field=weight, …}), позволяющая задавать веса для отдельных полей. Обратите внимание, что это отличается от обычных field_weights (об этом позже) и требует опцию index_field_lenghts.
На уровне полей TF-IDF можно получить несколькими способами: **tf\_idf** — это сумма значений tf_idf для каждого вхождения найденного ключевого слова в поле. Кроме того, **sum\_idf** — это сумма каждого значения tf_idf для каждого найденного ключевого слова (то есть без умножения на количество вхождений), а **max\_idf** и **min\_idf** предоставляют соответственно наибольшее и наименьшее найденное значение tf_idf.
Вычисление IDF можно контролировать с помощью **idf** в клаузе
OPTION
. В первоначальной реализации (до Sphinx 2.1) значения IDF были нормализованы — это означало, что распространённые слова (найденные более чем в 50 % документов) получали штраф (idf='normalized'). Значения IDF находятся в диапазоне [-log(N),log(N)]. В этом режиме совпадение «распространённое слово + менее распространённое слово» давало сумму IDF ниже, чем совпадение, содержащее только менее распространённое слово. Чтобы исправить это (если требуется), был добавлен новый режим (idf='plain'), где штрафов нет, а значение IDF находится в диапазоне [0,log(N)]. Кроме того, значение IDF делится на количество ключевых слов запроса, так что сумма tf_idf попадает в диапазон [0,1].
Это создало проблему в булевых запросах, когда поиск «found-word» против «found-word OR not-found-word» давал разные веса для одинакового набора результатов из‑за деления.
Чтобы исправить это, idf получил ещё одну пару настроек, позволяющих контролировать, применяется ли деление. Настройки idf по умолчанию остаются normalized,tfidf_normalized по соображениям совместимости, но idf='plain,tfidf_unnormalized' должны давать лучшие результаты в общих случаях.
Что дальше?
На данном этапе мы можем создать оценку на основе frequency и specificity слов, но это лишь сообщает, что слова находятся в документе, а не то, сгруппированы ли они рядом друг с другом или просто разбросаны по тексту. Это приводит к необходимости добавить в коллекцию ещё один параметр: positions слов в тексте.
Наличие позиций позволяет определить, совпадают ли входные слова с точным полем документа, найдены ли они дословно, насколько близко/далеко они находятся друг от друга или насколько близко они к началу текста (поскольку можно считать, что более близкое расположение или даже начало текста более релевантно, чем конец документа).
Поскольку документ может содержать более одного поля, важность полей также может различаться. Например, если у нас есть колонки 'title' и 'body', совпадения в 'title' считаются более важными и должны получать более высокий рейтинг. Подробнее об этом в будущей статье.