Skip to content

feat(table): add primary-key hybrid (vector + full-text) search#568

Open
JunRuiLee wants to merge 2 commits into
apache:mainfrom
JunRuiLee:feat/pk-fulltext-hybrid
Open

feat(table): add primary-key hybrid (vector + full-text) search#568
JunRuiLee wants to merge 2 commits into
apache:mainfrom
JunRuiLee:feat/pk-fulltext-hybrid

Conversation

@JunRuiLee

@JunRuiLee JunRuiLee commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Purpose

Part of #567 (primary-key full-text + hybrid search on the Rust read side), the fourth and final read-side slice after #563 (reader foundation), #594 (data layer), and #599 (PK full-text read path).

This adds hybrid search on primary-key tables: fuse a primary-key vector route and a primary-key full-text route in physical-position space, mirroring Java PrimaryKeySearchRanker + HybridSearchBuilderImpl. Both routes search their own PK index, produce physical (data file, row position) hits, and are fused by a configurable ranker; the winning rows are materialized best-score-first with a __paimon_search_score column. FAST mode only on the full-text route (mirrors Java).

Brief change log

  • PrimaryKeySearchPosition (table/pk_search_position.rs): a shared physical hit (partition, bucket, data file, row position) whose identity (Eq/Hash) excludes score. Built from a vector candidate via distance_to_score or from a full-text candidate with its raw score; validates a non-negative row position and a finite score. Mirrors Java PrimaryKeySearchPosition.
  • PrimaryKeySearchRanker (table/pk_search_ranker.rs): weighted RRF (RRF_K = 60), weighted score (per-route normalization), and weighted MRR fusion, plus a score-descending top_k with a deterministic physical-position tie-break and duplicate-position handling — a faithful port of Java PrimaryKeySearchRanker including its Float.compare/signed-zero semantics.
  • Per-route candidate producers exposed with their source splits and the real pinned snapshot id (vector search_pk_route, full-text search_route), so the hybrid builder can fuse routes over one consistent snapshot.
  • Hybrid execute_read (primary-key path) (table/hybrid_search_builder.rs): require every route to be primary-key (else fail loud); pin one snapshot across routes (scan.snapshot-id + an equality backstop); enforce FAST mode on the full-text route; feed per-route limits into a builder-limit fusion; validate cross-route physical-source consistency; carry the fused raw scores into PkVectorIndexedSplits, materialize, reorder to best-score order, and strip the internal _PKEY_VECTOR_POSITION column. PK execute/execute_scored fail loud (physical positions, not global row ids); the data-evolution row-id hybrid path is unchanged.

Archive paths resolve under {table}/index/ (Java default index-file-in-data-file-dir=false), matching the PK vector and PK full-text reads; the bucket-directory layout (=true) is deferred, as it is for PK vector.

Tests

  • PrimaryKeySearchRanker: weighted RRF / score / MRR fusion and top_k ordering + tie-break, ported from the Java ranker's fixtures; duplicate physical-position and non-finite-score handling.
  • PrimaryKeySearchPosition: identity excludes score; from-vector vs from-full-text score handling; negative-position / non-finite-score rejection.
  • Hybrid execute_read: end-to-end fusion over a self-contained PK table with a real ANN segment and a full-text archive over the same compacted data file; one-snapshot pinning; FAST-only rejection of FULL/DETAIL on the FT route; all-routes-PK dispatch; PK execute/execute_scored fail loud.
  • Vector, full-text, and data-evolution regressions stay green.
  • cargo test -p paimon and --features fulltext green; cargo clippy -p paimon --all-targets (+ --features fulltext) -D warnings clean; cargo fmt --all --check clean; cargo build -p paimon-datafusion (Send boundary) passes.

API and Format

No storage-format change and no change to existing public read APIs. HybridSearchBuilder gains the primary-key execute_read path. The archive-reading code is behind the existing fulltext cargo feature; the ranker, search position, and dispatch are plain code.

Documentation

No documentation changes; module- and item-level doc comments describe the ranker, the shared search position, and the hybrid read.

Fuse primary-key vector and full-text search routes in physical-position space,
mirroring Java PrimaryKeySearchRanker + HybridSearchBuilderImpl:

- PrimaryKeySearchPosition: a shared physical hit (partition, bucket, data file,
  row position) whose identity excludes score; built from a vector candidate via
  distance_to_score or from a full-text candidate with its raw score, with
  finite-score validation.
- PrimaryKeySearchRanker: weighted RRF (RRF_K=60), weighted score, and weighted
  MRR fusion plus a score-descending top_k with a deterministic physical-position
  tie-break, a faithful port of the Java ranker.
- Expose per-route candidate producers that carry their source splits and the
  real pinned snapshot id.
- Hybrid execute_read on the primary-key path: require every route to be primary
  key, pin one snapshot across routes, enforce FAST mode on the full-text route,
  fuse the routed candidates, then materialize the winning physical rows,
  reorder them best-score-first, and append the unified search-score column.
  execute/execute_scored fail loud on the primary-key path; the data-evolution
  row-id hybrid path is unchanged.
@JunRuiLee
JunRuiLee force-pushed the feat/pk-fulltext-hybrid branch from f1451ac to 46ba28f Compare July 23, 2026 13:24
@JunRuiLee JunRuiLee changed the title feat(ftindex): primary-key full-text and hybrid search read path (full scope) feat(table): add primary-key hybrid (vector + full-text) search Jul 23, 2026
@JunRuiLee
JunRuiLee marked this pull request as ready for review July 23, 2026 13:26
@JingsongLi

Copy link
Copy Markdown
Contributor
  • crates/paimon/src/table/hybrid_search_builder.rs:483 directly projects all user columns without reusing the retained column checks from the vector read.
  • _PKEY_VECTOR_POSITION and __paimon_search_score will be appended later (crates/paimon/src/table/pk_vector_position_read.rs:224).
  • If the table itself contains a column with the same name, a positional lookup will target the user column (crates/paimon/src/table/vector_search_builder.rs:1925), which may result in sorting errors, deletion of the user column, and exposure of internal position columns; when the score column has the same name, duplicate fields will be generated, and the value read may not be the fused score.
  • A standalone vector read explicitly intercepts this pattern in crates/paimon/src/table/vector_search_builder.rs:582; the hybrid implementation should reuse the same checks and add regression tests for the two reserved columns.

…id read

The primary-key hybrid materialized read projects every user column and then
appends the internal _PKEY_VECTOR_POSITION and __paimon_search_score columns; a
user column colliding with either (or with _ROW_ID) would be shadowed by a
positional lookup, corrupting the best-first reorder / position strip and the
fused score. Reuse the primary-key vector read's reserved-column guard at the
top of the hybrid read so it fails loud up front, before any route runs and
regardless of results. Add regression tests for the three reserved names.
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