Оглавление курса
Query DSL · 10 / 28

Точные условия и диапазоны

Построим предсказуемые ограничения по брендам, ценам, датам и наличию полей.

Разделов: 8Elasticsearch 9.4.3Ещё не прочитан

Чему вы научитесь

  • Термы и идентификаторы
  • Границы и время
  • Проверка отсутствия

Построим предсказуемые ограничения по брендам, ценам, датам и наличию полей.

Примеры course-products используют учебную схему и товар из главы 5 «Mapping и типы полей». Разборы упражнений ниже используют отдельный публичный набор тренажёра.

Логика главы
  • Термы и идентификаторы
  • Границы и время
  • Проверка отсутствия

Построим предсказуемые ограничения по брендам, ценам, датам и наличию полей.

Курс ориентирован на Elasticsearch 9.4.3. Разборы упражнений используют публичный набор products; остальные запросы показывают отдельные учебные индексы и могут требовать подготовки или прав, недоступных в тренажёре. Ответы сокращены, score может меняться. Для расширенных функций проверяйте лицензию и вариант развёртывания.

01 / 08

Термы и идентификаторы

term ищет одно индексированное значение без полнотекстового анализа, terms — любое из списка. Для бренда это обычно keyword. Регистр и нормализация должны совпадать с mapping. ids выбирает документы по _id и отличается от поиска по пользовательскому полю sku. Даже если вход выглядит как обычный текст, точный запрос не разбивает его на слова.

Самопроверка
02 / 08

Границы и время

range задаёт gt, gte, lt, lte. Для соседних временных интервалов удобно использовать включённое начало и исключённый конец: так событие на границе не считается дважды. Date math вроде now-7d/d включает округление; time_zone влияет на интерпретацию локальных границ, но now представляет текущее время. Уточняйте, требуется календарная неделя или последние 168 часов.

Самопроверка
03 / 08

Проверка отсутствия

exists проверяет наличие индексированного значения. Для «рейтинг не задан» обычно используют must_not с exists. Причины отсутствия включают null, пустой массив и некоторые настройки mapping, например ignore_above. Ключ в _source и присутствие в поисковых структурах — разные свойства. Для обязательных бизнес-полей проверяйте вход до индексации.

Бренды объединены через OR, а диапазон добавлен через AND.

POST /course-products/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "terms": {
            "brand": [
              "Sony",
              "Bose"
            ]
          }
        },
        {
          "range": {
            "price": {
              "gte": 50,
              "lt": 200
            }
          }
        }
      ]
    }
  }
}

Как читать результат

Бренды объединены через OR, а диапазон добавлен через AND.

Самопроверка
04 / 08

Разбор на примере каталога

Проверьте граничные товары с ценами 49.99, 50, 199.99 и 200 для диапазона [50,200). Должны пройти только 50 и 199.99. Затем добавьте товар без цены: он не должен пройти range. Для дат аналогично проверьте начало и конец суток в выбранной зоне. Такой набор маленьких контрпримеров ловит ошибки gte/lte значительно лучше, чем проверка общего количества результатов на случайном каталоге. Для exists отдельно сравните null, пустой массив и пустую строку.

Корректная нулевая выдача

POST /course-products/_search
{
  "query": {
    "term": {
      "sku": "missing-sku"
    }
  }
}
Ответ Elasticsearch
Сокращённый ответ · JSON
{
  "hits": {
    "total": {
      "value": 0,
      "relation": "eq"
    },
    "hits": []
  }
}

Как читать результат

Ноль совпадений — успешный результат поиска, а не ошибка HTTP.

Самопроверка
05 / 08

Точный бренд

term ищет индексированное значение без анализа входной строки. Для идентификатора бренда выбирайте brand.keyword: он хранит полное значение с учётом регистра. Sony и sony здесь различаются. Само поле brand имеет тип text, поэтому его индексированные токены не равны исходной строке бренда.

Та же конструкция term работает с логическими и числовыми полями: передавайте true, а не строку "true", число, а не отформатированную цену. В запросе используются индексированные пути. В _source лежат исходные поля: brand.keyword — подполе mapping, а не отдельное свойство исходного JSON.

Разберём другой сценарий

POST /products/_search
{
  "query": {
    "term": {
      "brand.keyword": "Sony"
    }
  },
  "size": 2,
  "_source": [
    "name",
    "brand"
  ],
  "sort": [
    {
      "id": "asc"
    }
  ]
}
Ответ Elasticsearch
Сокращённый ответ · JSON
{
  "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.

Закрепить в задании 2
06 / 08

Несколько статусов

terms проверяет принадлежность списку: документ проходит, если поле равно хотя бы одному значению. Это логическое ИЛИ для точных значений одного поля, а не требование содержать все значения. status здесь — одно keyword-значение. Альтернативы передаются массивом.

Для компактного отбора точных значений используйте terms; когда альтернативы затрагивают разные поля или составные условия, понадобится bool. Список значений не создаёт самостоятельные блоки запроса. Отсутствующее поле не совпадает с обычным значением списка: товар без status не включается автоматически.

Разберём другой сценарий

POST /products/_search
{
  "query": {
    "terms": {
      "status": [
        "preorder",
        "discontinued"
      ]
    }
  },
  "size": 2,
  "_source": [
    "name",
    "status"
  ],
  "sort": [
    {
      "id": "asc"
    }
  ]
}
Ответ Elasticsearch
Сокращённый ответ · JSON
{
  "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 показывает размер всего отбора; ни один документ не обязан иметь оба статуса одновременно.

Закрепить в задании 3
07 / 08

Диапазон цен

range сравнивает индексированные значения, а не строки. gte и lte включают границу, gt и lt исключают её. Обе границы интервала одного поля задаются в одном объекте range. Пример выбирает цены от 200 до 800 с включением обеих границ.

Перед записью JSON переведите условие: «дешевле» означает lt, «не меньше» — gte. Числовые типы учитывают дробные значения, поэтому 499.99 и 500 могут оказаться по разные стороны границы. Эта же группа запросов поддерживает даты, но в price передавайте числа без символа валюты.

СравнениеГраница включена?
gt / ltНет
gte / lteДа

Разберём другой сценарий

POST /products/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 200,
        "lte": 800
      }
    }
  },
  "size": 2,
  "_source": [
    "name",
    "price"
  ],
  "sort": [
    {
      "price": "asc"
    },
    {
      "id": "asc"
    }
  ]
}
Ответ Elasticsearch
Сокращённый ответ · JSON
{
  "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.

Закрепить в задании 4
08 / 08

Товары с рейтингом

exists проверяет наличие индексированного значения. При нашем mapping отсутствующий rating и rating: null не проходят эту проверку. Числовой ноль является значением и не должен считаться отсутствием. Пустая текстовая строка также не равна отсутствующему полю.

Наличие свойства в _source не всегда гарантирует индексированное значение: на это влияют настройки индексирования. В упражнениях mapping фиксирован и не заменяет null через null_value. Чтобы найти документы без рейтинга, инвертируйте exists через bool.must_not; эта комбинация понадобится в продвинутом уроке.

Разберём другой сценарий

POST /products/_search
{
  "query": {
    "exists": {
      "field": "rating"
    }
  },
  "size": 2,
  "_source": [
    "name",
    "rating"
  ],
  "sort": [
    {
      "id": "asc"
    }
  ]
}
Ответ Elasticsearch
Сокращённый ответ · JSON
{
  "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 меньше размера индекса: у части товаров индексированного рейтинга нет.

Закрепить в задании 5

Самопроверка

Сначала ответьте самостоятельно, затем раскройте объяснение.

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.