Оглавление цикла:

  1. Часть первая

  2. Часть вторая (вы тут)


Это вторая статья из цикла. В первой части я рассказывал про самые базовые понятия Elasticsearch. В этом же посте разберем устройство анализа текста и немного пощупаем полнотекстовый поиск.

Несколько слов про анализ текста

Анализ текста — процесс преобразования оригинального текста в структурированный формат, оптимизированный под эффективное хранение и быстрый поиск.

Мы уже познакомились с некоторыми типами Elasticsearch, но в этом разделе будем рассматривать только два — keyword и text. Тип text анализируется для полнотекстового поиска. Тип keyword преимущественно остается без изменений для точного поиска, сортировки и агрегации.

Соответствие типов запросов и типов данных
Соответствие типов запросов и типов данных

Анализ текста происходит:

  • при индексации;

  • при поиске (анализируется сам запрос).

На этот процесс анализа возложены две функции:

Функции анализа текста
Функции анализа текста

Токенизация (tokenization) — разбиение текста на более мелкие части, которые называются токенами или термами.

Нормализация (normalization) — процесс, в котором токены приводятся к некой единой форме. Например, приведение к нижнему регистру.

Анализатор

За анализ текста в Elasticsearch отвечают анализаторы (analyzer). Внутреннее устройство анализатора:

Состав анализатора
Состав анализатора

Анализатор состоит из трёх последовательных частей:

  1. Фильтр символов (character filter). Компонент является необязательным в составе анализатора.

  2. Токенайзер (tokenizer). Компонент является обязательным в составе анализатора.

  3. Фильтр токенов (token filter). Компонент является необязательным в составе анализатора.

В Elasticsearch много встроенных анализаторов. Какие-то из них можно использовать прямо из коробки без какой-либо настройки. Пример встроенных анализаторов:

  • Standard Analyzer

  • Simple Analyzer

  • Whitespace Analyzer

  • Stop Analyzer

  • Keyword Analyzer

  • Pattern Analyzer

  • Language Analyzers

  • Fingerprint Analyzer

Character filter

Этот компонент подготавливает оригинальный текст перед разбиением на токены. В Elasticsearch существует три типа фильтра:

Типы character filter
Типы character filter
  • HTML strip убирает html-теги перед разбиением на токены.

  • Mapping фильтр (не путать с маппингом из первого поста) позволяет создавать соответствие между символами, то есть маппировать одни символы на другие.

  • Pattern replace заменяет одни символы на другие с помощью заданного паттерна.

Рассмотрим пример настройки mapping character filter в Elasticsearch. Для этого примера воспользуемся Analyze API. На языке Elasticsearch это выглядит следующим образом:

GET /_analyze
{
    "char_filter": [
        {
            "type": "mapping",
            "mappings": [
                "Ё => Е",
                "ё => е"
            ]
        }
    ],
    "text": "Жёлудь даёт рост и становится ветвистым дубом"
}

И натравим character filter на текст:

Пример работы mapping character filter
Пример работы mapping character filter

Фильтр успешно заменил все «ё» на «е» без учета регистра.

Tokenizer

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

Свойства токена
Свойства токена

Примеры типов: <ALPHANUM> (alphanumeric), <HANGUL>, <NUM>, <SYNONYM> и т.д.

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

Знание информации о начальной и конечной позициях оригинального слова, которое этот токен представляет, может быть использовано для выделения фрагментов поиска. Например, оригинальные слова, которые соответствовали параметрам запроса, могут быть выделены HTML-тегами.

Предлагаю рассмотреть несколько примеров встроенных токенайзеров.

Standard tokenizer

Стандартный токенайзер использует алгоритм Unicode Text Segmentation для нахождения границ токенов. Этот токенайзер включен в состав стандартного анализатора, который Elasticsearch использует по умолчанию. Разберем работу такого токенайзера на примере:

Пример работы стандартного токенайзера
Пример работы стандартного токенайзера

