Налаштування 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