The shards were the right count and the wrong size.
A read-heavy candidate index: ~500M documents, ~2 TB of primary store, serving job-to-candidate matching. Five primary shards held ~400 GB each — comfortably inside Elastic's document-count guidance and roughly 8× over its size guidance. Almost every latency and cost symptom traced back to that one number.
00The one-pager
Everything below in a single screen.
A read-heavy candidate search platform. ~500M candidate documents of ~20 mixed fields each, queried constantly for job-to-candidate matching on location, experience, salary fit, profile preferences and activity signals.
- 5 primaries → ~400 GB per shard.
- 100M docs/shard was inside the ~200M guidance.
- 400 GB was ~8× over the 10–50 GB guidance.
- Doc count looked fine, so nobody looked at bytes.
2 TB primary store ÷ a 30 GB target ≈ 67 primary shards, ×1 replica ≈ 120–140 shard copies. Not a benchmark guess — a capacity calculation from bytes, then validated.
- Shard count from primary-store size.
- Node count from the disk:RAM ratio.
- New index, not an in-place change.
- Explicit mappings, source filtering.
- Hard constraints into filter context.
- Coordinating-only nodes.
→ 8–10 data + 3 master + 2–3 coordinating ≈ 13–16 nodes.
Lighter shard-level search, more parallelism, a hot working set that could actually stay in filesystem cache, less scoring on irrelevant documents, and data nodes no longer doing coordination as a second job.
Cost. Once query CPU, heap pressure and latency had stabilised, the cluster could be right-sized — node count, node type, replica count, index bloat. Cost was the consequence, not the lever.
01The high-level design
Start here. Everything after this is a zoom into one box.
Elasticsearch is not the source of truth — the operational stores are. The index holds a denormalised copy shaped entirely for retrieval: every field the query filters on, scores on, or returns, and nothing else. That is what removes runtime joins from the hot path.
02The two-minute answer
“We had a read-heavy candidate search platform — around 500 million candidate documents, roughly 20 fields each. Job-to-candidate matching was slow at p95 and p99, the data nodes were under heap and CPU pressure, and the cluster was expensive.”
“The first thing was to stop reasoning about document count. 500 million documents across five shards is 100 million per shard, which is comfortably inside Elastic's ~200 million guidance — so on paper it looked fine. But the index was about 2 TB of primary store, which meant each shard was roughly 400 GB. Elastic's size guidance is 10 to 50 GB. We were eight times over a limit nobody had checked, because the other limit looked healthy.”
“So shard sizing became a capacity calculation rather than a guess: 2 TB divided by a 30 GB target gives about 67 primary shards, and with one replica that's 120 to 140 shard copies. Then node capacity came from disk — 32 GB RAM nodes at a 1:24 RAM-to-disk ratio is about 768 GB per node, of which maybe 550 GB is safely usable, so a 4 TB footprint needs around 8 to 10 data nodes.”
“The other half was work the cluster should never have been doing. Hard constraints — active, city, experience band, profile IDs — were in scoring clauses, so Elasticsearch was computing relevance across an enormous set before discarding most of it. Moving those into filter context means they answer yes/no, skip scoring, and become cacheable. Alongside that: explicit mappings instead of broad ones, source filtering so we return a handful of fields rather than whole profiles, and coordinating-only nodes so data nodes stop doing fanout and merge as a second job.”
“Shard count can't be changed in place, so all of it shipped as a new index — reindexed, replayed with production-like traffic, compared on latency, CPU, heap and disk, then cut over by alias with the old index kept for rollback. The cost reduction came last and deliberately so: once the waste was gone and latency was stable, the cluster could be right-sized.”
03Ownership — read this before the interview
A large Elasticsearch cluster is touched by application engineers, platform engineers and infra. Claiming all of it is both untrue and easy to catch — one follow-up about JVM flags or node provisioning finds the edge.
Claim a specific action only if it happened. Where work was shared, the honest verb is identified, validated, recommended, contributed to, or the team implemented. Precision here buys credibility.
| Only if true | Otherwise say |
|---|---|
| “I redesigned the shard strategy.” | I did the shard-sizing analysis from primary-store size and validated the target with production-like queries. |
| “I changed the node topology.” | I analysed cluster pressure and recommended separating master, coordinating and data roles; the infra team executed it. |
| “I rewrote all the queries.” | I optimised the job-to-candidate query path — hard constraints to filters, source filtering, validated scoring. |
| “I cut infra cost by 50%.” | The combined effort reduced it by about half; my part was the shard, mapping and query work that made right-sizing possible. |
| “I owned the Elasticsearch platform.” | I owned the candidate-search performance work on it. |
For each of: shard sizing, node capacity, mappings, query rewrite, topology, replicas, indexing — write down whether you implemented, recommended, or reviewed it.
04Context & the storage model
Everything downstream depends on getting this estimate right, and it is the step most people skip — they reason about document count because it is the number they happen to know.
500M candidate documents
average raw JSON ≈ 2 KB per document
raw JSON size ≈ 1 TB
Elasticsearch primary store ≈ 2 TB ← _source + inverted index + doc values
+ term dictionaries + segments + metadata
with 1 replica ≈ 4 TB total cluster storageThat ~2× expansion from raw JSON to primary store is the part worth internalising. Elasticsearch is not storing your documents — it is storing your documents plus everything that makes them searchable. Sizing from raw payload underestimates by roughly half.
The existing setup
node type 32 GB RAM data nodes JVM heap ~16 GB OS / off-heap ~16 GB filesystem cache index ~2 TB primary, 5 primary shards replica 1 cluster total ~4 TB → 2 TB / 5 shards ≈ 400 GB per primary shard → 500M / 5 shards ≈ 100M documents per primary shard
05Why it was slow
A shard is a Lucene index. The coordinating node fans a query out to the relevant shard copies, each executes locally, and results merge. With few very large shards, every one of those local searches is doing a lot of work.
| Cause | Mechanism |
|---|---|
| Large shard size | Hundreds of GB per shard means heavy shard-level search on every query. |
| Low parallelism | Five primaries means only five independent shard-level tasks to spread. |
| Overloaded data nodes | Same nodes doing Lucene search and request coordination and merge. |
| Filesystem cache too small | 16 GB of OS cache against hundreds of GB of shard data — the hot set could never stay resident. |
| Slow recovery | Very large shards take a long time to relocate or recover after a node failure. |
| Cost pressure | Because each query was expensive, the cluster needed more and bigger nodes to hold latency. |
“Search performance depends on query cost, the number of parallel searches, how many shards are involved, the sharding strategy and shard size. We had a problem in three of those five, and they all pointed at the same root cause.”
06Decision 1 — shard sizing
Elastic guidance target shard size 10–50 GB target document count below ~200M per shard we were 100M docs/shard ✓ inside the count guidance 400 GB/shard ✗ ~8× over the size guidance pick a middle target for a read-heavy workload 30 GB per shard derive 2 TB / 30 GB ≈ 67 primary shards × 1 replica → 120–140 total shard copies
There are two Elastic guidelines and you have to clear both. This cluster cleared the document-count one and failed the size one — which is exactly why it went unnoticed. Anyone checking “are we under 200M docs per shard?” would have concluded the sharding was fine.
Smaller shards buy three things at once: lighter per-shard search work, more shard-level parallelism, and dramatically faster recovery and relocation when a node is lost. The third one is invisible until an incident, and then it is the one that matters.
There is an upper bound too — more shards means more fanout per query, more cluster state, and more segment overhead. 67 shards across 8–10 nodes lands at 13–17 shard copies per node, which is a reasonable place to start measuring rather than a number to defend to the death.
07Decision 2 — node capacity
32 GB RAM node JVM heap ~16 GB OS cache ~16 GB ← what read-heavy search actually runs on 1:24 RAM-to-disk planning ratio 32 GB × 24 ≈ 768 GB disk per node × 70–75% safe ≈ 540–576 GB usable per node 4 TB total / ~550 GB per node ≈ 8 data nodes practical topology 8–10 data nodes · 32 GB RAM 60–70 primary shards · 1 replica 120–140 shard copies → 13–17 per node
The 32 GB node size is not arbitrary. Elasticsearch leans heavily on the OS filesystem cache for search, and heap is capped near 32 GB because beyond that the JVM loses compressed object pointers. So the sensible shape is ~half heap, ~half cache — and when you need more capacity you add nodes rather than growing them.
“These two decisions are the same decision. 16 GB of filesystem cache against a 400 GB shard means the hot working set can never stay resident, so every query pays disk. Shrinking shards and spreading copies across more nodes is what gives the cache a chance to do its job.”
08What “1 replica” actually means
Worth its own section because it is the most commonly misread number in a topology, and getting it wrong in an interview is an easy own goal. number_of_replicas: 1 means one replica copy per primary shard — not one replica node, and not one replica of the index.
67 primary shards
+ 67 replica shards ← one per primary
= 134 total shard copies
placement rule: a replica never sits on the same node as its own primary
(otherwise losing that node loses the data)
data per node
8 data nodes → 16–17 copies × ~30 GB ≈ 480–510 GB
10 data nodes → 13–14 copies × ~30 GB ≈ 390–420 GB
both inside the ~540–576 GB safely usable per nodeSo how many nodes is the cluster, actually?
| Role | Count | Scales with |
|---|---|---|
| Data nodes | 8–10 | Storage and search load — the 4 TB footprint. |
| Master-eligible | 3 | Nothing. Three is about quorum, not capacity. |
| Coordinating-only | 2–3 | Read concurrency and merge work. |
| Total | 13–16 | — |
“When I say one replica, I mean each primary shard had one replica shard. With about 67 primaries that is another 67 replicas — around 134 shard copies — spread across 8 to 10 data nodes, so each data node carried roughly 13 to 17 copies. On top of that, 3 master-eligible nodes and 2 to 3 coordinating-only nodes, so the cluster was around 13 to 16 nodes in total.”
Each replica is a full second copy of the index — it doubles storage, and every write is applied twice. It also adds read capacity and is what lets you survive a node loss. So the right count is the maximum of what availability needs and what read QPS needs, never a number inherited from a default.
09Decision 3 — a new index, not an in-place change
Primary shard count is fixed for the life of an index. That single constraint dictates the entire rollout: this is not a settings change, it is a new index, a backfill, a validation pass and a cutover.
PUT candidates_v2
{
"settings": {
"number_of_shards": 67,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"candidate_id": { "type": "keyword" },
"city_id": { "type": "integer" },
"job_profile_ids": { "type": "keyword" },
"experience_years": { "type": "short" },
"expected_salary": { "type": "integer" },
"last_active_at": { "type": "date" },
"skills_text": { "type": "text" },
"profile_title": { "type": "text" }
}
}
}Then reindex the existing candidate data into it and replay production-like matching queries against both. The fact that you must build a second index turns out to be an advantage: you get a free A/B comparison on identical data before anything is at risk.
10Decision 4 — mapping for read-heavy search
exact filter fields → keyword / numeric / date text relevance fields → text unused fields → do not index large, rarely used fields → keep out of the _source response candidate_id → keyword city_id → integer / keyword job_profile_ids → keyword experience_years → short / integer salary_min / salary_max → integer last_active_at → date skills_text → text profile_summary → text
Filters should be fast. Scoring should happen only on fields that genuinely carry relevance. And every unnecessary indexed field costs storage, segment metadata, heap pressure and merge time — forever, on a 500M-document index.
11Decision 5 — filter first, score second
| Hard filters | Scoring signals |
|---|---|
| city / location | preferred job profile priority |
| job profile eligibility | skills / profile text match |
| experience range | candidate activity and freshness |
| salary fit | profile quality |
| active candidate | location proximity |
| candidate availability | — |
{
"query": {
"bool": {
"filter": [
{ "terms": { "city_id": [10, 11] }},
{ "terms": { "job_profile_ids": ["field_sales", "retail_sales"] }},
{ "range": { "experience_years": { "gte": 1, "lte": 3 }}},
{ "term": { "active": true }}
],
"should": [
{ "term": { "primary_job_profile_id": { "value": "field_sales", "boost": 5 }}},
{ "match": { "skills_text": { "query": "sales customer handling", "boost": 2 }}}
],
"minimum_should_match": 1
}
},
"size": 100
}Source filtering is the other half. Returning a full candidate profile when the API needs an ID and four fields costs fetch-phase work, serialisation, network and heap — multiplied by 100 hits, multiplied by every query. It is among the cheapest changes available and the most commonly skipped.
12Decision 6 — coordinating-only nodes
Before, application traffic went straight to data nodes, which meant the same machines were doing four jobs: coordinating query fanout and merge, executing shard-level Lucene search, storing shard data, and absorbing indexing load. For a read-heavy workload that is one job too many.
A coordinating-only node stores no shard data — in Elasticsearch, a node with an empty node.roles list. It receives the query, fans it out to shard copies, gathers and merges the results, and returns the response. The effect usually shows up at p99 first, because merge work is exactly what was competing with shard execution under load.
13The experiment
Nothing here was changed blind in production. The sequence was the same every time.
- Capture the baseline. p95/p99 latency, CPU per node, heap usage, GC pressure, search thread-pool queue and rejections, disk usage, shard sizes, slow query logs.
- Create the new index. ~60–70 primaries, replica 1, optimised mappings, source filtering.
- Reindex the candidate data from old to new.
- Replay production-like read traffic. Job-to-candidate search, candidate filters, profile-based scoring, top-N retrieval.
- Compare old vs new on latency, CPU, heap, disk, query throughput and recovery behaviour.
- Cut over safely. Switch the alias or application config; keep the old index available for rollback.
Recovery behaviour. It is the one that only matters during an incident, and the one where 400 GB shards hurt most. If you only measure latency you will never see the improvement that saves you at 3am.
14Alias cutover
POST /_aliases
{
"actions": [
{ "remove": { "index": "candidates_v1", "alias": "candidates_current" }},
{ "add": { "index": "candidates_v2", "alias": "candidates_current" }}
]
}The step that gets skipped and should not: result parity. Latency parity is easy to check. A mapping change that quietly alters what matches — an analysed field that should have been a keyword, a boost that reorders the top 10 — produces no error and moves no graph. Shadow queries comparing document counts and top-N quality are the only defence.
15How it got cheaper
| Before | After |
|---|---|
| Large shards, heavy shard-level queries | Right-sized shards, lighter per-query work |
| Higher CPU and heap pressure | Lower CPU per query, stabler heap |
| Poor filesystem-cache behaviour | Hot working set can stay cached |
| Expensive data nodes, heavy over-provisioning | Less over-provisioning needed |
The tuning knobs, specifically
| Knob | Setting | Why |
|---|---|---|
| JVM heap | ~50% of RAM, under ~32 GB | Leaves the rest for filesystem cache, and stays inside compressed object pointers. |
| Disk headroom | Plan to ~70–75% usage | Elastic's low watermark defaults to 85% — past it the allocator stops placing shards on that node. |
| Query context | Hard constraints in filter | No scoring, and the filter cache can reuse the result. |
| Result size | Bounded, with source filtering | Cuts fetch-phase work, serialisation and network. |
| Replica count | Reviewed, not inherited | Each replica is a full storage copy and another write destination. |
| Slow logs | On, and actually read | Finds the queries that are slow rather than the ones that look slow. |
| Thread pools | Watch queue depth and rejections | A rejection is a dropped request, not a slow one. |
Elasticsearch's disk allocator works off watermarks — the low watermark defaults to 85%, and once a node crosses it the allocator stops assigning new shards there. That is why node capacity was planned at 70–75% of disk rather than to the edge: the last 15% is not usable space, it is the margin the allocator needs to keep working.
Once latency and node pressure improved, cost came down by:
- Right-sizing node count and node type.
- Avoiding oversized machines and unnecessary replicas.
- Removing over-indexed fields, and with them disk and index bloat.
- Reducing the search CPU each query required in the first place.
“The cost reduction did not come from one change. It came from removing wasted work, verifying stability, and only then re-sizing. Removing capacity before removing the work makes a cluster less stable, not cheaper.”
16The comparison brief
Four dimensions, before and after. If you only remember one screen of this page, make it this one.
| Area | Before | What we changed | Why it helped |
|---|---|---|---|
| Shard sizing | Few large shards — ~2 TB primary across 5 shards ≈ 400 GB each, 100M docs each | New index at ~60–70 primaries targeting ~30 GB per shard | Smaller shards cut per-shard search cost, raised parallelism, and made recovery and rebalancing far faster. Elastic recommends 10–50 GB and under ~200M docs per shard — we were passing one test and failing the other. |
| Node topology | 32 GB data nodes storing data, running Lucene search, and coordinating and merging results | 8–10 data nodes · 1 replica · ~120–140 shard copies · 13–17 copies per node · coordinating-only tier added | Data nodes could focus on shard-level search while coordinating nodes absorbed fanout and merge. Elastic notes coordinating-only nodes help large clusters by offloading that work from data and master nodes. |
| Indexing strategy | Unnecessary indexed fields and mixed mapping choices; large documents inflating index size and memory pressure | keyword/numeric/date for filters, text only where relevance matters, unused fields unindexed, _source trimmed | Less index bloat, segment metadata, heap pressure, disk and query work — which fed directly into both latency and infrastructure cost. |
| Cluster tuning | Large shards, limited filesystem cache, overloaded data nodes, expensive queries | Heap ~50% of RAM, disk kept under watermarks, filters in filter context, bounded result size, replica count reviewed, slow logs and thread-pool pressure watched | Better p95/p99, lower CPU and heap pressure, no disk-watermark allocation problems — and once usage stabilised, room to right-size. |
The cluster, stated precisely
before after
5 primary shards ~60–70 primary shards
~400 GB per shard ~30 GB per shard
100M docs per shard ~7–8M docs per shard
data nodes do everything 8–10 data nodes (search only)
+ 3 master-eligible
+ 2–3 coordinating-only
= 13–16 nodes total“The old system had around 500M candidate documents — roughly 1 TB of raw JSON and about 2 TB of Elasticsearch primary indexed data. The earlier layout used fewer, larger shards on 32 GB RAM data nodes, so each shard was hundreds of gigabytes. That caused high p95/p99 latency because shard-level searches were heavy, filesystem-cache locality was poor, and data nodes were handling both Lucene search and coordination work.”
“We changed four things. Shard sizing — used Elastic's 10–50 GB guidance and moved to roughly 60–70 primaries targeting about 30 GB each. Node topology — mapped total primary plus replica copies onto 8–10 data nodes and added coordinating-only nodes for read traffic. Indexing strategy — cleaned mappings so filter fields were keyword, numeric or date, relevance fields were text, and unused fields were not indexed at all. Cluster tuning — kept heap around 50% of RAM, preserved filesystem cache, watched disk watermarks, optimised query filters, and validated with slow logs and thread-pool metrics.”
“After experimenting with production-like candidate-search queries, the new layout improved latency and stability because searches were spread across right-sized shards, data nodes were under less pressure, and the index and query model did less unnecessary work. That let us right-size the infrastructure and cut cost.”
17Anti-patterns avoided
- Sizing shards by document count alone — the mistake that hid this problem for as long as it did.
- Changing shard count in place — not possible; it requires a reindex and alias strategy.
- Scoring before filtering — the single largest source of wasted query CPU.
- Returning the full _source — fetch, serialise and transfer cost for data the API discards.
- High-cardinality dynamic fields in the mapping — unbounded growth on an index you cannot easily change.
- Script scoring where a precomputed field would do — per-document work on every query.
- Deep pagination with from/size — cost grows with offset; search_after exists.
- Treating replicas as HA only — they are a storage and write-cost decision too.
- Scaling the cluster down before reducing query cost — that is how you turn a cost exercise into an outage.
- Tuning heap and CPU before reading the slow logs — optimising what you can see, not what is slow.
18Alternatives, argued fairly
- Throw hardware at it. Fastest fix, and sometimes correct. Rejected because profiling showed the work itself was wasteful, and scaling waste just costs more. The order matters: reduce work, verify, then resize.
- Bigger nodes instead of more. Tempting, but heap is capped near 32 GB for compressed object pointers, and past that extra RAM only helps filesystem cache. More nodes also buys parallelism and faster recovery, which a bigger box does not.
- Route by city or job profile. Would remove fanout. Rejected because a candidate can hold several job profiles and the largest metros would become hot shards — you would trade a uniform cost for a skewed one.
- A vector / semantic search layer. Better fuzzy relevance. Rejected for this problem because the dominant constraints — city, active, experience band, profile eligibility — are hard filters. Semantic ranking would sit after the filter step, and the filter step was the bottleneck.
- Precompute candidate lists per job. Fastest possible read, rejected on cardinality and freshness. The partial version — caching top-N for hot jobs — is worth it, and that is what the cache in the architecture is for.
- Hot/warm tiering by candidate activity. Real money, since inactive candidates are rarely searched. Sequenced after the changes with no correctness risk, because “inactive” needed a definition product had to own — getting it wrong silently excludes people from results.
19Question drill
Read the question, answer it out loud, then open the card and compare.
Sizing
500M documents across 5 shards is 100M each, which is within Elastic's guidance. So what was wrong?
That is exactly the trap. There are two guidelines — ≤ ~200M documents per shard and 10–50 GB per shard — and you have to clear both. At ~2 TB of primary store, five shards meant ~400 GB each, about 8× over the size ceiling. The count looked healthy, so nobody checked the bytes.
How did you get to 67 shards?
By derivation, not benchmark. ~2 TB of primary store divided by a 30 GB target — a middle value in the 10–50 GB band, chosen for a read-heavy workload — gives about 67 primaries. With one replica that is 120–140 shard copies. Then it was validated with production-like queries rather than trusted on arithmetic alone.
Where does 2 TB come from if the raw data is 1 TB?
Indexing overhead. The primary store holds _source plus the inverted index, doc values, term dictionaries, segment files and metadata. Roughly a 2× expansion over raw JSON, so sizing from raw payload underestimates by about half.
How many data nodes, and how did you decide?
From disk. A 32 GB RAM node at a 1:24 RAM-to-disk planning ratio is ~768 GB, of which 70–75% is safely usable — around 550 GB. A 4 TB cluster footprint divided by that is roughly 8 data nodes, so 8–10 is the practical range. That puts 13–17 shard copies on each node.
Why 32 GB nodes rather than something larger?
Heap is capped near 32 GB because beyond that the JVM loses compressed object pointers, so a 32 GB machine gives ~16 GB heap and ~16 GB filesystem cache. Since read-heavy search runs on the OS page cache, the sensible shape is roughly half and half — and when you need more capacity you add nodes, which also buys parallelism and faster recovery.
Is more shards always better, then?
No. Every shard adds query fanout, cluster state and segment overhead, and too many small shards is its own pathology. 67 across 8–10 nodes is 13–17 copies per node, which is a reasonable starting point to measure from — not a number to defend.
Mechanism
Why did large shards hurt the filesystem cache specifically?
A 32 GB node has ~16 GB of OS cache. With hundreds of GB of shard data on it, the hot working set simply cannot stay resident, so queries fall through to disk. Lucene reads segment files through the page cache, so cache locality is read performance. Smaller shards spread across more nodes give the hot set a chance of being served from memory.
Why is filter context so much cheaper than query context?
Filter clauses answer yes/no — no relevance score is computed, and results are cacheable and reusable. Query-context clauses score every document they consider. Putting active or a city constraint in scoring context means paying score computation on documents that were never eligible.
What do coordinating-only nodes actually buy you?
They take fanout and result-merge work off the nodes holding shards. Before, data nodes were doing four jobs at once: coordination, shard-level search, storage and indexing. A coordinating-only node stores no shard data — an empty node.roles — and the improvement typically shows at p99 first.
Can't you just change the shard count?
No — primary shard count is fixed for the life of an index. That is why this shipped as a new index plus a reindex plus an alias switch. It also turns out to be an advantage: you get a like- for-like comparison on identical data before anything is at risk.
Rollout and cost
How did you validate before cutting over?
Baseline everything first — p95/p99, CPU, heap, GC, thread-pool queues and rejections, disk, shard sizes, slow logs. Build the new index, reindex, replay production-like read traffic, and compare old versus new on latency, CPU, heap, disk, throughput and recovery behaviour. Then switch the alias, keeping the old index for rollback.
What was the riskiest part of the cutover?
Silent relevance drift. Latency regressions are obvious; a mapping or analyser change that quietly reorders the top 10 candidates is not — nothing errors and no graph moves. Result-parity checks on shadow queries are the only defence.
Walk me through how latency work became a cost cut.
In strict order: reduce wasted work (smaller shards, fewer scored documents, smaller responses, leaner index) → CPU, heap and storage pressure drop → verify p95/p99, rejections and recovery are stable → only then right-size node count, node type and replica count. Reversing that order makes the cluster less stable rather than cheaper.
Aren't replicas purely a high-availability setting?
They are both. Each replica is a full copy of the index — storage cost — and another destination for every write, while also adding read capacity. The right count is the maximum of what availability requires and what read QPS requires, not a number inherited from a default.
What would you do next?
- Hot/warm tiering once product defines “inactive” safely.
- Cache top-N for the hottest jobs rather than recomputing identical searches.
- Replace from/size with search_after wherever deep paging remains.
- Continuous shard-size monitoring, so the next reindex is triggered by evidence rather than a hunch.
- Revisit routing only if fanout is measured to be the bottleneck.
20What not to say
| Do not say | Say instead |
|---|---|
| “We had too many documents per shard.” | Document count was within guidance. Shard size was ~8× over. |
| “I optimised Elasticsearch.” | Name the six decisions: shard sizing, node capacity, new index, mappings, query structure, coordinating nodes. |
| “We added shards so it got faster.” | We re-derived shard count from primary-store size against Elastic's 10–50 GB guidance. |
| “I owned the Elasticsearch platform.” | I owned the candidate-search performance work on it. |
| “We scaled the cluster down to save money.” | We removed the wasted work first, verified stability, then right-sized. |
| “More shards is always better.” | There is a ceiling — fanout, cluster state and segment overhead all grow with shard count. |
21Gaps to fill before the interview
The storage model, shard target, node counts and ratios here are a capacity model — sound arithmetic against Elastic's published guidance, not measurements from your cluster. Before quoting any specific figure, confirm it is one you actually observed.
- Ownership, per area. Implemented / recommended / reviewed, for each of the six decisions.
- Real shard numbers. Original and final primary count, and the actual average shard size in GB.
- Real node counts. Data, master and coordinating nodes before and after, and the instance type.
- Real latency. Measured p95 and p99 before and after, by query type.
- The specific clauses that moved from scoring to filter context.
- Cost composition. Whether the ~50% came from node count, instance type, replicas, storage — or a mix.
- Mentorship story. One engineer from the five: gap, coaching, what they owned afterwards.
“The lesson was that we had been measuring the wrong dimension. Documents per shard was inside guidance and gigabytes per shard was eight times over, and every symptom — latency, heap, cache misses, slow recovery, cost — came from the number nobody was checking.”
That's the search platform. Three more deep dives sit alongside it — the allocation platform, mtptrace and the terabyte pipeline — and the notes cover the underlying topics.
← back to projects