4. Поиск и ранжирование
Цель
Реализовать алгоритм поиска документов по обратному индексу и простейшую функцию оценки релевантности документа.
Руководство
На входе у поисковика:
Текст запроса.
Конфиг: параметры парсера и прочие настройки, если потребуется.
Объект для доступа к индексу (IndexAccessor).
IndexAccessor предоставляет следующие возможности:
Получение содержимого документа по его идентификатору.
Получение списка документов по терму.
Алгоритм:
Распарсить текст запроса.
Для каждого терма запроса извлечь список документов, в которые он входит. На этом этапе уместно вычислить следующие величины:
\(\mathit{tf}(t, d)\) — term frequency, количество вхождений терма \(t\) в документ \(d\).
\(\mathit{df}(t)\) — document frequency, количество документов, в которых встречается терм \(t\).
Для каждого документа вычислить оценку по формуле:
\[\mathit{score}(q, d) = \sum_{t \in q}{\mathit{tf}\text{-}\mathit{idf}(t, d)}\]Где:
\[\mathit{tf}\text{-}\mathit{idf}(t, d) = \mathit{tf}(t, d) \cdot \mathit{idf}(t)\]\[\mathit{idf}(t) = ln \left( \frac{N}{\mathit{df}(t)} \right)\]\(N\) — общее количество документов в индексе.
Отсортировать результаты по убыванию \(\mathit{score}\). В качестве стабилизирующего фактора сортировки можно использовать id документа.
Пример
Пусть в индексе хранятся следующие документы:
id text
100 Hello World
101 Bye World
102 Hello Earth
Для компактности примера пропустим этап выделения n-грам и будем считать, что каждое слово является термом. Тогда обратный индекс выглядит так:
bye: {101: [0]}
earth: {102: [1]}
hello: {100: [0], 102: [0]}
world: {100: [1], 101: [1]}
Поиск:
query: bye earth
parsed_query: ["bye", "earth"]
# Для каждого терма получаем список документов и считаем tf и df.
bye:
doc_ids: [101]
tf:
101: 1
df: 1
earth:
doc_ids: [102]
tf:
102: 1
df: 1
score("bye earth", 101)
= tf-idf("bye", 101) + tf-idf("earth", 101)
= 1 * ln(3 / 1) + 0
≈ 1.098612
Нетрудно заметить, что для документа 102 оценка будет такой же. Тогда результирующая выдача примет вид:
id score text
101 1.098612 Bye World
102 1.098612 Hello Earth
Рассмотрим другой запрос:
query: Hello World
parsed_query: ["hello", "world"]
hello:
doc_ids: [100, 102]
tf:
100: 1
102: 1
df: 2
world:
doc_ids: [100, 101]
tf:
100: 1
101: 1
df: 2
score("hello world", 100)
= tf-idf("hello", 100) + tf-idf("world", 100)
= 1 * ln(3 / 2) + 1 * ln(3 / 2)
≈ 0.810930
score("hello world", 101)
= tf-idf("hello", 101) + tf-idf("world", 101)
= 0 + 1 * ln(3 / 2)
≈ 0.405465
score("hello world", 102)
= tf-idf("hello", 102) + tf-idf("world", 102)
= 1 * ln(3 / 2) + 0
≈ 0.405465
Выдача примет вид:
id score text
100 0.810930 Hello World
101 0.405465 Bye World
102 0.405465 Hello Earth
Структура проекта
Индексатор остается без изменений.
TextIndexAccsessor — класс, предоставляющий доступ на чтение к данным
построенного индекса.
Дополнительно
Что произойдет, если индексация и поиск будут выполнены с разными настройками парсера? Предложите и реализуйте решение этой проблемы.