Стандартный токенайзер из фразы "it's possible" вытащил два токена — it's и possible. Для разбиения токенайзер использует спецсимволы — знаки препинания, кавычки, проценты, пробелы и подобное. Стоит обратить внимание, что при разбиении апостроф сохранился.

N-gram tokenizer

N-gram tokenizer разбивает входящий поток символов «скользящим» окном, начало которого перемещается по мере прохождения символов. Рассмотрим принцип работы на примере:

Пример работы N-gram tokenizer
Пример работы N-gram tokenizer

Edge n-gram tokenizer

Edge n-gram tokenizer разбивает входящий поток символов «скользящим» окном, начало которого находится в начале слова. И снова пример:

Пример работы edge n-gram tokenizer
Пример работы edge n-gram tokenizer

Token filter

Token filter совершает преобразования над входящим потоком токенов следующими простыми операциями:

Операции token filter над токенами
Операции token filter над токенами

Познакомимся с некоторыми операциями поближе.

Stemming

Стемминг — это процесс нахождения стеммы (основы слова). Стемминг позволяет делать поисковый движок независимым от формы слова. Небольшой пример:

Пример стемминга
Пример стемминга

Все приведенные слова приводятся к единой основе.

Стемминг бывает двух видов:

Виды стемминга
Виды стемминга

Алгоритмический использует под капотом различные алгоритмы (porter, snowball и прочие) по поиску основы слова. Такой вид стемминга может неплохо работать из коробки, не требует много памяти и обычно быстрее, чем словарный. Из минусов нужно отметить, что бывают неожиданные результаты на особых словах или словах-исключениях.

Словарный стемминг использует справочник. Такой подход позволяет обрабатывать особые слова или слова-исключения и помогает различать слова, которые пишутся схоже, но отличаются по значению, например, организация и орган. Для правильной работы справочник должен включать огромное количество слов, а также своевременно обновляться в зависимости от языковых тенденций. Очевидно, что минус такого стеммера — ощутимое потребление оперативной памяти.

Предлагаю рассмотреть, как Elasticsearch справляется с русским языком. Пример:

Пример стемминга
Пример стемминга

По поводу корректности работы делайте выводы сами. Отмечу, что все слова сложные и имеют два корня — "фут" и "бол". Вероятно, что для некоторых кейсов правильно приводить все слова из примера к единой стемме.

Если вас не устраивает то, как Elasticsearch справляется с русским языком из коробки, то можно попробовать воспользоваться словарями для проверки орфографии — Hunspell. Просто скачиваете словарь для русского языка и указываете путь в конфигах Elasticsearch.

Если даже Hunspell-словарь не исправил ситуацию, то можно посмотреть в сторону морфологических плагинов для русского языка. Тут стоит предупредить, что многие из этих плагинов больше не поддерживаются и могут не подойти под вашу версию Elasticsearch.

Синонимы и графы токенов

При разбиении текста на токены собирается информация о позиции каждого токена, а также сколько позиций данный токен охватывает. Эти параметры называются position и positionLength (игнорируется при индексировании). Разберем их на примерах:

Пример графа токенов
Пример графа токенов

Скажем, у нас есть пример статьи из интернета — «Kubernetes — Service Types Overview». По дефолту Elasticsearch побьет этот документ на 4 токена и приведет их к нижнему регистру. Elasticsearch для каждого токена зафиксировал параметры position и positionLength. Все токены имеют значение positionLength равное 1, так как охватывают сами себя.

Предположим, что пользователь может искать название данной статьи с помощью сокращения для слова kubernetesk8s. Здесь нам на помощь приходят синонимы. У Elasticsearch есть synonym токен-фильтр, который может добавлять синонимы, охватывающие одну позицию.

Пример графа токенов c одним синонимом
Пример графа токенов c одним синонимом

Synonym токен-фильтр добавляет пятый токен k8s, который имеет position = 0 и positionLength = 0. Новый токен, который является синонимом, ставится в параллель оригинальному слову и имеет такие же параметры.

