How to Keep Selected Filter Counts Persistent While Updating Other Aggregations in Elasticsearch?

Title: How to Keep Selected Filter Counts Persistent While Updating Other Aggregations in Elasticsearch?

Body:

I'm working on an Elasticsearch-based search and filtering system where we use aggregations to display facet counts for different filter categories. Currently, Elasticsearch dynamically updates all filter counts based on the query results, but we need to modify this behavior.

Expected Behavior:

When a filter is applied to a category (e.g., Segment), the counts for all filters within Segment should remain unchanged, while counts for other filter categories (e.g., Products and Methods) should update dynamically.

Example:

You can see a similar behavior on this website: Titan Watches Filter

  • Click on "SHOW FILTERS"
  • Apply any filter from "Strap Material"
  • The count for Strap Material options remains the same, while counts for other categories (like Brand or Color) change accordingly.

Current Approach:

Right now, we're using Elasticsearch aggregations, but they automatically recalculate counts for all filters based on the query results. We need a way to persist the counts of the selected category while dynamically updating others.

Questions:

  1. Is there a built-in way in Elasticsearch to achieve this behavior?
  2. Would post-filtering or using global aggregations be a potential approach?
  3. Does anyone have an example of how to modify Elasticsearch queries to achieve this behavior?

Any insights, example queries, or workarounds would be really helpful!

@random_user_qna Welcome to the Elastic community!!

I think aggs with global aggregation is good enough. Here is the quick poc

POST _bulk
{ "index": { "_index": "watches", "_id": 1 } }
{ "name": "Titan Leather Watch", "gender": "Men", "brand": "Titan", "strap_material": "Leather", "price": 2500 }
{ "index": { "_index": "watches", "_id": 2 } }
{ "name": "Sonata Plastic Watch", "gender": "Women", "brand": "Sonata", "strap_material": "Plastic", "price": 1800 }
{ "index": { "_index": "watches", "_id": 3 } }
{ "name": "Fastrack Silicone Watch", "gender": "Unisex", "brand": "Fastrack", "strap_material": "Silicone", "price": 2200 }
{ "index": { "_index": "watches", "_id": 4 } }
{ "name": "Kenneth Cole Bimetal Watch", "gender": "Couple", "brand": "Kenneth Cole", "strap_material": "Bimetal", "price": 7500 }
{ "index": { "_index": "watches", "_id": 5 } }
{ "name": "Tommy Hilfiger Leather Watch", "gender": "Girls", "brand": "Tommy Hilfiger", "strap_material": "Leather", "price": 6500 }
{ "index": { "_index": "watches", "_id": 6 } }
{ "name": "SF 18 Karat Gold Watch", "gender": "Guys", "brand": "SF", "strap_material": "18 Karat Gold", "price": 10500 }
{ "index": { "_index": "watches", "_id": 7 } }
{ "name": "Zoop Acetate Watch", "gender": "Kids", "brand": "Zoop", "strap_material": "Acetate", "price": 1200 }
{ "index": { "_index": "watches", "_id": 8 } }
{ "name": "Titan Acetate & Metal Watch", "gender": "Men", "brand": "Titan", "strap_material": "Acetate & Metal", "price": 8000 }
{ "index": { "_index": "watches", "_id": 9 } }
{ "name": "Sonata Plastic Strap Watch", "gender": "Women", "brand": "Sonata", "strap_material": "Plastic", "price": 2100 }
{ "index": { "_index": "watches", "_id": 10 } }
{ "name": "Fastrack Leather Watch", "gender": "Unisex", "brand": "Fastrack", "strap_material": "Leather", "price": 3500 }



GET watches/_search
{
  "size": 0,
  "query": {
    "match": {
      "name": "Titan"
    }
  },
  "aggs": {
    "genders": {
      "terms": {
        "field": "gender.keyword"
      }
    },
    "brands": {
      "terms": {
        "field": "brand.keyword"
      }
    },
    "strap_materials": {
      "global": {},
      "aggs": {
        "materials": {
          "terms": {
            "field": "strap_material.keyword"
          }
        }
      }
    }
  }
}

Response

{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  },
  "aggregations": {
    "genders": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "Men",
          "doc_count": 2
        }
      ]
    },
    "brands": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "Titan",
          "doc_count": 2
        }
      ]
    },
    "strap_materials": {
      "doc_count": 10,
      "materials": {
        "doc_count_error_upper_bound": 0,
        "sum_other_doc_count": 0,
        "buckets": [
          {
            "key": "Leather",
            "doc_count": 3
          },
          {
            "key": "Plastic",
            "doc_count": 2
          },
          {
            "key": "18 Karat Gold",
            "doc_count": 1
          },
          {
            "key": "Acetate",
            "doc_count": 1
          },
          {
            "key": "Acetate & Metal",
            "doc_count": 1
          },
          {
            "key": "Bimetal",
            "doc_count": 1
          },
          {
            "key": "Silicone",
            "doc_count": 1
          }
        ]
      }
    }
  }
}

Here the strap_material facets remains unchanged.

Hey @ashishtiwari1993
I am more or less in the same case on my side, we use AppSearch though.
I would like to make sure to keep the facets when filtering, in the case of an e-commerce it is very important for us.

For example, a list of clothes with returned facets: Size, Color

Without filters, all the facets are displayed, but if the user comes to select the size M for example, the other sizes are no longer returned by the API.

To solve this issue at the moment, I make several requests, but I would like to find a more efficient way to do it, because the impact is felt on the performances