1

The goal is to filter products with multiple prices.

The data looks like this:

{
  "name":"a",
  "price":[
    {
      "membershipLevel":"Gold",
      "price":"5"
    },
    {
      "membershipLevel":"Silver",
      "price":"50"
    },
    {
      "membershipLevel":"Bronze",
      "price":"100"
    }
    ]
}

I would like to filter by membershipLevel and price. For example, if I am a silver member and query price range 0-10, the product should not appear, but if I am a gold member, the product "a" should appear. Is this kind of query supported by Elasticsearch?

Amit
  • 30,756
  • 6
  • 57
  • 88
MiDaa
  • 1,149
  • 1
  • 11
  • 25

3 Answers3

2

You need to make use of nested datatype for price and make use of nested query for your use case.

Please see the below mapping, sample document, query and response:

Mapping:

PUT my_price_index
{
  "mappings": {
    "properties": {
      "name":{
        "type":"text"
      },
      "price":{
        "type":"nested",
        "properties": {
          "membershipLevel":{
            "type":"keyword"
          },
          "price":{
            "type":"double"
          }
        }
      }
    }
  }
}

Sample Document:

POST my_price_index/_doc/1
{
  "name":"a",
  "price":[
    {
      "membershipLevel":"Gold",
      "price":"5"
    },
    {
      "membershipLevel":"Silver",
      "price":"50"
    },
    {
      "membershipLevel":"Bronze",
      "price":"100"
    }
    ]
}

Query:

POST my_price_index/_search
{
  "query": {
    "nested": {
      "path": "price",
      "query": {
        "bool": {
          "must": [
            {
              "term": {
                "price.membershipLevel": "Gold"
              }
            },
            {
              "range": {
                "price.price": {
                  "gte": 0,
                  "lte": 10
                }
              }
            }
          ]
        }
      },
      "inner_hits": {}           <---- Do note this. 
    }
  }
}

The above query means, I want to return all the documents having price.price range from 0 to 10 and price.membershipLevel as Gold.

Notice that I've made use of inner_hits. The reason is despite being a nested document, ES as response would return the entire set of document instead of only the document specific to where the query clause is applicable.

In order to find the exact nested doc that has been matched, you would need to make use of inner_hits.

Below is how the response would return.

Response:

{
  "took" : 128,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.9808291,
    "hits" : [
      {
        "_index" : "my_price_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.9808291,
        "_source" : {
          "name" : "a",
          "price" : [
            {
              "membershipLevel" : "Gold",
              "price" : "5"
            },
            {
              "membershipLevel" : "Silver",
              "price" : "50"
            },
            {
              "membershipLevel" : "Bronze",
              "price" : "100"
            }
          ]
        },
        "inner_hits" : {
          "price" : {
            "hits" : {
              "total" : {
                "value" : 1,
                "relation" : "eq"
              },
              "max_score" : 1.9808291,
              "hits" : [
                {
                  "_index" : "my_price_index",
                  "_type" : "_doc",
                  "_id" : "1",
                  "_nested" : {
                    "field" : "price",
                    "offset" : 0
                  },
                  "_score" : 1.9808291,
                  "_source" : {
                    "membershipLevel" : "Gold",
                    "price" : "5"
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
}

Hope this helps!

Kamal Kunjapur
  • 8,547
  • 2
  • 22
  • 32
1

Let me take show you how to do it, using the nested fields and query and filter context. I will take your example to show, you how to define index mapping, index sample documents, and search query.

It's important to note the include_in_parent param in Elasticsearch mapping, which allows us to use these nested fields without using the nested fields.

Please refer to Elasticsearch documentation about it.

If true, all fields in the nested object are also added to the parent document as standard (flat) fields. Defaults to false.

Index Def

{
    "mappings": {
        "properties": {
            "product": {
                "type": "nested",
                "include_in_parent": true
            }
        }
    }
}

Index sample docs

{
    "product": {
        "price" : 5,
        "membershipLevel" : "Gold"
    }
}
{
    "product": {
        "price" : 50,
        "membershipLevel" : "Silver"
    }
}

{
    "product": {
        "price" : 100,
        "membershipLevel" : "Bronze"
    }
}

Search query to show Gold with price range 0-10

{
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "product.membershipLevel": "Gold"
                    }
                }
            ],
            "filter": [
                {
                    "range": {
                        "product.price": {
                            "gte": 0,
                            "lte" : 10
                        }
                    }
                }
            ]
        }
    }
}

Result

"hits": [
            {
                "_index": "so-60620921-nested",
                "_type": "_doc",
                "_id": "1",
                "_score": 1.0296195,
                "_source": {
                    "product": {
                        "price": 5,
                        "membershipLevel": "Gold"
                    }
                }
            }
        ]

Search query to exclude Silver, with same price range

{
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "product.membershipLevel": "Silver"
                    }
                }
            ],
            "filter": [
                {
                    "range": {
                        "product.price": {
                            "gte": 0,
                            "lte" : 10
                        }
                    }
                }
            ]
        }
    }
}

Above query doesn't return any result as there isn't any matching result.

P.S :- this SO answer might help you to understand nested fields and query on them in detail.

Amit
  • 30,756
  • 6
  • 57
  • 88
  • Thanks for your detailed explanation! However, I am a bit confused by your "Index Sample Docs", are you suggesting to insert the same product multiple times with different price/membership information? (What I understand in the sample is that 3 products are inserted) – MiDaa Mar 11 '20 at 04:44
  • Yeah you are right. These samples are for different products – Amit Mar 11 '20 at 06:02
  • But what I need is one product with multiple price levels, do you mean we duplicate the same product * (num of prices) times? eg: Doc1: {productA,price1}, Doc2:{productA,price2} Doc3:{productA,price3} etc...? – MiDaa Mar 11 '20 at 06:47
  • @MiDaa, give me some time, I will post that in sometime – Amit Mar 11 '20 at 08:35
0

You have to use Nested fields and nested query to archive this: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-nested-query.html

Define you Price property with type "Nested" and then you will be able to filter by every property of nested object

Ihor
  • 299
  • 1
  • 11
  • this can be done with using a nested query, as OP is new to Elasticsearch, it would be difficult for him to understand it, please refer my answer on how to achieve it without nested query, Hint: achieved using `"include_in_parent": true` in nested properties. – Amit Mar 10 '20 at 16:00