Настройка Elasticsearch для фасетного поиска с агрегациями
Представьте каталог с миллионом товаров. Без фасетного поиска пользователь листает 200 страниц. С ним — за секунду находит нужный бренд и ценовой диапазон. Но счётчики врут, если не настроить post_filter и global aggregations. В этой статье разберём, как собрать корректные фасетные счётчики на Elasticsearch, избежав типовых ошибок. Экономия времени разработки при использовании готового решения — до 30%.
Мы интегрируем Elasticsearch с фасетным поиском в каталогах и интернет-магазинах. Это фильтры-счётчики: «Бренд: Samsung (143), Apple (89)», «Цена: до 30 000 (234), 30–60 000 (145)». При выборе фильтра список товаров сужается, а счётчики других значений пересчитываются. В реляционных базах такие операции дороги — GROUP BY с COUNT(*) по отфильтрованной выборке. В Elasticsearch задача решается агрегациями часто за единственный запрос. Оценим ваш проект и реализуем под ключ за 2–4 рабочих дня.
Как реализовать корректные счётчики фасетов?
Типичный запрос для каталога: поисковый запрос + активные фильтры + агрегации для подсчёта фасетов. Пример базового запроса:
POST /products/_search { "query": { "bool": { "must": [ { "match": { "title": "ноутбук" } } ], "filter": [ { "term": { "is_active": true } }, { "range": { "price": { "gte": 20000, "lte": 80000 } } } ] } }, "aggs": { "by_brand": { "terms": { "field": "brand", "size": 20, "order": { "_count": "desc" } } }, "price_ranges": { "range": { "field": "price", "ranges": [ { "key": "до 30 000", "to": 30000 }, { "key": "30–60 000", "from": 30000, "to": 60000 }, { "key": "60–100 000", "from": 60000, "to": 100000 }, { "key": "от 100 000", "from": 100000 } ] } }, "price_stats": { "stats": { "field": "price" } }, "by_category": { "terms": { "field": "category", "size": 10 } }, "by_rating": { "histogram": { "field": "rating", "interval": 1, "min_doc_count": 1 } } }, "size": 20, "from": 0 } Проблема исчезающих счётчиков при выборе фасета
Если пользователь выбирает бренд «Samsung» и добавляет его в filter, агрегация by_brand считает только внутри отфильтрованной выборки. Счётчики других брендов обнуляются — пользователь не видит, сколько товаров у других брендов. Решение — Post Filter в сочетании с Global Aggregation:
POST /products/_search { "query": { "bool": { "must": [ { "match": { "title": "ноутбук" } } ], "filter": [ { "term": { "is_active": true } } ] } }, "post_filter": { "bool": { "filter": [ { "term": { "brand": "Samsung" } } ] } }, "aggs": { "all_brands": { "global": {}, "aggs": { "filtered_brands": { "filter": { "bool": { "must": [ { "match": { "title": "ноутбук" } } ], "filter": [ { "term": { "is_active": true } } ] } }, "aggs": { "brands": { "terms": { "field": "brand", "size": 20 } } } } } }, "price_ranges": { "range": { "field": "price", "ranges": [ { "key": "до 30 000", "to": 30000 }, { "key": "30–60 000", "from": 30000, "to": 60000 }, { "key": "от 60 000", "from": 60000 } ] } } }, "size": 20 } Согласно документации Elasticsearch, post_filter применяется к результатам после агрегации, а global агрегация выходит за пределы контекста query. post_filter применяется к результатам после агрегации. Агрегации считаются по всей выборке (до post_filter), поэтому счётчики брендов остаются корректными. global агрегация выходит за пределы контекста query — позволяет считать агрегаты по всем документам индекса с дополнительными фильтрами.
Как работать с вложенными атрибутами?
Если атрибуты (размер, цвет, материал) хранятся как nested объекты:
"mappings": { "properties": { "attributes": { "type": "nested", "properties": { "name": { "type": "keyword" }, "value": { "type": "keyword" } } } } } Агрегация по вложенным атрибутам:
"aggs": { "attributes": { "nested": { "path": "attributes" }, "aggs": { "attribute_names": { "terms": { "field": "attributes.name", "size": 50 }, "aggs": { "attribute_values": { "terms": { "field": "attributes.value", "size": 20 } } } } } } } Это двухуровневые вложенные агрегации: сначала группируем по имени атрибута («Цвет», «Размер»), внутри каждой группы — значения («Чёрный», «Белый»).
Cardinality — подсчёт уникальных значений
Для отображения «Найдено 1 247 товаров от 89 брендов» используйте агрегацию cardinality с полем brand и precision_threshold 100. Это гарантирует точность до 100 уникальных значений, выше — погрешность 1–6%.
Настройка фасетного поиска за 5 шагов
- Определите поля для фильтрации: бренд, цена, категория, рейтинг, атрибуты. Типичный набор — 5–10 полей.
- Настройте маппинг: для ключевых полей используйте тип
keyword, для вложенных —nested. Убедитесь, что поля для агрегаций не анализируются. - Постройте запрос с агрегациями: включите все нужные агрегации (terms, range, histogram, nested). Для корректных счётчиков используйте
post_filterиglobalагрегации. - Примените post_filter: отделите фильтры от основного query. Это сохранит счётчики агрегаций.
- Оптимизируйте производительность: используйте
execution_hint: mapдля полей с низкой кардинальностью, кэширование для агрегаций без поискового запроса, ограничьте size до 20.
Оптимизация агрегаций
terms агрегация с большим size — дорогая операция: каждый шард возвращает top-N значений, координирующий узел мержит результаты. Для высококардинальных полей (тысячи уникальных значений) производительность падает. Способы оптимизации:
Таблица методов оптимизации
| Метод | Описание | Когда применять |
|---|---|---|
| execution_hint: map | Для полей с низкой кардинальностью (<1000) работает быстрее ordinals | Поля типа статус, категория |
| Кэширование агрегаций | Агрегации без поискового запроса кэшируются в shard request cache | Страницы каталога без поиска |
| Ограничение size | Для фасетов достаточно top-20 значений; глубокая пагинация не нужна | Всегда |
Сравнение типов агрегаций
| Тип | Пример использования | Производительность |
|---|---|---|
| Terms | Группировка по бренду | Быстро при кардинальности <10k |
| Range | Ценовые диапазоны | Очень быстро |
| Histogram | Рейтинг с шагом 1 | Быстро |
| Nested | Вложенные атрибуты | Средне (зависит от кол-ва объектов) |
| Cardinality | Уникальные бренды | Быстро (алгоритм HyperLogLog) |
Реализация на стороне приложения
PHP класс для построения фасетных запросов:
class FacetedSearchService { public function search(array $params): array { $query = $this->buildQuery($params); $response = $this->client->search([ 'index' => 'products', 'body' => $query, ]); return [ 'hits' => $response['hits']['hits'], 'total' => $response['hits']['total']['value'], 'facets' => $this->extractFacets($response['aggregations']), ]; } private function extractFacets(array $aggs): array { $facets = []; if (isset($aggs['by_brand'])) { $facets['brand'] = array_map(fn($b) => [ 'value' => $b['key'], 'count' => $b['doc_count'], ], $aggs['by_brand']['buckets']); } if (isset($aggs['price_ranges'])) { $facets['price'] = array_map(fn($r) => [ 'label' => $r['key'], 'count' => $r['doc_count'], ], $aggs['price_ranges']['buckets']); } return $facets; } } Что входит в настройку фасетного поиска под ключ
- Проектирование схемы маппингов и индексов.
- Настройка агрегаций всех типов (terms, range, nested, cardinality).
- Реализация логики post_filter + global aggregations для корректных счётчиков.
- Оптимизация производительности (execution_hint, кэширование).
- Написание серверного кода для построения запросов и обработки ответов.
- Документация, обучение команды, поддержка после внедрения.
Сроки
- Базовая реализация (3–5 типов агрегаций) — 2 рабочих дня.
- Сложный сценарий (post_filter, global, nested) — 3–4 дня.
- Оптимизация на реальных объёмах (>5 млн документов) — дополнительный день.
Наша команда имеет 6+ лет опыта в Elasticsearch и сертифицированных инженеров. Мы реализовали более 50 проектов с фасетным поиском, гарантируя точность счётчиков и производительность. Свяжитесь с нами для консультации — поможем избежать дорогих ошибок. Стоимость рассчитывается индивидуально, а экономия от внедрения может достигать 40 000 ₽ на этапе разработки и до 20 000 ₽ в месяц на инфраструктуре. Получите готовое решение с полной документацией и поддержкой.







