JunRuiLee opened a new pull request, #771:
URL: https://github.com/apache/paimon-rust/pull/771

   ## Why
   
   Final step of #755. paimon-rust can already run a primary-key vector search 
that it plans itself,
   but not act as the execution kernel for an engine that plans elsewhere — 
which is the shape Doris
   needs for an external Paimon table (apache/doris#65883): planning runs in 
Paimon Java on the FE,
   and each bucket is shipped to a BE that calls in here.
   
   Steps 1-3 built up to it — decode the split bytes (#746), resolve index 
files by external path and
   bucket layout (#752), plan from a decoded split (#757) — but 
`plan_for_bucket_vector_splits` had no
   caller, so none of it was reachable. This adds the part that runs the search 
and returns rows, and
   the C symbol a BE calls.
   
   ## How
   
   `VectorSearchBuilder::execute_read_for_bucket_splits` takes the splits as 
the plan: their payload
   files, per-file row ranges and pinned snapshot are used as given, and the 
index manifest is not
   read. Everything after planning is the ordinary primary-key vector read — 
search, optional refine,
   Top-K, materialization are shared with `execute_read`, not reimplemented — 
so the output is the
   same: projected user columns plus `__paimon_search_score`, best-first. The 
Top-K is local to the
   splits passed in; a caller distributing one call per bucket merges the 
per-bucket results itself.
   
   Rejected rather than worked around:
   
   - **A data-evolution column.** It plans through the global index, so 
answering it would silently use
     a different plan than the caller supplied.
   - **An empty split list.** It pins no snapshot, and would otherwise be 
indistinguishable from a
     query that matched nothing.
   
   `paimon_vector_search_builder_execute_read_for_bucket_splits` exposes this 
over the C ABI as a NEW
   symbol (the existing terminal's signature is pinned by the guard block). The 
array-of-buffers
   marshalling is the only logic the C layer adds: a null array, a zero count, 
or a null entry is the
   caller's input error and is reported as one, rather than reaching the 
decoder as corrupt data.
   
   Also drops the two `#[allow(dead_code)]` attributes #757 added for the entry 
point, now that it has
   a caller.
   
   ## The fixture, and what makes the test able to fail
   
   Both halves come from ONE Java run: the table directory written by the 
`ivf-flat` indexer, and the
   split bytes its own `PrimaryKeyVectorScan` planned over that same table. A 
split names its files by
   generated UUID, so bytes captured separately would reference files that do 
not exist. Generated by
   a new `PkVectorSplitFixtureGenerator` in `paimon-vector` — I'll send that to 
apache/paimon
   separately; the command and config are recorded in the test's header comment 
so the fixture can be
   regenerated.
   
   Worth flagging for review: **"the split route agrees with the manifest 
route" is not a sufficient
   test.** It is also what a read that quietly ignored the split and re-planned 
from the manifest would
   produce — I checked, by substituting the manifest plan: all of the obvious 
assertions still passed.
   
   What separates them is the split's per-file row ranges, which nothing else 
carries. The fixture's
   own `rangeFileCount` is zero — not an oversight, but the asymmetry #755 
called out: Java records a
   range only for a file its pre-filter narrowed, and this query has no 
pre-filter. So the committed
   bytes exercise the omitted-file normalization (omitted = whole file, against 
a kernel that reads a
   missing key as "no rows"), and `restricts_the_read_to_the_splits_row_ranges` 
builds the listed case
   on top. With the manifest plan substituted, that test is the one that fails.
   
   The split embeds its bucket directory as an absolute path, because that is 
what Java serializes and
   what a real engine ships, so the test stages the fixture into a temp dir and 
rewrites that one
   `writeUTF` string.
   
   ## Testing
   
   6 integration tests over the Java fixture, 2 C-binding tests for the 
marshalling.
   
   Gates: `cargo fmt --all -- --check`; `cargo clippy -p paimon --all-targets 
-- -D warnings` and
   `-p paimon-c`, both clean; `cargo test -p paimon --lib` 2586 passed, 
`--features fulltext` 2661
   passed, `--tests` all green.
   
   `cargo test -p paimon-c` is 72 passed / 1 failed — 
`vector_search_append_filter_returns_invalid_input`
   fails identically on unmodified `main` (70 passed / 1 failed, same test), so 
it is pre-existing and
   not from this change. Happy to look at it separately.
   
   ## Not here
   
   The staged Spark-style form (candidate-only search, driver-side global 
merge/rerank, deferred
   materialization) — #755 leaves it out deliberately: with the per-bucket 
form, refine and full-row
   reads happen within a bucket, so a multi-bucket query can materialize rows 
the global Top-K then
   discards. The candidate wire format shouldn't be frozen before there is a 
consumer.
   
   An unrestricted split still materializes a whole-file allow-list where Java 
returns no mask at all,
   which costs Lumina its batch-search path. #757 noted this belongs with the 
entry point that makes
   the route reachable, but saying "no mask" changes the plan's own type, so 
I'd rather do it as its
   own change than widen this one — say the word if you'd prefer it folded in.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to