Как построить фасетный поиск с помощью счетчиков фасетов?

{
   "query": {
      "and": [
          {
              "terms": {"country": ["be", "fr"]}             
          },
          {
              "terms": {"category": ["books", "movies"]}
          }
      ]
   }
}

Для счетчиков мы можем использовать встроенные агрегаты из Elasticsearch. Каждый из двух фасетов хранится как одно поле в индексе, поэтому мы можем использовать агрегирование терминов в каждом из этих полей. Агрегация вернет счетчик за значение этого поля.

{
   "query": {
      "and": [
          {
              "terms": {"country": ["be", "fr"]}             
          },
          {
              "terms": {"category": ["books", "movies"]}
          }
      ]
   },
   "aggregations": {
      "countries": {
         "terms": {"field": "country"}
      },
      "categories": {
         "terms": {"field": "category"}
      }
   }
}

Если бы вы выполнили этот запрос, вы заметите, что счетчики отключены. В двух не отобранных странах, в Португалии и Бразилии, есть счетчик 0. Хотя есть фактические результаты, если мы хотим их выбрать (из-за ORвнутренней грани). Это происходит потому, что по умолчанию Elasticsearch выполняет свои агрегирования в результирующем наборе. Это означает, что если вы выберете Францию, фильтры другой страны будут иметь счет 0, потому что в результирующем наборе содержатся только элементы из Франции.

Чтобы исправить это, нам нужно дать команду Elasticsearch выполнить агрегацию во всем наборе данных, игнорируя запрос. Мы можем сделать это, определив наши скопления как глобальные .

{
    "query": {
        "and": [
            {
                "terms": {"country": ["be", "fr"]}             
            },
            {
                "terms": {"category": ["books", "movies"]}
            }
        ]
   },
   "aggregations": {
      "all_products": {
         "global": {},
         "aggregations": {
            "countries": {
               "terms": {"field": "country"}
            },
            "categories": {
               "terms": {"field": "category"}
            }
         }
      }
   }
}

Если бы мы просто сделали это, наши счетчики всегда были бы одинаковыми, потому что они всегда будут рассчитывать на весь набор данных, независимо от наших фильтров. Наши агрегаты должны стать немного более сложными, чтобы это работало, нам нужно добавить к ним фильтры. Каждое агрегирование должно рассчитывать на набор данных со всеми применяемыми фильтрами, за исключением собственных. Таким образом, агрегация за счет во Франции рассчитывает на набор данных с применением фильтра категории, но не фильтр стран:

{
   "query": {
       "and": [
           {
               "terms": {"country": ["be", "fr"]}             
           },
           {
               "terms": {"category": ["books", "movies"]}
           }
       ]
   },
   "aggregations": {
      "all_products": {
         "global": {},
         "aggregations": {
            "countries": {
               "filter": {
                  "and": [
                     {
                        "terms": {"category": ["books","movies"]}
                     }
                  ]
               },
               "aggregations": {
                  "filtered_countries": {
                     "terms": {"field": "country"}
                  }
               }
            },
            "categories": {
               "filter": {
                  "and": [
                     {
                        "terms": {"country": ["be","fr"]}
                     }
                  ]
               },
               "aggregations": {
                  "filtered_categories": {
                     "terms": {"field": "category"}
                  }
               }
            }
         }
      }
   }
}

Вывод

{
   "took": 153,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "failed": 0
   },
   "hits": {
      "total": 3,
      "max_score": 0,
      "hits": ["..."]
   },
   "aggregations": {
      "all_products": {
         "doc_count": 21,
         "filterted categories": {
            "doc_count": 13,
            "categories": {
               "doc_count_error_upper_bound": 0,
               "sum_other_doc_count": 0,
               "buckets": [
                  {
                     "key": "movies",
                     "doc_count": 6
                  },
                  {
                     "key": "music",
                     "doc_count": 4
                  },
                  {
                     "key": "books",
                     "doc_count": 3
                  }
               ]
            }
         },
         "filtered_countries": {
            "doc_count": 15,
            "countries": {
               "doc_count_error_upper_bound": 0,
               "sum_other_doc_count": 0,
               "buckets": [
                  {
                     "key": "fr",
                     "doc_count": 6
                  },
                  {
                     "key": "br",
                     "doc_count": 4
                  },
                  {
                     "key": "be",
                     "doc_count": 3
                  },
                  {
                     "key": "pt",
                     "doc_count": 2
                  }
               ]
            }
         }
      }
   }
}

Yii2 framework

Применяется модуль: https://github.com/Mirocow/yii2-elasticsearch

$terms = QueryHelper::terms('categories.name', 'my category');
 
$nested[] = QueryHelper::nested('string_facet',
    QueryHelper::filter([
        QueryHelper::term('string_facet.facet_name', ['value' => $id, 'boost' => 1]),
        QueryHelper::term('string_facet.facet_value', ['value' => $value, 'boost' => 1]),
    ])
);
$filter[] = QueryHelper::should($nested);