Оглавление курса
Теоретический курс · 6 / 6

Продвинутые практические задачи

Объедините бизнес-условия, релевантность, стабильную выдачу API и трёхуровневую аналитику. Пять финальных задач соединяют навыки предыдущих уроков.

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

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

  • Переводить составное бизнес-правило в дерево запросов.
  • Сочетать фильтрацию, scoring, порядок и проекцию source.
  • Строить трёхуровневую аналитику с правильной областью допуска.

Финальные задачи соединяют предыдущие уроки, а не вводят неограниченный новый DSL. До JSON запишите короткую логическую формулу. Отметьте общие ограничения, альтернативные ветви, текст с оценкой и область каждой метрики. Так ошибки становятся заметны до запуска длинного запроса.

Проверяйте результат слоями: синтаксис, полный набор совпадений, порядок или значения агрегаций, затем нужный семантический контекст. Страница правдоподобных товаров не доказывает правильность запроса. Особенно внимательно проверяйте границы цен, отсутствие рейтинга, равные ключи сортировки и различия родительских и локально отфильтрованных счётчиков.

Определите область до JSON
  • Общие ограничения query
    • Поисковая ветвь A ИЛИ B
    • Группы
      • Локальный фильтр допуска
        • Метрики допущенных документов

Общие ограничения действуют на всё дерево. Альтернативные ветви и локальная аналитика сохраняют собственные области.

Примеры выполнены на публичном наборе products в Elasticsearch 9.4.3. Ответы — сокращённые фрагменты JSON: время, сведения о шардах и индексе опущены. Числовые оценки иллюстративны и могут меняться. Примеры объясняют способ решения; значения в заданиях могут отличаться.

01 / 05

Сложные бизнес-условия

Начните с формулы 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Принять?
ложьистинаистинаНет
истиналожьложьНет
истинаистиналожьДа
истиналожьистинаДа

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

POST /products/_search
{
  "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
Сокращённый ответ · JSON
{
  "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.

Закрепить в задании 26
03 / 05

Страница каталога для API

Контракт API может одновременно задавать отбор, порядок, страницу и проекцию. Сначала ограничьте наличие и индексированный рейтинг, исключите preorder. Затем отсортируйте rating по убыванию, price и id по возрастанию. Наконец выберите вторую страницу по три элемента и только name, price, rating в _source. Каждый шаг имеет самостоятельный смысл.

exists по rating до сортировки удаляет отсутствующие и null-значения из набора: не нужно полагаться на размещение пропусков по умолчанию. Уникальный id последним ключом делает равенство рейтинга и цены воспроизводимым. Даже правильные ID нарушают контракт, если возвращается лишнее исходное поле.

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

POST /products/_search
{
  "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
Сокращённый ответ · JSON
{
  "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 меняет запрошенную страницу.

Закрепить в задании 28
04 / 05

Аналитика категорий и брендов

Используйте три уровня: 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)

Каждый уровень вложенности уточняет набор документов до вычисления метрик.

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

POST /products/_search
{
  "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
Сокращённый ответ · JSON
{
  "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 в этой паре, даже если тот же бренд встречается в другом месте ответа.

Закрепить в задании 29
05 / 05

Условные показатели внутри групп

Разделяйте родительский счётчик и локальный допуск. Верхнеуровневый 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 стирает это различие и отвечает на другой вопрос.

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

POST /products/_search
{
  "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
Сокращённый ответ · JSON
{
  "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-метрик.

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

Краткая памятка

  • До дерева запроса запишите логическую формулу.
  • Общие фильтры размещаются вне альтернативных ветвей.
  • multi_match с оценкой должен быть в must вне путей filter.
  • Отдельно проверяйте набор, порядок, страницу и проекцию source.
  • Родительские счётчики считаются до локальных фильтров; пустые дочерние метрики равны null.

Официальная документация

Готовы применить знания?

Подтвердите прочтение, чтобы сохранить своё место. Чтение и решение заданий учитываются отдельно в этом браузере с помощью гостевой cookie.