Skip to content

feat(metadata): add MDT row families and payload schema for vector indexes - #19317

Open
chrevanthreddy wants to merge 4 commits into
apache:masterfrom
chrevanthreddy:rfc-109-pr02-schema-payload
Open

feat(metadata): add MDT row families and payload schema for vector indexes#19317
chrevanthreddy wants to merge 4 commits into
apache:masterfrom
chrevanthreddy:rfc-109-pr02-schema-payload

Conversation

@chrevanthreddy

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

Resolves #19097 (umbrella: #19094, RFC-109)

Second foundational slice of RFC-109 (Hudi Native Vector Index): the Metadata Table (MDT) storage model and payload schema for vector indexes. Builds on the option surface in #19096. DDL command dispatch, bootstrap/write execution, and the read path land in dependent follow-up PRs (#19098, #19099, #19100…).

Summary and Changelog

  • Avro schema (HoodieMetadata.avsc): add the VectorIndexMetadata union with all typed row families — HoodieVectorIndexManifest, HoodieVectorIndexQuantizer, HoodieVectorIndexCentroids, HoodieVectorIndexPostingBlock, HoodieVectorIndexPostingDelta, HoodieVectorIndexClusterStats, HoodieVectorIndexTombstone. Encoding metadata (RaBitQ bits, seed, residual/extended codes, scalars) is versioned per generation for multibit-aware reads.
  • HoodieMetadataPayload: new vectorIndexMetadata field + offset-based serde, field/entry-type constants, dedicated constructor, and factory builders for centroid/quantizer/manifest/cluster/posting records.
  • Binary record keys: VectorIndexMetadataKey (family-tagged binary key codec) + VectorClusterRawKey, VectorGenerationManifestRawKey, VectorPostingPrefixRawKey (RawKey impls).
  • HoodieTableMetadataUtil: binary key-family predicates/getters and cluster-aware file-group routing (mapVectorPostingKeyToFileGroupIndex).
  • MetadataPartitionType.VECTOR_INDEX: new partition type (ordinal 8) wired for availability, payload construction, partition path, and file-group mapping.
  • PostingBlockBuilder / PostingBlockView: packed posting-block encode/decode over the generated HoodieVectorIndexPostingBlock.
  • Tests: TestVectorIndexMetadataPayload (2) round-trip payload serde; TestVectorIndexMetadataKey (6) binary key encode/decode. Existing HoodieMetadataRecord positional callers updated for the new field.

Impact

Additive to the MDT schema (new optional union field, default null) and a new metadata partition type that is only populated via explicit vector CREATE INDEX (not enabled by any metadata config flag). No behavioral change to existing partitions, writers, or readers.

Risk Level

low

Schema change is an optional field defaulting to null (backward/forward compatible); new partition type is inert unless a vector index is created. hudi-common compiles clean and vector serde + affected existing serde tests pass on current master.

Documentation Update

Row families and encoding-metadata semantics are documented as Avro doc fields and Javadoc. User-facing RFC document tracked in #19309.

Contributor's checklist

@codecov-commenter

codecov-commenter commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 21.72131% with 382 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.03%. Comparing base (4a5d5b0) to head (7ef6a88).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
.../hudi/common/index/vector/PostingBlockBuilder.java 0.00% 149 Missing ⚠️
...che/hudi/common/index/vector/PostingBlockView.java 0.00% 104 Missing ⚠️
...rg/apache/hudi/metadata/HoodieMetadataPayload.java 27.61% 64 Missing and 12 partials ⚠️
.../apache/hudi/metadata/HoodieTableMetadataUtil.java 0.00% 31 Missing ⚠️
...g/apache/hudi/metadata/VectorIndexMetadataKey.java 85.89% 5 Missing and 6 partials ⚠️
...rg/apache/hudi/metadata/MetadataPartitionType.java 60.00% 6 Missing ⚠️
...pache/hudi/metadata/VectorPostingPrefixRawKey.java 0.00% 3 Missing ⚠️
.../org/apache/hudi/metadata/VectorClusterRawKey.java 0.00% 1 Missing ⚠️
.../hudi/metadata/VectorGenerationManifestRawKey.java 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19317      +/-   ##
============================================
+ Coverage     70.49%   72.03%   +1.54%     
+ Complexity    32775    32614     -161     
============================================
  Files          2700     2567     -133     
  Lines        152896   148793    -4103     
  Branches      19009    18711     -298     
