How to Decouple Elasticsearch Queries Using Mustache Search Templates

This article explains how to separate Elasticsearch DSL queries from application code by defining reusable search templates with Mustache placeholders, managing them via the scripts API, and rendering them with parameters to produce flexible, maintainable search requests.

System Architect Go
System Architect Go
System Architect Go
How to Decouple Elasticsearch Queries Using Mustache Search Templates

Search Template Overview

A search template is a reusable Mustache script that defines an Elasticsearch query DSL with {{}} placeholders. It separates the DSL definition from application code, allowing the query structure to be changed without modifying the client.

Workflow

Define the DSL structure and reserve placeholder parameters.

Provide concrete values for those parameters at query time.

Render the full DSL and execute the search.

Search Template API

Templates are stored and managed through the _scripts endpoint.

Save a template

POST _scripts/<template_id>
{
  "script": {
    "lang": "mustache",
    "source": {
      "query": {
        "match": {
          "{{my_field}}": "{{my_value}}"
        }
      }
    }
  }
}

Get a template

GET _scripts/<template_id>

Delete a template

DELETE _scripts/<template_id>

Execute a template

Supply the template id and a params object whose keys match the placeholders.

GET _search/template
{
  "id": "<template_id>",
  "params": {
    "my_field": "message",
    "my_value": "foo"
  }
}

The response contains the rendered DSL, e.g.:

{
  "query": {
    "match": {
      "message": "foo"
    }
  }
}

Render a template for inspection

GET _render/template
{
  "source": "{ \"query\": { \"terms\": {{#toJson}}statuses{{/toJson}} } }",
  "params": {
    "statuses": { "status": ["pending", "published"] }
  }
}
{
  "template_output": {
    "query": {
      "terms": {
        "status": ["pending", "published"]
      }
    }
  }
}

Mustache Helpers

{{#toJson}}parameter{{/toJson}}

– renders a parameter as JSON. {{#join}}array{{/join}} – joins an array into a comma‑separated string (default delimiter is ,). {{#join delimiter='||'}}array{{/join}} – joins with a custom delimiter. {{= <% %> =}} – changes the delimiters for a section. {{var}}{{^var}}default{{/var}} – provides a default value when var is missing or falsy. {{#key}}…{{/key}} – renders the block only when key is truthy. {{^key}}…{{/key}} – renders the block only when key is falsy or absent. {{! comment }} – comment that is ignored. {{> partial}} – includes another stored template. {{#url}}value{{/url}} – URL‑encodes the value.

Practical Examples

Simple value substitution

GET _search/template
{
  "source": {
    "query": { "term": { "message": "{{query_string}}" } }
  },
  "params": { "query_string": "search words" }
}

Joining an array

GET _search/template
{
  "source": {
    "query": { "match": { "emails": "{{#join}}emails{{/join}}" } }
  },
  "params": { "emails": ["aaa", "bbb"] }
}

Resulting DSL:

{
  "query": {
    "match": { "emails": "aaa,bbb" }
  }
}

Using toJson for complex objects

GET _search/template
{
  "source": "{ \"query\": { \"terms\": {{#toJson}}statuses{{/toJson}} } }",
  "params": {
    "statuses": { "status": ["pending", "published"] }
  }
}

Conditional sections and defaults

{
  "source": {
    "query": {
      "range": {
        "born": {
          "gte": "{{date.min}}",
          "lte": "{{date.max}}",
          "format": "{{#join delimiter='||'}}date.formats{{/join}}"
        }
      }
    }
  },
  "params": {
    "date": {
      "min": "2016",
      "max": "31/12/2017",
      "formats": ["dd/MM/yyyy", "yyyy"]
    }
  }
}

Rendered format value becomes dd/MM/yyyy||yyyy.

Default numeric values

{
  "source": {
    "query": {
      "range": {
        "line_no": {
          "gte": "{{start}}",
          "lte": "{{end}}{{^end}}20{{/end}}"
        }
      }
    }
  },
  "params": { "start": "10" }
}

If end is omitted, the rendered DSL uses 20 as the upper bound.

URL encoding

GET _render/template
{
  "source": {
    "query": { "term": { "http_access_log": "{{#url}}{{host}}/{{page}}{{/url}}" } }
  },
  "params": { "host": "https://www.elastic.co/", "page": "learn" }
}
{
  "template_output": {
    "query": {
      "term": { "http_access_log": "https%3A%2F%2Fwww.elastic.co%2F%2Flearn" }
    }
  }
}

Mustache Syntax Quick Reference

{{key}}

– inserts the value of key (HTML‑escaped). {{{key}}} or {{&key}} – inserts without escaping. {{#section}}…{{/section}} – renders the block when section is truthy. {{^section}}…{{/section}} – renders the block when section is falsy or missing. {{! comment }} – comment. {{> partial}} – includes another template. {{= <% %> =}} – changes delimiters for the following sections.

Conclusion

Search templates let developers keep application code focused on parameters and results while delegating DSL construction to reusable Mustache scripts, improving maintainability and flexibility of Elasticsearch queries.

backendDSLElasticsearchTemplatingMustacheSearch Template
System Architect Go
Written by

System Architect Go

Programming, architecture, application development, message queues, middleware, databases, containerization, big data, image processing, machine learning, AI, personal growth.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.