Skip to content

[core] Add primary-key vector read kernel#517

Open
JunRuiLee wants to merge 4 commits into
apache:mainfrom
JunRuiLee:feat/pk-vector-position-read
Open

[core] Add primary-key vector read kernel#517
JunRuiLee wants to merge 4 commits into
apache:mainfrom
JunRuiLee:feat/pk-vector-position-read

Conversation

@JunRuiLee

@JunRuiLee JunRuiLee commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Purpose

Linked issue: part of #514 — this PR implements the crate-private PK-vector read kernel / orchestration layer. It does not close the issue.

This is the Rust read-kernel counterpart of Apache Paimon Java apache/paimon#8576 (PrimaryKeyVectorPositionReader / PrimaryKeyIndexedSplitRead) plus the read-kernel subset of apache/paimon#8579 (PrimaryKeyVectorRead + PrimaryKeyVectorResult.splits()). It is intentionally not the full user-facing PK-vector search integration.

Given synthetic per-bucket search inputs, this PR can:

  • materialize selected physical rows from one data file while preserving physical positions and aligned scores;
  • validate and read one-file indexed splits over physical-position ranges;
  • run bucket-local search via the already-merged bucket kernel ([core] Add primary-key vector bucket search kernel #516), merge deterministic global Top-K candidates across buckets, group survivors per data file, and build indexed splits for materialization.

#516 has already absorbed the current Java-master behavior for inactive ANN sources (file == null -> continue) and bounded heap merging, so this PR builds on that behavior. The inactive-source handling is therefore intentional and aligned with current Java master, not a fail-loud mismatch.

Scope

In scope:

  1. pk_vector_position_read.rs — Rust equivalent of Java PrimaryKeyVectorPositionReader.
  2. pk_vector_indexed_split_read.rs — Rust equivalent of Java PrimaryKeyIndexedSplitRead.
  3. pk_vector_orchestrator.rs — synthetic-input orchestration mirroring the read-kernel part of Java PrimaryKeyVectorRead / PrimaryKeyVectorResult.splits().
  4. VectorSearchMetric::distance_to_score, used when converting final distances into Java-compatible higher-is-better scores.

Out of scope for this PR, handled by the follow-up integration PR:

  • PrimaryKeyVectorScan / snapshot and index-manifest planning;
  • VectorSearchBuilder / table routing and public API wiring;
  • construction of real ANN and exact-reader dependencies;
  • real vindex ANN segment bytes and end-to-end public-path validation;
  • GlobalIndexSearchMode::Fast routing behavior;
  • refine/rerank from newer Java master.

Because there is no crate-level production caller for these new reader/orchestrator modules yet, they intentionally keep narrow #[allow(dead_code)] annotations. The follow-up integration PR wires the production caller and removes the allowances for the wired search path.

Brief Change Log

pk_vector_position_read.rs

Rust equivalent of Java PrimaryKeyVectorPositionReader.

  • Reads selected 0-based physical positions from a single data file.
  • Appends internal metadata columns:
    • _PKEY_VECTOR_POSITION (Int64, always);
    • _PKEY_VECTOR_SCORE (Float32, only when scores are supplied).
  • Derives returned physical positions from the existing _ROW_ID channel (position = _ROW_ID - first_row_id) instead of guessing from batch offsets.
  • Injects _ROW_ID internally and strips it from final output.
  • Preserves null/deletion-vector physical-position alignment by using the actual rows returned by read_single_file_stream.
  • Rejects reserved metadata-column name conflicts, requested _ROW_ID, missing first_row_id, predicate-bearing readers, empty positions, out-of-range positions, and score-key mismatches.

pk_vector_indexed_split_read.rs

Rust equivalent of Java PrimaryKeyIndexedSplitRead.

  • Adds PkVectorIndexedSplit: one data file + inclusive physical-position ranges + optional scores aligned to expanded position order.
  • Validates single-file input, range bounds, range ordering/non-overlap, non-empty selection, and score length.
  • Expands ranges to physical positions and delegates materialization to PkVectorPositionRead.
  • Rust intentionally fails loud on descending/overlapping physical ranges to protect score alignment. Java normalizes through a bitmap; this is a stricter validation, not a capability regression.

pk_vector_orchestrator.rs

Rust equivalent of the synthetic-input read-kernel part of Java PrimaryKeyVectorRead and PrimaryKeyVectorResult.splits().

  • Calls [core] Add primary-key vector bucket search kernel #516 bucket_search for each synthetic bucket split.
  • Merges candidates with a deterministic 5-level BEST_FIRST key:
    1. distance ascending;
    2. partition bytes unsigned lexicographic;
    3. bucket ascending;
    4. data file name ascending;
    5. row position ascending.
  • Converts final distances to Java-compatible scores via VectorSearchMetric::distance_to_score.
  • Groups survivors by data file into PkVectorIndexedSplits for materialization.
  • Fails loud on cross-split ambiguity, duplicate (file, position), or a hit referencing a file absent from its bucket split.
  • The materialized stream is file/position ordered, matching the indexed-split materialization contract; ranking order is preserved in the candidate list before grouping.

Validation

Synthetic unit/e2e tests cover:

  • position/score alignment using real _ROW_ID values;
  • null rows and deletion-vector interaction;
  • indexed split validation and score alignment;
  • global Top-K tie-breaks across buckets/files/positions;
  • distance-to-score conversion;
  • inactive ANN-source behavior inherited from [core] Add primary-key vector bucket search kernel #516;
  • error paths for malformed inputs.

Validated locally:

  • cargo fmt --all --check
  • cargo clippy -p paimon --lib --tests -- -D warnings
  • cargo test -p paimon pk_vector --lib

Full public-path validation with snapshot/index-manifest planning and real ANN/exact dependencies is intentionally deferred to the follow-up integration PR.

API and Format

No storage-format change. All additions are crate-private (pub(crate) / pub(super)); no public API is added or changed in this PR.

Documentation

No user-facing documentation changes.

@JunRuiLee JunRuiLee force-pushed the feat/pk-vector-position-read branch 2 times, most recently from 048df10 to ff5bb39 Compare July 13, 2026 12:49
@JunRuiLee JunRuiLee changed the title [WIP][core] Add primary-key vector position reader kernel [WIP][core] Add primary-key vector read kernel (position read, indexed split, orchestration) Jul 13, 2026
@JunRuiLee JunRuiLee force-pushed the feat/pk-vector-position-read branch from ff5bb39 to f336b6a Compare July 14, 2026 04:08
@JunRuiLee JunRuiLee changed the title [WIP][core] Add primary-key vector read kernel (position read, indexed split, orchestration) [core] Add primary-key vector read kernel Jul 14, 2026
@JunRuiLee JunRuiLee marked this pull request as ready for review July 14, 2026 06:25
@JunRuiLee JunRuiLee force-pushed the feat/pk-vector-position-read branch 2 times, most recently from ff5bb39 to ae7342f Compare July 14, 2026 06:29
Rust equivalent of Java `PrimaryKeyVectorPositionReader` (reader-kernel
subset of apache/paimon#8576). Given one data file, a set of selected
0-based physical positions, and an optional position->score map, it
materializes the selected rows and appends internal metadata columns
`_PKEY_VECTOR_POSITION` (+ optional `_PKEY_VECTOR_SCORE`).

Physical positions are recovered from the `_ROW_ID` column the existing
selective read emits (position = _ROW_ID - first_row_id), injected
internally and stripped from the output, so a deletion vector dropping
rows never desyncs position/score alignment. Reuses the existing
single-file read (`read_single_file_stream`, its `_ROW_ID` mechanism, DV
intersection) rather than a new read path.

Validation (all DataInvalid): empty / negative / out-of-range positions;
scores key-set must equal the selected positions; a requested `_ROW_ID`
or a reserved metadata column name is rejected; a row-filtering
predicate is rejected (`_ROW_ID` + a residual filter desync); a data
file without first_row_id.

Adds three `pub(super)` `DataFileReader` accessors (read_type,
has_row_filtering_predicate, with_read_type) for the sibling module. No
crate-level caller yet, so the module and accessors carry a temporary
`#[allow(dead_code)]`.
Rust equivalent of Java `PrimaryKeyIndexedSplitRead` (read-path subset
of apache/paimon#8576). `PkVectorIndexedSplit` carries one data file +
inclusive physical-position ranges + an optional aligned score array;
`PkVectorIndexedSplitRead` validates the split, expands the ranges into
an ascending position set and a position->score map, and delegates to
the sibling position reader.

A pure consumer: no bucket/ANN search, no orchestration, no
serialization. Validation: exactly one data file; ranges within
[0, row_count), strictly ascending and non-overlapping (touching
allowed); score length must match the expanded positions. No crate-level
caller yet, so it carries a temporary `#[allow(dead_code)]`.
Rust equivalent of Java `PrimaryKeyVectorRead` + the read subset of
`PrimaryKeyVectorResult.splits()` (apache/paimon#8579). Per-bucket search
via `bucket_search`, cross-bucket global Top-K merge, grouping survivors
by data file into `PkVectorIndexedSplit`s, and lazy materialization via
`PkVectorIndexedSplitRead`.

Global Top-K is a full sort + truncate over the collected candidates
(N <= buckets * limit) on a five-level BEST_FIRST key
(distance, partition bytes, bucket, file name, row position; `total_cmp`
for NaN-safety). Grouping fails loud on cross-split ambiguity, duplicate
(file, position), or a hit referencing a file absent from its bucket
split.

Adds `VectorSearchMetric::distance_to_score` (mirrors Java
`PrimaryKeyVectorResult.score(distance)`). Inputs are synthetic
per-bucket splits; snapshot/manifest planning, table routing, and real
dependency construction are not yet implemented, so the module carries a
temporary `#[allow(dead_code)]`.
@JunRuiLee JunRuiLee force-pushed the feat/pk-vector-position-read branch from ae7342f to 53abc01 Compare July 14, 2026 12:09
@JingsongLi

Copy link
Copy Markdown
Contributor

pk_vector_orchestrator.rs:86 uses f32::total_cmp to simulate Java sorting, but negative NaNs are sorted before finite values; after taking the negative of the distance to NaN in the inner product, an anomalous row may be selected as Top-1, whereas Java’s Float.compare places NaNs at the end. It is recommended to implement a Java-style NaN comparator and add signed-NaN tests. The existing pk_vector passes 68 out of 68 tests, but this scenario is not covered.

…ering

Distance comparators used f32::total_cmp, which orders a negative NaN before
every finite value — so a NaN distance (e.g. a non-finite stored vector under
inner product, which negates the dot product) would be selected as Top-1.
Java Float.compare orders NaN after all finite values. Add a shared
java_float_compare helper (NaN last, -0.0 < +0.0) and use it in every
comparator that produces or trims Top-K candidates: the bucket-local exact
heap and final sort, the ANN result sort, the per-bucket best-first order, and
the cross-bucket orchestrator key. Fixing only the orchestrator would be
insufficient because the bucket-local Top-K can evict a finite candidate in
favor of a NaN before the orchestrator runs. Add signed-NaN unit tests and an
exact-search test asserting a NaN candidate never beats a finite Top-1.
@JunRuiLee

Copy link
Copy Markdown
Contributor Author

@JingsongLi good catch. I fixed this with a shared Java-compatible float comparator (java_float_compare: NaN sorts after all numeric values, non-NaN keeps total_cmp so -0.0 < +0.0) instead of using f32::total_cmp directly. The fix is applied across the full pk-vector Top-K path — ANN result ordering, bucket heap ordering, exact-search heap/final ordering, and orchestrator merge — because fixing only the orchestrator cannot recover candidates already dropped by the bucket/exact heaps. Tests now cover negative-NaN ordering, and both exact- and bucket-heap Top-1 behavior (a NaN-distance candidate never beats a finite one).

@JunRuiLee JunRuiLee closed this Jul 15, 2026
@JunRuiLee JunRuiLee reopened this Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants