Elasticsearch — поисковый движок. Индекс объединяет документы с идентификаторами и типизированными полями. Учебный products содержит 300 фиксированных товаров. Mapping определяет способ индексирования: число допускает числовые сравнения, а text анализируется в термины. Исходный JSON хранится в _source. Наличие поля в mapping не означает, что оно заполнено в каждом документе.
Каждый пример ниже — тело POST /products/_search. В тренажёр вставляйте только JSON: физический индекс выбирает сервер, он же включает точный подсчёт совпадений. «Выполнить запрос» помогает изучить данные; «Проверить решение» проверяет результат и изучаемую конструкцию. Значения примеров специально отличаются от заданий. Сначала найдите документы, затем объясните, почему конкретный товар попал в ответ.
- Документ: JSON
- Mapping: типы полей
- Query: условия
- hits: найденные документы
Mapping определяет индексированные значения. Query отбирает документы, hits возвращает страницу совпадений.
Примеры выполнены на публичном наборе products в Elasticsearch 9.4.3. Ответы — сокращённые фрагменты JSON: время, сведения о шардах и индексе опущены. Числовые оценки иллюстративны и могут меняться. Примеры объясняют способ решения; значения в заданиях могут отличаться.
Все товары
Корень тела поиска — JSON-объект. query задаёт условия совпадения. match_all с пустым объектом не ограничивает документы и удобен для знакомства с набором данных. size управляет числом возвращаемых документов, а не числом совпадений; sort делает небольшую демонстрацию воспроизводимой. В JSON нужны двойные кавычки, недопустимы комментарии и завершающая запятая.
hits.total.value показывает число совпавших документов; relation: eq означает точный подсчёт. hits.hits содержит возвращённую страницу. _id идентифицирует документ, _source содержит запрошенные исходные поля, _score — оценку релевантности. took измеряется в миллисекундах и меняется между запусками. Фрагменты ниже опускают took, сведения о шардах и индексе: это сокращение для чтения, а не отдельный формат ответа Elasticsearch.
| Поле | Тип | Назначение |
|---|---|---|
| id | integer | Стабильный ID товара и дополнительный ключ сортировки |
| name / description | text | Анализируемый полнотекстовый поиск |
| brand / brand.keyword | text / keyword | Поиск по тексту / точное значение бренда |
| category / status | keyword | Точные значения с учётом регистра |
| price / rating | double / float | Числа; rating может отсутствовать |
| in_stock / created_at | boolean / date | Наличие / дата создания |
Разберём другой сценарий
{
"query": {
"match_all": {}
},
"size": 2,
"_source": [
"name",
"price"
],
"sort": [
{
"id": "asc"
}
]
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 300,
"relation": "eq"
},
"hits": [
{
"_id": "1",
"_score": null,
"_source": {
"name": "Wireless",
"price": 100
}
},
{
"_id": "2",
"_score": null,
"_source": {
"name": "Reference device",
"price": 100
}
}
]
}
}Как читать результат
Возвращены два документа, но total равен 300. Порядок id задаёт sort, а не релевантность. _source ограничен name и price для читаемости. Управление выдачей подробно рассматривается в четвёртом уроке.
Точный бренд
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 меньше размера индекса: у части товаров индексированного рейтинга нет.
Краткая памятка
- query отбирает документы; size ограничивает возвращаемую страницу.
- Для полных точных строк используйте keyword и сохраняйте регистр.
- term — одно значение; terms — любое значение массива.
- Выбирайте gt/gte/lt/lte по формулировке каждой границы.
- exists проверяет индексированное значение; ноль не означает отсутствие.
Официальная документация
Готовы применить знания?
Подтвердите прочтение, чтобы сохранить своё место. Чтение и решение заданий учитываются отдельно в этом браузере с помощью гостевой cookie.