Получение пустого результата (hits.total.value = 0) — одна из самых частых проблем, с которой сталкиваются разработчики, интегрирующие Elasticsearch. Часто причина кроется не в отсутствии данных, а в несовпадении того, как данные проиндексированы, и того, как выполняется запрос.
В этой статье мы разберем основные причины пустых результатов и дадим конкретные рекомендации по настройке маппинга, анализаторов и стратегий поиска, чтобы ваш поиск был максимально релевантным и устойчивым к ошибкам пользователя.
Понимание разницы между text и keyword
Это причина №1 пустых результатов. Elasticsearch имеет два основных типа строковых полей:
text— для полнотекстового поиска. Значение проходит через анализатор (токенизируется, приводится к нижнему регистру, удаляются стоп-слова, применяется стемминг). Поиск по этому полю также анализирует запрос.keyword— для точного совпадения, фильтрации, сортировки и агрегаций. Значение сохраняется как есть (без токенизации и изменения регистра).
Типичная ошибка
Вы индексируете email User@Example.COM в поле типа keyword и ищете user@example.com (в другом регистре) или используете match запрос, который анализирует ввод.
Лучшая практика: Multi-fields (Подполя)
Всегда используйте маппинг с подполями для строковых данных, которые могут требовать и точный, и полнотекстовый поиск.
PUT /my_index
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "standard",
"fields": {
"keyword": { "type": "keyword" }, // Для точного совпадения / фильтров
"suggest": { "type": "completion" } // Для автодополнения (опционально)
} }, "email": {
"type": "keyword", // Email обычно не нужен в full-text, только exact
"normalizer": "lowercase_normalizer" // Важно! См. раздел про нормализаторы
}
} }}
Правило: Ищите по field.keyword (через term), если нужно точное совпадение (ID, email, статусы, теги). Ищите по field (через match), если нужен полнотекстовый поиск (названия, описания, статьи).
Несоответствие анализаторов при индексации и поиске
Если вы используете кастомный анализатор (например, с нграмами для автодополнения или специфическим стеммером для русского языка), он обязательно должен применяться и при индексации, и при поиске.
Проблема
Вы создали анализатор russian_ngram для поля title. При индексации «Эластиксерч» превращается в токены: «э», «ел», «ела», «элас»…
Вы делаете запрос match без указания анализатора. Elasticsearch использует standard анализатор по умолчанию для поисковой строки. Токены запроса («эластиксерч») не совпадают с токенами в индексе («эла», «элас»). Результат пуст.
Решение
- Явно указывайте анализатор в запросе:
GET /my_index/_search { "query": { "match": { "title": { "query": "Эластик", "analyzer": "russian_ngram" // Тот же, что при индексации! } } } } - Или используйте
search_analyzerв маппинге: Укажите отдельный анализатор для поиска (обычно без нграмов, но с тем же стеммером/фильтрами нижнего регистра), а для индексации оставьте нграмы."title": { "type": "text", "analyzer": "index_ngram_analyzer", // При индексации: нграмы "search_analyzer": "search_standard_analyzer" // При поиске: целые слова + стемминг }
Использование match вместо term (и наоборот)
Непонимание того, как парсятся запросы, ведет к нулевым хитам.
| Тип запроса | Анализирует ввод? | Для чего нужен | Тип поля |
|---|---|---|---|
match | Да | Полнотекстовый поиск | text |
match_phrase | Да | Поиск точной фразы (порядок слов) | text |
term | Нет (ищет точное значение в инвертированном индексе) | Точное совпадение, фильтры | keyword, integer, date |
terms | Нет | Точное совпадение с массивом значений | keyword |
Пример ошибки
Поле status имеет тип keyword со значением "Published".
- Запрос
{ "match": { "status": "published" } }-> Анализирует «published» -> ищет токен «published». В индексе лежит «Published» (с большой буквы). Пусто. - Запрос
{ "term": { "status": "Published" } }-> Ищет точно «Published». Нашел.
Совет: Для полей keyword всегда используйте term/terms внутри filter контекста (для кэширования и производительности).
Проблемы с регистром и нормализаторы (Normalizers)
Поля типа keyword чувствительны к регистру. «Moscow» != «moscow».
Решение: Normalizer
Нормализатор — это анализатор без токенизатора. Он применяет фильтры (lowercase, asciifolding) к целому значению.
PUT /my_index
{
"settings": {
"analysis": {
"normalizer": {
"lowercase_normalizer": {
"type": "custom",
"filter": ["lowercase", "asciifolding"] // Убирает акценты: "café" -> "cafe"
}
}
}
},
"mappings": {
"properties": {
"city": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
}
}
}
Теперь при индексации «Москва» и «МОСКВА» оба превратятся в «москва». Запрос term: { "city": "москва" } найдет оба варианта.
Оператор AND в match запросе (Слишком строгий поиск)
По умолчанию match использует оператор OR. Запрос «быстрый коричневый лис» найдет документы, содержащие хотя бы одно из слов.
Если вы поставите "operator": "and", документ должен содержать все слова. Опечатка в одном слове или отсутствие стоп-слова в документе дадут пустой результат.
Лучшая практика: minimum_should_match
Вместо жесткого AND используйте процентное совпадение. Это делает поиск устойчивым к опечаткам и лишним словам.
GET /articles/_search
{
"query": {
"match": {
"content": {
"query": "быстрый коричневый лис прыгает",
"minimum_should_match": "75%" // Найдет, если совпало 3 из 4 значимых слов
} }
}
}
Игнорирование несуществующих полей и lenient параметр
Если вы ищете по полю, которого нет в маппинге (или оно динамически не создано), Elasticsearch по умолчанию выдаст ошибку 400 Bad Request, а не пустой результат. Но если вы используете мульти-поиск или шаблоны, где поля могут отсутствовать, добавьте параметр.
GET /_search
{
"query": {
"multi_match": {
"query": "текст",
"fields": ["title", "description", "tags"],
"lenient": true // Игнорирует ошибки, если поля нет или типы не совпадают
}
}
}
Проблемы с вложенными объектами (nested)
Если у вас массив объектов:
{
"actors": [
{ "first": "Keanu", "last": "Reeves" },
{ "first": "Carrie-Anne", "last": "Moss" }
]
}
И маппинг "actors": { "type": "object" } (по умолчанию), то Elasticsearch «выпрямит» это в:
{ "actors.first": ["Keanu", "Carrie-Anne"], "actors.last": ["Reeves", "Moss"] } Запрос «Keanu Moss» найдет этот документ (ложное срабатывание), а запрос «Keanu Reeves» тоже найдет. Но если вы ищете точное сочетание, стандартный object не сработает.
Решение: Тип nested
В маппинге укажите "type": "nested". При поиске используйте nested запрос:
GET /movies/_search
{ "query": {
"nested": {
"path": "actors",
"query": {
"bool": {
"must": [
{ "match": { "actors.first": "Keanu" }},
{ "match": { "actors;last": "Reeves" }}
]
}
}
}
}}
Без nested запроса поиск по вложенным полям часто дает неожиданные (или пустые, если логика сложная) результаты.
Пейджинг и track_total_hits
Иногда результаты есть, но вы их не видите.
- По умолчанию Elasticsearch возвращает только первые 10 хитов (
size: 10). - Если
track_total_hits= 10000 (дефолт в новых версиях), и совпадений 15000, в ответе будет"total": { "value": 10000, "relation": "gte" }.
Всегда явно указывайте size и from (или используйте search_after для глубокого пейджинга) и проверяйте hits.total.value в ответе, а не длину массива hits.hits.
Кэш и «свежесть» данных (Refresh Interval)
Elasticsearch — near real-time (почти в реальном времени). По умолчанию refresh_interval: "1s".
Сценарий: Вы проиндексировали документ -> сразу сделали поиск -> пусто.
Решения:
- Дождаться 1 секунды (обычно достаточно).
- Использовать
?refresh=wait_forпри индексации (ждет рефреша, замедляет запись). - Использовать
?refresh=true(форсирует рефреш сразу, очень нагружает кластер, не для продакшн-объема).
Диагностика: API Validate и Explain
Если запрос возвращает пусто, не гадайте, используйте встроенные инструменты.
Validate API (Проверка синтаксиса и понимания запроса)
GET /my_index/_validate/query?explain=true
{
"query": {
"match": { "title": "elasticsearch" }
}
}
В ответе поле valid: true и explanation покажет, во что Elasticsearch преобразовал ваш запрос (какие токены ищет, какие поля затрагивает). Если вы видите ConstantScore(:) или странные токены — проблема в анализаторе.
Explain API (Почему конкретный документ не попал/попал)
Если вы знаете ID документа, который должен был найтись:
GET /my_index/_explain/DOC_ID
{
"query": { ;.. ваш запрос ... }
}
Ответ покажет детальное расчет скоринг (score) для каждого куска запроса. Вы увидите, какое условие дало score 0 и отсеяло документ.
Чек-лист: Почему поиск вернул 0 результатов
- Тип поля: Ищете по
keywordчерезmatch? -> Используйтеtermили ищите поtextподполю. - Регистр: Поле
keywordбез нормализатора? -> Добавьтеnormalizer: lowercase. - Анализатор: Кастомный анализатор при индексации, но стандартный при поиске? -> Укажите
search_analyzerилиanalyzerв запросе. - Оператор: Слишком строгий
operator: "and"? -> Используйтеminimum_should_match. - Опечатки: Пользователь ошибся? -> Добавьте
fuzziness: "AUTO"вmatchзапрос. - Nested: Ищете по массиву объектов? -> Проверьте маппинг на
nestedи используйтеnestedзапрос. - Свежесть: Только что записали? -> Подождите рефреш или используйте
refresh=wait_for. - Права доступа: (Security) У пользователя есть права на чтение индекса/полей? (Document Level Security / Field Level Security).
Избегание пустых результатов в Elasticsearch — это вопрос правильного проектирования маппинга (schema design) и понимания конвейера анализа (analysis chain).
Главные правила:
- Всегда используйте
multi-fields(text+keyword). - Синхронизируйте анализаторы индексации и поиска (
search_analyzer). - Используйте
normalizerдля полей точного совпадения; - Применяйте
minimum_should_matchиfuzzinessдля устойчивости к ошибкам пользователя. - Диагностируйте через
_validate/query?explainи_explain.
Следуя этим практикам, вы сделаете поиск предсказуемым, быстрым и удобным для конечных пользователей.