Skip to content

feat(index): add RaBitQ encoder and multibit-aware scorer contract - #19318

Open
chrevanthreddy wants to merge 6 commits into
apache:masterfrom
chrevanthreddy:rfc-109-pr03-rabitq-encoder
Open

feat(index): add RaBitQ encoder and multibit-aware scorer contract#19318
chrevanthreddy wants to merge 6 commits into
apache:masterfrom
chrevanthreddy:rfc-109-pr03-rabitq-encoder

Conversation

@chrevanthreddy

@chrevanthreddy chrevanthreddy commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

Resolves #19098 (umbrella: #19094, RFC-109).

Third RFC-109 slice: an engine-neutral RaBitQ encoder and metric-aware, multibit scorer contract.

Stacked PR. This branch includes #19317 (MDT schema/payload, #19097) and #19310 (options, #19096). The top commit, feat(index): add RaBitQ encoder and multibit-aware scorer contract, is the unique change for this PR.

Summary and Changelog

  • RaBitQEncoder / QuantizedVector: deterministic 1-8 bit encoding, residual encoding, packed sign/extended codes, validated immutable encoded payloads, and finite-input checks.
  • Metric scoring: explicit MetricQueryState ranking contracts for L2 (squared ranking distance), cosine, and dot product; validated query/rotation state; separate RaBitQDistanceScorer for packed-code reconstruction.
  • Posting scan primitives: RaBitQByteLutScorer, RaBitQPlaneKernel, and VectorQueryPlanes, including persisted-plane ordering and posting-layout validation.
  • Neutral factors: safe pass-1 bounds, normalized quality gating, exact D=1 handling, and posting-block-owned factor-layout versioning.
  • Conflict resolution: preserves feat(index): add vector index option validation #19310's VectorQuantizer option enum; the old speculative runtime interface is replaced by concrete encoder/query/value types.
  • Tests: 67 encoder/scorer tests in this slice; 82 focused stacked tests pass overall. Coverage includes deterministic encoding, all metrics, bits 1/2/4/8, byte-LUT vs scalar identity through persisted posting planes, non-byte-aligned dimensions, buffer offsets, malformed layouts/inputs, neutral-factor bounds, rotation, residual recall, and metadata/options integration.

Validation

Java 17 reactor validation:

mvn -Punit-tests -Djava17 -Djava.version=17 -pl hudi-common -am \
  -Dtest='TestVectorIndexOptions,TestRaBitQEncoder,TestRaBitQByteLutScorer,MetricEstimatorIdentityTest,TestMetricQueryStateRotation,TestRaBitQNeutralFactors,TestRaBitQPlaneKernel,TestRaBitQResidualHypothesis,TestRaBitQResidualRecall,TestVectorDistanceMetric,TestVectorIndexMetadataKey,TestVectorIndexMetadataPayload' \
  -Dsurefire.failIfNoSpecifiedTests=false test

Result: 82 tests, 0 failures/errors/skips; Checkstyle 0 violations; Apache RAT passed; residual multibit Recall@10 = 0.918.

Impact

Additive classes under hudi-common; no write/read runtime path is wired in this PR. Bootstrap, lifecycle/publication, incremental maintenance, and query integration remain follow-up slices.

Risk Level

Medium: isolated from existing runtime paths, but this establishes persisted-code and numeric contracts used by later slices.

Documentation Update

Contracts and multibit decomposition are documented in Javadoc. The user-facing RFC is tracked in #19309.

Contributor's checklist

@github-actions github-actions Bot added the size:XL PR with lines of changes > 1000 label Jul 17, 2026
@chrevanthreddy
chrevanthreddy force-pushed the rfc-109-pr03-rabitq-encoder branch from c4189f9 to 0682b18 Compare July 30, 2026 18:00
@codecov-commenter

codecov-commenter commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.72265% with 393 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.50%. Comparing base (633d142) to head (0682b18).

Files with missing lines Patch % Lines
...che/hudi/common/index/vector/PostingBlockView.java 25.00% 64 Missing and 14 partials ⚠️
...rg/apache/hudi/metadata/HoodieMetadataPayload.java 27.61% 64 Missing and 12 partials ⚠️
...apache/hudi/common/index/vector/RaBitQEncoder.java 77.85% 38 Missing and 22 partials ⚠️
.../hudi/common/index/vector/PostingBlockBuilder.java 75.83% 20 Missing and 16 partials ⚠️
.../apache/hudi/metadata/HoodieTableMetadataUtil.java 0.00% 31 Missing ⚠️
...ache/hudi/common/index/vector/QuantizedVector.java 55.10% 17 Missing and 5 partials ⚠️
...hudi/common/index/vector/RaBitQDistanceScorer.java 61.22% 8 Missing and 11 partials ⚠️
...che/hudi/common/index/vector/MetricQueryState.java 81.72% 9 Missing and 8 partials ⚠️
.../hudi/common/index/vector/RaBitQByteLutScorer.java 76.59% 5 Missing and 6 partials ⚠️
...g/apache/hudi/metadata/VectorIndexMetadataKey.java 85.89% 5 Missing and 6 partials ⚠️
... and 9 more
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19318      +/-   ##
============================================
- Coverage     75.56%   75.50%   -0.07%     
- Complexity    32651    32955     +304     
============================================
  Files          2574     2595      +21     
  Lines        142995   144293    +1298     
  Branches      17530    17743     +213     
============================================
+ Hits         108050   108942     +892     
- Misses        26908    27205     +297     
- Partials       8037     8146     +109     
Components Coverage Δ
hudi-common 81.80% <69.72%> (-0.48%) ⬇️
hudi-client 81.77% <ø> (-0.06%) ⬇️
hudi-flink 84.04% <ø> (+0.01%) ⬆️
hudi-spark-datasource 68.34% <ø> (+<0.01%) ⬆️
hudi-utilities 71.23% <ø> (+0.03%) ⬆️
hudi-cli 15.32% <ø> (ø)
hudi-hadoop 63.49% <ø> (ø)
hudi-sync 70.67% <ø> (-0.06%) ⬇️
hudi-io 79.57% <ø> (ø)
hudi-timeline-service 83.44% <ø> (ø)
hudi-cloud 64.00% <ø> (ø)
hudi-kafka-connect 53.96% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 49.42% <69.72%> (+0.19%) ⬆️
flink-integration-tests 48.15% <0.53%> (-0.71%) ⬇️
hadoop-mr-java-client 42.56% <0.53%> (-0.76%) ⬇️
integration-tests 13.51% <0.53%> (-0.12%) ⬇️
spark-client-hadoop-common 47.89% <0.53%> (-0.83%) ⬇️
spark-java-tests 51.58% <69.72%> (+0.20%) ⬆️
spark-scala-tests 45.61% <0.53%> (-0.48%) ⬇️
utilities 36.24% <0.53%> (-0.42%) ⬇️

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

Files with missing lines Coverage Δ
...e/hudi/common/index/vector/RaBitQFactorConfig.java 100.00% <100.00%> (ø)
...hudi/common/index/vector/RaBitQNeutralFactors.java 100.00% <100.00%> (ø)
...ache/hudi/common/index/vector/VectorQuantizer.java 100.00% <100.00%> (ø)
...ache/hudi/common/index/vector/VectorQueryMode.java 100.00% <100.00%> (ø)
...he/hudi/common/index/vector/VectorStalePolicy.java 100.00% <100.00%> (ø)
...a/org/apache/hudi/metadata/HoodieIndexVersion.java 88.57% <100.00%> (+0.33%) ⬆️
...hudi/common/index/vector/VectorDistanceMetric.java 96.55% <96.55%> (ø)
.../org/apache/hudi/metadata/VectorClusterRawKey.java 0.00% <0.00%> (ø)
.../hudi/metadata/VectorGenerationManifestRawKey.java 0.00% <0.00%> (ø)
...che/hudi/common/index/vector/RaBitQQueryState.java 86.66% <86.66%> (ø)
... and 15 more

... and 23 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 adds an engine-neutral RaBitQ encoder and quantized-vector representation, metric-aware multibit scorer contracts (MetricQueryState, RaBitQByteLutScorer, RaBitQPlaneKernel, VectorQueryPlanes) with posting-block build/view primitives, and the corresponding vector-index metadata-table key/payload/partition plumbing, plus the accompanying test suite. No correctness issues found. A few style/readability suggestions in the inline comments. Please take a look, and this should be ready for a Hudi committer or PMC member to take it from here. One naming inconsistency worth fixing in the new test files. a few naming and readability suggestions below, most notably the Object-typed vector index field that breaks the specific-typed pattern of every other payload field, and a factory overload that silently discards parameters.

cc @yihua


private static final int DIM = 96;
private static final int TRIALS = 200;
private static final int BITS = 4;

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 rename this to TestMetricEstimatorIdentity? Every other test class in this PR and the surrounding package uses the Test* prefix convention (TestRaBitQEncoder, TestMetricQueryStateRotation, etc.) — the *Test suffix stands out as an outlier.

⚠️ 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: every other metadata payload field has its specific Avro type (HoodieMetadataBloomFilter, HoodieRecordIndexInfo, HoodieSecondaryIndexInfo, …), but this one is Object. Could you introduce a marker interface (e.g. HoodieVectorIndexEntry) that the Avro-generated types implement, or at least use a common base type? As-is, readers have no way to know what concrete types can land here without searching all constructors.

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

1,
randomSeed,
assumeNormalized,
partitionPath);

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: this overload accepts quantizedCodeBytes, rabitqBits, and assumeNormalized but silently drops all three when delegating to the lower overload. Could the unused parameters be removed from the signature, or have a comment explaining why they're intentionally ignored? A caller passing a non-default rabitqBits here would have no indication their value is discarded.

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


