two different caching layers for aggregations and queries
Elasticsearch ships with two structurally different, often confused caching mechanisms that both improve response time for repeated requests, but operate on completely different levels: the shard-level request cache, which stores complete search responses including aggregations on the node side, and the node query cache, often also called the shard query cache, which caches individual, recurring filter clauses at the shard level. Anyone trying to improve performance for a facet-heavy Magento store page with layered navigation needs to understand which of the two caches actually applies in which situation, how both get invalidated on write operations, and how query structure can be shaped deliberately so both caching layers get used optimally.
Table of Contents
- 1. Two different caches that are often confused
- 2. The shard-level request cache in detail
- 3. The node query cache in detail
- 4. Which cache applies when: query structure decides
- 5. Invalidation behavior on write operations
- 6. Practical impact on facet-heavy store pages
- 7. Configuring cache size and measuring the hit rate
- 8. Practical optimization: query structure for better cache use
- 9. Pitfalls: real-time search versus cache effectiveness
- 10. Summary
- 11. FAQ
1. Two different caches that are often confused
The term caching gets used in Elasticsearch for two structurally different mechanisms, which differ both in what they store and in when they actually apply. The shard-level request cache stores the complete, fully computed JSON response of a search request, including aggregation results, while the node query cache only holds the result of individual filter clauses as an efficiently compressed bitset at the segment level.
This distinction matters because both caches favor different request patterns: the request cache shows its strength mainly for identical, repeatedly executed aggregation requests, while the query cache can still apply even when two overall requests differ slightly, as long as a particular, reusable filter clause is part of both requests.
2. The shard-level request cache in detail
The request cache operates at the node level per shard and only stores a search request's result when size is set to 0, meaning no individual hit documents get returned, only aggregation results and the total hit count matter. This behavior is deliberate, since aggregations are often computed across a very large number of documents and are therefore comparatively expensive, yet frequently queried repeatedly in identical form.
The cache key consists of the exact request body combined with the shard state at the time of the request. Two syntactically different but semantically identical requests, for instance with the JSON fields in a different order, produce different cache entries in practice, which is why a consistent, always identically structured request layout is critical for a good hit rate.
GET products/_search
{
"size": 0,
"query": {
"term": { "category_id": 42 }
},
"aggs": {
"brands": { "terms": { "field": "brand.keyword", "size": 20 } },
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 50 },
{ "from": 50, "to": 150 },
{ "from": 150 }
]
}
}
}
}
3. The node query cache in detail
The node query cache, often also called the shard query cache, doesn't store the whole request but the result of individual, reusable filter clauses as a bitset per Lucene segment. Only clauses inside a filter context, not inside a scoring query context with relevance calculation, are eligible for this caching at all, because only there is the result independent of a specific score computation.
Elasticsearch doesn't automatically cache every filter clause, it uses an internal heuristic that favors clauses that have run against at least an eighth of an index's segments, suggesting a certain likelihood of reuse. Rarely executed or very specific filter clauses never make it into the cache in the first place as a result.
GET products/_search
{
"query": {
"bool": {
"must": { "match": { "name": "hiking boots" } },
"filter": [
{ "term": { "visible": true } },
{ "term": { "category_id": 42 } }
]
}
}
}
4. Which cache applies when: query structure decides
Whether the request cache applies depends entirely on whether the complete request, including all parameters, is identical to a previous request and size was set to 0. Even a single differing parameter, for instance a different value in the search term or a different sort direction, produces its own new cache entry, even if the vast majority of the request stays identical.
The query cache, in contrast, applies at the level of individual clauses and can therefore still provide benefits even when two overall requests differ significantly in other parts, as long as one particular filter clause, for instance excluding disabled products, appears identically in both requests. A deliberately consistent, reused formulation of common filters raises the hit rate of both caches equally.
5. Invalidation behavior on write operations
Both caches are tied to the Lucene segment state of a shard and get implicitly invalidated on every refresh that makes new segments visible, since the underlying segment state changes. In concrete terms: even without explicit purge commands, both caches lose at least part of their existing entries with every refresh cycle, because new or changed segments require a new caching basis.
With a default refresh interval of one second, this means that for heavily writing indices, both caches get rebuilt practically all the time and rarely reach a high hit rate. For predominantly read-heavy workloads with less frequent write operations, as is the case for most Magento product indices outside of reindex windows, the caches instead stay valid over noticeably longer periods.
6. Practical impact on facet-heavy store pages
Category pages with layered navigation typically run the same basic structure of aggregations for attributes, price ranges, and brands on every page load, combined with the same recurring base filters such as category ID and the visibility flag. This exact pattern, many identical or heavily overlapping requests using the same filter clauses, favors both caching layers at once particularly strongly.
If, instead, individual, customer-specific factors such as personalized price rules or customer group discounts get baked directly into the same aggregation request on every call, the request cache's hit rate drops substantially, since practically every request becomes technically unique as a result. A cleaner separation, where personalized aspects get applied only after the cached base request, preserves cache effectiveness far better.
7. Configuring cache size and measuring the hit rate
The size of the request cache can be configured via the node setting indices.requests.cache.size as a percentage of the heap, with a default of one percent that is often too tight for very aggregation-heavy workloads. The node query cache, on the other hand, is controlled via indices.queries.cache.size, set to ten percent of the heap by default.
The actual hit rate of both caches can be read out via the indices stats API per index, with separate counters for hits, misses, and the currently occupied cache size. A persistently low hit rate despite genuinely recurring requests usually points to an overly inconsistent query structure or too short a refresh interval, not necessarily to an undersized cache configuration.
# Query the hit rate of the request cache and the query cache per index
curl "localhost:9200/products/_stats/request_cache,query_cache?pretty" \
| jq '.indices.products.total | {request_cache, query_cache}'
8. Practical optimization: query structure for better cache use
For good use of the request cache, aggregation requests should consistently be formulated with size equal to 0 whenever only aggregation results, not individual hit documents, are actually needed, and with an always identical, not dynamically reassembled JSON structure, for instance through a fixed field order in the application layer.
For the query cache, it pays off to consistently place frequently recurring, static filter conditions inside a separate filter block rather than in the scoring query part, even if both variants would functionally produce the same result. Only the variant inside the filter context gives the query cache any chance at all to reuse the affected clause independently of relevance scoring.
9. Pitfalls: real-time search versus cache effectiveness
A common pitfall is configuring a very short refresh interval for supposedly fresher search results, without considering that both caching layers constantly lose their basis as a result and effectively stop applying almost entirely. In most Magento contexts, a delay of a few seconds between a data change and its visibility in search is entirely uncritical and rarely justifies the associated loss of cache effectiveness.
A second, more subtle pitfall is accidentally mixing personalized and generic aggregations in the same request, which renders even well-intentioned optimization attempts ineffective. Anyone who consistently keeps generic, well-cacheable base data separate from individual, deliberately uncached additional information benefits far more reliably from both caching layers.
| Aspect | Shard-level request cache | Node query cache | Practical relevance |
|---|---|---|---|
| Stores | Complete aggregation response | Bitset of individual filter clauses | Different granularity |
| Requirement | size equal to 0 in the request | Clause inside the filter context | Both require deliberate query structure |
| Invalidation | On every segment-changing refresh | On every segment-changing refresh | Both depend on the refresh interval |
| Configuration | indices.requests.cache.size, one percent default | indices.queries.cache.size, ten percent default | Both adjustable via node settings |
| Ideal for | Identical, repeated aggregation requests | Recurring, static base filters | Layered navigation benefits from both |
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
Request Cache and Query Cache: The Essentials at a Glance
Two layers
The request cache stores complete aggregation responses, the query cache stores individual, reusable filter clauses as a bitset.
Requirements
The request cache needs size equal to 0, the query cache needs clauses in the filter context rather than the query context.
Invalidation
Both caches depend on Lucene segment state and lose at least part of their validity on every segment-changing refresh.
Practical benefit
Facet-heavy category pages with consistent base filters benefit strongly, personalized aggregations should be kept separate.