Порса с помощью комтей парноеопытных жиаотных

При работе с полнотекстовым поиском в Elasticsearch разработчики часто сталкиваются с неочевидным поведением wildcard-запросов (запросов с масками, например, Авто*) при использовании стеммеров для русского языка, таких как Snowball. Данная статья подробно разбирает причину, по которой поиск по префиксу перестает находить точные совпадения коротких слов, и предлагает рабочие решения этой проблемы.

Суть проблемы: Стемминг vs Wildcard

Проблема проявляется при следующей конфигурации:

  • Используется кастомный анализатор indexAnalyzer с фильтром snowball (language: Russian).
  • При индексации слово «Авто» проходит через стеммер и превращается в токен «авт».
  • При поиске используется QueryString с wildcard-маской Авто.

Пример из практики (Elastic Stack Discuss, 2012)

Пользователь k4Rla описал кейс: в индексе есть документы с полем name: «Авто», «Автомобиль», «Автосалон». При запросе Авто возвращаются только «Автомобиль» и «Автосалон», но не сам документ «Авто». При этом для английского языка («Auto», «Automobile») всё работает корректно.

Техническое объяснение: Почему это происходит?

Ключевой момент раскрыл Igor Motov (разработчик Elasticsearch) в ответе на форуме:

Lucene не применяет анализатор к wildcard-запросам. Он лишь приводит термы к нижнему регистру (lowercasing).

Давайте проследим путь токена:

  1. Индексация: Текст "Авто" → Анализатор (Snowball Russian) → Токен в индексе: "авт".
  2. Поиск: Запрос QueryString("Авто") → Lucene видит wildcard → Анализатор НЕ применяется → Терм запроса: "авто" (в нижнем регистре).
  3. Сравнение: Lucene ищет в индексе термы, начинающиеся на "авто".
    • Токен "авт" (от слова «Авто») НЕ начинается на "авто" → Не найдено.
    • Токен "автомобиль" (от слова «Автомобиль») начинается на "авто" → Найдено.

Именно поэтому находится «Автомобиль» (стемминг не отрезает корень так сильно, или корень длиннее префикса), но не находится точное совпадение «Авто».

Решения проблемы

Использование Edge N-grams (Рекомендуемый подход для автодополнения)

Вместо wildcard-запросов на стеммированном поле лучше использовать отдельное поле с анализатором на основе edge_ngram. Это позволяет искать по префиксам без потерь, связанных со стеммингом.

PUT /my_index
{
 "settings": {
 "analysis": {
 "filter": {
 "autocomplete_filter": {
 "type": "edge_ngram",
 "min_gram": 2,
 "max_gram": 15
 }
 },
 "analyzer": {
 "autocomplete_index": {
 "type": "custom",
 "tokenizer": "standard",
 "filter": ["lowercase", "autocomplete_filter"]
 },
 "autocomplete_search": {
 "type": "custom",
 "tokenizer": "standard",
 "filter": ["lowercase"]
 }
 }
 }
 },
 "mappings": {
 "properties": {
 "name": { "type": "text", "analyzer": "russian" }, // Для полнотекстового поиска
 "name_suggest": { 
 "type": "text", 
 "analyzer": "autocomplete_index", 
 "search_analyzer": "autocomplete_search" 
 } // Для префиксного поиска/автодополнения
 }
 }
}

Теперь запрос match по полю name_suggest с текстом "Авто" найдет все варианты, включая точное «Авто».

Отключение стемминга для поля, используемого в wildcard

Если wildcard-поиск критичен и должен работать на основном поле, можно создать sub-field (подполе) с типом keyword или анализатором без стемминга (только lowercase).

"name": {
 "type": "text",
 "analyzer": "russian", // С стеммингом для обычного поиска
 "fields": {
 "raw": { 
 "type": "text", 
 "analyzer": "standard" // Без стемминга, только токенизация и lowercase
 },
 "keyword": { "type": "keyword" } // Для точного совпадения/агрегаций
 }
}

Запрос QueryString("Авто") нужно направить в поле name.raw. Тогда токен в индексе будет "авто", и маска "авто" сработает корректно.

Использование Query-Time анализатора (Не работает для Wildcard)

В исходном коде пользователя была попытка задать анализатор для запроса:

$query->setAnalyzer(searchAnalyzer);

Важно: Как подтвердил Igor Motov, Lucene игнорирует анализатор для термов с wildcard (* ?). Эта настройка влияет только на обычные термы. Поэтому данный метод не решит проблему.

Переход на Morphology (Morfologik / Russian Morphology)

Стеммер Snowball (Портер) довольно агрессивен. Плагины вроде analysis-morphology (Morfologik) или встроенный russian анализатор (на основе Lucene RussianAnalyzer) могут давать другие корни, но фундаментальная проблема wildcard остается: запрос не анализируется. Решение через edge_ngram или raw подполе остается универсальным.

Резюме: Best Practices
Подход Плюсы Минусы Когда использовать
Edge N-grams (completion/suggest) Мгновенный поиск по префиксу, поддержка опечаток (fuzzy), не зависит от стемминга. Увеличивает размер индекса, требует отдельного поля/анализатора. Автодополнение (type-ahead), поиск по префиксу для UI.
Sub-field без стемминга (raw/standard) Простая реализация, точный контроль токенов, работает с wildcard. Не учитывает морфологию (не найдет «Автомобиль» по запросу «Авто*», если не использовать ngram). Административные панели, точный поиск по кодам/артикулам/именам.
Wildcard на стеммированном поле Не требует доп. настроек. Не работает для коротких слов (как показано в статье), непредсказуемое поведение. Никогда не рекомендуется для продакшена на кириллице с стеммерами.

Проблема «Поиска с помощью комтей парноеопытных жиаотных» (корректно: поиска с помощью wildcard-запросов на стеммированных полях) является классической ловушкой Elasticsearch. Причина кроется в архитектуре Lucene: wildcard-запросы не проходят через цепочку анализаторов.

Правильная архитектура поиска подразумевает разделение ответственности:

  • Поле с анализатором (Snowball/Russian/Morphology) — для релевантного полнотекстового поиска (match, match_phrase).
  • Поле с Edge N-grams — для быстрого префиксного поиска и автодополнения.
  • Поле keyword / raw (standard analyzer) — для точной фильтрации, сортировки и wildcard-запросов, где важна точность токена.

Следуя этому подходу, вы избежите «пропажи» документов вроде «Авто» и обеспечите пользователям предсказуемое, быстрое и качественное поведение поиска;