============================================
- Hits         107778   107181     -597     
+ Misses        36596    33128    -3468     
+ Partials       8522     8484      -38     
Components Coverage Δ
hudi-common 80.54% <21.72%> (-0.87%) ⬇️
hudi-client 81.57% <ø> (ø)
hudi-flink 78.63% <ø> (+0.01%) ⬆️
hudi-spark-datasource 58.66% <ø> (+0.28%) ⬆️
hudi-utilities 70.42% <ø> (+0.04%) ⬆️
hudi-cli 15.32% <ø> (ø)
hudi-hadoop 63.49% <ø> (ø)
hudi-sync 70.62% <ø> (ø)
hudi-io 79.57% <ø> (ø)
hudi-timeline-service 83.54% <ø> (-0.20%) ⬇️
hudi-cloud 64.00% <ø> (ø)
hudi-kafka-connect 53.96% <ø> (+0.76%) ⬆️
Flag Coverage Δ
common-and-other-modules 46.60% <21.72%> (+0.50%) ⬆️
flink-integration-tests 47.46% <1.43%> (-0.08%) ⬇️
hadoop-mr-java-client 43.02% <1.43%> (-0.32%) ⬇️
integration-tests 13.55% <1.43%> (-0.03%) ⬇️
spark-client-hadoop-common 48.40% <1.43%> (-0.31%) ⬇️
spark-java-tests 49.22% <21.72%> (-0.03%) ⬇️
spark-scala-tests 44.69% <1.43%> (+0.03%) ⬆️
utilities 36.32% <1.43%> (-0.10%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...a/org/apache/hudi/metadata/HoodieIndexVersion.java 88.57% <100.00%> (+0.33%) ⬆️
.../org/apache/hudi/metadata/VectorClusterRawKey.java 0.00% <0.00%> (ø)
.../hudi/metadata/VectorGenerationManifestRawKey.java 0.00% <0.00%> (ø)
...pache/hudi/metadata/VectorPostingPrefixRawKey.java 0.00% <0.00%> (ø)
...rg/apache/hudi/metadata/MetadataPartitionType.java 83.67% <60.00%> (-1.97%) ⬇️
...g/apache/hudi/metadata/VectorIndexMetadataKey.java 85.89% <85.89%> (ø)
.../apache/hudi/metadata/HoodieTableMetadataUtil.java 79.75% <0.00%> (-2.45%) ⬇️
...rg/apache/hudi/metadata/HoodieMetadataPayload.java 67.39% <27.61%> (-15.88%) ⬇️
...che/hudi/common/index/vector/PostingBlockView.java 0.00% <0.00%> (ø)
.../hudi/common/index/vector/PostingBlockBuilder.java 0.00% <0.00%> (ø)

... and 156 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for the contribution! This PR lays the MDT storage foundation for RFC-109 vector indexes — new Avro row families, a binary record-key codec (VectorIndexMetadataKey), posting-block encode/decode (PostingBlockBuilder/PostingBlockView), payload factory builders, and a new VECTOR_INDEX metadata partition type. It's additive scaffolding; the DDL/write/read paths land in follow-ups. I traced the key codec, the binary packing, the file-group routing, and the payload merge path (preCombine/combineMetadataPayloads) and they look internally consistent, and confirmed the mid-enum insertion is safe (no persisted ordinals). A couple of non-blocking items worth double-checking in the inline comments — round-trip test coverage for the posting-block packing, and some factory parameters that appear to be silently dropped. Please take a look at any inline comments, and this should be ready for a Hudi committer or PMC member to take it from here. A couple of naming and clarity suggestions below.

return this;
}

public HoodieVectorIndexPostingBlock build() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 The posting-block packing here (struct-of-arrays scalar layout, little-endian offsets, dictionary indices, and the record-key offset table) is intricate and correctness-critical, but I don't see a PostingBlockBuilder -> PostingBlockView round-trip test in this PR. Could you add one that packs a few rows (including the includeVectorNorm variant and multi-plane exPlanes) and asserts every field decodes back? A silent offset error here would corrupt vector reads without failing loudly.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

