Search code examples
ruby-on-railselasticsearchfilteraggregationsearchkick

Elastic search - aggregation filter for product options


I have a products catalogue where every product is indexed as follows (queried from http://localhost:9200/products/_doc/1) as sample:

{
  "_index": "products_20201202145032789",
  "_type": "_doc",
  "_id": "1",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "title": "Roncato Eglo",
    "description": "Amazing LED light made of wood and description continues.",
    "price": 3990,
    "manufacturer": "Eglo",
    "category": [
      "Lights",
      "Indoor lights"
    ],
    "options": [
      {
        "title": "Mount type",
        "value": "E27"
      },
      {
        "title": "Number of bulps",
        "value": "4"
      },
      {
        "title": "Batteries included",
        "value": "true"
      },
      {
        "title": "Ligt temperature",
        "value": "warm"
      },
      {
        "title": "Material",
        "value": "wood"
      },
      {
        "title": "Voltage",
        "value": "230"
      }
    ]
  }
}

Every option contains different value, so there are many Mount type values, Light temperature values, Material values, and so on.

How can I create an aggregation (filter) where I can let customers choose between various Mount Type options:

[ ] E27
[X] E14
[X] GU10
...

Or let them choose from different Material options displayed as checkboxes:

[X] Wood
[ ] Metal
[ ] Glass
...

I can handle it on frontend once the buckets are created. Creation of different buckets for these options is What I am struggling with.

I have succesfully created and displayed and using aggregations for Category, Manufacturer and other basic ones. Thes product options are stored in has_many_through relationships in database. I am using Rails + searchkick gem, but those allow me to create raw queries to elastic search.


Solution

  • The prerequisite for such aggregation is to have options field as nested.

    Sample index mapping:

    PUT test
    {
      "mappings": {
        "properties": {
          "title": {
            "type": "keyword"
          },
          "options": {
            "type": "nested",
            "properties": {
              "title": {
                "type": "keyword"
              },
              "value": {
                "type": "keyword"
              }
            }
          }
        }
      }
    }
    

    Sample docs:

    PUT test/_doc/1
    {
      "title": "Roncato Eglo",
      "options": [
        {
          "title": "Mount type",
          "value": "E27"
        },
        {
          "title": "Material",
          "value": "wood"
        }
      ]
    }
    
    PUT test/_doc/2
    {
      "title": "Eglo",
      "options": [
        {
          "title": "Mount type",
          "value": "E27"
        },
        {
          "title": "Material",
          "value": "metal"
        }
      ]
    }
    

    Assumption: For a given document a title under option appears only once. For e.g. there can exists only one nested document under option having title as Material.

    Query for aggregation:

    GET test/_search
    {
      "size": 0, 
      "aggs": {
        "OPTION": {
          "nested": {
            "path": "options"
          },
          "aggs": {
            "TITLE": {
              "terms": {
                "field": "options.title",
                "size": 10
              },
              "aggs": {
                "VALUES": {
                  "terms": {
                    "field": "options.value",
                    "size": 10
                  }
                }
              }
            }
          }
        }
      }
    }
    

    Response:

    {
      "took" : 2,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 2,
          "relation" : "eq"
        },
        "max_score" : null,
        "hits" : [ ]
      },
      "aggregations" : {
        "OPTION" : {
          "doc_count" : 4,
          "TITLE" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : "Material",
                "doc_count" : 2,
                "VALUES" : {
                  "doc_count_error_upper_bound" : 0,
                  "sum_other_doc_count" : 0,
                  "buckets" : [
                    {
                      "key" : "metal",
                      "doc_count" : 1
                    },
                    {
                      "key" : "wood",
                      "doc_count" : 1
                    }
                  ]
                }
              },
              {
                "key" : "Mount type",
                "doc_count" : 2,
                "VALUES" : {
                  "doc_count_error_upper_bound" : 0,
                  "sum_other_doc_count" : 0,
                  "buckets" : [
                    {
                      "key" : "E27",
                      "doc_count" : 2
                    }
                  ]
                }
              }
            ]
          }
        }
      }
    }