Эффективная настройка Elasticsearch

Самое сложное, в этом движке — это его настройка на получение релевантных результатов. Так же хотелось бы отметить, что документация написана довольно плохо. Конечно, это сугубо моё мнение и я не могу сказать, что мы ничего полезного в ней не нашли, но поиски нам давались с большим трудом. Мы провели довольно глубокое исследование, чтобы понять как работает ElasticSearch. И, наконец, мы поняли его основы и готовы сами задокументировать его возможности.

Но давайте вернемся немного назад. Как я уже говорил, основная проблема — получение релевантных результатов (релевантных не для поискового движка, а для пользователя). Иногда мы получали довольно странные результаты, например, поиск по запросу “funny pony” выдавал следующее:

Title : This pony is cool
Description: Who wants a bun with lot of jam?
Title: Everybody loves horses
Description: Horses are sort of big ponies with more furry.
Title: Funny ponies are the best ones
Description: Lorem ipsum dolor sit amet consectetur adipiscing elit

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

Базовые фильтры

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

Например, если вы не применяете фильтры для запроса на французском языке “Mon prénom est Grégory” (меня зовут Грегори), то он будет индексирован как есть. При поисковом запросе “gregory” вы врят ли получите нужный результат, так как эта строка регистрозависимая. Подходящий фильтр для этой цели — lowercase, он преобразует все символы верхнего регистра в нижний. И фильтр asciifolding, который заменяет все Unicode символы, не входящие в латинский алфавит на их ascii эквивалент. Проще говоря, он преобразует все страно выглядящие символы, такие как é, à, ç, ð, æ, å и т.д.

Фильтр worddelimiter

Фильтр worddelimiter разбивает слова на несколько частей. Небольшой пример: представьте, что вы допустили опечатку в предложении “To be or not to be.That is the question”. Если вы обратите внимание, то заметите отсутствие пробела после точки. Без этого фильтра Elasticsearch проиндексирует “be.That” как одно слово — “bethat”. При помощи этого фильтра мы указываем ему индексировать эти слова по отдельности — “be” и “that”.

Фильтр stopwords

Фильтр stopwords состоит из списка слов запрещенных к индексированию. Например он исключает такие слова, как “and”, “a”, “the”, “to” и т.д. Конечно этот список уникален для каждого языка, но существует довольно много заготовок, которые вы можете использовать.

Фильтр snowball

Фильтр snowball используется для группировки слов по их основе. Фильтр применяет набор правил для правильного определения основы слова. Это означает, что разные настройки могут выдавать разные результаты. Например слова “indexing”, “indexable”, “indexes”, “indexation” и т.д. получать основу “index”. Хотелось бы отметить, что вы получите результат “Make my string indexable”, запросив “Indexing string”.

Фильтр elison

Фильтр elison имеет большее значения для некоторых языков (например французский) и не так важен для других (например английский). Он исключает маловажные слова перед индексированием, например “j’attends que tu m’appelles” (Я жду вашего звонка) проиндексируется как “attends que tu appelles” (наконце слова “que” и “tu” будут исключены фильтром stopwords). Как вы видите слова “j’” и “m’” (Я) были удалены из-за настройки фильтра elison.

Собственные фильтры

Вы можете создавать собственные фильтры. В конце статьи вы найдете пример настройки такого фильтра. Также фильтр “stopwords” — наш собственный фильтр построенный на базе стоп слов для французского языка. Если вам нужен фильтр для английского языка, вы можете добавить другой фильтр с название, например, “stopwords_en”.

Лексер nGram

Сначала мы нашли пару примеров такого лексера в интернете и просто использовали их в наших настройках, в чем и заключалась наша ошибка — мы не пытались их понять. Роль лексера nGram очень высока. Например, наш поиск “funny pony” был разбит на несколько частей. Вот два примера:

1-ый пример с настройкой:

"min_gram" : "2",
"max_gram" : "3"

Результат:

fu, fun, un, unn, nn, nny, po, pon, on, ony, ny

2-ой пример с лучшей настройкой:

"min_gram" : "3",
"max_gram" : "20"

Результат:

fun, funn, funny, unn, unny, nny, pon, pony, ony

То есть лексер разбивает каждое слово на комбинации между min_gram и max_gram символов. С настройками 3 и 20 слово “elasticsearch” будет преобразовано в “ela, elas, elast, …, elasticsearc, elasticsearch”. Такой же процесс повторяется со второй буквы — “las, last, lasti, lastic, …, lasticsearch”, далее с третьей и т.д. В нашем случае max_gram не доходит до конца, так как в запросе только 13 символов, так что, задав значение больше 13, не изменит результата.

Основной плюс этого лексера в том, что из запроса “funny” мы можем получить результаты, содержащие “fun”. Но в первом примере мы получаем слишком много коротких слов, которые совпадают с большим количеством значений. Например “on” очевидно совпадет также с “of”, “ony” совпадет с “any”, “pon” — “non” и т.д.

Теперь мы понимаем почему первый результат был наиболее релевантен. Наш поиск нашел несколько слов похожих на “pony” в заголовке (как и ожидалось), но также он встретил “bun”(fun) и “of”(on) в описании (не ожидалось). Параметры min_gram и max_gram решают эту проблему. Теперь рассмотрим анализаторы.

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

Очень приветствуется создавать и использовать собственные анализаторы. Анализатор используется для очистки документа и поискового запроса перед индексацией и поиском соответственно. В целом, фильтры никогда не применяются напрямую, их используют анализаторы. В следующем примере мы создали 2 анализатора: один для запроса текста (custom_search_analyzer), а второй для индексации документа лексером ngram (custom_analyzer).

Лексер ngram не используется в поисковом запросе (используется только для индексирования). Если пользователь ищет слово “keyboard”, мы должны найти слова “key”, “board”, “oar” и т.д. Такое поведение может привести к неожиданным результатам — “boar, “boardwalk”, “keynote” и т.д. А мы считали, что наша настройка отлично работает. Всегда надо дорабатывать свои настройки и подвергать их тестированию.

Пример

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

Тем временем, протестируйте следующую настройку (FosElasticaBundle):

# ...
            settings:
                index:
                    analysis:
                        analyzer:
                            app_analyzer:
                                type: custom
                                tokenizer: nGram
                                filter   : [stopwords, app_ngram, asciifolding, lowercase, snowball, worddelimiter]
                            app_search_analyzer:
                                type: custom
                                tokenizer: standard
                                filter   : [stopwords, app_ngram, asciifolding, lowercase, snowball, worddelimiter]
                        tokenizer:
                            nGram:
                                type:     "nGram"
                                min_gram: 2
                                max_gram: 20
                        filter:
                            snowball:
                                type:     snowball
                                language: English
                            app_ngram:
                                type: "nGram"
                                min_gram: 2
                                max_gram: 20
                            worddelimiter :
                                 type: word_delimiter
                            stopwords:
                                 type:      stop
                                 stopwords: [_french_]
                                 ignore_case : true