Построим предсказуемые ограничения по брендам, ценам, датам и наличию полей.
Примеры course-products используют учебную схему и товар из главы 5 «Mapping и типы полей». Разборы упражнений ниже используют отдельный публичный набор тренажёра.
- Термы и идентификаторы
- Границы и время
- Проверка отсутствия
Построим предсказуемые ограничения по брендам, ценам, датам и наличию полей.
Курс ориентирован на Elasticsearch 9.4.3. Разборы упражнений используют публичный набор products; остальные запросы показывают отдельные учебные индексы и могут требовать подготовки или прав, недоступных в тренажёре. Ответы сокращены, score может меняться. Для расширенных функций проверяйте лицензию и вариант развёртывания.
Термы и идентификаторы
term ищет одно индексированное значение без полнотекстового анализа, terms — любое из списка. Для бренда это обычно keyword. Регистр и нормализация должны совпадать с mapping. ids выбирает документы по _id и отличается от поиска по пользовательскому полю sku. Даже если вход выглядит как обычный текст, точный запрос не разбивает его на слова.
СамопроверкаГраницы и время
range задаёт gt, gte, lt, lte. Для соседних временных интервалов удобно использовать включённое начало и исключённый конец: так событие на границе не считается дважды. Date math вроде now-7d/d включает округление; time_zone влияет на интерпретацию локальных границ, но now представляет текущее время. Уточняйте, требуется календарная неделя или последние 168 часов.
СамопроверкаПроверка отсутствия
exists проверяет наличие индексированного значения. Для «рейтинг не задан» обычно используют must_not с exists. Причины отсутствия включают null, пустой массив и некоторые настройки mapping, например ignore_above. Ключ в _source и присутствие в поисковых структурах — разные свойства. Для обязательных бизнес-полей проверяйте вход до индексации.
Бренды объединены через OR, а диапазон добавлен через AND.
{
"query": {
"bool": {
"filter": [
{
"terms": {
"brand": [
"Sony",
"Bose"
]
}
},
{
"range": {
"price": {
"gte": 50,
"lt": 200
}
}
}
]
}
}
}Как читать результат
Бренды объединены через OR, а диапазон добавлен через AND.
Разбор на примере каталога
Проверьте граничные товары с ценами 49.99, 50, 199.99 и 200 для диапазона [50,200). Должны пройти только 50 и 199.99. Затем добавьте товар без цены: он не должен пройти range. Для дат аналогично проверьте начало и конец суток в выбранной зоне. Такой набор маленьких контрпримеров ловит ошибки gte/lte значительно лучше, чем проверка общего количества результатов на случайном каталоге. Для exists отдельно сравните null, пустой массив и пустую строку.
Корректная нулевая выдача
{
"query": {
"term": {
"sku": "missing-sku"
}
}
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 0,
"relation": "eq"
},
"hits": []
}
}Как читать результат
Ноль совпадений — успешный результат поиска, а не ошибка HTTP.
Точный бренд
term ищет индексированное значение без анализа входной строки. Для идентификатора бренда выбирайте brand.keyword: он хранит полное значение с учётом регистра. Sony и sony здесь различаются. Само поле brand имеет тип text, поэтому его индексированные токены не равны исходной строке бренда.
Та же конструкция term работает с логическими и числовыми полями: передавайте true, а не строку "true", число, а не отформатированную цену. В запросе используются индексированные пути. В _source лежат исходные поля: brand.keyword — подполе mapping, а не отдельное свойство исходного JSON.
Разберём другой сценарий
{
"query": {
"term": {
"brand.keyword": "Sony"
}
},
"size": 2,
"_source": [
"name",
"brand"
],
"sort": [
{
"id": "asc"
}
]
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 50,
"relation": "eq"
},
"hits": [
{
"_id": "3",
"_score": null,
"_source": {
"name": "Sony Travel Speaker 3",
"brand": "Sony"
}
},
{
"_id": "9",
"_score": null,
"_source": {
"name": "Sony Premium Travel Laptop 9",
"brand": "Sony"
}
}
]
}
}Как читать результат
У каждого найденного товара brand равен Sony. В _source возвращается brand, хотя условие обращается к brand.keyword. Небольшой size не ограничивает число совпадений в total.
Несколько статусов
terms проверяет принадлежность списку: документ проходит, если поле равно хотя бы одному значению. Это логическое ИЛИ для точных значений одного поля, а не требование содержать все значения. status здесь — одно keyword-значение. Альтернативы передаются массивом.
Для компактного отбора точных значений используйте terms; когда альтернативы затрагивают разные поля или составные условия, понадобится bool. Список значений не создаёт самостоятельные блоки запроса. Отсутствующее поле не совпадает с обычным значением списка: товар без status не включается автоматически.
Разберём другой сценарий
{
"query": {
"terms": {
"status": [
"preorder",
"discontinued"
]
}
},
"size": 2,
"_source": [
"name",
"status"
],
"sort": [
{
"id": "asc"
}
]
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 200,
"relation": "eq"
},
"hits": [
{
"_id": "1",
"_score": null,
"_source": {
"name": "Wireless",
"status": "preorder"
}
},
{
"_id": "2",
"_score": null,
"_source": {
"name": "Reference device",
"status": "discontinued"
}
}
]
}
}Как читать результат
На странице допустим любой из двух статусов. total показывает размер всего отбора; ни один документ не обязан иметь оба статуса одновременно.
Диапазон цен
range сравнивает индексированные значения, а не строки. gte и lte включают границу, gt и lt исключают её. Обе границы интервала одного поля задаются в одном объекте range. Пример выбирает цены от 200 до 800 с включением обеих границ.
Перед записью JSON переведите условие: «дешевле» означает lt, «не меньше» — gte. Числовые типы учитывают дробные значения, поэтому 499.99 и 500 могут оказаться по разные стороны границы. Эта же группа запросов поддерживает даты, но в price передавайте числа без символа валюты.
| Сравнение | Граница включена? |
|---|---|
| gt / lt | Нет |
| gte / lte | Да |
Разберём другой сценарий
{
"query": {
"range": {
"price": {
"gte": 200,
"lte": 800
}
}
},
"size": 2,
"_source": [
"name",
"price"
],
"sort": [
{
"price": "asc"
},
{
"id": "asc"
}
]
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 60,
"relation": "eq"
},
"hits": [
{
"_id": "5",
"_score": null,
"_source": {
"name": "Dell USB Cable 5",
"price": 499
}
},
{
"_id": "15",
"_score": null,
"_source": {
"name": "Sony USB Cable 15",
"price": 499
}
}
]
}
}Как читать результат
Страница начинается с самого дешёвого подходящего товара; total считает весь интервал. Товар с ценой ровно 800 включается, в отличие от условия lt: 800.
Товары с рейтингом
exists проверяет наличие индексированного значения. При нашем mapping отсутствующий rating и rating: null не проходят эту проверку. Числовой ноль является значением и не должен считаться отсутствием. Пустая текстовая строка также не равна отсутствующему полю.
Наличие свойства в _source не всегда гарантирует индексированное значение: на это влияют настройки индексирования. В упражнениях mapping фиксирован и не заменяет null через null_value. Чтобы найти документы без рейтинга, инвертируйте exists через bool.must_not; эта комбинация понадобится в продвинутом уроке.
Разберём другой сценарий
{
"query": {
"exists": {
"field": "rating"
}
},
"size": 2,
"_source": [
"name",
"rating"
],
"sort": [
{
"id": "asc"
}
]
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 258,
"relation": "eq"
},
"hits": [
{
"_id": "1",
"_score": null,
"_source": {
"name": "Wireless",
"rating": 3.1
}
},
{
"_id": "2",
"_score": null,
"_source": {
"name": "Reference device",
"rating": 3.2
}
}
]
}
}Как читать результат
Каждый найденный товар имеет числовой рейтинг. total меньше размера индекса: у части товаров индексированного рейтинга нет.
Самопроверка
Сначала ответьте самостоятельно, затем раскройте объяснение.
1. terms требует все значения?
Нет, достаточно любого значения списка.
2. Что выбрать для артикула?
term по keyword-полю либо ids, если артикул используется как _id.
3. Как избежать двойного счёта дат?
Использовать gte для начала и lt для конца.
4. Как найти отсутствующий рейтинг?
must_not с exists по rating, с учётом mapping.
5. Почему term по name не ищет фразу?
Он сопоставляет один терм, а text-поле обычно разбито анализатором.
Краткая памятка
- Нет, достаточно любого значения списка.
- term по keyword-полю либо ids, если артикул используется как _id.
- Использовать gte для начала и lt для конца.
- must_not с exists по rating, с учётом mapping.
- Он сопоставляет один терм, а text-поле обычно разбито анализатором.
Официальная документация
Готовы применить знания?
Подтвердите прочтение, чтобы сохранить своё место. Чтение и решение заданий учитываются отдельно в этом браузере с помощью гостевой cookie.