separating query logic from parameters with Mustache templates
When the full Elasticsearch query DSL gets assembled directly in application code, search logic ends up scattered across many places in the codebase, and every adjustment to a central query requires changes in multiple spots at once. Search templates solve this by storing a Mustache-based query template once, centrally in Elasticsearch, after which application code only ever passes named parameters without needing to know a single line of query DSL. This moves the actual search logic to where it should technically be managed, and application developers work against a stable, clearly documented interface. How search templates are structured, what advantages this brings for collaboration between search teams and application teams, and what a parametrized product search looks like in practice is what this article covers.
Table of Contents
- 1. The baseline problem: scattered query logic in application code
- 2. Mustache syntax: how placeholders work in the query DSL
- 3. Creating a search template through the _scripts API
- 4. Running a search template
- 5. Benefits for application developers: no query DSL in application code
- 6. Versioning and controlled changes to templates
- 7. Performance: compilation and caching of templates
- 8. Practical example: parametrized product search with defaults
- 9. Pitfalls and recommendations for production use
- 10. Summary
- 11. FAQ
1. The baseline problem: scattered query logic in application code
In many grown applications, the same or a very similar query DSL structure shows up in multiple places in the code, once for the frontend product search, once for an internal search service, and once for a batch export, each with small deviations. When the underlying relevance strategy changes, for instance because additional boosting for current offers gets introduced, all these spots need to be found and adjusted consistently, which in practice regularly leads to inconsistencies.
Search templates move the actual query structure out of application code into a template managed centrally in Elasticsearch. The application calls this template by name and only passes the concrete search parameters, while the entire structure of the query, including boosting rules, filters, and sorting, is maintained in a single place.
2. Mustache syntax: how placeholders work in the query DSL
A search template is, at its core, a regular query DSL structure in which individual values are replaced by Mustache placeholders like {{search_term}}. When the template runs, Elasticsearch substitutes every placeholder with the passed parameter value and evaluates the resulting, complete query exactly like a normal search request. Mustache also supports conditional blocks through {{#parameter}}...{{/parameter}}, which only insert a query component when the corresponding parameter was actually passed.
These conditional blocks are particularly valuable for optional filters: a price filter should only be added to the query when the user actually selected a price range, otherwise the query should run without that filter component. Without Mustache conditionals, the application itself would need to assemble multiple variants of the query, with Mustache a single template covers every combination of set and unset parameters.
3. Creating a search template through the _scripts API
Search templates get stored through the _scripts API, the same API also used for other stored scripts. Every template receives a unique name it is later referenced by, and a source block containing the actual query DSL structure including Mustache placeholders. Optionally, a params block with default values can also be defined, which applies whenever the application does not pass a particular parameter on a given call.
This central storage has a practical side effect: a change to the template takes effect immediately for every application referencing it, without any of those applications needing to be redeployed. For relevance tuning, which typically iterates more often than the actual application code, this is a significant speed advantage.
PUT _scripts/product_search_standard
{
"script": {
"lang": "mustache",
"source": {
"query": {
"bool": {
"must": [
{ "multi_match": { "query": "{{search_term}}", "fields": ["name^3", "description"] } }
],
"filter": [
{{#category}}{ "term": { "category": "{{category}}" } },{{/category}}
{ "range": { "price": { "gte": "{{price_min}}{{^price_min}}0{{/price_min}}", "lte": "{{price_max}}{{^price_max}}100000{{/price_max}}" } } }
]
}
},
"size": "{{page_size}}{{^page_size}}20{{/page_size}}"
},
"params": { "page_size": 20 }
}
}
4. Running a search template
A stored template gets executed through the _search/template endpoint, where the application only passes the id reference to the stored template and a params object with the concrete values. The response matches exactly the response of a regular search request, so existing response processing code can be reused without modification.
For local development and debugging, the _render/template endpoint is helpful, since it merely resolves a template with given parameters and returns the resulting, complete query DSL without actually executing a search. This makes it quick to verify that a template with certain parameters produces the expected query before running it against the index in production.
GET /products/_search/template
{
"id": "product_search_standard",
"params": {
"search_term": "cordless drill",
"category": "power_tools",
"price_max": 89.90
}
}
5. Benefits for application developers: no query DSL in application code
For teams providing a search interface to other application teams, a search template means a clear separation of responsibility: the search or data team maintains relevance, boosting, and filter logic centrally in the template, while application developers only need to know a named, documented parameter list, for instance search term, category, and price range. Changes to the relevance strategy no longer require coordination with every single application team, as long as the parameter interface itself does not change.
This also reduces the risk of faulty query DSL construction in application code, which typically is not specialized in Elasticsearch query syntax. Instead of debugging nested bool structures scattered through application code, a single look at the central template, maintained by people who actually know the query DSL, suffices.
6. Versioning and controlled changes to templates
Since changes to a search template take effect immediately and without deployment for every calling application, a deliberate versioning strategy through the template name itself is advisable, for instance product_search_v2 instead of directly overwriting product_search_standard. This lets new application versions deliberately switch to the new template version, while existing callers keep working unchanged until they are explicitly migrated.
For controlled rollouts, an A/B testing approach is also useful, where a portion of the traffic gets routed to a new template on a trial basis while the majority still uses the proven template. Since both templates are stored independently in _scripts, this approach can be steered purely through the called template ID, without any application code change.
7. Performance: compilation and caching of templates
Elasticsearch compiles a stored template on its first call and keeps the compiled form in an internal script cache, so repeated calls to the same template do not require recompiling the Mustache template again. The actual execution overhead of a search template therefore practically does not differ from a directly formulated, equivalent query, the additional cost is limited to the one-time parameter substitution per call.
For very complex templates with many conditional blocks and deeply nested logic, it is still worth watching the size of the script cache, especially when many different templates are in use in parallel. The cache can be adjusted through script.cache.max_size if frequently used templates get evicted from the cache and therefore need to be recompiled repeatedly.
8. Practical example: parametrized product search with defaults
In the shown product search template, the search term is checked mandatorily through multi_match against name and description, while category and price range remain optional. If the price filter is missing entirely, sensible defaults kick in through the Mustache negation syntax {{^price_min}}, so the query contains a valid, sensible range condition even without an explicit price given, instead of failing with an empty or invalid filter.
The application itself only needs to pass the user-entered values as parameters, without knowing that a bool query with multi_match, a conditional term filter, and a range filter is being assembled behind the scenes. This separation remains stable even when the search team later adds, say, additional function score boosting for stock levels, as long as the parameter interface itself does not change.
9. Pitfalls and recommendations for production use
A common mistake is inserting parameters into a template unchecked and without escaping, especially for free-text fields that could theoretically contain Mustache special characters. Elasticsearch escapes parameter values inside Mustache expressions correctly by default, it only becomes problematic when parameters are accidentally inserted as raw query rather than as a value, which can blur the line between parametrization and potential query injection.
It is also advisable to back every production template with a small test suite of _render/template calls covering typical and edge cases of parameter combinations, for instance missing optional parameters, extreme price ranges, or empty search terms. This catches regressions from template changes early, before they show up in production against real search requests.
| Aspect | Query DSL in application code | Search Template | Practical relevance |
|---|---|---|---|
| Where query logic lives | Scattered across codebases | Centralized in _scripts | Templates prevent inconsistencies |
| Deployment on changes | New application deployment required | Effective immediately, no deployment | Faster relevance iteration |
| Domain knowledge in application code | Query DSL knowledge required | Only named parameters needed | Clear separation of responsibility |
| Debugging | Through application logs | Directly verifiable via _render/template | Faster troubleshooting of templates |
| Optional filters | Manual branching in code | Mustache conditional blocks | One template for every parameter combination |
Mironsoft
Search index setup, relevance tuning, and Magento search
Magento search that shows the wrong products first?
We set up Elasticsearch or OpenSearch for Magento cleanly, tune relevance and facets to the actual catalog, and optimize indexing processes for large catalogs.
Relevance Tuning
Match search results and facets to actual customer needs.
Search Migration
Guide a clean migration from Solr or MySQL search to Elasticsearch/OpenSearch.
Index Performance
Make indexing processes for large catalogs reliable and performant.
10. Summary
Search Templates: The Essentials at a Glance
Core principle
Search templates separate the Mustache-based query DSL structure from the concrete search parameters, which an application only ever passes by name.
Management
Templates are stored centrally through the _scripts API and executed via _search/template with parameters, with no application code change needed for query adjustments.
Benefit for teams
Application developers only know a documented parameter interface, while the search team evolves relevance and filter logic independently.
Practical example
A parametrized product search with optional category and price filters shows how Mustache conditional blocks make a single template viable for every parameter combination.