Статьи

Анатомия настройки анализатора слов Elasticsearch N-Gram

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

Хорошо, я шучу по поводу этого последнего. Но если вы являетесь разработчиком, настраивающим использование Elasticsearch для поиска в вашем приложении, есть очень хороший шанс, что вам потребуется практическая работа с анализаторами n-граммы для некоторых ваших поисков, и вам может потребоваться некоторая целевая информация для получения искать, чтобы вести себя так, как вы ожидаете. Существует множество возможностей для поиска n-граммы в Elasticsearch . Этот блог даст вам представление о том, как использовать их в своих поисках.

Пример

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

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

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

Анализатор

Теперь давайте подумаем о том, что мы хотим с точки зрения анализатора. Во-первых, мы уже знаем, что нам нужен n-грамм. Мы хотим частичное соответствие. Во-вторых, мы уже решили выше, что мы хотим искать частичное совпадение в слове. В этом случае это будет только в некоторой степени, как мы увидим позже, но теперь мы можем определить, что нам нужен NGram Tokenizer, а не Edge NGram Tokenizer, который хранит только n граммов, которые начинаются в начале токена.

Неграммы ElasticSearch учитывают минимальные и максимальные граммы. Начиная с минимума, какую часть имени мы хотим сопоставить? Ну, по умолчанию это единица, но поскольку мы уже имеем дело с данными, состоящими в основном из одного слова, если мы будем использовать одну букву (униграмму), мы, безусловно, получим слишком много результатов. Реально, то же самое относится и к биграмме. Тем не менее, у достаточного количества людей есть домашние животные с трехбуквенными именами, которые нам лучше не продолжать, иначе мы можем никогда не вернуть щенков с именами «туз» и «рекс» в результаты поиска. Теперь мы знаем, что наш минимальный грамм будет три. Как насчет максимального грамма? По умолчанию два, и мы уже превысили это с нашим минимумом. Наша цель — включить как можно больше потенциальных точных соответствий, но все же не сойти с ума с точки зрения хранения размера индекса.

Подумайте о том, чтобы выбрать слишком большое число, например, 52, и разбить имена для всех потенциальных возможностей от 3 до 52 символов, и вы увидите, как это быстро складывается по мере роста ваших данных. Здесь есть немного компромисса, поскольку в некоторых случаях вы можете исключить данные, которые превышают максимальный грамм.

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

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

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

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

Вот наш первый анализатор, создающий собственный анализатор и использующий ngram_tokenizer с нашими настройками. Если вы здесь, вы, вероятно, знаете это, но токенизатор используется для разбиения строки на поток терминов или токенов. Вы можете добавить пробел и много других вариантов здесь в зависимости от ваших потребностей:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
curl -XPUT 'localhost:9200/searchpets' -d '
    {
        "settings" : {
            "analysis" : {
                "analyzer" : {
                    "ngram_analyzer" : {
                        "tokenizer" : "ngram_tokenizer"
                    }
                },
                "tokenizer" : {
                    "ngram_tokenizer" : {
                        "type" : "nGram",
                        "min_gram" : "3",
                        "max_gram" : "8"
                    }
                }
            }
        }
    }'

И наш ответ на создание этого индекса {«подтвержден»: правда}. Превосходно.

Хорошо, теперь, когда у нас есть индекс, как будут выглядеть данные при использовании нашего нового анализатора?

1
curl -XGET'localhost:9200/searchpets/_analyze?analyzer=ngram_analyzer' -d 'Raven'

И ответ:

1
{"tokens":[{"token":"Rav","start_offset":0,"end_offset":3,"type":"word","position":1},{"token":"Rave","start_offset":0,"end_offset":4,"type":"word","position":2},{"token":"Raven","start_offset":0,"end_offset":5,"type":"word","position":3},{"token":"ave","start_offset":1,"end_offset":4,"type":"word","position":4},{"token":"aven","start_offset":1,"end_offset":5,"type":"word","position":5},{"token":"ven","start_offset":2,"end_offset":5,"type":"word","position":6}]}

Это разумно. Все токены генерируются от 3 до 5 символов (поскольку слово меньше 8, очевидно).

Хорошо, теперь давайте применим это к полю. И, да, вы можете сделать все это за один шаг, я просто ломаю это.

01
02
03
04
05
06
07
08
09
10
11
12
{
    "pet": {
        "properties": {
            "name": {
                "type": "string",
                "analyzer": "ngram_analyzer"
            }
        }
    }
}
'

Тестируем анализ на поле:

И снова мы получаем ожидаемые результаты:

1
{"tokens":[{"token":"Rav","start_offset":0,"end_offset":3,"type":"word","position":1},{"token":"Rave","start_offset":0,"end_offset":4,"type":"word","position":2},{"token":"Raven","start_offset":0,"end_offset":5,"type":"word","position":3},{"token":"ave","start_offset":1,"end_offset":4,"type":"word","position":4},{"token":"aven","start_offset":1,"end_offset":5,"type":"word","position":5},{"token":"ven","start_offset":2,"end_offset":5,"type":"word","position":6}]}

