Search code examples
elasticsearchelasticsearch-painless

How to use for loop exactly in script plainess elasticsearch?


I using elasticsearch version 8.10 . I'm writing a script to check input field 'seniority' have enough condition to search result.

Here is demo my index:

PUT target_index
{
  "mappings": {
    "properties": {
      "targetoperator": { "type": "keyword" },
      "targetvalue": { 
        "type": "float"
      }
    }
  }
}

After, I create a document above:

PUT target_index/_doc/1
{
  "targetoperator": [">"],
  "targetvalue": [4]
}

I run script above:

GET target_index/_search
{
  "query": {
    "script": {
      "script": { 
        "lang": "painless",
        "source": """
          double userCriteria = params.userTarget.seniority;
          if (doc['targetoperator'].length > 0) {
            for (int i = 0; i < doc['targetoperator'].length; ++i) {
              if (doc['targetoperator'][i] == "<=") {
                if (!(userCriteria <= doc['targetvalue'][i])) {
                  return false;
                }
              } else if (doc['targetoperator'][i] == ">") {
                if (!(userCriteria > doc['targetvalue'][i])) {
                  return false;
                }
              } else if (doc['targetoperator'][i] == "==") {
                if (!(userCriteria == doc['targetvalue'][i])) {
                  return false;
                }
              }
            }
          }
          return true;
        """, 
        "params": {
          "userTarget": {
            "gender": "male",
            "seniority": 5
          }
        }
      }
    }
  },
  "_source": [
    "targetoperator",
    "targetvalue"
  ]
}

After run query, I found result return about document = 1. But I update document = 1 again:

PUT target_index/_doc/1
{
  "targetoperator": [">", "<="],
  "targetvalue": [4, 8]
}

I run again script query. No matching results were found.

If this correct, it will return document = 1 because seniority = 5 > 4 and 5 <= 8


Solution

  • There are a few issues here.

    Since you need AND logic between all your conditions, you need to change your script to not return true/false immediately, but instead store that into a variable (e.g. result), like this:

    GET target_index/_search
    {
      "query": {
        "script": {
          "script": { 
            "lang": "painless",
            "source": """
              double userCriteria = params.userTarget.seniority;
              boolean result = true;
              if (doc['targetoperator'].length > 0) {
                for (int i = 0; i < doc['targetoperator'].length; ++i) {
                  if (doc['targetoperator'][i] == "<=") {
                    if (!(userCriteria <= doc['targetvalue'][i])) {
                      result = result && false;
                    }
                  } else if (doc['targetoperator'][i] == ">") {
                    if (!(userCriteria > doc['targetvalue'][i])) {
                      result = result && false;
                    }
                  } else if (doc['targetoperator'][i] == "==") {
                    if (!(userCriteria == doc['targetvalue'][i])) {
                      result = result && false;
                    }
                  }
                }
              }
              return result;
            """, 
            "params": {
              "userTarget": {
                "gender": "male",
                "seniority": 5
              }
            }
          }
        }
      },
      "_source": [
        "targetoperator",
        "targetvalue"
      ]
    }
    

    However, we are not done yet, since your target* doc values arrays are not stored the way you have specified, but in lexicographical order.

    Running the following query shows that:

    GET target_index/_search
    {
      "docvalue_fields": ["targetoperator", "targetvalue"]
    }
    

    Returns

        "fields": {
          "targetvalue": [
            4,
            8
          ],
          "targetoperator": [
            "<=",
            ">"
          ]
        }
    

    As you can see the operators are not in the same order, i.e. <= comes before >. Hence, even with the modified logic it's not going to work.

    You need to store your conditions differently, using an object array would suffer from the same issue as objects fields are flattened. Using nested helps, but the script would be evaluated in the context of each single nested condition and you would need some post-processing logic to make sure that all your conditions match, i.e. in this case, you'd need to verify in your application code that the nested matching doc_count is 2.

    Aside from the fact that using scripting is discouraged because of performance issues, I'd strongly suggest you revise the way you want to apply your conditions. I can suggest using search templates, which can help you build very powerful queries.