Агрегации вместе с поиском и фильтрами
В предыдущих статьях мы запускали агрегации по всему индексу. На практике это редкость: обычно нужно сначала что-то сузить — по тексту запроса, по категории, по дате — и уже по этой выборке считать статистику. Разберём, как запрос влияет на агрегации, зачем нужен post_filter и как реализовать фасетную навигацию.
Как запрос сужает выборку для агрегаций
Когда в тело поиска добавляется "query", ES сначала находит все документы, удовлетворяющие этому запросу, а потом применяет агрегации только к этой отфильтрованной выборке. Это логично: если ищете ноутбуки, статистика по ценам и брендам должна быть про ноутбуки, а не про весь каталог.
GET /products/_search
{
"size": 0,
"query": {
"match": { "name": "ноутбук" }
},
"aggs": {
"по_бренду": {
"terms": { "field": "brand", "size": 10 }
},
"диапазон_цен": {
"stats": { "field": "price" }
}
}
}"size": 0 говорит ES: «документы мне не нужны, верни только агрегации». match отберёт документы с «ноутбук», а terms и stats посчитают по ним бренды и ценовую статистику.
flowchart TD
A[Все документы индекса] -->|query| B[Отфильтрованные документы]
B --> C[Агрегации считаются здесь]
B -->|post_filter| D[hits.hits — результаты поиска]
C --> E[Бакеты и метрики в ответе]
style C fill:#d4edda,stroke:#28a745
style D fill:#cce5ff,stroke:#004085
style A fill:#f8f9fa,stroke:#6c757dФасетная навигация: в чём задача
Фасетная навигация — это боковые фильтры на странице поиска, которые вы видите на любом маркетплейсе: бренды с количеством товаров, диапазоны цен, рейтинги. Пользователь выбирает фасет (например, «Бренд: Apple»), список товаров обновляется, но счётчики у остальных брендов должны остаться, чтобы можно было переключиться.
Вот в чём ловушка: если применить выбранный фасет прямо в "query", агрегации тоже будут считаться только по Apple — и счётчики других брендов пропадут или окажутся неверными. Для этой задачи ES предоставляет post_filter.
post_filter: фильтр только для хитов
post_filter применяется после того, как агрегации уже посчитаны. Документы для агрегаций он не трогает — только отсекает хиты в hits.hits.
GET /products/_search
{
"query": {
"match": { "name": "ноутбук" }
},
"aggs": {
"по_бренду": {
"terms": { "field": "brand", "size": 10 }
}
},
"post_filter": {
"term": { "brand": "apple" }
}
}Последовательность шагов внутри ES:
1. "query" → находим все ноутбуки.
2. "aggs" → считаем количество по всем брендам среди найденных ноутбуков.
3. "post_filter" → в hits.hits попадают только Apple-ноутбуки.
Пользователь видит только Apple, а в боковой панели — корректные счётчики для всех брендов. Именно то, что нужно.
Агрегация filter: считать по подвыборке внутри агрегации
Иногда нужно посчитать метрику не по всей выборке запроса, а по её части — например, «средняя цена только среди товаров с рейтингом $\geq 4{,}5$». Для этого есть тип агрегации filter (не путайте с filter context в Query DSL — это другое):
GET /products/_search
{
"size": 0,
"query": {
"match": { "name": "ноутбук" }
},
"aggs": {
"топовые_товары": {
"filter": { "range": { "rating": { "gte": 4.5 } } },
"aggs": {
"средняя_цена": { "avg": { "field": "price" } }
}
}
}
}Агрегация filter создаёт бакет из одной корзины: в него попадают только документы, прошедшие условие. Дочерняя avg работает уже по этой подвыборке, не трогая другие агрегации.
Агрегация global: вырваться из контекста запроса
Иногда нужен обратный эффект — показать глобальный показатель рядом с результатами поиска. Например: «средняя цена всех товаров в каталоге» рядом со «средней ценой найденных ноутбуков». Это даёт пользователю контекст.
Агрегация global игнорирует "query" и работает по всему индексу:
GET /products/_search
{
"size": 0,
"query": {
"match": { "name": "ноутбук" }
},
"aggs": {
"ср_цена_ноутбуков": {
"avg": { "field": "price" }
},
"ср_цена_по_каталогу": {
"global": {},
"aggs": {
"значение": { "avg": { "field": "price" } }
}
}
}
}"global": {} — пустой объект без параметров. Дочерняя avg видит все документы индекса вне зависимости от запроса.
Полный пример: страница каталога с фасетами
Соберём всё вместе: полнотекстовый поиск, фасеты по бренду и ценовым диапазонам, применённый фасет только на хиты.
GET /products/_search
{
"size": 12,
"query": {
"bool": {
"must": { "match": { "name": "ноутбук" } },
"filter": { "term": { "in_stock": true } }
}
},
"aggs": {
"бренды": {
"terms": { "field": "brand", "size": 20 }
},
"ценовые_диапазоны": {
"range": {
"field": "price",
"ranges": [
{ "to": 50000, "key": "до 50к" },
{ "from": 50000, "to": 100000, "key": "50–100к" },
{ "from": 100000, "key": "от 100к" }
]
}
}
},
"post_filter": {
"term": { "brand": "lenovo" }
}
}Разберём по частям:
boolсmust+filter— полнотекстовый поиск по «ноутбук» и фильтр по наличию. Агрегации работают по этой выборке."aggs"— считаем бренды и ценовые диапазоны для всех ноутбуков в наличии."post_filter"— вhits.hitsпопадут только Lenovo, но агрегации уже посчитаны до этого шага.
Пользователь видит ноутбуки Lenovo, а в боковой панели — счётчики по всем брендам и диапазонам цен.
Когда что применять
| Задача | Инструмент |
|---|---|
| Сузить и хиты, и агрегации | query / filter в bool |
| Фасет: сузить только хиты, агрегации — по всей выборке | post_filter |
| Метрика по подмножеству внутри агрегации | агрегация filter |
| Считать по всему индексу вне текущего запроса | агрегация global |
См. также
- Метрические и бакетные агрегации — базовые классы, с которых начинается любая аналитика
- Вложенные агрегации и аналитические сценарии — под-агрегации внутри бакетов
- Query DSL: структура запроса и контекст query vs filter — filter context и кеширование
- Составные запросы: bool и комбинирование условий —
must,filter,shouldвнутриbool - Точные совпадения и фильтры: term, range, exists — запросы, которые используются в
post_filterи агрегацииfilter - Типы полей: text, keyword, числа и даты — почему агрегации
termsиrangeтребуют нужного типа поля