Elasticsearch Search Performance Tuning
Search speed shapes how a product feels. Visitors leave pages that take more than a second or two to respond. Most slow searches trace back to a small set of causes: scoring work that nobody needs, expensive query types, oversized responses, and poor shard layouts. Each fix below targets one cause and takes minutes to apply.
Where Search Time Goes
Search request
│
▼
Coordinating node ──→ sends query to every shard
│
▼
Each shard: match → score → sort → collect top hits
│
▼
Coordinating node ──→ merges lists → fetches documents → responds
Slow spots: heavy queries, many shards, large documents, deep result lists
Use Filters for Exact Conditions
Conditions such as status, category, and date range do not need relevance scores. Place them in the filter clause of a bool query. Elasticsearch skips scoring and caches the matching document sets for reuse.
Slower: "must": [ { "match": { "title": "laptop" } },
{ "term": { "status": "published" } } ]
Faster: "must": [ { "match": { "title": "laptop" } } ],
"filter": [ { "term": { "status": "published" } } ]
Avoid Expensive Query Types
| Query Habit | Problem | Better Choice |
|---|---|---|
| Wildcard starting with * | Scans every term in the field | Edge n-grams or prefix queries |
| Regular expressions on big fields | High CPU cost | Keyword fields with exact values |
| Scripts in queries or sorts | Runs code per document | Precomputed fields at index time |
| Deep from and size values | Large merge lists | search_after |
| Fuzzy search on very short words | Many candidate terms | Raise prefix_length |
Return Less Data
Large documents travel across the network on every hit. Request only the fields the page displays with source filtering, and skip the exact total count when the page does not show it.
GET /products/_search
{
"_source": ["name", "price"],
"track_total_hits": false,
"size": 20,
"query": { "match": { "name": "laptop" } }
}
The track_total_hits setting stops Elasticsearch from counting every match beyond ten thousand. Pages with infinite scroll or "next" buttons rarely need the exact number.
Understand the Caches
| Cache | Stores | Helps With |
|---|---|---|
| Node query cache | Filter results per segment | Repeated filters |
| Shard request cache | Full results of size 0 requests, such as aggregations | Dashboards that repeat |
| Operating system page cache | Index files in memory | Almost everything |
The operating system cache matters most. Leave at least half of the machine's memory to the system instead of giving everything to the Elasticsearch heap. A heap of 30 GB or less keeps pointers compact, and the remaining memory holds index files for fast reads.
Server with 64 GB RAM:
Elasticsearch heap: 30 GB
OS file system cache: 34 GB ← keeps hot index data in memory
Right-Size the Shards
Every search touches every shard of the target index. A hundred tiny shards create a hundred small tasks and a long merge. Aim for shards between 10 GB and 50 GB, and combine small indexes through lifecycle policies and the shrink action.
Index with 100 shards of 500 MB → 100 tasks per search
Index with 2 shards of 25 GB → 2 tasks per search
Merge Segments on Read-Only Indexes
An index stores data in segments. Fewer segments mean faster reads. An index that no longer receives writes benefits from a force merge down to one segment.
POST /logs-2026.09/_forcemerge?max_num_segments=1
Never force merge an index that still accepts writes. The operation creates a huge segment that future merges struggle to manage.
Map Fields for Their Real Use
Numbers used only for exact matching and ranges can map as keyword instead of integer in some cases, since keyword term lookups run quickly. Fields that never need searching can disable indexing with "index": false. Fields that never need sorting or aggregating skip doc values. Smaller mappings mean smaller files and faster reads.
Measure with the Profile API
Guessing wastes time. The profile API reports how long each part of a query took on each shard.
GET /products/_search
{
"profile": true,
"query": { "match": { "name": "laptop" } }
}
The response breaks timing down by query component. A single slow clause usually stands out immediately.
Find Slow Queries with the Slow Log
The search slow log records queries that exceed time limits. Set thresholds per index, and review the log files regularly.
PUT /products/_settings
{
"index.search.slowlog.threshold.query.warn": "2s",
"index.search.slowlog.threshold.query.info": "1s",
"index.search.slowlog.threshold.fetch.warn": "1s"
}
Tuning Checklist
| Check | Action |
|---|---|
| Exact conditions inside must | Move to filter |
| Wildcards with leading * | Replace with n-grams |
| Full documents returned | Limit with _source |
| Too many small shards | Combine or shrink |
| Old indexes still searched | Force merge or archive |
| Unknown slow queries | Enable the slow log and profile |
