Оглавление курса

Фасеты каталога

Согласуем выдачу и счётчики при множественном выборе фильтров.

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

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

  • Что означает счётчик
  • query, post_filter и global
  • Счёт товара и счёт значения

Согласуем выдачу и счётчики при множественном выборе фильтров.

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

Логика главы
  • Что означает счётчик
  • query, post_filter и global
  • Счёт товара и счёт значения

Согласуем выдачу и счётчики при множественном выборе фильтров.

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

01 / 06

Что означает счётчик

Счётчик бренда может означать число товаров в текущей выдаче или число, которое получится после выбора бренда. Это разные интерфейсные контракты. Для независимых фасетов обычно сохраняют все активные ограничения, кроме фильтра самого фасета. Бренды A и B внутри одного выбора часто объединяются через OR, а бренд и размер — через AND. Правило должно быть явно определено.

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

query, post_filter и global

query ограничивает hits и агрегации. post_filter применяется к hits после расчёта агрегаций и полезен, когда счётчики должны видеть более широкое множество. global выходит за область query: внутри него нужно повторно применить обязательные ограничения доступа. Для нескольких независимых фасетов часто нужны отдельные filter aggregation с разным набором активных ограничений.

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

Счёт товара и счёт значения

Документ с несколькими брендами или категориями может попасть в несколько buckets: сумма doc_count не обязана равняться числу уникальных товаров. Nested-фасет сначала считает вложенные документы, reverse_nested возвращает область корневых товаров. Когда нужны уникальные товары, нельзя выдавать число предложений за число товаров. Проверяйте результат на документе с двумя подходящими offers.

Hits ограничены Sony; счётчики брендов относятся ко всем совпавшим наушникам.

POST /course-products/_search
{
  "query": {
    "match": {
      "name": "headphones"
    }
  },
  "aggs": {
    "brands": {
      "terms": {
        "field": "brand"
      }
    }
  },
  "post_filter": {
    "term": {
      "brand": "Sony"
    }
  }
}

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

Hits ограничены Sony; счётчики брендов относятся ко всем совпавшим наушникам.

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

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

В категории есть Sony:5 и Bose:3. После выбора Sony выдача содержит пять товаров. Если facet brands рассчитан над этой же выдачей, Bose исчезает. Если интерфейс позволяет добавить Bose к выбору, полезнее показать оба бренда в области категории без текущего бренд-фильтра. Но цена и доступность могут оставаться активными. Для нескольких фасетов каждая область исключает только собственное ограничение; один общий post_filter не описывает автоматически все такие правила.

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

Агрегация товаров в наличии

filter-агрегация создаёт один bucket документов, проходящих вложенный запрос. Ограничение наследуют только её дочерние агрегации. В примере hits.total по-прежнему учитывает все товары, но out_of_stock содержит недоступные и его среднее описывает только их.

Верхнеуровневый query ограничил бы все агрегации и поисковый результат. Выбирайте область по бизнес-вопросу: локальное сравнение требует filter-bucket, общее ограничение для всего — query. filter-bucket содержит doc_count, помогающий проверить набор метрики.

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

POST /products/_search
{
  "size": 0,
  "aggs": {
    "out_of_stock": {
      "filter": {
        "term": {
          "in_stock": false
        }
      },
      "aggs": {
        "average_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}
Ответ Elasticsearch
Сокращённый ответ · JSON
{
  "hits": {
    "total": {
      "value": 300,
      "relation": "eq"
    },
    "hits": []
  },
  "aggregations": {
    "out_of_stock": {
      "doc_count": 100,
      "average_price": {
        "value": 724.3
      }
    }
  }
}

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

Сравните hits.total.value и out_of_stock.doc_count. Они описывают разные наборы, потому что ограничение действует локально внутри агрегации.

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

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

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

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

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

1. Почему сумма buckets больше total?

Один многозначный документ входит в несколько групп.

2. Что меняет post_filter?

Hits, но не уже рассчитанную область агрегаций.

3. Чем опасен global в multi-tenant?

Он выходит за query, поэтому ограничения tenant нужно применить внутри явно.

4. Что возвращает reverse_nested?

Область корневых документов из nested-контекста.

5. Как объединять выбранные бренды?

Обычно OR внутри фасета, при явно зафиксированном контракте интерфейса.

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

  • Один многозначный документ входит в несколько групп.
  • Hits, но не уже рассчитанную область агрегаций.
  • Он выходит за query, поэтому ограничения tenant нужно применить внутри явно.
  • Область корневых документов из nested-контекста.
  • Обычно OR внутри фасета, при явно зафиксированном контракте интерфейса.

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

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

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