Основы полнотекстового поиска в ElasticSearch. Часть первая

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет! Меня зовут Глеб, я разработчик команды продукта «Сервис персонализации» в SM Lab. В цикле из трех постов я расскажу про основы полнотекстового поиска в Elasticsearch.

Данный цикл статей предназначен для всех, но будет особенно актуальным для тех читателей, кто только начинает свое знакомство с Elasticsearch. Я надеюсь, каждый из вас найдет что-то полезное для себя.

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

Итак, начнём с самых базовых понятий.

В двух словах про Elasticsearch

Сперва разберемся — что такое Elasticsearch. Одно из определений, которое можно дать — это распределенная документно-ориентированная БД с мощными возможностями поиска. Для быстрого поиска Elasticsearch использует под капотом специальные структуры данных. Вы наверняка слышали такое понятие как обратный индекс (inverted index). Ниже приведен простой пример, который освежит в памяти суть этого понятия:

Обратный индекс
Обратный индекс

У нас есть два документа. Их содержимое анализируется и строится обратный индекс. Например, документ с id = 1 разбивается на токены (термы) — pear и tart. Полученным токенам ставится в соответствии id-шник документа. Вы, скорее всего, заметили, что содержимое документа разбилось на самые обычные слова. Достаточно часто обратный индекс строится именно таким образом. Благодаря такому виду Elasticsearch сможет быстро найти документы со словами pear или tart.

Помимо обратного индекса Elasticsearch может использовать другие структуры данных. Например, BKD (Block K-Dimensional)-деревья. Такая экзотическая структура данных применяется для хранения числовых и геополей.

Индекс

В Elasticsearch индекс (index) — это логическое хранилище документов, которые объединены одним смыслом. Такое хранилище является по сути коллекцией, которая оптимизирована под поисковые запросы. Содержимое полей документов сканируется и сохраняется в соответствующие структуры данных.

Индекс в Elasticsearch
Индекс в Elasticsearch

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

Маппинг

Маппинг (mapping) — это процесс, который определяет, как именно будут храниться и проиндексированы документ и его поля.

JSON-документ можно рассматривать как коллекцию пар «поле-значение». Каждое поле имеет какой-то тип данных. В зависимости от этого Elasticsearch будет использовать различные структуры данных для индексации содержимого документа.

Рассмотрим пример:

В столбце Type приведены типы данных, которые Elasticsearch по умолчанию проставит для каждого поля документа. Например, поле name имеет тип Text. Elasticsearch для такого типа поля будет производить анализ текста при поиске и индексации.

Поле age имеет тип Long, и Elasticsearch не будет применять анализ текста при индексации и поиске.

В Elasticsearch существует два вида маппинга:

  • dynamic — динамический маппинг;

  • explicit — явный маппинг.

Виды маппинга
Виды маппинга

Рассмотрим каждый из них поподробнее.

Динамический маппинг

В процессе сохранения документов в индекс Elasticsearch сканирует содержимое документа и каждому полю ставит в соответствии свой тип данных. Стандартные правила определения типов данных приведены на следующей картинке:

Правила маппинга полей
Правила маппинга полей

Правила тривиальные, но стоит обратить внимание на последний три JSON-типа.
Когда Elasticsearch парсит поле и определяет, что это дата, то ставит тип date. Если это число, то ставит либо float, либо long.

Самое интересное происходит, когда значение является самой тривиальной строкой, т.е. не является ни датой, ни числом. В этом случае Elasticsearch создает два поля — с типами text и keyword.

Поле text предназначено для полнотекстового поиска, а поле c типом keyword подходит для точного поиска. Значения с типом keyword сохраняются преимущественно без каких-либо изменений. Разберем на примере:

{
    "name": "Ivan",
    "age": 28,
    "joiningDate": "2023/01/01",
    "active": true
}

Cохраняем данный документ в индекс под названием custom_index . Описание маппинга на языке Elasticsearch:

{
    "custom_index": {
        "mappings": {
            "properties": {
                "active": {
                    "type": "boolean"
                },
                "age": {
                    "type": "long"
                },
                "joiningDate": {
                    "type": "date",
                    "format": "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis"
                },
                "name": {
                    "type": "text",
                    "fields": {
                        "keyword": {
                            "type": "keyword",
                            "ignore_above": 256
                        }
                    }
                }
            }
        }
    }
}
  • custom_index — название индекса в Elasticsearch;

  • mappings — описание маппинга;

  • properties — описание полей и их свойств;

  • type — тип поля в Elasticsearch.