Теперь давайте предположим, что я пошел дальше и добавил сюда несколько записей и выполнил простой запрос на совпадение для: {«query»: {«match»: {«name»: «Pegasus»}}}.

С моими данными мы получаем следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"hits": {
    "total": 2,
    "max_score": 0.29710895,
    "hits": [
        {
            "_index": "searchpets",
            "_type": "pet",
            "_id": "3",
            "_score": 0.29710895,
            "_source": {
                "name": "Pegasus"
            }
        }
        ,{
            "_index": "searchpets",
            "_type": "pet",
            "_id": "2",
            "_score": 0.0060450486,
            "_source": {
                "name": "Degas"
            }
        }
    ]
}
}

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

Пользовательский анализатор

Хорошо, но сейчас мы используем довольно простой случай анализатора. Что если нам понадобится специальный анализатор, чтобы мы могли справиться с ситуацией, когда нам нужен другой токенизатор при поиске, а не при индексации? Что если мы хотим ограничить поиск с помощью ключевого слова tokenizer?

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ curl -XPUT 'localhost:9200/searchpets' -d '
 {
    "settings": {
        "analysis": {
            "analyzer": {
                "namegrams": {
                    "type": "custom",
                    "tokenizer": "keyword",
                    "filter": [
                        "ngrams_filter"
                    ]
                }
            },
            "filter": {
                "ngrams_filter": {
                    "type": "ngram",
                    "min_gram": 3,
                    "max_gram": 8
                }
            }
        }
    }
}'

Теперь мы добавляем отображение и данные, как и раньше:

01
02
03
04
05
06
07
08
09
10
11
12
{
    "pet": {
        "properties": {
            "name": {
                "type": "string",
                "analyzer": "namegrams"
            }
        }
    }
}
'

Я запускаю другой запрос на совпадение: {«query»: {«match»: {«name»: «Pegasus»}}} и получаю ответ:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
hits": {
"total": 2,
"max_score": 1.1884358,
"hits": [
    {
        "_index": "searchpets",
        "_type": "pet",
        "_id": "2",
        "_score": 1.1884358,
        "_source": {
            "name": "Pegasus"
        }
    }
    ,{
        "_index": "searchpets",
        "_type": "pet",
        "_id": "3",
        "_score": 0.08060065,
        "_source": {
            "name": "Degas"
        }
    }
]
}

Итак, мы настроили это, и мы получаем ожидаемые результаты и оценки, основываясь на ключевом слове tokenizer и фильтре n-грамм. Допустим, мы делаем несколько более сложных запросов. Возможно, мы также добавили некоторые другие фильтры или токенизаторы. Вещи выглядят великолепно, верно? Ну, почти.

Один маленький фактор, который нужно иметь в виду со всем этим, что я упоминал ранее. У нас максимум 8 грамм. Итак, что происходит, когда у нас есть имя, которое превышает этот размер в качестве критерия поиска? Ну, в зависимости от вашего поиска вы можете не получить никаких данных обратно.

Вероятно, не то, что вы ожидали, чтобы произошло здесь! Как избежать этой ситуации? Один из способов — использовать разные index_analyzer и search_analyzer. Разделив их, вы получаете гораздо больший контроль над поиском.

Итак, вот как может выглядеть ваша окончательная настройка, если предположить, что все, что мы сказали об этом первоначальном поиске, верно. Я не буду вдаваться в детали самого запроса, но мы предполагаем, что он будет использовать указанный search_analyzer (я рекомендую прочитать иерархию выбора анализаторов для поиска в документации ES).

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ curl -XPUT 'localhost/searchpets' -d '
 {
    "settings": {
        "analysis": {
            "analyzer": {
                "namegrams": {
                    "type": "custom",
                    "tokenizer": "keyword",
                    "filter": [
                        "ngrams_filter"
                    ]
                },
                "search_ngram": {
                    "type": "custom",
                    "tokenizer": "lowercase",
                    "filter": [
                        "truncate_filter"
                    ]
                }
            },
            "filter": {
                "ngrams_filter": {
                    "type": "ngram",
                    "min_gram": 3,
                    "max_gram": 8
                },
                "truncate_filter": {
                    "type": "truncate",
                    "length": 8
                }
            }
        }
    }
}

И, наконец, мы снова настроили наше отображение:

01
02
03
04
05
06
07
08
09
10
11
12
{
    "pet": {
        "properties": {
            "name": {
                "type": "string",
                "index_analyzer": "namegrams",
                "search_analyzer": "search_trigram"
            }
        }
    }
}'

Последние мысли

И там у вас есть это. Это делает предположение, однако, что данные, которые превышают 8 символов, менее важны. Если бы у вас было много данных, которые были бы больше, чем максимальный грамм и тому подобное, вы могли бы нуждаться в дальнейшей настройке.

Существует множество возможностей для поиска n-граммы в Elastisearch. Я надеюсь, что это даст вам представление о том, как использовать их в своих поисках.

Ссылка: Анатомия настройки анализатора слов Elasticsearch N-Gram от нашего партнера JCG Эдриенн Гесслер в блоге