metadataPartitionPath);
}

public static HoodieRecord<HoodieMetadataPayload> createVectorIndexClusterManifestRecord(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 createVectorIndexClusterManifestRecord takes shardCount and fileGroupIds but neither appears to be written into the HoodieVectorIndexClusterStats record — the constructor gets (vectorCount, 0L, 0L, 0L, null, lastUpdatedTs) with the list slot passed as null. Is that intentional for this slice, or should those be persisted? A method named ...ClusterManifestRecord that silently drops the file-group membership it's handed seems like it could bite the write-path follow-up. Similarly the convenience createVectorIndexQuantizerMetadataRecord/createVectorIndexManifestRecord overloads drop randomSeed/assumeNormalized/quantizedCodeBytes — worth a comment noting those live on other records so a future caller doesn't assume they're persisted here.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

protected HoodieMetadataColumnStats columnStatMetadata = null;
protected HoodieRecordIndexInfo recordIndexMetadata;
protected HoodieSecondaryIndexInfo secondaryIndexMetadata;
protected Object vectorIndexMetadata;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: could you introduce a marker interface (e.g. HoodieVectorIndexEntry) implemented by the valid Avro types (HoodieVectorIndexPostingDelta, HoodieVectorIndexPostingBlock, HoodieVectorIndexCentroids, etc.) and type this field as that interface instead of Object? Right now there's no way to know which types are legal here without reading every factory method.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

}

