Sort both hits and inner hits

Hello. I'm using Elasticsearch 8.4.3 and I need help to sort both the hits and inner hits.

My data set is a list of product types, each containing a list of products. Each product then contains a price object with fields such as day_price and day_price_week.

My goal is to sort the product types by cheapest product, based on a number_of_days parameter, and calculated by a script.

So far, I managed to sort the product types, but I don't know to sort the products inner hits using the same script, or at least to mention which product was the cheapest in the results.

I set anonymous public read access for my index, so feel free to test the following examples: https://skoon-search-poc.es.westeurope.azure.elastic-cloud.com/test05

Here are the relevant mappings of my index:

PUT https://skoon-search-poc.es.westeurope.azure.elastic-cloud.com/test05/_search
{
  "mappings": {
    "properties": {
      "products": {
        "type": "nested",
        "properties": {
          "price": {
            "type": "nested",
            "properties": {
              "day_price": {
                "type": "scaled_float",
                "scaling_factor": 100
              },
              "day_price_week": {
                "type": "scaled_float",
                "scaling_factor": 100
              },
              "day_price_four_weeks": {
                "type": "scaled_float",
                "scaling_factor": 100
              }
            }
          }
        }
      }
    }
  }
}

Here's an example of my data, 2 product types, each containing 2 products, and they are not already sorted by cheapest product:

[
  {
    "id": 1,
    "product_type_name": "Battery 1",
    "products": [
      {
        "product_name": "Battery 1 in Eindhoven",
        "price": {
          "day_price": 12,
          "day_price_week": 10,
          "day_price_four_weeks": 8
        }
      },
      {
        "product_name": "Battery 1 in Delft",
        "price": {
          "day_price": 10,
          "day_price_week": 8,
          "day_price_four_weeks": 6
        }
      }
    ]
  },
  {
    "id": 2,
    "product_type_name": "Battery 2",
    "products": [
      {
        "product_name": "Battery 2 in Amsterdam",
        "price": {
          "day_price": 8,
          "day_price_week": 6,
          "day_price_four_weeks": 4
        }
      },
      {
        "product_name": "Battery 2 in Utrecht",
        "price": {
          "day_price": 5,
          "day_price_week": 4,
          "day_price_four_weeks": 3
        }
      }
    ]
  }
]

When sorting by cheapest product, my expectation is that "Battery 2" should appear first, and inside the Battery 2 product type, the product "Battery 2 in Utrecht" should be first, because its day_price is cheaper (see the script below). And for "Battery 1", "Battery 1 in Delft" should be first because it's also cheaper than the other one.

I have 2 partial solutions, one using sort, and another using _score.


Partial solution 1: I can sort the ProductTypes, but I can't re-sort the inner hits with the same script (it doesn't seem to be supported within the inner_hits), and I can't know which Product was the cheapest without reverse-engineering the sort value from the results.

GET https://skoon-search-poc.es.westeurope.azure.elastic-cloud.com/test05/_search
{
  "query": {
    "nested": {
      "path": "products",
      "query": {
        "match_all": {}
      },
      "inner_hits": {
        // TODO: can't sort the inner hits with script
      }
    }
  },
  "sort": [
    {
      "_script": {
        "nested": {
          "path": "products.price"
        },
        "type": "number",
        "order": "asc",
        "script": {
          "lang": "painless",
          "source": """
            if (params["number_of_days"] >= 28) {
              return params["number_of_days"] * doc['products.price.day_price_four_weeks'].value;
            }

            if (params["number_of_days"] >= 7) {
              return params["number_of_days"] * doc['products.price.day_price_week'].value;
            }

            return params["number_of_days"] * doc['products.price.day_price'].value;
          """,
          "params": {
            "number_of_days": 4
          }
        }
      }
    }
  ]
}

Partial solution 2: I'm replacing the _score by my own script calculation, and then trying to sort both the product types and the products by _score ascending. The inner_hits are sorted properly, but the sort value for the product types is an average of the scores of the inner_hits. I want it to be a minimum instead, but "mode": "min" is not accepted for sorting by _score, even though the code completion suggests it.

GET https://skoon-search-poc.es.westeurope.azure.elastic-cloud.com/test05/_search
{
  "query": {
    "nested": {
      "path": "products.price",
      "query": {
        "function_score": {
          "script_score": {
            "script": {
              "lang": "painless",
              "source": """
                if (params["number_of_days"] >= 28) {
                  return params["number_of_days"] * doc['products.price.day_price_four_weeks'].value;
                }

                if (params["number_of_days"] >= 7) {
                  return params["number_of_days"] * doc['products.price.day_price_week'].value;
                }

                return params["number_of_days"] * doc['products.price.day_price'].value;
              """,
              "params": {
                "number_of_days": 4
              }
            }
          },
          "boost_mode": "replace"
        }
      },
      "inner_hits": {
        "sort": [
          {
            "_score": {
              "order": "asc"
            }
          }
        ]
      }
    }
  },
  "sort": [
    {
      "_score": {
        "order": "asc"
        // "mode": "min" TODO: mode doesn't work on _score
      }
    }
  ]
}

Thank you in advance for your help.

I fixed it by adding the score_mode field. The auto-completion doesn't include the min value, but the documentation mentions it, and it works:

{
  "query": {
    "nested": {
      "path": "products.price",
      "query": {
        "function_score": {
          "script_score": {
            "script": {
              "lang": "painless",
              "source": """
              if (params["number_of_days"] >= 28) {
                return params["days"] * doc['products.price.day_price_four_weeks'].value;
              }

              if (params["number_of_days"] >= 7) {
                return params["days"] * doc['products.price.day_price_week'].value;
              }

              return params["number_of_days"] * doc['products.price.day_price'].value;
              """,
              "params": {
                "number_of_days": 4
              }
            }
          },
          "boost_mode": "replace"
        }
      },
      "inner_hits": {
        "sort": [
          {
            "_score": {
              "order": "asc"
            }
          }
        ]
      },
      "score_mode": "min" // <<< this fixed it
    }
  },
  "sort": [
    {
      "_score": {
        "order": "asc"
      }
    }
  ]
}

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.