This is an automated email from the ASF dual-hosted git repository.
danny0405 pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/asf-site by this push:
new f2e4bd68a7cf docs(blog): why MOR enables async indexing (MOR series
part 3) (#19070)
f2e4bd68a7cf is described below
commit f2e4bd68a7cf66745303e33303a0a65907b7b68c
Author: Sivabalan Narayanan <[email protected]>
AuthorDate: Sun Jun 28 21:38:24 2026 -0700
docs(blog): why MOR enables async indexing (MOR series part 3) (#19070)
Third post in the MOR-as-architectural-shift series. Walks through
RFC-45's async indexing design and explains why the three primitives
it depends on (stable file-group container, INDEXING timeline action,
two-stage partition lifecycle) only exist because MOR put them in
place years earlier.
Also adds 'async indexing' to the blog tag allowlist.
---
.github/scripts/validate-blog.py | 1 +
...6-06-25-building-indexes-on-a-moving-target.mdx | 189 +++++++++++++++++++++
.../async-indexing-architectural-consequence.png | Bin 0 -> 2981809 bytes
.../async-indexing-end-end-flow.png | Bin 0 -> 2354420 bytes
4 files changed, 190 insertions(+)
diff --git a/.github/scripts/validate-blog.py b/.github/scripts/validate-blog.py
index 847afbb43f38..63cee6129d2e 100644
--- a/.github/scripts/validate-blog.py
+++ b/.github/scripts/validate-blog.py
@@ -49,6 +49,7 @@ ALLOWED_TAGS = {
'bytearray', 'best practices', 'backfilling', 'architecture',
'apicurio registry', 'apache zeppelin', 'apache orc', 'apache
dolphinscheduler',
'apache avro', 'apache', 'access control', 'lakehouse', 'merge on read',
'record level index','rli', 'penn interactive', 'southwest airlines', 'metica',
+ 'async indexing',
}
# Tags that should not be used
diff --git a/website/blog/2026-06-25-building-indexes-on-a-moving-target.mdx
b/website/blog/2026-06-25-building-indexes-on-a-moving-target.mdx
new file mode 100644
index 000000000000..58cdfc6101f6
--- /dev/null
+++ b/website/blog/2026-06-25-building-indexes-on-a-moving-target.mdx
@@ -0,0 +1,189 @@
+---
+title: "Building Indexes on a Moving Target"
+excerpt: "Async indexing in Hudi lets you add a new index to a multi-petabyte
table without stopping ingestion — and the design only works because MOR put
the right primitives in place years earlier."
+authors: [sivabalan]
+category: deep-dive
+image:
/assets/images/blog/2026-06-25-building-indexes-on-a-moving-target/async-indexing-architectural-consequence.png
+tags:
+- mor
+- merge on read
+- indexing
+- async indexing
+- metadata
+- architecture
+- data lakehouse
+---
+
+---
+
+_This is the third post in a series on Merge-On-Read as an architectural shift
in Apache Hudi. The [first
post](/blog/2026/05/14/mor-isnt-a-storage-optimization-its-an-architectural-shift)
made the broad case; the
[second](/blog/2026/06/05/why-metadata-has-to-be-mutation-friendly) showed how
the metadata table inherits the same append-first model; this one follows the
pattern one step further — into how new indexes can be added to a live table
without stopping it._
+
+---
+
+<div style={{ fontSize: '1.15rem', lineHeight: '1.75' }}>
+
+The first post in this series argued that Merge-On-Read is not a storage
optimization but an [architectural
shift](/blog/2026/05/14/mor-isnt-a-storage-optimization-its-an-architectural-shift)
— a strategy for time-shifting work in systems where mutation and analytical
scan run at different rhythms. The second grounded that argument in Hudi's
metadata table, showing how metadata itself became another [append-first
workload](/blog/2026/06/05/why-metadata-has-to-be-mutation-friendly). [Asyn
[...]
+
+This article assumes some familiarity with Hudi's metadata table and its
indexing capabilities. If you're new to the topic, the motivation for metadata
indexes, their role in query planning, and the evolution of the metadata table
are covered in detail in
[RFC-45](https://github.com/apache/hudi/blob/master/rfc/rfc-45/rfc-45.md) as
well as our earlier articles on the [metadata table](/docs/metadata) and
[mutation-friendly
metadata](/blog/2026/06/05/why-metadata-has-to-be-mutation-friendly [...]
+
+The title is literal. In Hudi, you can build a new index — record-level,
secondary, expression, column stats, bloom filter — over an existing
multi-petabyte table that doesn't stop while you build it. Ingest writers keep
committing. No outage. No drained pipeline. No swap window. The bootstrap reads
historical state into a freshly initialized metadata partition; concurrent
writers route new mutations into the same partition's file groups; a
reconciliation step at the end stitches the seam.
+
+That capability does not exist in any other open lakehouse format at the spec
level — not because nobody implemented it, but because the storage layer every
other format is built on cannot express the pattern. Async indexing is not a
feature added to Hudi; it is a consequence of decisions Hudi made in 2017. The
only systems that can build indexes this way are the ones whose mutation model
is already append-first.
+
+[RFC-45](https://github.com/apache/hudi/blob/master/rfc/rfc-45/rfc-45.md) is
the design doc, and the [Onehouse engineering
blog](https://www.onehouse.ai/blog/asynchronous-indexing-using-hudi) walks
through the mechanics. Those explain *what* async indexing does and *how* it's
wired in. This post is about why the design works at all — what architectural
primitives have to exist underneath before something like it becomes a coherent
thing to build.
+
+---
+
+## The Shape of the Problem
+
+Adding a new index over an existing large table has always been an
operationally expensive thing for analytical storage to do. Scan every row,
project the indexed columns or compute the indexed expression, materialize the
index. The trouble is that "scan every row" of a multi-petabyte table takes
hours, and during those hours the table either has to stop accepting writes or
has to coordinate with a table that won't sit still.
+
+Two responses dominate the lakehouse conversation, and both inherit the
limitations of their foundation.
+
+The first is to bind the index to the write path: compute the value at
ingestion time and write it alongside the data — column-level statistics,
min/max metadata, file summaries. This is what Iceberg and Delta do by default.
It works, but it cannot retroactively add an index. If the table was written
for two years without it, the write path can't help; the data is already on
disk in its final form.
+
+The second is to lock-and-rebuild: global or per-partition lock, scan,
materialize, atomic swap, release. Fine at small scale. At petabyte scale on a
continuously ingesting table the lock duration is hours, the ingest pipeline
has to stop or buffer indefinitely, and any failure during the rebuild discards
the work.
+
+Hudi's predicament when
[RFC-15](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=147427331)
introduced the metadata table was that MDT was going to host indexes —
`files`, `column_stats`, `bloom_filters`, `record_index`, `partition_stats`,
`secondary_index`, `expression_index`. Every one of those would eventually need
to be added over an existing table by someone who'd been running Hudi for a
year before the index existed. If index addition required taking the table offl
[...]
+
+---
+
+## What COW Makes Impossible — Not Hard, Impossible
+
+Imagine implementing async indexing on a Copy-On-Write storage layer — a
generic columnar lakehouse format where the unit of mutation is the file
rewrite. Bootstrap scans history, computes, materializes. Concurrent writers
continue ingesting. Index has to be queryable at the end.
+
+The bootstrap is slow under any architecture; that's not what breaks. What
breaks is that there is no place for concurrent writers to put their
contribution to the new index while the bootstrap is running.
+
+Under COW, every index update is a rewrite of a finalized index file. There is
no append channel. If the indexer has materialized `idx_000.parquet` for
history up to `t`, and a writer commits at `t+1`, the writer cannot contribute
to `idx_000.parquet`. The writer's only option is `idx_001.parquet` — which
means the bootstrap has to coordinate with the writer about which file was
produced, validate it, possibly merge it. That coordination is the
synchronization we were trying to avoid. Th [...]
+
+The workaround in rewrite-oriented systems is *bootstrap-and-swap*: build
against a frozen snapshot at `t`; writers ingest normally but don't touch the
new index; at `t + Δ`, replay all writes between `t` and `t + Δ` into the
index, then atomically swap. This works, but the replay window has no upper
bound (if ingest exceeds replay rate, catchup never converges), and the index
is unavailable for query during the entire bootstrap-plus-replay duration.
+
+Hudi doesn't need bootstrap-and-swap because the foundation offers something
neither Iceberg nor Delta has: a stable file group with an append channel, and
a timeline that expresses index construction as a first-class action with its
own reconciliation semantics. Both primitives are downstream of MOR.
+
+Before examining the architectural primitives individually, it's helpful to
look at the end-to-end lifecycle of an async index build. The figure below
intentionally presents the high-level execution flow rather than the
implementation details. For readers interested in the complete design,
state-machine transitions, and failure handling,
[RFC-45](https://github.com/apache/hudi/blob/master/rfc/rfc-45/rfc-45.md)
provides the authoritative specification. Here, we focus on the architectural
[...]
+
+<div style={{ textAlign: 'center' }}>
+ <img
src="/assets/images/blog/2026-06-25-building-indexes-on-a-moving-target/async-indexing-end-end-flow.png"
alt="Async indexing end-to-end flow" width="1000"/>
+</div>
+
+---
+
+## The Three Primitives, All of Them MOR Consequences
+
+Async indexing in Hudi rests on three substrate-level primitives. Each one is
something the MOR design forced into existence for other reasons, and each one
is what makes RFC-45's design coherent rather than a heroic engineering effort
against the storage layout.
+
+### Primitive 1: The File Group as a Stable Mutation Container
+
+The [file group](/docs/storage_layouts) is a stable logical mutation
container. Once initialized, multiple independent processes — not just ingest
writers, but background services such as index builders — can deterministically
contribute mutations into the same logical object while deferring physical
reconciliation. Async indexing is one application of this primitive. Later in
the series, we'll see the same stable mutation containers reused by other Hudi
capabilities, including async com [...]
+
+When the indexer initializes a new metadata partition, it creates empty file
groups for that partition. From that moment, they exist on disk and are
addressable. A writer that sees the partition is inflight (via table config)
can immediately append log blocks into those file groups, even though base
files haven't been written yet. The writer doesn't need to know whether the
indexer has finished the base file or coordinate physical layout at all. It
needs only to know which file group to [...]
+
+Under COW this primitive doesn't exist. There's no "container the indexer
prepares and writers append into" — only files, produced atomically and
consumed read-only. The closest analog is a shared directory both processes
write new files into; but that's a namespace, not a container, and the
synchronization problem returns immediately.
+
+### Primitive 2: INDEXING as a First-Class Timeline Action
+
+[Hudi's timeline](/docs/timeline) is an append-only event log: commits,
deltacommits, cleans, compactions, restores, rollbacks. RFC-45 adds
**INDEXING_ACTION**. An index build is not a side process; it is represented as
its own sequence of timeline instants: `<t>.indexing.requested`,
`<t>.indexing.inflight`, and `<t>.indexing` (completed), alongside data writes
and other table services.
+
+That placement matters. Because the action lives on the timeline, concurrent
writers discover it through the same timeline refresh they already perform for
commits and table services; no separate coordination channel is required.
Because it follows the standard requested → inflight → completed lifecycle,
existing mechanisms for recovery, rollback, and lock-protected state
transitions apply without introducing a parallel state machine. Async indexing
is built by extending the timeline rat [...]
+
+More importantly, the timeline and table configuration together govern
**visibility**, not just execution. Bootstrap and Catchup may execute for
minutes while the table continues accepting writes, but the new index remains
invisible to readers throughout that period.
+
+Internally, the indexing action progresses through the standard `requested →
inflight → completed` lifecycle on the timeline, while the table configuration
independently tracks whether the metadata partition is still under construction
(`metadata.partitions.inflight`) or has become queryable
(`metadata.partitions`). Readers observe the new index only after both
transitions have completed. This separation is subtle but fundamental.
Bootstrap and Catchup describe **how** the index is const [...]
+
+Although async indexing predates the completion-time semantics introduced
later for non-blocking concurrency control, it already exhibits the same
architectural pattern: background work progresses independently, reconciliation
completes asynchronously, and publication is deferred until the system can
present a consistent view. Later in the series, we'll see how Hudi generalizes
this idea for concurrent writers using completion-time ordering.
+
+### Primitive 3: Two-Stage Partition Lifecycle in Table Config
+
+The subtlest of the three, and the one most directly inherited from MOR.
`hoodie.properties` carries two related fields:
`hoodie.table.metadata.partitions.inflight` lists partitions initialized and
receiving writes but not yet queryable; `hoodie.table.metadata.partitions`
lists partitions fully built and safe to read.
+
+That split is what makes async indexing safe under split-brain reader/writer
views. When scheduling completes — file groups initialized, plan persisted —
the partition is added to `*_INFLIGHT`. Writers see it and start routing log
blocks. Readers see it too, and ignore the partition: table config is the
source of truth for query readiness. Only after the bootstrap base is written,
Catchup reconciles any missing metadata updates from concurrent commits, and
the indexing action completes d [...]
+
+The two-stage config is MOR's base-file-vs-log-file separation lifted to the
partition level. Log files are writable before the next base file is
materialized; readers reconcile both at query time. Inflight partitions are
writable before the bootstrap completes; readers ignore them until done. Same
idea, different scope. You can see this in code at
`RunIndexActionExecutor.updateTableConfigAndTimeline`: the partition
transitions from inflight to active as part of the indexing completion s [...]
+
+The abort path at `RunIndexActionExecutor.abort` removes the partition from
both lists and deletes the physical partition; on any build failure, the
partition reverts cleanly to "doesn't exist."
+
+Because both scheduling and completion execute under Hudi's timeline lock
provider, these metadata transitions remain serialized even though the
bootstrap itself proceeds concurrently with foreground writers.
+
+Interestingly, these primitives turn out to be useful well beyond index
construction. Stable mutation containers, timeline-ordered visibility, and
deferred reconciliation reappear repeatedly throughout Hudi's architecture.
Async indexing is one application. Later in the series, we'll see the same
foundations support background optimization and eventually reshape how
concurrent writers coordinate against the same data.
+
+<div style={{ textAlign: 'center' }}>
+ <img
src="/assets/images/blog/2026-06-25-building-indexes-on-a-moving-target/async-indexing-architectural-consequence.png"
alt="Async indexing as an architectural consequence of MOR" width="1000"/>
+</div>
+
+---
+
+## Indexing Catchup as Deferred Reconciliation, Generalized
+
+The three primitives set up the most interesting piece of the design: indexing
catchup. The implementation lives in `AbstractIndexingCatchupTask`,
orchestrated by `RunIndexActionExecutor`. It's where the MOR-as-foundation
argument becomes concrete.
+
+The indexing plan is scheduled against a bootstrap target — the latest
completed data-table instant at scheduling time. Bootstrap builds the
historical portion of the index by scanning the table up to that point.
Meanwhile, the data timeline continues advancing.
+
+A common misconception is that Bootstrap alone constructs the index. In
reality, every commit that arrives while Bootstrap is running is equally part
of the index build — it simply reaches the metadata partition through the
incremental write path rather than the historical scan. Writers that observe
the indexing action after it becomes inflight begin appending metadata updates
into the new file groups. By the time Bootstrap completes, the metadata
partition consists of a historical base [...]
+
+The indexer cannot simply assume that every eligible commit has been reflected
in the metadata timeline. Some writers may have started before the indexing
action became visible. Others may still be inflight. Catchup exists to
reconcile that uncertainty before the index becomes visible.
+
+Catchup walks the relevant portion of the data timeline and compares each
eligible data-table instant against the metadata timeline using the indexing
action's requested-time identifier. If the corresponding metadata update
already exists, the writer has already contributed its portion of the index.
Otherwise, Catchup synthesizes the missing metadata update on the writer's
behalf.
+
+Inflight writers receive special treatment. Rather than immediately treating
them as missing, Catchup waits for active writers to complete while monitoring
their heartbeats. Writers that complete normally are incorporated through the
usual reconciliation path. Writers whose heartbeats have expired are treated as
abandoned and skipped, allowing index construction to converge without waiting
indefinitely. The wait itself is bounded by
`hoodie.metadata.index.check.timeout.seconds` (900 seco [...]
+
+This is deferred reconciliation in its purest form. Bootstrap constructs
historical state. Concurrent writers continue making forward progress. Catchup
reconciles any remaining divergence between the data timeline and metadata
timeline before publication. Readers never observe intermediate state.
+
+> The pattern is exactly what Merge-On-Read readers already do for every file
group: reconcile a stable base with subsequent incremental mutations to produce
a consistent view. Async indexing applies the same architectural idea at the
scope of an entire metadata partition.
+
+The mechanism is different, but the underlying principle is identical:
foreground progress continues independently, while background reconciliation
converges the system before publication.
+
+---
+
+## The Races, and How the Architecture Absorbs Them
+
+RFC-45 enumerates four error cases. Each one is a place where synchronous
coordination would have required a lock, and each one is handled instead by
deferred reconciliation against timeline state. They're worth walking through
because they make the architectural argument concrete.
+
+**Writer fails while the indexer is inflight.** The writer crashed before
logging its update. The indexer doesn't care. Catchup observes the failed
instant, sees no metadata deltacommit, and either skips it (if data-table
rollback already cleaned up) or fills it in. The metadata partition isn't
corrupted because reader visibility hasn't flipped yet.
+
+**Indexer fails while a writer is inflight.** The writer continues logging
into the new file groups. The partition stays in `*_INFLIGHT` because no commit
metadata was written. Readers continue ignoring it. When the indexer is
re-triggered, it sees the partition is inflight, picks up the build (idempotent
by partition name in `ScheduleIndexActionExecutor`), and proceeds. The writer's
log files aren't lost — they're already in the file group, waiting for catchup.
+
+**Writer goes inflight just after the indexing request, before the indexer
executes.** A writer begins after the indexing action becomes visible and
therefore participates in index construction from the outset. By bootstrap
completion, log files already exist. Catchup observes them, sees the
corresponding deltacommit, moves on. No coordination — writer and indexer were
independently pointing at the same stable container.
+
+**Inflight writer about to commit but indexing just completed.** The trickiest
race. The writer started knowing the partition was inflight; during its
execution the partition flipped to completed. The writer commits, producing a
log file in the now-completed partition, and the indexer missed it. Because the
reader engine is designed from day one to merge base + logs on the fly, **the
reader's merge layer inherently immunizes the system against race conditions.**
The storage layout itself [...]
+
+In each case, the response shape is the same: stable container, timeline
record, reconciliation at the next quiet moment. None of this is
async-indexing-specific. It's what every MOR table service does.
+
+Notice that none of these cases require the bootstrap itself to synchronize
with foreground writers. Synchronization is reserved for timeline state
transitions, while correctness is achieved through deferred reconciliation
before publication.
+
+---
+
+## Why This Isn't Expressible in Rewrite-Oriented Formats
+
+Can async indexing be added to Iceberg or Delta with sufficient engineering
effort? It can be approximated, but the approximation either gives up the "no
synchronization with writers" property or requires inventing the three
primitives above on a foundation that doesn't naturally support them.
+
+Iceberg has manifests, manifest lists, snapshots — no stable logical container
with an append channel. Each commit produces new manifest files; there's no
"manifest the indexer prepares and writers contribute to." Row lineage and v3
deletion vectors give stable row identity, but that's not the same primitive as
a stable append container. Adding async indexing to Iceberg would require a new
manifest layout where indexers stage skeletons that subsequent writers
contribute to — a spec chang [...]
+
+Delta has the transaction log, deletion vectors, and (in Databricks Runtime)
row-level concurrency for non-overlapping MERGE/UPDATE/DELETE. None provide an
append container stable across commits. Coordinated commits in 4.0 improve
multi-engine atomic commit, but the unit of commit is still a set of new files;
there's no notion of writers appending into a file the indexer staged. Delta's
data skipping and statistics are written during the data commit; they cannot be
retroactively built wi [...]
+
+Async indexing isn't a missing feature in those formats; it's missing because
the foundation cannot express the pattern. Adding it would require lifting
append-first mutation into the storage model itself. That is most of what MOR
is.
+
+The "async-indexing-ish" capabilities those formats have shipped are either
write-path additions (compute during writes, can't retroactively add) or
batch-and-swap (block readers during a window, schedule a maintenance window).
Neither is what Hudi async indexing actually does.
+
+---
+
+## What This Unlocks Operationally
+
+The architectural argument lands on a small set of capabilities that change
how teams operate Hudi tables.
+
+The most immediate: *adding indexes to existing tables without a maintenance
window.* RLI on a 1 PB table for primary-key lookup. Secondary index on
`customer_email` for a query pattern that wasn't important when the table was
created. Expression index on `LOWER(country_code)` because the workload
changed. Issue `CREATE INDEX` (or run `HoodieIndexer`) against the running
table. Bootstrap fills history. Writers continue. Catchup closes the seam.
+
+Second: *index re-initialization as a routine operation.* If an index gets
into a bad state — schema evolution mismatch, corrupted base file, missed
catchup window — recovery is "delete the partition, re-run the indexer." Not a
migration event. The same `ScheduleIndexActionExecutor.abort` path handles
cleanup before re-initialization.
+
+Third: *staged index rollouts.* You don't have to enable every metadata
partition at table creation. Enable `files` and `column_stats` initially, add
`record_index` later when point-lookup load justifies it, `secondary_index`
later still when a new query pattern emerges. The metadata table's footprint
grows with the workload's actual needs. A table running for years can adopt a
new index without dropping a write.
+
+None of these are theoretical. Take any one primitive away — make the file
group non-append, take INDEXING off the timeline, collapse the two-stage
partition config — and the operational story collapses with it.
+
+---
+
+## What MOR Ultimately Made Possible
+
+A single architectural decision — append-first mutation with deferred
reconciliation — produces a family of capabilities that look like independent
features but are the same idea applied to different scopes. The metadata table
is MOR because the commit cadence demands it. Async indexing is possible
because the same stable mutation containers can be initialized empty and
evolved by concurrent contributors.
+
+Looked at in isolation, each capability has its own moving parts: RFC-15 for
MDT, RFC-45 for async indexing, and later in the series we'll examine how Hudi
extends the same architectural principles — stable mutation containers,
deferred reconciliation, and timeline-mediated publication — to support
non-blocking concurrency control. The descriptions are all correct. They miss
the bigger point, which is that the moving parts exist because MOR established
the underlying mutation model — and [...]
+
+</div>
diff --git
a/website/static/assets/images/blog/2026-06-25-building-indexes-on-a-moving-target/async-indexing-architectural-consequence.png
b/website/static/assets/images/blog/2026-06-25-building-indexes-on-a-moving-target/async-indexing-architectural-consequence.png
new file mode 100644
index 000000000000..397dc1b48700
Binary files /dev/null and
b/website/static/assets/images/blog/2026-06-25-building-indexes-on-a-moving-target/async-indexing-architectural-consequence.png
differ
diff --git
a/website/static/assets/images/blog/2026-06-25-building-indexes-on-a-moving-target/async-indexing-end-end-flow.png
b/website/static/assets/images/blog/2026-06-25-building-indexes-on-a-moving-target/async-indexing-end-end-flow.png
new file mode 100644
index 000000000000..ebe3b56fe2c3
Binary files /dev/null and
b/website/static/assets/images/blog/2026-06-25-building-indexes-on-a-moving-target/async-indexing-end-end-flow.png
differ