Step 10: Engineering Write-Up
The PR is merged. The issue is closed. The backport is green. Most contributors stop here. The ones who become maintainers write the post. The write-up is the artifact that travels with you when you change jobs, request a maintainer nomination, or get cited by another contributor working a similar bug.
Five hundred to a thousand words. Most of it written in the few hours right after merge, while the dead ends are still fresh in your memory.
Why It Matters
Three audiences:
- Future you. Six months from now you'll touch
TermsAggregatoragain and want to remember what you tried and why the fix is shaped the way it is. - The next contributor working a similar bug. They'll find your post via search ("OpenSearch terms agg missing bucket min_doc_count") and shortcut a week of work.
- The maintainers / TSC evaluating you. A maintainer nomination is a judgment about whether you can communicate engineering reasoning, not just produce diffs. The write-up is the evidence.
A good write-up is not a press release. It is a postmortem: honest about what you tried, including the approaches that failed.
The Template
Sections in order, with suggested word counts. A worked excerpt follows the aggregation Capstone bug throughout, so you can see the shape.
Title (one line)
Fixing #NNNN: <one-line technical summary>
Examples:
- "Fixing #NNNN: terms aggregation dropped the
missingbucket on empty shards" - "Fixing #NNNN: a ClusterApplierService listener that leaked on coalesced states"
- "Fixing #NNNN: a seqno race in InternalEngine under concurrent same-
_idupdates"
Technical and specific. Not "My first OpenSearch contribution" — write that post separately. The engineering post stands on its own and gets cited; the journey post gets one-time clicks.
Problem (100–150 words)
What broke, for whom, under what conditions. Plain English, but precise. Symptom, trigger condition, affected version range. No code yet.
A
termsaggregation requested withmin_doc_count: 0and amissingvalue would return an emptybucketsarray instead of the synthetic bucket, whenever at least one queried shard held zero matching documents. The simplest trigger is a single-shard index where the aggregated field exists in the mapping but in no document: the response is"buckets": []rather than the documented[{ "key": "N/A", "doc_count": 1 }]. Dashboards visualizations that rely on "show empty buckets" silently lost their zero rows. Reproducible onmainand on the2.xline; the behavior regressed in #4567, which optimized the shard-local empty-segment path.
Investigation Log (200–300 words)
The most valuable section. Walk through what you tried, including the hypotheses that were wrong. Three to five hypotheses, in order, each with one sentence on what suggested it and one on what disproved it.
My first hypothesis was that the reduce step (
InternalTerms.reduce) was discarding the bucket while merging shards. I added a unit test driving reduce directly with two hand-built shard results, one containing themissingbucket — reduce kept it. That ruled out the coordinating node and pointed back at the shards.Second hypothesis: the bucket was never produced on the empty shard. TRACE logging on
org.opensearch.search.aggregationsfor a one-doc, one-shard repro confirmed it: the shard-local aggregator emitted zero buckets for the empty-segment case, so reduce had nothing to merge. The bug was upstream of reduce, in shard-local collection.Reading
TermsAggregator.doPostCollection()top to bottom surfaced an empty-segment fast path:if (!hasValuesForField()) return;. Agit blameplaced it in #4567 — a performance optimization that skipped bucket-building when a segment had no ordinals for the field. The assumption ("no values ⇒ no buckets to emit") is true formin_doc_count >= 1, where empty buckets are discarded anyway, but false formin_doc_count == 0, where the synthetic bucket must be emitted by every shard.git bisectbetween2.11.0(good) andmain(bad) confirmed #4567 as the introducing change in four steps.
Root Cause (50–100 words)
One paragraph, the truth as you now understand it. Cite the introducing PR and the bisect commit.
The shard-local terms aggregator short-circuits bucket construction when a segment has no values for the field. That fast path (introduced in #4567, commit
a1b2c3d4) is correct formin_doc_count >= 1but skips the synthetic andmissingbuckets thatmin_doc_count == 0requires every shard to emit. With no shard producing the bucket, the coordinating-node reduce has nothing to merge and the response omits it.
Final Design (150–200 words)
What you changed and why this design over the alternatives. Show the diff size and the principle.
The fix gates the fast path on
min_doc_count:if (!hasValuesForField() && bucketCountThresholds.getMinDocCount() != 0) { return; }One guard condition in
doPostCollection(). Behavior formin_doc_count >= 1is byte-for-byte identical (the extra clause istruein that case, so the branch is unchanged). The synthetic bucket is now emitted per shard exactly whenmin_doc_count == 0, restoring the input reduce expects. No serialization change, so noVersionguard and no BWC test — only the intended observable change (the bucket reappears). Public API surface unchanged; no new setting. The production diff is two lines plus a comment citing the issue.
Alternatives Considered (100–150 words)
Two or three alternatives you rejected, with the reason. This is the section that separates contributor-quality from maintainer-quality write-ups.
Synthesize the bucket at reduce time when
min_doc_count == 0and no shard produced it. Rejected: reduce lacks the per-shardmissing-value context to build a correct bucket without risking double-counting; it would push correctness into the merge path that runs for every terms agg.Revert #4567's fast path entirely. Rejected: it's a real and measured optimization for the common
min_doc_count >= 1case; reverting it regresses performance to fix a narrow parameterization.Document the combination as unsupported. Rejected: there is a clear correct answer; documenting around a fixable bug taxes every user.
Performance / Behavior Impact (50–100 words)
If perf-relevant, numbers. Otherwise, one honest sentence.
No measurable performance impact. The added clause is a single integer comparison on a path already gated by
hasValuesForField(), and it only changes behavior in themin_doc_count == 0case, which previously did less work incorrectly. The common path is unchanged. Validated by re-running the aggregation microbenchmark suite: no statistically significant delta across 10 runs.
Lessons Learned (100–150 words)
Three to five bullets, each concrete enough to be reusable by a peer.
- Fast-path optimizations are where correctness goes to die. Any
if (cheapCheck) return;added "for performance" is a candidate for a missed parameterization. When you find one inblame, ask which inputs it didn't consider.- Bisect against release tags is cheaper than archaeology. Four
git bisectsteps between2.11.0andmainfound #4567 faster than reading the file's history would have.- Prove the layer before you fix the layer. Driving
reducedirectly in a unit test ruled out the coordinating node in 20 minutes and saved me from "fixing" the wrong place.- A negative-control test is non-negotiable for behavior fixes — it's the only thing proving your change is scoped, not a blanket flip.
Links
- Issue: https://github.com/opensearch-project/OpenSearch/issues/NNNN
- PR: https://github.com/opensearch-project/OpenSearch/pull/NNNN
- Merged commit: <SHA>
- Backport (2.x): https://github.com/opensearch-project/OpenSearch/pull/MMMM
- Introducing PR #4567: <SHA>
Where to Publish
Three venues, in roughly decreasing order of effort and impact.
1. Personal or company engineering blog
The full ~1000-word write-up. SEO-friendly title with the symptom phrase a user would search ("OpenSearch terms aggregation missing bucket"). Link prominently to the issue and PR. This is the version that follows you across jobs and into a maintainer nomination.
2. The OpenSearch community forum
Post a shorter version (300–500 words) on forum.opensearch.org, focused on
the lesson and the user-facing behavior, under the relevant category. This reaches
operators who hit the symptom and search the forum before the code. Link the blog
post for the deep version.
3. The PR itself + a Slack/community-meeting mention
The PR description already carries the engineering reasoning (you wrote it in
Step 8). For a non-obvious or widely-felt fix, a one-line mention
in the OpenSearch public Slack (opensearch.org/slack) or a community meeting —
"fixed a terms-agg empty-bucket bug that affected Dashboards zero-rows, details in
#NNNN" — lets maintainers and watchers see the reasoning without reading the whole
diff. Optional, earns goodwill, and is how people start to know your name.
Anti-Patterns
What separates write-ups that help from ones that don't:
- "I learned so much!" — We know. Cut it. The artifact is the engineering.
- Personal narrative dominating the engineering. Save the "my journey into open source" angle for a separate post. Engineering posts get reread and cited.
- The sanitized "I knew the answer all along" version. Nobody believes it, and it misleads new contributors into thinking their messy investigation is abnormal. Be honest about the dead ends — they are the work.
- No code and no log line. A write-up that never shows the diff or the symptomatic response is unfalsifiable.
- No links. Issue, PR, merged commit — three minimum. Without the PR link the write-up is unreviewable.
- Padding to look thorough. A tight 600 words that respects the reader beats a 2000-word slog.
Validation / Self-check
Before declaring the Capstone complete:
- The write-up is published at a shareable URL (blog, forum, or a public
capstone-work/writeup.md), 500–1000 words — not 200 (thin), not 3000 (padding). - The Investigation Log contains at least two hypotheses you ruled out, not only the winning one.
- Alternatives Considered names at least two rejected designs with reasons.
- Lessons Learned has three to five bullets, each reusable by another contributor.
- Issue, PR, and merged-commit SHA are all linked (plus the introducing PR).
- The post reads as something a peer engineer would respect — a postmortem, not a triumph lap.
- You picked a publish venue and actually posted (or have it queued), not "I'll write it later."
Then close the loop with the Evaluation Rubric and grade yourself honestly.