public static String getVectorIndexPostingPrefix(int generationId, int clusterId) {
return VectorIndexMetadataKey.postingPrefix(generationId, clusterId, 0).substring(0, 9);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: the 9 here (and the identical .substring(0, 9) in VectorPostingPrefixRawKey) is a magic number derived from family(1) + generation(4) + clusterId(4). It might be worth surfacing it as a named constant in VectorIndexMetadataKey, e.g. CLUSTER_PREFIX_BYTES = 9, so both call sites stay in sync and the derivation is self-documenting.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

@chrevanthreddy
chrevanthreddy force-pushed the rfc-109-pr02-schema-payload branch from 7ef6a88 to 39bb0ed Compare August 4, 2026 18:30
@chrevanthreddy

Copy link
Copy Markdown
Contributor Author

Rebased this PR onto current master and aligned the persisted vector-index schema with the RFC-109 consistency/freshness contract.

Key corrections:

  • remove persisted centroid epochs from manifests, centroid chunks, and cluster stats;
  • add the singleton active-generation pointer;
  • add generation bootstrap baseline and verified contiguous frontier;
  • add source-instant marker payload/key support with marker-key validation;
  • add cluster routing version, shard count, and file-group routing fields;
  • retain generation-scoped keys and additive cluster-stat update behavior.

Validation on Java 17:

  • reactor dependency rebuild: passed;
  • focused vector metadata tests: 14 passed;
  • complete hudi-common suite: 2,205 passed, 0 failures/errors/skips;
  • Checkstyle and git diff --check: passed.

The earlier UTF-8 comparator compilation failure was caused by a stale locally installed shaded hudi-io artifact. Rebuilding reactor dependencies from current master resolved it without unrelated source changes.

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR lays down the MDT storage model and payload schema for RFC-109 vector indexes — the Avro row families, the family-tagged binary key codec, the packed posting-block encode/decode, and the VECTOR_INDEX partition wiring. The binary key codec and posting-block serde are carefully constructed and have solid round-trip test coverage. One tombstone-serialization edge case is worth double-checking in the inline comment. Please take a look at any inline comments, and this should be ready for a Hudi committer or PMC member to take it from here. A few naming and readability suggestions below, most notably an overload that silently discards three of its own parameters.

this.key = key;
this.type = MetadataPartitionType.VECTOR_INDEX.getRecordType();
this.vectorIndexMetadata = vectorIndexInfo;
this.isDeletedRecord = vectorIndexInfo instanceof HoodieVectorIndexTombstone;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Setting isDeletedRecord = true for any HoodieVectorIndexTombstone means getInsertValue() returns Option.empty() for tombstones, so the tombstone payload built in createVectorIndexPostingDeleteRecord (with its deleteInstant/deleteReason) is never serialized — deletes degrade to plain delete markers and that provenance is dropped. Is that intended, given the schema models HoodieVectorIndexTombstone as a distinct union member with those fields? If tombstones are meant to be persisted and read back, this would need isDeletedRecord = false plus tombstone-aware handling in constructMetadataPayload/combineMetadataPayloads.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

String quantizerType,
int quantizedCodeBytes,
int rabitqBits,
long randomSeed,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: could you either pass quantizedCodeBytes, rabitqBits, and assumeNormalized through to the delegate, or remove them from this overload's signature? Right now the overload accepts those params and then silently drops all three, which will confuse any caller who passes non-default values.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

protected HoodieMetadataColumnStats columnStatMetadata = null;
protected HoodieRecordIndexInfo recordIndexMetadata;
protected HoodieSecondaryIndexInfo secondaryIndexMetadata;
protected Object vectorIndexMetadata;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: the other metadata fields (HoodieMetadataBloomFilter, HoodieSecondaryIndexInfo, etc.) are all strongly typed — have you considered a common marker interface for the vector payloads so vectorIndexMetadata doesn't have to be Object? That would save callers from unchecked casts.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

@Override
public String encode() {
return shardId == null
? VectorIndexMetadataKey.postingPrefix(generationId, clusterId, 0).substring(0, 9)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: the bare 9 here (and at HoodieTableMetadataUtil.getVectorIndexPostingPrefix(int, int)) is easy to miss if the binary key layout ever changes — could you expose a named constant like POSTING_CLUSTER_PREFIX_BYTES from VectorIndexMetadataKey, or add a postingClusterPrefix(generation, clusterId) helper to keep the magic in one place?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

@chrevanthreddy

Copy link
Copy Markdown
Contributor Author

Follow-up schema finalization is now pushed as 09327bdacd:

  • persist generation-scoped factorVersion, kappa, gMin, eps1Max, and epsNRel;
  • persist posting blockFormatVersion, immutable MDT fileGroupCount, and centroid chunk count/checksum;
  • clarify that readers use the singleton active-generation pointer rather than selecting the maximum ACTIVE row;
  • remove unused manifest-builder overloads that silently created placeholder ACTIVE generations while ignoring quantizer arguments.

Java 17 validation after this change:

  • Avro generation: passed;
  • focused vector metadata tests: 14 passed;
  • complete hudi-common suite: 2,205 passed, 0 failures/errors/skips;
  • Checkstyle and git diff --check: passed.
@hudi-bot

hudi-bot commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands @hudi-bot supports the following commands:
  • @hudi-bot run azure re-run the last Azure build

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for the updates! This revision expands createVectorIndexManifestRecord with the new generation-scoped fields (fileGroupCount, blockFormatVersion, factorVersion, kappa/gMin/eps1Max/epsNRel, centroidChunkCount, centroidChecksum) and drops the earlier convenience overloads. I traced the positional HoodieVectorIndexManifest constructor call field-by-field against the Avro schema order and all 30 fields line up exactly, so there's no silent field misalignment — that was the main risk here. The removed overloads have no remaining callers and the test was updated consistently. One prior reviewer comment (cluster manifest not writing shardCount/fileGroupIds) now appears addressed. A few earlier comments remain open but weren't part of these changes and are already on the PR: the tombstone serialization question (isDeletedRecord=true means tombstones never persist their deleteReason/deleteInstant), the Object vectorIndexMetadata typing/marker-interface suggestion, the quantizer overload that still accepts quantizedCodeBytes/rabitqBits/assumeNormalized (fields the quantizer schema doesn't even model), and the magic 9 in the posting-prefix key logic. No new issues flagged from this pass. Please take a look at the still-open inline comments, and this should be ready for a Hudi committer or PMC member to take it from here.

@vinothchandar vinothchandar self-assigned this Aug 14, 2026
{"name": "indexVersion", "type": "int"},
{"name": "generationId", "type": "string", "doc": "Creating Hudi instant + optional human tag. The gen ordinal lives in the key."},
{"name": "state", "type": "string", "doc": "BUILDING | ACTIVE | RETIRED. Readers serve only the generation referenced by the singleton active manifest."},
{"name": "dim", "type": "int"},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is dim here dimension?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL PR with lines of changes > 1000

6 participants