@Override
public String encode() {
return shardId == null

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 substring(0, 9) magic number is duplicated from HoodieTableMetadataUtil.getVectorIndexPostingPrefix — could this delegate to that helper, or at least reference a named constant for the cluster-prefix length? It's non-obvious why 9 bytes is the right cut-off here.

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

@chrevanthreddy
chrevanthreddy force-pushed the rfc-109-pr03-rabitq-encoder branch from 0682b18 to 6e11f97 Compare August 4, 2026 20:44
@chrevanthreddy

Copy link
Copy Markdown
Contributor Author

Rebased this stack onto current master and aligned it with the finalized RFC-109 schema contract from #19317.

Key correction:

  • the first shipped RaBitQ factor semantics are persisted as factorVersion = 1;
  • RaBitQFactorConfig.defaults() returns version 1;
  • unsupported factor versions are rejected explicitly;
  • neutral-factor results retain the generation factor version for contract validation.

Java 17 validation after the refresh:

  • focused encoder/factor tests: 46/46 passed;
  • full hudi-common: 2,280/2,280 passed;
  • Checkstyle: zero violations;
  • git diff --check: passed.

The earlier internal v2/v3 terminology was intentionally removed: this is a new persisted format, not a migration from the experimental pre-shipping implementation.

@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 adds an engine-neutral RaBitQ encoder and a metric-aware multibit scorer contract, along with the vector-index MDT key/routing plumbing and a supporting encoder/scorer certification test suite. The main thing worth confirming is the extended bit-plane weighting/ordering in RaBitQByteLutScorer and whether it stays consistent with the RaBitQ encoding across the posting-scan kernels. Please take a look at the inline comments, and this should be ready for a Hudi committer or PMC member to take it from here. Two small nits below — the rest of the code is clean and well-commented. a few notable readability concerns — primarily the silently-dropped parameters in a quantizer factory overload and the very high parameter counts in several factory/builder methods.

}
double extendedDot = 0.0;
for (int p = 0; p < exBits; p++) {
extendedDot += (double) (1L << (exBits - 1 - p)) * planeDot(exBuffer, view.exPlaneOffset(vectorIndex, p));

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 weights extended plane p by 2^(exBits-1-p) (plane 0 = MSB), but RaBitQPlaneKernel.scorePass2 weights plane b by 2^b (plane 0 = LSB). Both docs claim to reconstruct the same centered code as multibitDotTerm. Could you confirm the posting-block exPlanes writer and the long[][] plane writer really emit opposite bit orderings? If a single repack ever feeds both with the same plane index, one path would silently return wrong scores.

⚠️ 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 literal 9 in .substring(0, 9) is opaque — could you extract a named constant (e.g. POSTING_PREFIX_SCAN_LENGTH) or add a short comment explaining what those 9 bytes cover (generation + cluster id, no shard)? A future developer changing the key layout will have no signal that this number needs updating.

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

}

private static int[] exactTopK(float[] q, float[][] data, int k) {
float[] d = new float[data.length];

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: System.out.printf in a unit test gets lost or interleaved in CI logs and is inconsistent with how Hudi surfaces diagnostics — have you considered LOG.debug(...) or just dropping this line?

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

return createVectorIndexQuantizerMetadataRecord(
quantizerType,
quantizedCodeBytes,
1,

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 6-param overload accepts quantizedCodeBytes and rabitqBits but then silently ignores both and delegates to the 3-param version — could you either use those arguments (pass rabitqBits instead of 1) or remove the overload entirely? A caller who passes a custom bits value will be surprised to find their setting discarded.

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

return createVectorIndexQuantizerMetadataRecord(1, 0, quantizerType, randomSeed, null, partitionPath);
}

public static HoodieRecord<HoodieMetadataPayload> createVectorIndexQuantizerMetadataRecord(

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: createVectorIndexManifestRecord has ~30 parameters, which makes call sites very hard to read and maintain. Have you considered introducing a VectorIndexManifestSpec value object (or a builder) to carry these fields? Even grouping the factor-config params (kappa, gMin, eps1Max, epsNRel) into the existing RaBitQFactorConfig would meaningfully reduce the arity here.

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

rowPosition);
}

public PostingBlockBuilder addRow(String recordKey,

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 14-parameter addRow overload is hard to call without a named-argument IDE. Have you considered introducing a small RowSpec (or builder) to bundle the scalar factors and location fields? Even just grouping the six scalar floats (fAdd1, fRescale1, err1, fAddEx, fRescaleEx, residualNorm) into an existing type like QuantizedVector would cut the arity significantly.

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

String recordKey = VectorIndexMetadataKey.clusterStats(generation, clusterId);
HoodieVectorIndexClusterStats stats = new HoodieVectorIndexClusterStats(
routingVersion,
shardCount,

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: fileGroupIds == null ? java.util.Collections.emptyList() : fileGroupIds.stream().collect(Collectors.toList()) — the fully-qualified java.util.Collections is unusual (there are imports already), and when fileGroupIds is non-null new ArrayList<>(fileGroupIds) is simpler and avoids the stream allocation.

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

@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
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

5 participants