Skip to content

feat: add predicate pushdown for Vortex format - #511

Merged
jiaqizho merged 3 commits into
milvus-io:mainfrom
tedxu:feat/predicate-pushdown
May 13, 2026
Merged

feat: add predicate pushdown for Vortex format#511
jiaqizho merged 3 commits into
milvus-io:mainfrom
tedxu:feat/predicate-pushdown

Conversation

@tedxu

@tedxu tedxu commented May 8, 2026

Copy link
Copy Markdown
Collaborator

See #512

Implement predicate pushdown for Vortex files using sqlparser-rs
to parse SQL WHERE-clause predicates into native Vortex expressions.
The predicate string from get_record_batch_reader(predicate) is
parsed once in Rust, converted to a vortex::expr::Expression, and
applied via ScanBuilder::WithFilter() for zone-map pruning and
row-level filtering. No expression tree crosses the FFI boundary.

Changes:

  • Rust: sqlparser-based predicate parser (comparisons, AND/OR/NOT,
    IN lists, quoted column names for all-digit fields)
  • CXX bridge: expr::parse_predicate() for one-time parse,
    ScanBuilder integration
  • C++: predicate plumbing through Reader -> PackedRecordBatchReader
    -> ColumnGroupReader -> FormatReader::set_predicate()
  • VortexFormatReader: parse once in set_predicate(), apply by-ref
    in read() via WithFilter()
  • Single CG only; multi-CG logs warning and ignores predicate
  • Parquet/Lance/Iceberg: no-op (default set_predicate)
  • Handle empty filtered results from Vortex scan
  • loon read: add --predicate and --verbose flags
  • Benchmark: sorted vs unsorted, int64 vs string predicates,
    filesystem I/O metrics

Benchmark highlights (40K rows, Vortex, local SSD):

  • Sorted + 10% selectivity: 75% I/O reduction, 2.5x faster
  • String predicates within 9-14% of int64 on sorted data
  • Unsorted data: predicate adds overhead with no pruning benefit

Signed-off-by: Ted Xu <ted.xu@zilliz.com>
@sre-ci-robot

Copy link
Copy Markdown
Collaborator

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: tedxu

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

See milvus-io#512

Implement predicate pushdown for Vortex files using sqlparser-rs
to parse SQL WHERE-clause predicates into native Vortex expressions.

- Add sqlparser-based predicate parser in Rust bridge that converts
  SQL predicates to vortex::expr::Expression (supports comparisons,
  AND/OR/NOT, IN lists, all-digit quoted column names)
- Add expr::parse_predicate() to CXX bridge for one-time parsing
- Wire predicate string through Reader -> PackedRecordBatchReader ->
  ColumnGroupReader -> FormatReader::set_predicate()
- VortexFormatReader parses predicate once in set_predicate(),
  applies via ScanBuilder::WithFilter() by-ref on each read()
- For single CG: use get_chunk() per chunk to avoid chunk-slicing
  crash when predicate filtering changes row counts
- For multi CG: log warning and ignore predicate (row alignment
  requires row-index tracking, not yet implemented)
- Parquet/Lance/Iceberg: predicate is a no-op (default set_predicate)
- Handle empty filtered results (0 chunks from Vortex scan)
- Add --predicate and --verbose flags to loon read command
- Add predicate pushdown benchmark (sorted vs unsorted, int64 vs
  string, with filesystem I/O metrics)
- Add benchmark results doc with analysis

Signed-off-by: Ted Xu <ted.xu@zilliz.com>
@tedxu
tedxu force-pushed the feat/predicate-pushdown branch from 0785c1d to 261f62a Compare May 8, 2026 02:16
@codecov

codecov Bot commented May 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.54839% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.56%. Comparing base (8268add) to head (30e2d00).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
cpp/src/format/vortex/vortex_format_reader.cpp 89.47% 2 Missing ⚠️
cpp/src/reader.cpp 93.75% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #511      +/-   ##
==========================================
+ Coverage   72.61%   74.56%   +1.94%     
==========================================
  Files         148      149       +1     
  Lines       13794    13873      +79     
  Branches     2053     2078      +25     
==========================================
+ Hits        10017    10344     +327     
+ Misses       3777     3529     -248     
Flag Coverage Δ
cpp 77.72% <93.54%> (+2.13%) ⬆️
python 44.77% <ø> (ø)

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

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread cpp/src/format/bridge/rust/src/vortex_bridge.cpp Outdated
Comment thread cpp/src/reader.cpp
*/
[[nodiscard]] arrow::Result<std::unique_ptr<ChunkReader>> get_chunk_reader(
int64_t column_group_index,
const std::shared_ptr<std::vector<std::string>>& needed_columns = nullptr) const override {

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.

also add const std::string& /*predicate*/ ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The chunk reader, per our previous design, does not support predicate. Filtering on chunks may break the caching preconditions.

Comment thread cpp/src/reader.cpp

// Predicate pushdown only supported for single column group.
// Multi-CG filtering requires row-index tracking for cross-CG alignment.
if (!predicate_.empty() && column_groups_.size() > 1) {

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.

We can support Multi-CG filtering if lower level reader return the mask. Still can align the rows

}

if (parsed_predicate_) {
scan_builder.WithFilter(*parsed_predicate_);

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.

I think the main issue with the current PR is that it performs full filtering rather than sparse filtering. The performance of full filtering is much worse than sparse filtering. My understanding is that, for “filter pushdown,” the storage layer only needs to provide sparse filtering capability at this stage.

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.

Performing two complete filter, especially with a low filter rate, will reduce the end-to-end performance.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Vortex's with_filter() couples zone-map pruning with row-level evaluation in the patched-in version; there is no prune-only knob today. Refreshed benchmark numbers (stats now default-on after this PR) still show net wins on selective sorted data, e.g. 75% I/O reduction and 2.2× speedup at 10% selectivity vs the unsorted baseline. If/when Vortex exposes a prune-only path, we'll switch. Tracking as a follow-up in #524.

Comment thread cpp/benchmark/benchmark_predicate.cpp
Comment thread cpp/src/format/bridge/rust/src/predicate_parser.rs Outdated
Comment thread cpp/src/format/bridge/rust/src/predicate_parser.rs Outdated
Drop the SELECT-wrap hack in the Rust parser and parse the SQL
expression directly. Reject trailing tokens after the expression
so things like "1=1; DROP TABLE x" no longer silently truncate.

Enable Vortex writer statistics by default so zone-map pruning
is exercised under the default writer configuration.

Make the predicate parser schema-aware. Syntax errors, unknown
columns and empty predicates remain hard errors and surface as
arrow::Status::Invalid. Unsupported expression forms (function
calls, NULL literals, ...) and comparison type mismatches now
cause only the affected sub-expression to be dropped: an
offending AND conjunct is removed and the surviving conjuncts
pushed down; an OR or NOT with a dropped child degrades the
whole filter to no-filter (safe over-approximation). Each drop
is logged to stderr inside the Rust parser.

See milvus-io#512

Signed-off-by: Ted Xu <ted.xu@zilliz.com>
@tedxu
tedxu force-pushed the feat/predicate-pushdown branch from 33c64f8 to 30e2d00 Compare May 13, 2026 02:04
@jiaqizho

Copy link
Copy Markdown
Collaborator

/lgtm

@jiaqizho
jiaqizho added this pull request to the merge queue May 13, 2026
Merged via the queue into milvus-io:main with commit 2c7e42f May 13, 2026
11 of 12 checks passed
@tedxu
tedxu deleted the feat/predicate-pushdown branch May 13, 2026 06:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants