При работе с полнотекстовым поиском в 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).
Давайте проследим путь токена:
- Индексация: Текст
"Авто"→ Анализатор (Snowball Russian) → Токен в индексе:"авт". - Поиск: Запрос
QueryString("Авто")→ Lucene видит wildcard → Анализатор НЕ применяется → Терм запроса:"авто"(в нижнем регистре). - Сравнение: 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-запросов, где важна точность токена.
Следуя этому подходу, вы избежите «пропажи» документов вроде «Авто» и обеспечите пользователям предсказуемое, быстрое и качественное поведение поиска;