elasticsearchmustache

Control sections of Elastic mustache template logic


I have an Elastic mustache template which looks like the following:

PUT /_scripts/get_content_thin
{
  "script": {
    "lang": "mustache",
    "source": {
      "_source": [
        "type",
        "code",
        "name",
        "brands"
      ],
      "query": {
        "bool": {
          "must": [
            {
              "terms": {
                "code": ["{{#codes}}","{{.}}","{{/codes}}"]
              }
            },
            {
              "term": {
                "type": {
                  "value": "{{type}}"
                }
              }
            },
            {{#brand}}
            {
              "term": {
                "brands": "{{brand}}"
              }
            },
            {{/brand}}
          ]
        }
      },
      "size": "{{size}}{{^size}}10{{/size}}"
  }
}

This would be called via the following:

POST /content/_search/template
{
  "id": "get_content_thin",
  "params": {
    "codes": ["123456","654321"],
    "size": 250,
    "type": "football-club"
  }
}

As you can see here, I'm passing in a codes, size, and type params but no brands param.

However, the template is not valid JSON and therefore it cannot be inserted into the templates.

Is there a way to achieve what I'm looking for where, if brand is sent in (as a string) then the template will evaluate the "term": { "brands": "{{brand}}" } condition, and if it isn't sent in, then Elastic will not use that terms property?

My understanding of inverted sections seems to be correct, but I think this conflicts with the Elastic requirement that the incoming template must be valid JSON.

All I can see in Elastic's documentation is that only the value can be in a string (like the size query param above), rather than an entire object.

Many thanks in advance for any guidance.

Edit: As noted in the accepted answer, there is an end curly bracket (}) missing in the script template. This was a copy error which I will leave in place for posterity, lest the accepted answer cause confusion.

Answer: Below is what I have used from the accepted answer from Julian:

PUT _scripts/get_content_thin
{
  "script": {
    "lang": "mustache",
    "source": """{
      "_source": [
        "type",
        "code",
        "name",
        "brands"
      ],
      "query": {
        "bool": {
          "must": [
            {
              "terms": {
                "code": {{#toJson}}codes{{/toJson}}
              }
            },
            {{#brand}}
            {
              "term": {
                "brands": "{{brand}}"
              }
            },
            {{/brand}}
            {
              "term": {
                "type": {
                  "value": "{{type}}"
                }
              }
            }
          ]
        }
      },
      "size": "{{size}}{{^size}}10{{/size}}"
    }"""
  }
}

Solution

  • I see four problems with your template:

    1. The {{#brand}} and {{/brand}} tags appear outside of any string, which is not allowed by the JSON syntax.
    2. You have a trailing comma at the end of the query.bool.must array, irrespective of whether brand is present or not.
    3. {{#codes}} and {{/codes}} appear in separate strings. Either, Elasticsearch's templating mechanism is not going to recognize this and blow up (once you make it past the invalid JSON), or it will silently insert empty strings at the start and end of the query.bool.must[0].terms.code array, which is probably not what you want, either.
    4. You are missing a closing curly brace for the script.source.

    The Elasticsearch documentation page that you linked to, holds some of the answers:

    These ingredients are sufficient to solve problems 1 and 3. For problem 2, we can simply drop the trailing commas and use an initial comma in the brand section instead. Problem 4 is trivial.

    Bringing this all together, this should work:

    PUT /_scripts/get_content_thin
    {
      "script": {
        "lang": "mustache",
        "source": """{
          "_source": [
            "type",
            "code",
            "name",
            "brands"
          ],
          "query": {
            "bool": {
              "must": [
                {
                  "terms": {
                    "code": {{#toJson}}codes{{/toJson}}
                  }
                },
                {
                  "term": {
                    "type": {
                      "value": "{{type}}"
                    }
                  }
                }{{#brand}},
                {
                  "term": {
                    "brands": "{{brand}}"
                  }
                }{{/brand}}
              ]
            }
          },
          "size": "{{size}}{{^size}}10{{/size}}"
        }"""
      }
    }
    

    Do keep in mind that the way Elasticsearch implements lambdas is nonstandard. In general, lambdas cannot access context values in the way displayed here, nor do they accept arguments in the way illustrated in the join documentation ({{#join delimiter='||'}}). That being said, I was able to make the source template work in the Mustache playground by fiddling a bit with the input data. You will see that there is no trailing comma, whether you remove the brand from the input data or not. Copy-paste this savestate into its load/store box:

    {"data":{"text":"  {\n    \"codes\": [\"123456\",\"654321\"],\n    \"size\": 250,\n    \"type\": \"football-club\",\n    \"brand\": 'x',\n    toJson(param) {\n      return JSON.stringify(this[param]);\n    }\n  }\n"},"templates":[{"name":"source","text":"    {\n      \"_source\": [\n        \"type\",\n        \"code\",\n        \"name\",\n        \"brands\"\n      ],\n      \"query\": {\n        \"bool\": {\n          \"must\": [\n            {\n              \"terms\": {\n                \"code\": {{#toJson}}codes{{/toJson}}\n              }\n            },\n            {\n              \"term\": {\n                \"type\": {\n                  \"value\": \"{{type}}\"\n                }\n              }\n            }{{#brand}},\n            {\n              \"term\": {\n                \"brands\": \"{{brand}}\"\n              }\n            }{{/brand}}\n          ]\n        }\n      },\n      \"size\": \"{{size}}{{^size}}10{{/size}}\"\n    }"}]}