Настройка Elasticsearch для фасетного поиска (Aggregations)

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Настройка Elasticsearch для фасетного поиска (Aggregations)
Сложная
~3-5 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Настройка Elasticsearch для фасетного поиска (Aggregations)

Фасетный поиск — это фильтры-счётчики сбоку от результатов: «Бренд: Samsung (143), Apple (89), Lenovo (56)», «Цена: до 30 000 (234), 30–60 000 (145)». При выборе фасета список результатов сужается, остальные счётчики пересчитываются. В реляционных базах это дорогие GROUP BY с COUNT(*) по отфильтрованной выборке. В Elasticsearch это решается агрегациями, часто за единственный запрос.

Базовые агрегации для фасетов

Типичный запрос для каталога товаров: поисковый запрос + активные фильтры + агрегации для подсчёта фасетов.

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
}

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 брендов»:

"aggs": {
  "unique_brands": {
    "cardinality": {
      "field": "brand",
      "precision_threshold": 100
    }
  }
}

precision_threshold — точность HyperLogLog алгоритма. Значения до precision_threshold — точные. Выше — с погрешностью ~1–6%. Для UI достаточно 100.

Реализация на стороне приложения

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 агрегация с большим size — дорогая операция: каждый шард возвращает top-N значений, координирующий узел мержит результаты. Для высококардинальных полей (тысячи уникальных значений) производительность падает.

Способы оптимизации:

execution_hint: map — для полей с малым числом уникальных значений (< 1000) работает быстрее стандартного ordinals:

"terms": {
  "field": "status",
  "size": 10,
  "execution_hint": "map"
}

Кэширование агрегаций — агрегации без поискового запроса (чистые фильтры) кэшируются в shard request cache. Для страниц каталога без поиска — работает быстро из кэша.

Глубокая пагинацияfrom + size дороже для агрегаций, чем для hits. Для фасетов пагинация обычно не нужна — показываем top-20 значений.

Сроки

Реализация базового фасетного поиска с 3–5 типами агрегаций — 2 рабочих дня. Сложный сценарий с post_filter, global aggregation и nested атрибутами — 3–4 дня. Оптимизация производительности агрегаций на реальных объёмах (>5 млн документов) — дополнительный день.