Допустим, что нам нужно настроить наш поисковый движок таким образом, чтобы он умел работать с синонимами, которые охватывают несколько позиций. У Elasticsearch есть токен-фильтр, который называется synonym graph. Он позволяет корректно работать с синонимами, которые охватывают несколько позиций — аббревиатуры, акронимы, различные термины и так далее. И снова пример статьи из интернета — «Java Garbage Collection Basics». При поиске словосочетание «Garbage Collection» можно сократить до «GC»:

Пример графа токенов с синонимом
Пример графа токенов с синонимом

Elasticsearch добавляет пятый токен gc c параметрами position = 1 и positionLength = 2, то есть он охватывает два токена — garbage и collection.

Мы с вами рассмотрели, как Elasticsearch добавляет новые токены в виде синонимов к уже существующему множеству. Но, к сожалению, не все так просто. Взглянем на нюансы.

Подводные камни синонимов

Итак, разберем тонкие моменты работы с синонимами в Elasticsearch. Для примера зададим следующие синонимы:

  • k8s, kubernetes

  • dns, domain name system

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

PUT /article
{
    "settings": {
        "index": {
            "analysis": {
                "analyzer": {
                    "custom_index_analyzer": {
                        "tokenizer": "standard",
                        "filter": [
                            "lowercase"
                        ]
                    },
                    "search_analyzer_w_synonym_graph": {
                        "tokenizer": "standard",
                        "filter": [
                            "lowercase",
                            "custom_synonym_graph_filter"
                        ]
                    }
                },
                "filter": {
                    "custom_synonym_graph_filter": {
                        "type": "synonym_graph",
                        "synonyms_path": "data/synonyms.csv"
                    }
                }
            }
        }
    },
    "mappings": {
        "properties": {
            "name": {
                "type": "text",
                "analyzer": "custom_index_analyzer",
                "search_analyzer": "search_analyzer_w_synonym_graph"
            }
        }
    }
}

Начнем разбор этого JSON с маппинга:

{
    "mappings": {
        "properties": {
            "name": {
                "type": "text",
                "analyzer": "custom_index_analyzer",
                "search_analyzer": "search_analyzer_w_synonym_graph"
            }
        }
    }
}

Документы имеют одно поле — name. Для данного поля устанавливаем тип text, чтобы содержимое анализировалось при поиске и индексации. Для этого поля задано два анализатора — custom_index_analyzer и search_analyzer_w_synonym_graph. Эти значения стоят в полях analyzer и search_analyzer. Первый анализатор будет использоваться при индексации документа, а второй — для полнотекстового поиска по этому полю.

Теперь перейдем к настройке самих анализаторов:

{
    "settings": {
        "index": {
            "analysis": {
                "analyzer": {
                    "custom_index_analyzer": {
                        "tokenizer": "standard",
                        "filter": [
                            "lowercase"
                        ]
                    },
                    "search_analyzer_w_synonym_graph": {
                        "tokenizer": "standard",
                        "filter": [
                            "lowercase",
                            "custom_synonym_graph_filter"
                        ]
                    }
                },
                "filter": {
                    "custom_synonym_graph_filter": {
                        "type": "synonym_graph",
                        "synonyms_path": "data/synonyms.csv"
                    }
                }
            }
        }
    }
}

В поле analyzer описаны два анализатора, которые мы прописали в маппинге.

Первый анализатор custom_index_analyzer состоит из стандартного токенайзера и токен-фильтра, который приводит токены к нижнему регистру.

Второй анализатор search_analyzer_w_synonym_graph состоит из стандартного токенайзера и двух токен-фильтров. Первый фильтр приводит токены к нижнему регистру, а второй фильтр custom_synonym_graph_filter нужен для добавления синонимов. Настройка последнего находится в поле filter, и там указаны тип фильтра и путь к файлу с нашими синонимами.

