Настройка Elasticsearch для фасетного поиска с агрегациями

Сложно найти нужный товар в каталоге, когда фильтры показывают неточные счётчики и замедляют поиск. Мы настраиваем Elasticsearch для фасетного поиска с корректными агрегациями, чтобы клиенты мгновенно находили нужное. Наша команда реализует проект под ключ — от аудита до внедрения и последующей поддержки.

Разработка и обслуживание любых видов сайтов:

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

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

Настройка Elasticsearch для фасетного поиска с агрегациями

Бэкенд-разработка: Laravel, Node.js, Django, Rails, GoСложный~3-5 дней
Часто задаваемые вопросы

Настройка 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 шагов

  1. Определите поля для фильтрации: бренд, цена, категория, рейтинг, атрибуты. Типичный набор — 5–10 полей.
  2. Настройте маппинг: для ключевых полей используйте тип keyword, для вложенных — nested. Убедитесь, что поля для агрегаций не анализируются.
  3. Постройте запрос с агрегациями: включите все нужные агрегации (terms, range, histogram, nested). Для корректных счётчиков используйте post_filter и global агрегации.
  4. Примените post_filter: отделите фильтры от основного query. Это сохранит счётчики агрегаций.
  5. Оптимизируйте производительность: используйте 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 проектов с фасетным поиском, гарантируя точность счётчиков и производительность. Свяжитесь с нами для консультации — поможем избежать дорогих ошибок. Стоимость рассчитывается индивидуально, а экономия от внедрения может достигать $360–520 на этапе разработки и до $180–260 в месяц на инфраструктуре. Получите готовое решение с полной документацией и поддержкой.

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1543
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1394
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1374
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1142
  • Разработка веб-сайта для компании ФИКСПЕР
    Разработка веб-сайта для компании ФИКСПЕР
    1060