Choosing between force_merge and shrink for warm indices

When a rolled-over log index lands on the warm tier it is usually both over-segmented and over-sharded, and the two ISM actions that fix those problems — force_merge and shrink — are frequently confused, run in the wrong order, or run against an index that fails their preconditions. This procedure separates the two decisions: force_merge (max_num_segments) reduces the segment count to cut per-segment memory and query overhead, while shrink (num_new_shards) reduces the primary shard count and requires every primary on one node plus a read-only source. It shows both actions in a single warm state, the mandatory shrink-then-force_merge ordering, the preconditions each enforces, and how to verify the result. It sits under ISM Policy Actions Comparison.

Prerequisites

Confirm each of these before adding the actions to your warm state — the usual first-run failure is a shrink that aborts because the source shards were never colocated or the index was never made read-only.

Which action solves which problem

The two actions look similar — both rewrite the index and both are one-way — but they attack different overhead. The decision flow below routes a warm index to shrink, force_merge, both, or neither based on its shard count and segment count.

Decision flow: shrink then force_merge for a warm index A top-to-bottom decision tree. Start at a rolled-over warm index. First decision: is the shard count above the warm-tier target? Yes leads to the shrink action reducing to the target shard count; no bypasses shrink. Both paths join at the second decision: are there too many segments per shard? Yes leads to force_merge to one segment; no leaves the index unchanged. The shrink box precedes the force_merge box on the combined path, showing the mandatory ordering. Rolled-over warm index shard count > warm target? shrink num_new_shards = target too many segments/shard? force_merge max_num_segments = 1 Allocate to warm · done yes no yes no shrink before force_merge

The reason the order is fixed: shrink builds a new index with fewer shards and deletes the source, so any force_merge done first is thrown away with the source. Merge the shrunk target and you rewrite the data exactly once.

Sizing the target shard count

Pick the shrink target from the physical shard-size sweet spot, not from a round number. If SS is the total primary store size and TT is your target per-shard size (20–50 GB is the usual warm-tier band), the target primary shard count is

nnew=STsubject tonsourcemodnnew=0n_{new} = \left\lceil \frac{S}{T} \right\rceil \quad\text{subject to}\quad n_{source} \bmod n_{new} = 0

so a 90 GB index aiming at ~45 GB shards wants 2 primaries — and if it started at 6, then 6 mod 2 = 0 makes 2 a legal target. The memory you reclaim with the subsequent force_merge is roughly proportional to the segment count you remove, since each segment carries fixed per-segment structures:

ΔMmseg×(nsegntarget)\Delta M \approx m_{seg} \times (n_{seg} - n_{target})

where msegm_{seg} is the average per-segment heap overhead. Merging 40 segments down to 1 reclaims 39 segments’ worth of that fixed overhead per shard.

Step-by-step procedure

1. Make the source read-only and colocate its primaries

shrink requires every primary shard on a single node and the source blocked for writes. Do both in one settings call before the shrink runs.

HTTP
PUT logs-000042/_settings
{
  "index.blocks.write": true,
  "index.routing.allocation.require._name": "warm-node-3"
}
Text
{ "acknowledged": true }

Gotcha: index.blocks.write blocks writes but still allows metadata changes; do not use index.blocks.read_only, which blocks the shrink’s own settings update and makes the action fail.

2. Encode both actions in one warm state, in order

Put shrink ahead of force_merge in the same state’s actions array. ISM runs a state’s actions sequentially, so the shrunk target is what gets merged.

JSON
{
  "name": "warm",
  "actions": [
    { "shrink": { "num_new_shards": 2, "switch_aliases": false } },
    { "force_merge": { "max_num_segments": 1 } },
    { "allocation": { "require": { "data": "warm" } } }
  ],
  "transitions": [
    { "state_name": "cold", "conditions": { "min_index_age": "30d" } }
  ]
}

Gotcha: if you reverse the two actions, the force_merge runs on the soon-to-be-discarded source and the new shrunk index arrives un-merged — you pay the merge I/O twice.

3. Attach the policy and let the state run

Register the policy and confirm the index is picked up on the next sweep.

Shell
curl -s -X POST "https://<cluster>:9200/_plugins/_ism/change_policy/logs-000042" \
  -H "Content-Type: application/json" \
  -d '{"policy_id": "warm-optimize-policy"}'
Text
{ "updated_indices": 1, "failures": false }

Gotcha: the shrink produces a new index named shrink_logs-000042 by default. If a downstream alias or query targets the original name, set switch_aliases or repoint the alias explicitly after the shrink completes.

Verification

Confirm each action ran and produced the intended physical layout. Run all three checks.

Shell
# 1. Which action is the index currently on, and did it fail?
GET _plugins/_ism/explain/logs-000042
# "action": { "name": "force_merge", "failed": false }
Shell
# 2. Did the shard count actually drop?
GET _cat/indices/shrink_logs-000042?v&h=index,pri,rep,store.size
# index               pri rep store.size
# shrink_logs-000042  2   1   88.4gb        <- 2 primaries, down from 6
Shell
# 3. Did the merge collapse segments to one per shard?
GET _cat/segments/shrink_logs-000042?v&h=shard,segment,size
# shard 0 -> a single segment; shard 1 -> a single segment

A healthy result shows the shrunk index at the target primary count, exactly one segment per shard, and the ISM action reporting failed: false. The tier placement that finishes the state follows the data tier routing patterns — the shards should now sit on data: warm nodes.

Common failures

Symptom Root cause Fix command
shrink fails: primaries not on one node Source shards spread across warm nodes PUT logs-000042/_settings {"index.routing.allocation.require._name":"warm-node-3"} then retry
shrink rejected: shard count not a divisor num_new_shards does not divide the source count Pick a divisor of the source (6 → 3, 2, or 1); POST _plugins/_ism/retry/logs-000042
force_merge never converges Index still receiving writes; segments keep re-forming Set index.blocks.write: true; ensure the index has rolled over
Shrunk index sits UNASSIGNED No warm node with room for the second full copy Free disk below the high watermark; GET _cluster/allocation/explain
Merge finished but memory unchanged max_num_segments left at default (>1) Re-run with max_num_segments: 1; verify via _cat/segments

Frequently asked questions

Can I skip shrink and just force_merge?

Yes, if the index has an appropriate shard count already and only suffers segment bloat. force_merge alone reclaims per-segment memory and speeds queries without touching the shard layout. Skip shrink whenever the per-shard size is already in the 20–50 GB band; run it only when you are over-sharded.

Why does shrink need the index read-only?

shrink hard-links the source segments into the new index and cannot do so consistently while writes are landing. The index.blocks.write: true setting freezes the source so the link-and-recover step sees a stable set of segments. Writes resume against the new index (or the rolled-over write index), never the shrunk source.

Is force_merge to a single segment always right for warm indices?

For read-only warm indices, yes — one segment per shard minimises per-segment overhead and gives the fastest queries. It is only wrong when the index is still being updated, because updates create new segments and mark old documents deleted, so a single giant segment accumulates unreclaimable deletes. Merge only after the write phase is truly over.

Up: ISM Policy Actions Comparison