В индекс сохранены следующие документы:

Документы в индексе article
Документы в индексе article

При индексации Elasticsearch разобьет данные документы на токены:

Токены документов
Токены документов

Мы закончили настраивать наш индекс. Переходим к экспериментам.

У нас есть поисковый запрос — "k8s guide". Посмотрим, как Elasticsearch разобьет его на токены:

Пример разбиения поискового запроса на токены с одним синонимом
Пример разбиения поискового запроса на токены с одним синонимом

Как видите, Elasticsearch разбил поисковый запрос на токены и будет искать в индексе article все документы, которые содержат один из токенов — k8s, kubernetes и guide. Посмотрим, какие документы выдаст поисковый запрос:

Пример полнотекстового поиска c добавлением простого синонима
Пример полнотекстового поиска c добавлением простого синонима

Пример полнотекстового поиска c добавлением простого синонима Приведенный в примере запрос (match query) в виде JSON-документа интуитивно понятный и не должен вызвать каких-либо трудностей. Его упрощенная версия приведена на рисунке (см. Query view).


Результат, который выдал Elasticsearch, тоже приведен в виде JSON-документа. Из него нам нужно только поле hits, в котором перечислены подходящие документы. Запрос нам выдал ровно один документ, что и ожидалось.

Рассмотрим следующий запрос — "domain name system cache":

Пример разбиения поискового запроса на токены с одним синонимом (аббревиатурой)
Пример разбиения поискового запроса на токены с одним синонимом (аббревиатурой)

Elasticsearch добавляет пятый токен dns, который имеет параметры position = 0 и positionLength = 3. То есть новый токен охватывает три токена — domain, name и system. Посмотрим, какие документы выдаст поисковый запрос:

Пример полнотекстового поиска c добавлением синонима (аббревиатуры)
Пример полнотекстового поиска c добавлением синонима (аббревиатуры)

Пример полнотекстового поиска c добавлением синонима (аббревиатуры)

Единственное отличие от предыдущего match query запроса — наличие поле operator. По умолчанию Elasticsearch все токены связывает условием OR, но для разнообразия был использован AND. То есть изменился логический оператор для токена cache. Запрос нам выдал ровно один документ, что и ожидалось.

Для демонстрации последнего примера внесем правки в созданный индекс: изменим тип токен-фильтра с synonym graph на synonym. Все остальные настройки остаются такими же. В этом примере хочу продемонстрировать, что такой тип токен-фильтра не умеет работать с синонимами, которые охватывают несколько позиций.

Найдем все документы, в которых есть упоминание dns:

Пример неправильного разбиения поискового запроса на токены
Пример неправильного разбиения поискового запроса на токены

Elasticsearch добавляет три новых токена — domain, name и system. Стоит обратить внимание, что такой тип фильтра не умеет корректно работать с параметром positionLength. Для всех токенов он равен единице. То есть токену аббревиатуры dns ставится в параллель токен domain, что неправильно. Посмотрим, какие документы выдаст поисковый запрос:

Пример полнотекстового поиска c некорректным добавлением синонима (аббревиатуры)
Пример полнотекстового поиска c некорректным добавлением синонима (аббревиатуры)

Elasticsearch выдал три документа, причем один из них неожиданно попал в нашу выборку — «Domain-Driven Design in the era of Microservices». А все из-за неправильного значения параметра positionLength. Это в свою очередь повлияло на неправильную расстановку логических операторов между токенами в запросе.

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

Стандартный анализатор

Стандартный анализатор состоит из одного токенайзера и одного токен-фильтра:

Состав стандартного анализатора
Состав стандартного анализатора

Стандартный анализатор разбивает содержимое на токены согласно алгоритму Unicode Text Segmentation и приводит полученные токены к нижнему регистру. В качестве примера возьмем цитату А.П. Чехова про карасей в сметане:

Пример работы стандартного анализатора
Пример работы стандартного анализатора

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

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