Skip to content

fix(invertedindex): stamp flushed posting keys with the deduped doccount - #114

Merged
oc-engteam merged 1 commit into
mainfrom
fix/invertedindex-flush-doccount
Jul 11, 2026
Merged

fix(invertedindex): stamp flushed posting keys with the deduped doccount#114
oc-engteam merged 1 commit into
mainfrom
fix/invertedindex-flush-doccount

Conversation

@oc-engteam

Copy link
Copy Markdown
Collaborator

What & why

flushPendingWrites stamped a posting row's on-disk key doccount with the pre-dedup buffer
length. updateIndex appends a docid to a keyword's buffer once per occurrence without dedup (for
ingest speed), so a hot keyword (e.g. every function name in a file contains get) accumulates a flush
row whose buffer length ≫ its unique docid count. But the row VALUE is encodeInvertedValue(docids),
which sorts and dedups. So the key's doccount could be many times the number of docids actually
stored.

The background merger uses that doccount to decide "already well batched": a row with
doccount > maxInvertedIndexSize/2 is skipped from merge groups (keywords_merger.go:242), and a
keyword group with a high DocCount/len(rows) is early-returned (rewriteIndex). An inflated doccount
therefore permanently quarantines hot-keyword rows from compaction — inflating index disk usage
and the number of rows a hot-term Search must scan.

  • Search results are unaffecteddoccount (3rd return of decodeInvertedKey) is read only by
    the merger; Search/GetDocs read the value bytes. This is a build-throughput / footprint /
    defeated-compaction defect, not a wrong-results bug.
  • The delete and merge paths already stamped the true deduped count (the comment at
    invertedindex_internal.go even names this exact "inflated doccount → quarantined forever" hazard) —
    only the common flush path was wrong.

Fix

Make the stamped doccount always equal the value's post-dedup unique count, computed in the same
pass
that encodes the value, and route all three writers through one seam:

  • encodeInvertedValueCounted(docids) ([]byte, int) — the existing encoder body, now also returning the
    post-Compact unique count (len(us) == the varint count). encodeInvertedValue delegates to it.
  • defaultWriteInvertedIndex(idx, batch, tableId, kw, docids, tick) — a single named seam that builds
    the key from that unique count, so a stamped doccount can never again disagree with the row it
    labels. flush / delete / merge all call it (delete & merge are behavior-identical — they already
    passed a deduped set).

No on-disk format change (the key layout is unchanged; only the doccount value in new keys becomes
correct), no reindex, no StorageVersion bump. Existing inflated rows on disk heal only on a natural
re-index — merger self-heal was considered and deliberately left out of scope.

Tests

Two new behavior tests (real Index on a t.TempDir KV, real on-disk format):

  • TestFlush_StampsDedupedDoccount — a keyword flushed with duplicate occurrences: asserts the stamped
    doccount equals the value's unique docid count. RED before (3 ≠ 1), GREEN after.
  • TestMerge_CompactsHotKeywordRows — several inflated hot-keyword rows (MaxInvertedIndexSize=10,
    each doccount 6 > 5): asserts the merger compacts them and the searchable docid set is preserved.
    RED before (rows quarantined, count unchanged), GREEN after.

encodeInvertedValueCounted and defaultWriteInvertedIndex are both 100% covered; the seam's 10 test
overrides + 1 direct caller were migrated; full package passes with -race.

🤖 Generated with Claude Code
via Happy

flushPendingWrites stamped a posting row's key doccount with the pre-dedup
buffer length (updateIndex appends a docid per keyword occurrence without
dedup), while the row value is sorted+deduped. For hot keywords the inflated
doccount crosses the merger's maxInvertedIndexSize/2 "already batched"
threshold, so those rows were quarantined from compaction forever — inflating
index disk and hot-term scan cost (search results were unaffected; doccount is
read only by the merger). The delete and merge paths already stamped the true
deduped count; only the common flush path was wrong.

Route all three writers through a single named seam defaultWriteInvertedIndex
that builds the key from encodeInvertedValueCounted's post-dedup unique count in
the same pass that encodes the value, so a stamped doccount can never again
disagree with the row it labels. No on-disk format change, no reindex; existing
inflated rows heal only on a natural re-index (merger self-heal was out of scope).

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@oc-engteam
oc-engteam merged commit 3ec3b8c into main Jul 11, 2026
4 checks passed
@oc-engteam
oc-engteam deleted the fix/invertedindex-flush-doccount branch July 11, 2026 02:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants