hudi-agent commented on code in PR #19613:
URL: https://github.com/apache/hudi/pull/19613#discussion_r3789226521


##########
rfc/rfc-110/rfc-110.md:
##########
@@ -0,0 +1,975 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+# RFC-110: Hudi Full-Text Search Index
+
+## Proposers
+
+- @danny0405
+
+## Reviewers
+
+- @vinothchandar
+
+## Approvers
+
+- TBD
+
+## Status
+
+Issue: TBD
+
+RFC number: [RFC-110 reservation PR](https://github.com/apache/hudi/pull/19614)
+
+Status: Under review
+
+## Abstract
+
+This RFC proposes a native full-text index for Apache Hudi tables. Users can
+filter text through Spark SQL predicates or search directly through the Hudi RS
+Python API. Index creation, visibility, rollback, and cleaning follow the Hudi
+metadata table (MDT) indexing lifecycle.
+
+The design draws from established inverted-index systems and formats, including
+Apache Lucene, Elasticsearch/OpenSearch, Tantivy, Apache Paimon, and Lance. It
+does not embed any of them as a storage dependency. A term dictionary maps an
+analyzed word or token to a posting list: the Hudi records containing that 
term,
+their term frequencies, and optional token positions. BM25 statistics rank
+records for direct search.
+
+The main Hudi repository owns Spark SQL, the Java index builder and reader, MDT
+integration, and the versioned index-file specification. Hudi RS implements an
+independent Rust builder and reader plus Python bindings against that same
+specification. This makes the first Spark release independently useful without
+introducing Rust or JNI into the main repository.
+
+The MDT stores small authoritative records describing which index data files 
are
+visible for each data table file slice. The larger term dictionaries and 
posting
+lists live in immutable index data files below an MDT-owned auxiliary 
directory.
+Immutable means a published file is never edited in place; catch-up and
+consolidation write new files and atomically switch MDT references.
+
+Queries are snapshot-safe. Compatible index data accelerates unchanged data
+table file slices, while changed or unindexed file slices use the normal Hudi
+scan. Therefore enabling the index does not change Spark SQL results. The 
direct
+search API also defaults to complete results and may offer an explicitly
+incomplete low-latency mode.
+
+## Background
+
+Hudi indexes currently answer questions such as which files might contain a
+record key or a value range. Full-text search has a different contract. It
+analyzes a string column into terms, maps each term to matching Hudi record 
keys,
+optionally verifies term positions for phrases, and can rank the matching 
records.
+
+A posting is one entry connecting a term to a matching Hudi record. A posting
+list is the ordered collection of those entries for one term. An index data 
file
+is a Hudi-owned auxiliary file containing term dictionaries, posting lists,
+record-key mappings, and optional positions. These files are not data table 
base
+files, log files, or MDT HFile values; MDT records control their lifecycle and
+visibility.
+
+### Motivation and use cases
+
+Native full-text search is useful when the searchable text and Hudi timeline
+must remain one system of record:
+
+- **Logs and observability.** Find records containing an error signature or
+  phrase while applying normal predicates such as service, environment, and
+  event time.
+- **Product catalogs, support tickets, and CRM text.** Match user terms across
+  title and description while filtering on category, tenant, status, or access
+  policy.
+- **Security, audit, and eDiscovery.** Investigate text as of an incident or
+  legal-hold snapshot and reproduce the exact records visible at that instant.
+
+The correctness benefit is concrete. Suppose a GDPR deletion commits to Hudi
+while a CDC pipeline into an external search cluster is delayed or fails. The
+external cluster can continue serving the deleted record until it catches up.
+With this design, the Hudi timeline controls both table and index visibility. A
+query on the new snapshot either uses index data that exactly represents the
+file slice or scans the changed file slice, so the deleted record cannot be
+returned from stale index data.
+
+The same lifecycle enables time-travel search. A query `AS OF` an older instant
+selects index data for that snapshot when retained and falls back to the 
retained
+table files otherwise. An external search service would need its own versioned
+index and retention coordination to provide the same behavior.
+
+Native text and vector indexes could eventually support hybrid BM25 and 
semantic
+retrieval over one Hudi snapshot. This is a user-facing reason to keep both
+indexes snapshot-aware, but this RFC does not define a hybrid operator or 
depend
+on RFC-109 implementation or packaging.
+
+This proposal builds on:
+
+- [RFC-45](../rfc-45/rfc-45.md), which introduced asynchronous MDT indexing;
+- [RFC-77](../rfc-77/rfc-77.md), which established dynamically named secondary
+  index partitions and index definitions;
+- Spark SQL scalar predicates, which keep index acceleration transparent to
+  relational queries; and
+- Hudi RS, which provides Rust table readers and Python bindings.
+
+### Design principles
+
+1. The Hudi timeline is the source of snapshot truth.
+2. Index creation, visibility, rollback, and cleaning use MDT components.
+3. Published index data files are immutable and visible only through MDT 
records.
+4. Index use never changes the result of a Spark SQL predicate.
+5. Ranking is independent of how the index is physically partitioned.
+6. JVM and Hudi RS clients share query semantics and format versions.
+
+### Goals
+
+- Match token, boolean, prefix, fuzzy, phrase, and multi-column queries.
+- Support copy-on-write (COW) and merge-on-read (MOR) tables.
+- Build asynchronously and incrementally using the MDT indexer lifecycle.
+- Guarantee snapshot-correct results for SQL and the default direct API mode.
+- Range-read index blocks from object storage without loading an entire index
+  into a Spark executor or Hudi RS process. Builders spill sorted runs and
+  readers bound dictionary, posting-block, and query-expansion memory.
+- Provide a direct Hudi RS Python API alongside Spark SQL.
+
+Object storage provides persistence, not a memory bound. A broad or
+high-frequency term can reference millions of records, and each query task must
+decode part of that posting list. Block directories, range reads, bounded
+caches, streaming decoders, and expansion limits prevent one task from loading
+memory proportional to an entire segment.
+
+### Non-goals
+
+- Elasticsearch API, aggregation, highlighting, or percolator compatibility.
+- Highlighting and custom relevance models in the first format version.
+- Updating posting lists in place.
+- Replacing SQL predicate indexes or the record index.
+- Adding Rust code or native build integration to the main Hudi repository.
+
+## Implementation
+
+### Terminology
+
+| Term | Meaning |
+| --- | --- |
+| Text index definition | The named `HoodieIndexDefinition`, indexed columns, 
analyzer options, and `HoodieIndexVersion`. |
+| Text index MDT partition | Dynamic `text_index_<name>` metadata partition 
containing the authoritative index lifecycle records. |
+| Data table file group | The normal Hudi file group identified by partition 
path and file ID. |
+| Data table file slice | One base file and its ordered log files for a file 
group at a particular Hudi instant. |
+| Index segment | An immutable batch of text-index data files built from one 
or more data table file slices. It is not a Hudi or MDT file group. |
+| Index shard | A size-bounded part of an index segment whose local record 
ordinals fit in `u32`. |
+| File-slice identity | The exact table UUID, partition, file ID, base file, 
log files, schema, and merger state represented by an index segment. |
+| Coverage record | An MDT record mapping one data table file group to 
candidate index segments and their file-slice identities. |
+| Fallback scan set | Data table file slices without compatible index coverage 
for the pinned snapshot. |
+| Indexed record | One Hudi table record, located by its Hudi record key and 
data table file slice. |
+| Term | A token produced from an indexed string by the configured analyzer. |
+| Posting list | For one term, the ordered local record ordinals containing 
it, with term frequencies and optional positions. |
+
+An index segment may contain records from many data table file groups so that
+small file slices do not create many tiny index objects. Each segment stores a
+table from `file_slice_ordinal` to the corresponding partition path, file ID,
+and exact file-slice identity. The `COVERAGE` record for every included file
+group references that segment and ordinal. A file group can reference older and
+newer candidate segments for time travel, but the planner selects at most one
+exact file-slice match for a query snapshot.
+
+### SQL interface
+
+In this RFC, **SQL** means Spark SQL backed by Java code in the main Hudi
+repository. The **direct API** means Hudi RS Python backed by Rust code in the
+Hudi RS repository. Both are first-class readers of the same index-file format;
+the direct API is not invoked by Spark and Spark does not require a native
+runtime.
+
+Creation follows Hudi's secondary-index syntax:
+
+```sql
+CREATE INDEX log_message_fts
+ON application_logs
+USING text_index (message)
+OPTIONS (
+  'base_tokenizer' = 'simple',
+  'lower_case' = 'true',
+  'language' = 'und',
+  'with_position' = 'true',
+  'posting_block_size' = '128'
+);
+```
+
+The existing `HoodieIndexDefinition` is populated as follows:
+
+```json
+{
+  "indexName": "text_index_log_message_fts",
+  "indexType": "text_index",
+  "version": "V1",
+  "sourceFields": ["message"],
+  "indexFunction": "tokenize",
+  "indexOptions": {
+    "base_tokenizer": "simple",
+    "lower_case": "true",
+    "language": "und",
+    "with_position": "true",
+    "posting_block_size": "128"
+  }
+}
+```
+
+`version` is the existing `HoodieIndexDefinition.version` field and is required
+for current table versions. `TEXT_INDEX` is added to
+`HoodieIndexVersion.getCurrentVersion`; the first implementation returns `V1`.
+An incompatible change to index-definition interpretation or MDT record layout
+requires a new `HoodieIndexVersion`. Index-file and analyzer versions remain
+separate because either can evolve without changing the Hudi index definition.
+
+#### Spark SQL predicates
+
+The primary Spark interface is a Hudi-specific scalar predicate in `WHERE`.
+`hudi_match(field, query, options)` is modeled after the Elasticsearch SQL and
+OpenSearch SQL `MATCH(field, query, options)` predicates, while phrase and
+multi-field behavior follows their `match_phrase` and `multi_match` query
+concepts. It is not ANSI SQL; the `hudi_` prefix avoids claiming a Spark or 
ANSI
+built-in. Whether the optimizer uses the text index is not observable in query
+results.
+
+```sql
+SELECT event_ts, service, message
+FROM application_logs
+WHERE hudi_match(message, 'connection refused', 'operator=AND')
+  AND service = 'payments';
+```
+
+Phrase and multi-column queries use companion predicates:
+
+```sql
+SELECT event_ts, service, message
+FROM application_logs
+WHERE hudi_match_phrase(message, 'out of memory', 0)
+  AND environment = 'production';
+
+SELECT _hoodie_record_key, subject, description
+FROM support_tickets
+WHERE hudi_multi_match('payment declined', subject, description)
+  AND status = 'open';
+```
+
+The v1 signatures are:
+
+```text
+hudi_match(column, query [, 'key=value,...']) -> boolean
+hudi_match_phrase(column, query [, slop]) -> boolean
+hudi_multi_match(query [, 'operator=AND|OR'], column, ...) -> boolean
+```
+
+`hudi_match` options include `operator`, `fuzziness`, `prefix_length`, and
+`max_expansions`. Unknown options are rejected. The functions compose with
+normal SQL predicates. Conjunctive text predicates can be pushed into index
+planning; expressions whose `OR` semantics cannot be preserved are evaluated
+by Spark without index pushdown.
+
+SQL predicate evaluation is always complete. Compatible segments accelerate
+covered data table file slices, while uncovered or incompatible slices are
+evaluated by the normal Hudi scan. There is no `_score` column and `LIMIT` does
+not imply relevance order. Ranked top-k is a separate direct-search contract.
+
+#### Hudi RS Python table API
+
+Python users should not need to construct Spark SQL strings. Hudi RS extends
+its existing `HudiTableBuilder` and `read_snapshot` API with the same 
structured
+query model used by the index. The proposed predicate-style API is:
+
+```python
+import pyarrow as pa
+
+from hudi import HudiTableBuilder
+from hudi.search import FullTextOperator, MatchQuery, PhraseQuery
+
+table = (
+    HudiTableBuilder
+    .from_base_uri("s3://warehouse/support_tickets")
+    .build()
+)
+
+query = MatchQuery(
+    "payment declined",
+    column="description",
+    operator=FullTextOperator.AND,
+)
+
+batches = table.read_snapshot(
+    columns=["_hoodie_record_key", "subject", "description"],
+    filters=[("status", "=", "open")],
+    full_text_query=query,
+)
+tickets = pa.Table.from_batches(batches)
+```
+
+Structured queries compose without inventing a second query language:
+
+```python
+query = (
+    MatchQuery("refund", column="description")
+    & PhraseQuery("credit card", column="description", slop=1)
+)
+
+batches = table.read_snapshot(full_text_query=query)
+```
+
+For search applications, a Lance-style fluent API exposes ranked top-k and an
+explicit score:
+
+```python
+results = (
+    table.search_text(
+        MatchQuery("payment declined", column="description"),
+    )
+    .where([("status", "=", "open")])
+    .select(["_hoodie_record_key", "subject", "description"])
+    .limit(20)
+    .to_arrow()
+)
+
+# Ranked by BM25 descending; `_score` is included in `results`.
+```
+
+`read_snapshot(full_text_query=...)` has predicate semantics and returns every
+match. `search_text(...).limit(k)` has ranked-search semantics and returns
+`_score`. Both pin one Hudi snapshot, use identical analyzer/query objects, and
+fallback-scan uncovered data table file slices by default. The fluent API may
+expose `allow_incomplete_index=True`, but it must mark the result metadata as
+incomplete rather than silently changing defaults.
+
+Index creation remains an MDT table-service operation in the initial release,
+invoked through Spark SQL or the Java API. A future Hudi RS writer API may add
+`create_text_index` only after it can publish the corresponding MDT timeline
+changes safely.
+
+The three query entry points share one query model and coverage planner, but
+their result contracts differ:
+
+```mermaid
+flowchart LR
+    SQL["Spark SQL<br/>hudi_match(...)"]
+    SNAPSHOT["Hudi RS Python<br/>read_snapshot(full_text_query=...)"]
+    SEARCH["Hudi RS Python<br/>search_text(...).limit(k)"]
+    MODEL["Shared query objects,<br/>analyzer contract, and snapshot pinning"]
+    MATCHES["Complete unordered<br/>match set"]
+    RANKED["BM25-ranked top-k<br/>with _score"]
+
+    SQL --> MODEL
+    SNAPSHOT --> MODEL
+    SEARCH --> MODEL
+    MODEL -->|"predicate semantics"| MATCHES
+    MODEL -->|"ranked-search semantics"| RANKED
+```
+
+### Architecture
+
+```mermaid
+flowchart LR
+    DATA["Data table file slices<br/>at snapshot S"]
+    INDEXER["Spark TextSearchIndexer<br/>Java builder"]
+    FILES["Immutable index data files<br/>terms, postings, positions"]
+    MDT["text_index_&lt;name&gt; MDT partition<br/>visibility and coverage 
records"]
+    QUERY["Spark SQL or<br/>Hudi RS query"]
+    PLANNER["Pin snapshot S and<br/>resolve each file slice"]
+    INDEX_READ["Read matching postings"]
+    HUDI_SCAN["Normal Hudi scan"]
+    RESULT["Union Hudi record keys<br/>then materialize rows"]
+
+    DATA -->|"build merged records"| INDEXER
+    INDEXER -->|"write"| FILES
+    INDEXER -->|"atomically publish references"| MDT
+    QUERY --> PLANNER
+    DATA -->|"enumerate file slices"| PLANNER
+    MDT -->|"exact coverage lookup"| PLANNER
+    PLANNER -->|"covered"| INDEX_READ
+    FILES --> INDEX_READ
+    PLANNER -->|"changed or unindexed"| HUDI_SCAN
+    DATA --> HUDI_SCAN
+    INDEX_READ --> RESULT
+    HUDI_SCAN --> RESULT
+```
+
+The SQL definition is stored in `.hoodie/.index/index.json`. A dynamic MDT
+partition stores small control records. Immutable index data files live
+under an MDT-owned auxiliary namespace. Readers never infer visibility by
+listing that namespace; they use descriptors visible in the pinned MDT
+snapshot.
+
+### Metadata table integration
+
+Add `TEXT_INDEX` to `MetadataPartitionType` with the dynamic prefix
+`text_index_`. `getPartitionPath(metaClient, indexName)` and index-definition
+lookup follow the secondary and expression index conventions. Add a
+`TextSearchIndexer` to `IndexerFactory`, implementing `BaseIndexer` lifecycle
+operations.
+
+Add a tagged Avro metadata record named `HoodieTextIndexInfo` to
+`HoodieMetadata.avsc`. The record is deliberately descriptor-sized and has
+four logical kinds:
+
+| Kind | Record key | Purpose |
+| --- | --- | --- |
+| `HEAD` | `head` | Index and file-format versions, analyzer identity, latest 
publication instant, and aggregate statistics. |
+| `SEGMENT` | `segment/<uuid>` | Index-data paths, sizes, checksums, 
statistics, and the covered file-slice ordinal map. |
+| `COVERAGE` | `coverage/<encoded-partition>/<file-id>` | Exact file-slice 
identity and candidate segment references ordered by data instant. |
+| `TOMBSTONE` | `tombstone/<uuid>` | Segment retirement instant and deletion 
eligibility. |
+
+The record includes a schema version, index name, analyzer fingerprint, segment
+UUID, index-file format version, file-slice identities, file-slice-ordinal 
mapping,
+aggregate record and token counts, file descriptors, and optional tombstone
+instant. Large term statistics, dictionaries, and postings are never placed in
+the Avro record. Active file-slice masks are computed for the pinned snapshot 
rather
+than persisted as a single current value.
+
+The coverage planner does not scan the complete `text_index_<name>` partition
+for every query. It first applies normal partition pruning and enumerates the
+eligible data file slices, then issues batched point lookups for their
+`coverage/<encoded-partition>/<file-id>` keys. It loads only the `SEGMENT`
+descriptors referenced by those records and may cache immutable descriptors for
+the lifetime of the pinned MDT snapshot. Coverage storage is `O(F_table)` in 
the
+number of table file groups, while lookup and comparison work is `O(F_query)` 
in
+the number of file groups selected by the query. An unpartitioned full-table
+query still has `F_query = F_table`, consistent with its data-scan planning
+scope. The dynamic MDT partition uses normal MDT file-group sharding,
+compaction, and key lookup rather than a driver-side enumeration of all control
+records.
+
+Index data files use this default path:
+
+```text
+<table>/.hoodie/metadata/.aux/text-index/
+  <escaped-index-name>/<segment-uuid>/...
+```
+
+The directory is below MDT ownership but outside normal MOR partition 
discovery.
+A future external index-data tier may be configured, but every path must be 
scoped
+by table UUID and validated by readers. Only an MDT commit makes a segment
+visible. Failed writers may leave unpublished files; the cleaner removes them
+after a safety interval.
+
+### Hudi record identity and file-slice coverage
+
+The indexed unit is one Hudi record from the merged view of a data table file
+slice at snapshot `S`. The index does not introduce an independent 
search-engine
+record identity. Version 1 requires a stable Hudi record key and stores this
+compact logical address for every indexed record:
+
+```text
+HudiTextIndexRecordAddress {
+  file_slice_ordinal: u32,
+  record_key: bytes,
+  row_position_hint: optional u64
+}
+```
+
+The segment metadata maps `file_slice_ordinal` to the Hudi partition path, file
+ID, base instant, and exact file-slice identity. Query results are grouped by
+that partition path and file ID before Hudi materializes current rows. The
+`row_position_hint` is only an optimization and is used when the file-slice
+identity matches exactly; the record key remains authoritative.
+
+A file-slice identity includes:
+
+- table UUID, partition path, and file ID;
+- base instant and base-file identity (path, length, and checksum when known);
+- ordered log-file identities (path, length, and latest block instant);
+- writer schema identifier; and
+- record-merger implementation and relevant options.
+
+At snapshot `S`, the coverage planner enumerates eligible data table file 
slices
+and compares each computed identity with the candidate identities in its
+`COVERAGE` record. An exact match activates that segment's file-slice ordinal.
+A changed base file or added MOR log puts the file slice in the fallback scan 
set
+until it is rebuilt. A segment created from a later file-slice state is not 
used
+for an older snapshot.
+This avoids attempting to delete or mutate old postings after compaction,
+clustering, rollback, or MOR updates.
+
+Freshness is measured in changed data table file slices, not merely elapsed
+commits. Let `F` be the eligible file slices and `R` the slices without an 
exact
+identity match at the query snapshot. The coverage ratio is `C = 1 - R/F`. Once
+a MOR file group receives its first new log block it contributes one fallback
+scan slice until catch-up; additional blocks increase scan bytes but not the
+fallback-slice count. A complete predicate query therefore has the qualitative
+cost `index_scan(C * F) + fallback_scan(R)`. Complete ranked search 
additionally
+analyzes the fallback scan set to obtain exact global record frequencies. As 
`C`
+approaches zero, performance intentionally approaches a normal Hudi scan while
+correctness is unchanged.
+
+There is no universal freshness SLA because `R`, log size, analyzer cost, and
+query selectivity depend on the workload. Deployments schedule incremental
+catch-up by time or changed-slice thresholds and observe data instant lag,
+coverage ratio, fallback-scan bytes, and fallback-scan analysis time. The
+performance plan must publish the latency envelope across those dimensions
+before the feature is enabled by default.
+
+### Text-index file format
+
+All files begin with an eight-byte `HUDIFTS1` magic value followed by little-
+endian format version, feature flags, variable-header length, and header
+checksum. Independently checksummed blocks follow the header, and a footer
+contains the block directory for range reads. Readers reject unknown required
+feature bits and enforce configured allocation limits before reading lengths.
+
+Each segment contains:
+
+- `metadata.hfts`: analyzer fingerprint, data table, file-slice descriptors,
+  segment statistics, index shard descriptors, and checksums;
+- `part_<n>.tokens.hfts`: a minimal finite-state transducer (FST) mapping
+  analyzed term bytes to term ordinals and posting metadata;
+- `part_<n>.docs.hfts`: columnar file-slice ordinal, record-key offsets and 
bytes,
+  record token count, and optional row-position hint;
+- `part_<n>.postings.hfts`: record frequency, posting-block offsets,
+  delta-encoded local record ordinals, term frequencies, and block-max 
metadata;
+  and
+- `part_<n>.positions.hfts`: optional delta-encoded token positions and 
offsets.
+
+Index shards use local `u32` record ordinals. A builder starts a new index 
shard
+before that space is exhausted. Posting blocks default to 128 records and use
+bit packing or variable-byte encoding, selected per block. Each block records
+maximum term frequency and minimum indexed-record token count; these
+values provide a conservative BM25 upper bound for block-max WAND. Positions
+are stored only when enabled by the immutable index definition.
+
+A phrase clause requires compatible position data. For an index created with
+`with_position=false`, the complete Spark SQL and Hudi RS paths treat its data
+table file slices as uncovered for that query and evaluate the phrase through
+`RawTextSearchSplit`. They never silently downgrade a phrase to an `AND` of its
+terms. In version 1, `allow_incomplete_index=True` rejects a phrase query 
against
+a positionless index rather than returning an approximate result.
+
+No implementation-specific collection serialization is persisted directly.
+Every field is defined by the Hudi format specification, so upgrading a Java or
+Rust dependency cannot silently change files.
+
+### Implementation ownership
+
+The main Hudi repository owns the SQL extension, MDT index lifecycle, Spark
+planning, a pure-Java index-file builder and reader, and the language-neutral
+persistent format. The Java implementation is used by the Phase 1 Spark indexer
+and predicates, so Phase 1 performs real indexed reads rather than always 
taking
+the fallback scan. It does not add an in-tree Rust crate or JNI build.
+
+Any Rust reader, builder, tokenizer, or Python binding is developed and 
released
+from Hudi RS. Hudi RS already owns Hudi's Rust implementation and Python
+bindings, so this keeps native code, packaging, and Python API compatibility in
+the appropriate project. The two repositories coordinate through versioned
+contracts rather than source-code coupling:
+
+```text
+Main Hudi repository                 Hudi RS repository
+--------------------                 ------------------
+Spark SQL predicates                 Python query objects
+MDT indexer lifecycle                read_snapshot(full_text_query=...)
+Java builder, reader, tokenizer      Rust builder, reader, tokenizer
+HoodieIndexDefinition                search_text(...) fluent API
+HoodieTextIndexInfo Avro schema      Python bindings
+normative index-file specification   Rust conformance tests
+```
+
+The shared contracts are the analyzer fingerprint, query AST semantics,
+index-file format version, segment descriptor schema, record-address encoding,
+and completeness rules. Hudi RS must reject unsupported required feature bits;
+the Spark implementation must do the same. Neither implementation may infer
+compatibility from a library version alone.
+
+These are cross-engine conformance contracts: a conforming engine must search a
+segment built by another conforming engine without rebuilding it, produce the
+same match set for the same snapshot and query AST, and implement the same BM25
+formula and operation order. Format-versioned golden fixtures define the
+accepted floating-point tolerance and binary record-key tie break. 
Bit-identical
+floating-point scores are not required. Java and Rust need not share one 
runtime
+query library; a versioned, language-neutral query AST plus shared analyzer,
+format, match-set, and scoring fixtures prevent semantic drift.
+
+The JVM side uses `HoodieStorage` for range reads, credentials, retries, and
+metrics. The Java and Rust implementations communicate only through persisted
+Hudi contracts and golden fixtures. This RFC does not establish a JNI ABI,
+invoke Hudi RS from Spark, or package native artifacts in the main repository.
+
+### Build and publication lifecycle
+
+A bootstrap build performs these steps:
+
+1. Pin a completed data-table instant and corresponding MDT snapshot.
+2. Enumerate data table file slices and construct their exact identities.
+3. Use Hudi's merged reader to emit stable record key, text, file-slice 
ordinal,
+   and optional row-position hint.
+4. Analyze Hudi records and build bounded-memory sorted runs using the selected
+   implementation.
+5. Merge runs into dictionaries, record-address tables, postings, and 
positions.
+6. Write index blocks to UUID-scoped temporary paths.
+7. Finalize checksums, statistics, and file-slice descriptors.
+8. Move or copy index data files to their final immutable UUID paths when
+   required by the storage implementation.
+9. Return `SEGMENT`, `COVERAGE`, and `HEAD` metadata records to the MDT writer.
+10. Publish all descriptors and partition state in one MDT commit.
+
+Visibility begins at step 10. An indexer retry may reuse an index data file set
+only after validating every checksum and build identity; otherwise it writes a
+new UUID.
+
+Incremental catch-up is file-slice replacement, not posting mutation. The
+indexer compares current file-slice identities with coverage records and builds
+segments for new or changed slices. Unchanged slices retain their existing
+segment and file-slice-mask membership.
+
+Each build and descriptor records its pinned data instant `S`. A data commit
+that completes after enumeration does not make publication for `S` incorrect:
+a reader at a later snapshot compares the later file-slice identity and sends
+changed slices to the fallback scan set. Immediately before MDT publication,
+the indexer must verify that `S` is still a completed, retained instant and 
that
+the index definition and analyzer fingerprint have not changed. It aborts
+publication if `S` was rolled back or is no longer a valid build base.
+
+The MDT commit uses Hudi's existing indexing transaction, OCC, and 
lock-provider
+configuration. Since concurrent indexers can update the same `HEAD` and
+`COVERAGE` keys, a conflict must retry against the latest MDT snapshot and 
merge
+candidate segments in data-instant order. `HEAD` advancement is monotonic: an
+older build may remain a time-travel candidate but cannot replace a newer head
+or discard newer coverage. This rule covers concurrent data writers and
+concurrent text-index table services without introducing a separate lock
+protocol.
+
+The publication order prevents a partially written segment from becoming
+visible:
+
+```mermaid
+sequenceDiagram
+    participant I as Text indexer
+    participant D as Hudi data timeline
+    participant S as Index data storage
+    participant M as Metadata table
+    participant Q as Query reader
+
+    I->>D: Pin completed instant S
+    I->>D: Enumerate data table file slices and identities
+    I->>S: Write immutable index data files to UUID paths
+    S-->>I: Return checksums and descriptors
+    I->>D: Revalidate S and the index definition
+    D-->>I: S is retained and the definition is unchanged
+    I->>M: OCC commit HEAD, SEGMENT, and COVERAGE records
+    alt MDT key conflict
+        M-->>I: Reject stale write
+        I->>M: Reload, merge by data instant, and retry
+    end
+    M-->>I: MDT commit completes atomically
+    Q->>M: Pin MDT snapshot compatible with S
+    M-->>Q: Return visible descriptors
+    Q->>S: Range-read only published index data files
+```
+
+If index data writing fails, no MDT descriptor is committed and readers cannot
+discover the orphan. If the MDT commit fails, retry validation may reuse the
+index data; otherwise the orphan cleaner removes it after the grace interval.
+
+### Consolidation, rollback, and cleaning
+
+Small segments are consolidated when configurable count, byte, or inactive
+file-slice ratios are exceeded. Consolidation copies only active Hudi records,
+requires the same analyzer fingerprint, writes new index data files, publishes
+the new descriptors, and tombstones old segments in the same MDT commit. 
Readers
+pinned before that commit retain the old view.
+
+Rollback and restore reconcile visible descriptors to the restored MDT/data
+timeline state. If exact file-slice coverage no longer exists, those slices 
join
+the fallback scan set; a query is never allowed to use merely similar postings.
+The cleaner deletes tombstoned index data files only after both data and MDT
+retention guarantee that no supported query can see the descriptor. Orphan 
index
+data files without a published descriptor are deleted after a separate grace

Review Comment:
   🤖 The orphan cleaner deletes index files that have "no published descriptor" 
and exceed the grace period (default 24h). But an in-flight build writes its 
UUID files at steps 6-8 and only publishes the descriptor at step 10 — so a 
build that stalls between write and MDT commit is indistinguishable from a dead 
one, and the cleaner can delete files out from under a live builder. The 
revalidate step (line 663) re-checks S and the definition but not that the 
written index files still exist, so the subsequent commit can publish a SEGMENT 
descriptor pointing at already-deleted data. Would a heartbeat/lease (as Hudi 
already uses to protect in-flight writers from cleaning) be needed here, rather 
than a fixed grace interval? @nsivabalan could you confirm whether the 
timeline/heartbeat protocol covers this MDT-aux path?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to