Финальные задачи соединяют предыдущие уроки, а не вводят неограниченный новый DSL. До JSON запишите короткую логическую формулу. Отметьте общие ограничения, альтернативные ветви, текст с оценкой и область каждой метрики. Так ошибки становятся заметны до запуска длинного запроса.
Проверяйте результат слоями: синтаксис, полный набор совпадений, порядок или значения агрегаций, затем нужный семантический контекст. Страница правдоподобных товаров не доказывает правильность запроса. Особенно внимательно проверяйте границы цен, отсутствие рейтинга, равные ключи сортировки и различия родительских и локально отфильтрованных счётчиков.
- Общие ограничения query
- Поисковая ветвь A ИЛИ B
- Группы
- Локальный фильтр допуска
- Метрики допущенных документов
- Локальный фильтр допуска
Общие ограничения действуют на всё дерево. Альтернативные ветви и локальная аналитика сохраняют собственные области.
Примеры выполнены на публичном наборе products в Elasticsearch 9.4.3. Ответы — сокращённые фрагменты JSON: время, сведения о шардах и индексе опущены. Числовые оценки иллюстративны и могут меняться. Примеры объясняют способ решения; значения в заданиях могут отличаться.
Сложные бизнес-условия
Начните с формулы S И (A ИЛИ B). S — общий допуск: наличие и разрешённые статусы. Каждая ветвь связывает свою категорию со своим диапазоном. В примере S разрешает только preorder, A — Accessories дешевле 200, B — Smartphones от 200 включительно до 900 исключительно.
Разместите общие ограничения во внешнем filter, две ветви во внешнем should и minimum_should_match: 1 на том же уровне. Внутри ветви используйте bool.filter для категории И цены. Плоский should из четырёх отдельных условий пропускает, например, дешёвый товар чужой категории.
Проверьте нижнюю границу, верхнюю, документ только с нужной категорией, документ только с подходящей ценой и отсутствующий на складе товар, полностью подходящий ветви. Такие случаи отличают правильную группировку от случайно широкого запроса. В практическом задании примените ту же логику к другим значениям.
| Общее S | Ветвь A | Ветвь B | Принять? |
|---|---|---|---|
| ложь | истина | истина | Нет |
| истина | ложь | ложь | Нет |
| истина | истина | ложь | Да |
| истина | ложь | истина | Да |
Разберём другой сценарий
{
"query": {
"bool": {
"filter": [
{
"term": {
"in_stock": true
}
},
{
"term": {
"status": "preorder"
}
}
],
"should": [
{
"bool": {
"filter": [
{
"term": {
"category": "Accessories"
}
},
{
"range": {
"price": {
"lt": 200
}
}
}
]
}
},
{
"bool": {
"filter": [
{
"term": {
"category": "Smartphones"
}
},
{
"range": {
"price": {
"gte": 200,
"lt": 900
}
}
}
]
}
}
],
"minimum_should_match": 1
}
},
"size": 2,
"_source": [
"name",
"category",
"price",
"status",
"in_stock"
],
"sort": [
{
"id": "asc"
}
]
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 15,
"relation": "eq"
},
"hits": [
{
"_id": "4",
"_score": null,
"_source": {
"name": "Logitech Gaming Keyboard 4",
"category": "Accessories",
"price": 199,
"in_stock": true,
"status": "preorder"
}
},
{
"_id": "25",
"_score": null,
"_source": {
"name": "Apple USB Cable 25",
"category": "Smartphones",
"price": 499,
"in_stock": true,
"status": "preorder"
}
}
]
}
}Как читать результат
Каждый товар проходит общие ограничения и целую ветвь категория–цена. Условие из ветви A нельзя соединить с несвязанным условием из B.
Релевантность с ограничениями
Разделите три роли: must описывает текстовую релевантность, filter — допуск, must_not — исключения. Пример ищет travel по name^3 и description, требует наличие и цену ниже 800, затем исключает preorder. multi_match должен оставаться в поисковом пути must, а не внутри локального или внешнего filter.
Не добавляйте сортировку по полю, если цель — порядок по релевантности. Boost в несвязанном условии или агрегации не придаёт нужный вес реальному текстовому запросу. Проверяйте полный набор совпадений и взаимный порядок, а не фиксированную оценку одного документа.
Разберём другой сценарий
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "travel",
"fields": [
"name^3",
"description"
]
}
}
],
"filter": [
{
"term": {
"in_stock": true
}
},
{
"range": {
"price": {
"lt": 800
}
}
}
],
"must_not": [
{
"term": {
"status": "preorder"
}
}
]
}
},
"size": 2,
"_source": [
"name",
"price",
"status",
"in_stock"
]
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 34,
"relation": "eq"
},
"hits": [
{
"_id": "23",
"_score": 4.8997154,
"_source": {
"name": "Dell Travel Speaker 23",
"price": 100,
"in_stock": true,
"status": "discontinued"
}
},
{
"_id": "53",
"_score": 4.8997154,
"_source": {
"name": "Dell Travel Speaker 53",
"price": 100,
"in_stock": true,
"status": "discontinued"
}
}
]
}
}Как читать результат
Текстовый запрос задаёт взаимный порядок допущенных и не исключённых товаров. Наличие, цена и исключения определяют состав, а не бонус релевантности.
Страница каталога для API
Контракт API может одновременно задавать отбор, порядок, страницу и проекцию. Сначала ограничьте наличие и индексированный рейтинг, исключите preorder. Затем отсортируйте rating по убыванию, price и id по возрастанию. Наконец выберите вторую страницу по три элемента и только name, price, rating в _source. Каждый шаг имеет самостоятельный смысл.
exists по rating до сортировки удаляет отсутствующие и null-значения из набора: не нужно полагаться на размещение пропусков по умолчанию. Уникальный id последним ключом делает равенство рейтинга и цены воспроизводимым. Даже правильные ID нарушают контракт, если возвращается лишнее исходное поле.
Разберём другой сценарий
{
"query": {
"bool": {
"filter": [
{
"term": {
"in_stock": true
}
},
{
"exists": {
"field": "rating"
}
}
],
"must_not": [
{
"term": {
"status": "preorder"
}
}
]
}
},
"size": 3,
"_source": [
"name",
"price",
"rating"
],
"sort": [
{
"rating": "desc"
},
{
"price": "asc"
},
{
"id": "asc"
}
],
"from": 3
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 86,
"relation": "eq"
},
"hits": [
{
"_id": "299",
"_score": null,
"_source": {
"name": "Dell Premium Travel Laptop 299",
"price": 1299,
"rating": 4.9
}
},
{
"_id": "38",
"_score": null,
"_source": {
"name": "Samsung Headphones Premium 38",
"price": 1000,
"rating": 4.8
}
},
{
"_id": "158",
"_score": null,
"_source": {
"name": "Samsung Headphones Premium 158",
"price": 1000,
"rating": 4.8
}
}
]
}
}Как читать результат
Отдельно проверьте total, три упорядоченных ID и точные ключи source. Их сочетание определяет контракт страницы; изменение size или from меняет запрошенную страницу.
Аналитика категорий и брендов
Используйте три уровня: buckets категорий, затем брендов, затем листья числовых метрик. Пример анализирует active, группирует по category и brand.keyword и вычисляет три показателя цены для каждой пары. Метрики внутри бренда наследуют и категорию, и бренд.
Query расположен вне aggs и ограничивает все группы. Каждый terms.size локален для своего уровня. При размерах 10 и 20 консервативная оценка числа buckets равна 10 + 10 × 20 = 210, что укладывается в бюджет 1000. Метрики занимают третий уровень, но не умножают число buckets.
Читайте ответ как categories.buckets → brands.buckets → metric.value. Не смешивайте метрики разных пар категория–бренд без отдельного требования. Среднее средних групп в общем случае не равно среднему всех документов.
- query: active
- categories (уровень 1)
- brands (уровень 2)
- avg / min / max (уровень 3)
- brands (уровень 2)
- categories (уровень 1)
Каждый уровень вложенности уточняет набор документов до вычисления метрик.
Разберём другой сценарий
{
"size": 0,
"query": {
"term": {
"status": "active"
}
},
"aggs": {
"categories": {
"terms": {
"field": "category",
"size": 10
},
"aggs": {
"brands": {
"terms": {
"field": "brand.keyword",
"size": 20
},
"aggs": {
"average_price": {
"avg": {
"field": "price"
}
},
"min_price": {
"min": {
"field": "price"
}
},
"max_price": {
"max": {
"field": "price"
}
}
}
}
}
}
}
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 100,
"relation": "eq"
},
"hits": []
},
"aggregations": {
"categories": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Accessories",
"doc_count": 25,
"brands": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "apple",
"doc_count": 25,
"average_price": {
"value": 859.4
},
"min_price": {
"value": 99
},
"max_price": {
"value": 2499
}
}
]
}
},
{
"key": "Audio",
"doc_count": 25,
"brands": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "apple",
"doc_count": 25,
"average_price": {
"value": 859.4
},
"min_price": {
"value": 99
},
"max_price": {
"value": 2499
}
}
]
}
},
{
"key": "Computers",
"doc_count": 25,
"brands": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Sony",
"doc_count": 25,
"average_price": {
"value": 589.2
},
"min_price": {
"value": 49
},
"max_price": {
"value": 1299
}
}
]
}
},
{
"key": "Smartphones",
"doc_count": 25,
"brands": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Sony",
"doc_count": 25,
"average_price": {
"value": 589.2
},
"min_price": {
"value": 49
},
"max_price": {
"value": 1299
}
}
]
}
}
]
}
}
}Как читать результат
Каждый бренд находится внутри одной категории. Его метрики описывают только active в этой паре, даже если тот же бренд встречается в другом месте ответа.
Условные показатели внутри групп
Разделяйте родительский счётчик и локальный допуск. Верхнеуровневый query примера выбирает active или preorder. Bucket brands считает все подходящие документы бренда. Дочерний eligible затем требует наличие И (рейтинг не ниже 4.5 ИЛИ отсутствие индексированного рейтинга). Внутренний minimum_should_match делает альтернативы рейтинга обязательными.
Отсутствующий рейтинг выражается через exists внутри bool.must_not. Наличие вынесите за альтернативы рейтинга: товар без рейтинга и без наличия не должен пройти. При нашем mapping null также не индексируется. Нулевой рейтинг существует и не проходит порог: он не должен попасть в ветвь отсутствия.
Поместите average_price, min_price и max_price в eligible.aggs. Если у бренда есть active или preorder, но нет допущенных товаров, родительский bucket остаётся с положительным doc_count, eligible.doc_count равен нулю, а все три метрики — null. Перенос допуска в query стирает это различие и отвечает на другой вопрос.
Разберём другой сценарий
{
"size": 0,
"query": {
"terms": {
"status": [
"active",
"preorder"
]
}
},
"aggs": {
"brands": {
"terms": {
"field": "brand.keyword",
"size": 20
},
"aggs": {
"eligible": {
"filter": {
"bool": {
"filter": [
{
"term": {
"in_stock": true
}
}
],
"should": [
{
"range": {
"rating": {
"gte": 4.5
}
}
},
{
"bool": {
"must_not": [
{
"exists": {
"field": "rating"
}
}
]
}
}
],
"minimum_should_match": 1
}
},
"aggs": {
"average_price": {
"avg": {
"field": "price"
}
},
"min_price": {
"min": {
"field": "price"
}
},
"max_price": {
"max": {
"field": "price"
}
}
}
}
}
}
}
}Ответ Elasticsearch
{
"hits": {
"total": {
"value": 200,
"relation": "eq"
},
"hits": []
},
"aggregations": {
"brands": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Apple",
"doc_count": 50,
"eligible": {
"doc_count": 19,
"min_price": {
"value": 49
},
"average_price": {
"value": 864.8421052631579
},
"max_price": {
"value": 1299
}
}
},
{
"key": "Logitech",
"doc_count": 50,
"eligible": {
"doc_count": 15,
"min_price": {
"value": 99
},
"average_price": {
"value": 919.7333333333333
},
"max_price": {
"value": 2499
}
}
},
{
"key": "Sony",
"doc_count": 50,
"eligible": {
"doc_count": 0,
"min_price": {
"value": null
},
"average_price": {
"value": null
},
"max_price": {
"value": null
}
}
},
{
"key": "apple",
"doc_count": 50,
"eligible": {
"doc_count": 0,
"min_price": {
"value": null
},
"average_price": {
"value": null
},
"max_price": {
"value": null
}
}
}
]
}
}
}Как читать результат
Сравните doc_count бренда и eligible.doc_count: они могут различаться. В фиксированных данных некоторые бренды не имеют допущенных товаров: ответ явно показывает случай null-метрик.
Краткая памятка
- До дерева запроса запишите логическую формулу.
- Общие фильтры размещаются вне альтернативных ветвей.
- multi_match с оценкой должен быть в must вне путей filter.
- Отдельно проверяйте набор, порядок, страницу и проекцию source.
- Родительские счётчики считаются до локальных фильтров; пустые дочерние метрики равны null.
Официальная документация
Готовы применить знания?
Подтвердите прочтение, чтобы сохранить своё место. Чтение и решение заданий учитываются отдельно в этом браузере с помощью гостевой cookie.