Практически все интуитивно понятно, но стоит обратить внимание на поле name. Как уже говорилось, для полей со строковым значением, которое не является ни датой, ни числом, Elasticsearch создает два поля для хранения — с типом text и keyword. Другими словами, Elasticsearch под капотом создал два поля для оригинального поля name:

  • name с типом text;

  • name.keyword с типом keyword.

Реляционный вид полученного маппинга:

Реляционный вид маппинга
Реляционный вид маппинга

Elasticsearch проанализировал содержимое поля name и привел его к нижнему регистру — ivan. Данное значение будет сохранено в одноименном поле маппинга.
Также стоит обратить внимание на поле маппинга с названием name.keyword. Здесь Elasticsearch сохранил значение без каких-либо изменений — Ivan.

Стандартные правила маппинга можно переопределять с помощью динамических темплейтов (dynamic template). Благодаря этому возможно создавать свои хитрые правила маппинга.

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

Плюсы:

  • Динамическое определение типа данных.

Минусы:

  • Потребление дополнительной памяти для индексации text-полей.

  • Перечень типов данных ограничен.

  • Стандартные правила определения типов могут привести к неожиданным результатам поиска.

Явный маппинг

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

  • Геополя (geo_points);

  • Поля диапазоны (date_range);

  • Поля автокомплита (search_as_you_type).

В минусах динамического маппинга был отмечен пункт про неожиданные результаты. Так давайте разберем это на примере. У нас есть тривиальный JSON-документ:

{
    "id": 1,
    "values": [
        {
            "id": 1,
            "value": 10
        },
        {
            "id": 2,
            "value": 20
        }
    ]
}

У этого документа есть поле values, которое представляет собой массив вложенных объектов. Elasticsearch проиндексирует этот документ особым образом:

{
	"id": 1,
	"values.id": [1, 2],
	"values.value": [10, 20]
}

Представлен упрощенный вид документа при индексации. Стоит обратить внимание на поля values.id и values.value. Это были поля вложенных документов в массиве. Связь значений к конкретному объекту теряется и внутреннее «размазываются» по массивам values.id и values.value. Если искать документы в индексе, которые содержат вложенный объект с полями values.id = 1 и values.value = 20, то Elasticsearch выдаст рассматриваемый документ, что неправильно.

Такое поведение объясняется стандартным типом поля values object. Иерархия вложенных объектов «расплющилась» до простого списка.

Чтобы исправить ситуацию, необходимо явно задать особый тип nested для этого поля. Тут нам помощь и приходит явный маппинг.

Обозначим плюсы и минусы.

Плюсы:

  • Поиск без сюрпризов.

  • Комбинированное использование с динамическим маппингом. Поля, которые не были явно заданы при создании индекса, могут быть без особых проблем проиндексированы.

Минусы:

  • Необходимость первоначальной настройки.

На этом первая часть закончена. В следующей части будут рассмотрены механизмы анализа текста и полнотекстового поиска.

Источник: https://habr.com/ru/companies/sportmaster_lab/articles/756004/


Интересные статьи

Интересные статьи

Всем привет!  Мы уже рассказывали о внутренней системе подачи, сбора и обработки инициатив сотрудников «Банк идей», реализованной в логистической компании ПГК, в одной из прошлых статей. В этот р...
Всем привет! Меня зовут Дима, и я не люблю Redux. Я люблю MobX. И в своем сборнике статей я показываю, как можно использовать MobX так, чтобы он стал ещё удобнее.В своей прошлой статье я описал структ...
Мы продолжаем разрабатывать систему заметок с нуля. В третьей части серии материалов мы познакомимся с графовой базой Neo4j, напишем CategoryService и реализуем клиента к новому сервису в APIService.В...
Привет хабр! Продолжаю изучать Scala. Большинство бекендов так или иначе интегрированы с другими и делают HTTP запросы. Так как я на стек Cats и http4s ориентирован то буду рассматривать ...
В первой публикации рассказывалось о том, что есть подзабытая теорема Эрдёша-Реньи, из которой следует, что в случайном ряде, длины N, с вероятностью близкой к 1 существует подряд из одинаковых з...