This is an automated email from the ASF dual-hosted git repository.

voonhous 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 4d83ac93024d docs(tech-specs): secondary index and index versions for 
table version 9 (#19650)
4d83ac93024d is described below

commit 4d83ac93024da07687cfe6d23f33b22394c082ea
Author: deepakpanda93 <[email protected]>
AuthorDate: Mon Aug 24 15:21:38 2026 +0530

    docs(tech-specs): secondary index and index versions for table version 9 
(#19650)
    
    * docs(tech-specs): secondary index and index versions for table version 9
    
    Closes #17068. (JIRA: HUDI-9550.)
    
    Corrects an inverted statement already published in the tech spec, and 
documents
    the table version 9 index changes the issue asks for.
    
    The existing text claimed that V2 "shards records by the primary (record) 
key
    rather than by the secondary key", at the cost of "secondary-key range scans
    having to visit more file groups". Both halves are backwards. 
MetadataPartitionType
    dispatches with getSecondaryKeyToFileGroupMappingFunction(indexVersion >=
    HoodieIndexVersion.V2), and that function hashes the unescaped secondary-key
    prefix when the flag is set and the full record key otherwise. So V1 hashes 
the
    whole <secondary>$<primary> key and scatters one secondary value across 
every
    file group, while V2 hashes only the <secondary>$ prefix so that a lookup by
    secondary value alone reads a single file group. V2 is the improvement for
    secondary-key lookups, not a regression.
    
    Also adds, all verified against release-1.2.0:
    
    - The index definition JSON shape under hoodie.table.index.defs.path. The 
path
      and the property were already documented but the schema was not, and the
      secondary index section pointed at #indexing-functions for "the on-disk 
shape",
      which is a two-line RFC stub. That cross-reference now resolves to the new
      section.
    - The version attribute, and which version a new index gets per index type.
      Worth stating explicitly because it is not secondary-index specific:
      column_stats, partition_stats and expr_index also default to V2 at table
      version 9, while record_index, bloom_filters and files stay V1.
    - Upgrade and downgrade behaviour. The issue describes this as 
"downgrade/upgrade
      will drop new/old SI index", but only downgrade drops. Upgrading 8 to 9 
stamps
      definitions that lack a version with V1 and drops nothing
      (EightToNineUpgradeHandler#populateIndexVersionIfMissing). Downgrading 9 
to 8
      drops partitions whose version is above V1, plus partition_stats when a V2
      column_stats partition goes 
(UpgradeDowngradeUtils#dropNonV1IndexPartitions).
    - Secondary index limitations: one indexed column only; the supported column
      types; and that an indexed column's type cannot change while the index 
exists.
    
    Two facts differ from the abandoned PR #13713, which was the starting 
reference:
    
    - Its supported-type list, "string, double, timestamp and any integral 
types", is
      wrong. HoodieIndexUtils#isSecondaryIndexSupportedType admits string, int, 
long,
      float, double, date and time, and admits timestamp only when 
isUtcAdjusted, so
      local timestamps are rejected.
    - Its claim that schema evolution is not allowed on an indexed column is too
      broad. HoodieTable throws SchemaCompatibilityException on a column type 
change,
      but explicitly permits a change that only alters nullability.
    
    That PR's .gitignore change is not carried over, being unrelated to the 
docs, and
    neither is its "Indexes" to "Indices" heading rename.
    
    npm run build passes with the warning set byte-identical to a baseline 
built at
    the same base commit.
    
    * docs(tech-specs): cover the integral SQL types a secondary index accepts
    
    Review feedback on apache/hudi#19650: the supported-types list omitted 
smallint
    and tinyint.
    
    The reviewer is right, and the reason is worth stating rather than just 
patching
    the list. HoodieIndexUtils#isSecondaryIndexSupportedType is an allow-list 
over
    schema types, and it has no SHORT or BYTE entries because Avro has neither.
    Spark's ShortType and ByteType serialize to Avro INT (AvroSerializer has 
explicit
    "case (ByteType, INT)" and "case (ShortType, INT)" branches), so a smallint 
or
    tinyint column reaches the INT branch of the allow-list and is accepted. 
Listing
    only "int" was therefore accurate about the schema type but misleading to 
anyone
    reading it as SQL.
    
    TestSecondaryIndexDataTypes confirms the behaviour from the other side: it
    asserts index creation succeeds on col_string, col_int, col_bigint, 
col_long,
    col_smallint, col_tinyint, col_timestamp, col_date, col_float and 
col_double, and
    fails on col_decimal, col_boolean, col_binary, col_array, col_map and 
col_struct.
    
    The bullet now gives both vocabularies, and adds the rejected types, which 
the
    earlier wording only covered as "all other types".
    
    Build passes with the warning set still byte-identical to a baseline at the 
same
    base commit.
    
    * docs(tech-specs): describe indexFunction by what actually populates it
    
    Review feedback on apache/hudi#19650. The bullet read "indexFunction is the
    transform applied to the source column, identity unless the index is an
    expression index", which is wrong in both directions: an expression index 
created
    without an explicit function also gets identity, so identity does not 
distinguish
    the two, and non-expression indexes do not reliably get identity either.
    
    Traced all four sites that build a HoodieIndexDefinition, and the field is
    populated inconsistently across them:
    
    - HoodieTableMetadataUtil#getIndexPartitionsToInit calls withIndexFunction 
only
      when the partition name prefix is expr_index_, so a secondary index 
registered
      through that path is left empty.
    - The other builder in HoodieTableMetadataUtil never calls 
withIndexFunction, so
      built-in partitions are left empty.
    - HoodieIndexUtils#getSecondaryOrExpressionIndexDefinition, which serves SQL
      CREATE INDEX for both secondary and expression indexes, uses
      options.getOrDefault(EXPRESSION_OPTION, IDENTITY_TRANSFORM), so a 
secondary
      index created that way gets identity.
    - HoodieSparkIndexClient#createOrUpdateColumnStatsIndexDefinition passes
      withIndexFunction(PARTITION_NAME_COLUMN_STATS), so that definition carries
      "column_stats" rather than an empty value or identity.
    
    Unset values are stored as the empty string, not identity: 
HoodieIndexDefinition
    normalises with nonEmpty(indexFunction) ? indexFunction : EMPTY_STRING.
    
    The bullet now says the field carries the expression-index transform and 
defaults
    to identity for expression indexes, that it is not meaningful for other 
index
    types, what each registration path leaves behind, and that indexType is the 
field
    to identify an index by. This is a case where the honest description is 
that the
    value is inconsistent, rather than inventing a rule the code does not 
follow.
    
    Build passes with the warning set still byte-identical to a baseline at the 
same
    base commit.
---
 website/learn/tech-specs.md | 104 +++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 97 insertions(+), 7 deletions(-)

diff --git a/website/learn/tech-specs.md b/website/learn/tech-specs.md
index f542c4c5b651..8f8c653d459a 100644
--- a/website/learn/tech-specs.md
+++ b/website/learn/tech-specs.md
@@ -463,6 +463,74 @@ The other fields can also be optional for writers 
depending on whether protectio
 ### Naming
 Indexes are stored under `.hoodie/metadata` storage path, with separate 
partitions of the  form `<index_type>_<index_name>`.
 
+### Index Definitions
+
+Every index carries a definition, serialized to JSON under the path in 
`hoodie.table.index.defs.path`
+(default `.hoodie/.index_defs/index.json`). All definitions for a table live 
in a single file, keyed by the
+metadata-table partition name:
+
+```json
+{
+  "indexDefinitions": {
+    "<partition_name>": {
+      "indexName": "<partition_name>",
+      "indexType": "<index_type>",
+      "indexFunction": "<index_function>",
+      "version": "<index_version>",
+      "sourceFields": ["<column_1>", "<column_2>"],
+      "indexOptions": {}
+    }
+  }
+}
+```
+
+*   `indexType` is one of `files`, `column_stats`, `partition_stats`, 
`bloom_filters`, `record_index`,
+    `secondary_index` or `expr_index`.
+*   `indexFunction` carries the transform for an expression index, and 
defaults to `identity` when an expression index
+    is created without an explicit function. For every other index type the 
field is not meaningful, and the value it
+    ends up with depends on the code path that registered the definition: 
empty when the built-in initialisation path
+    registers it, `identity` for a secondary index created through SQL `CREATE 
INDEX`, and the partition name for the
+    column-stats registration path. Use `indexType` to identify an index, not 
this field.
+*   `sourceFields` are the data-table columns the index is derived from. 
Metadata columns are permitted.
+*   `indexOptions` carries index-type-specific options, such as the 
expression-index function arguments.
+*   `version` is the storage-layout version of the index, described below.
+
+#### Index Versions
+
+`version` holds a `HoodieIndexVersion`, an index-level attribute introduced in 
table version 9. It allows the physical
+layout of an index to change without forcing a table-version upgrade or 
downgrade, so that readers and writers built
+against different releases agree on how to interpret a given index partition. 
Values take the form `V1`, `V2` and so on.
+
+A table version 8 definition may omit `version` entirely. From table version 9 
onward a definition without a version is
+rejected as invalid, and secondary indexes on a table still at version 8 must 
be `V1`.
+
+Which version a newly created index gets depends on the index type and the 
table version:
+
+| Index type | Table version 8 | Table version 9 |
+|---|---|---|
+| `secondary_index` | `V1` | **`V2`** |
+| `column_stats` | `V1` | **`V2`** |
+| `partition_stats` | `V1` | **`V2`** |
+| `expr_index` | `V1` | **`V2`** |
+| `record_index` | `V1` | `V1` |
+| `bloom_filters` | `V1` | `V1` |
+| `files` | `V1` | `V1` |
+
+Only the layout changes; the logical contents of an index are unaffected by 
its version.
+
+#### Table Upgrade and Downgrade
+
+Index versions are reconciled when the table version changes.
+
+*   **Upgrading from table version 8 to 9** does not rebuild or drop any 
index. Definitions that carry no `version` are
+    stamped `V1`, so existing indexes keep their on-disk layout and continue 
to be read as `V1`. Indexes created after
+    the upgrade pick up the table version 9 defaults above.
+*   **Downgrading from table version 9 to 8** drops every metadata partition 
whose index version is newer than `V1`,
+    because table version 8 readers cannot interpret those layouts. 
Additionally, if a `V2` `column_stats` partition is
+    dropped, `partition_stats` is dropped with it, since partition stats may 
have no definition of their own to inspect.
+
+Dropped indexes have to be rebuilt after the downgrade.
+
 ### Bloom Filter Index
 
 The bloom filter index is used to accelerate 'presence checks' — validating 
whether a particular record is present in a file — which is used during 
merging, hash-based joins, point-lookup queries, etc.
@@ -506,7 +574,7 @@ Hudi supports near-standard [SQL 
syntax](/docs/sql_ddl#create-index) for creatin
 via Spark SQL, along with an asynchronous indexing table service that builds 
indexes without interrupting writers.
 
 A secondary index definition is serialized to JSON and saved at the path 
specified by `hoodie.table.index.defs.path`
-(see [Indexing Functions / Index Definitions](#indexing-functions) for the 
on-disk shape).
+(see [Index Definitions](#index-definitions) for the on-disk shape).
 The index itself is stored in the Hudi metadata table under the partition 
`secondary_index_<index_name>`. As with other
 metadata partitions the entry is a key/value, but the encoding is a little 
more nuanced.
 
@@ -532,12 +600,34 @@ For example, a secondary index on the `city` column, for 
a record with `city = C
 chennai$id1 -> {"isDeleted": false}
 ```
 
-Each secondary-index partition is tagged with a `HoodieIndexVersion` (stored 
on the corresponding `HoodieIndexDefinition`).
-Table version 8 constrained every secondary-index partition to `V1` (the 
encoding described above). Table version 9
-introduces `V2`, which shards records by the primary (record) key rather than 
by the secondary key. This makes secondary-index
-updates cheaper on writes with skewed secondary values, at the cost of 
secondary-key range scans having to visit more file
-groups. Readers pick their scan strategy from the per-partition 
`HoodieIndexVersion`. New tables created on version 9
-default to `V2` for secondary indexes; existing `V1` partitions from upgraded 
tables continue to be read with the V1 encoding.
+**Partitioning** decides which file group of the index partition an entry is 
written to. Hudi hashes a portion of the
+record key and takes that value modulo the number of file groups. Which 
portion is hashed is governed by the
+[`HoodieIndexVersion`](#index-versions) recorded on the index's 
`HoodieIndexDefinition`:
+
+*   **`V1`**, the default for table version 8, hashes the whole 
`<escaped-secondary-key>$<escaped-primary-key>` key.
+    Entries sharing a secondary value are distributed across all file groups, 
so resolving a secondary value to its
+    records reads every file group unless the primary key is already known.
+*   **`V2`**, the default for table version 9, hashes only the leading 
`<escaped-secondary-key>$` portion. All entries
+    sharing a secondary value therefore reside in one file group, and a lookup 
by secondary value alone reads that
+    single file group.
+
+The strategy is selected per partition from its recorded version, so `V1` 
partitions on an upgraded table continue to be
+read as `V1` while indexes created afterwards on the same table use `V2`.
+
+#### Limitations
+
+*   A secondary index may be defined on **exactly one column**. Attempting 
more fails with
+    `Only one column can be indexed for functional or secondary index.`
+*   The indexed column must resolve to one of the schema types `string`, 
`int`, `long`, `float`, `double`, `date`,
+    `time`, or a **UTC-adjusted** `timestamp`. In Spark SQL terms that covers 
`string`, `tinyint`, `smallint`, `int`,
+    `bigint`, `float`, `double`, `date` and `timestamp`, since `tinyint` and 
`smallint` are represented as `int`.
+    Rejected are `decimal`, `boolean`, `binary`, the nested types `array`, 
`map` and `struct`, and local
+    (non-UTC-adjusted) timestamps. A nullable column is supported when its 
non-null branch is a supported type.
+*   The **type of an indexed column cannot be changed** while the index 
exists. Both a SQL
+    `ALTER TABLE ... ALTER COLUMN ... TYPE ...` and a write that evolves the 
column through schema-on-read fail with
+    `Column '<column>' has secondary index '<index>' and cannot evolve from 
schema '<old>' to '<new>'`. Drop the index,
+    change the type, then rebuild the index. Changing only a column's 
nullability is permitted and does not require
+    dropping the index.
 
 ### Expression Indexes
 

Reply via email to