Skip to content

feat(vortex-geo): geometry Bbox zone-map statistic + distance-filter pruning#8646

Merged
HarukiMoriarty merged 4 commits into
developfrom
nemo/geo-bounds-zonemap
Jul 15, 2026
Merged

feat(vortex-geo): geometry Bbox zone-map statistic + distance-filter pruning#8646
HarukiMoriarty merged 4 commits into
developfrom
nemo/geo-bounds-zonemap

Conversation

@HarukiMoriarty

Copy link
Copy Markdown
Contributor

Summary

Adds spatial chunk-pruning to Vortex. A new GeometryBounds aggregate stores a per-chunk minimum bounding box (MBR) as a zone-map statistic, and a stats-rewrite rule uses it to skip chunks that cannot satisfy a ST_Distance(geom, const) <= r filter.

Limitation

  • Only the <= / < are pruned. > / >= are soundly prunable via the symmetric farthest-corner bound but are intentionally omitted (rarely?)
  • Pruning is sound, but the performance is highly related with the geo column write order, selectivity depends on a spatially clustered layout (e.g. a Hilbert/Z-order sort) so chunk MBRs are tight and non-overlapping.

Testing

8 new vortex-geo tests. Point bbox across batches; Polygon bbox over all ring vertices, empty group → null, and registry self-declaration. only <=/< prune while >/>=/==/!= don't (parameterized), distance symmetry, non-distance comparisons ignored, and an end-to-end falsify.

Performance

SF=1

┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Query ┃ duckdb:parquet (base) ┃   duckdb:vortex ┃ duckdb:vortex-geo-native ┃
┡━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 1     │                39.8ms │  15.5ms (0.39x) │            3.1ms (0.08x) │
│ 2     │               147.8ms │  39.9ms (0.27x) │           47.4ms (0.32x) │
│ 3     │                66.1ms │  24.0ms (0.36x) │            5.1ms (0.08x) │
│ 4     │               563.1ms │  60.2ms (0.11x) │          109.7ms (0.19x) │
│ 5     │               356.8ms │ 284.1ms (0.80x) │          287.9ms (0.81x) │
│ 6     │               677.3ms │  93.9ms (0.14x) │          149.5ms (0.22x) │
│ 7     │               174.0ms │  70.3ms (0.40x) │           96.7ms (0.56x) │
│ 8     │               142.4ms │  48.4ms (0.34x) │           66.1ms (0.46x) │
│ 9     │                18.7ms │  17.0ms (0.91x) │           19.3ms (1.03x) │
└───────┴───────────────────────┴─────────────────┴──────────────────────────┘

SF=3

┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Query ┃ duckdb:parquet (base) ┃   duckdb:vortex ┃ duckdb:vortex-geo-native ┃
┡━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 1     │                47.8ms │  36.4ms (0.76x) │            4.0ms (0.08x) │
│ 2     │               143.8ms │  61.0ms (0.42x) │          113.1ms (0.79x) │
│ 3     │                83.1ms │  68.2ms (0.82x) │            7.2ms (0.09x) │
│ 4     │               557.2ms │  67.4ms (0.12x) │          146.2ms (0.26x) │
│ 5     │               949.6ms │ 882.9ms (0.93x) │          897.8ms (0.95x) │
│ 6     │               674.5ms │ 124.5ms (0.18x) │          231.7ms (0.34x) │
│ 7     │               332.5ms │ 330.9ms (1.00x) │          277.2ms (0.83x) │
│ 8     │               183.7ms │ 162.0ms (0.88x) │          209.2ms (1.14x) │
│ 9     │                28.5ms │  25.9ms (0.91x) │           28.7ms (1.01x) │
└───────┴───────────────────────┴─────────────────┴──────────────────────────┘

SF=10

┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Query ┃ duckdb:parquet (base) ┃   duckdb:vortex ┃ duckdb:vortex-geo-native ┃
┡━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 1     │               161.6ms │ 109.2ms (0.68x) │            7.8ms (0.05x) │
│ 2     │               358.4ms │ 187.9ms (0.52x) │          329.1ms (0.92x) │
│ 3     │               270.7ms │ 237.7ms (0.88x) │           14.3ms (0.05x) │
│ 4     │               912.7ms │ 131.0ms (0.14x) │          157.8ms (0.17x) │
│ 5     │                 3.37s │   3.10s (0.92x) │            3.14s (0.93x) │
│ 6     │                 1.25s │ 301.7ms (0.24x) │          501.0ms (0.40x) │
│ 7     │                 1.27s │ 914.1ms (0.72x) │          969.6ms (0.76x) │
│ 8     │               693.5ms │ 918.7ms (1.32x) │          683.1ms (0.99x) │
│ 9     │                36.3ms │  35.7ms (0.98x) │           44.5ms (1.22x) │
└───────┴───────────────────────┴─────────────────┴──────────────────────────┘

Takeaway: when the data is pre-sorted, the bounding box pruning can significantly reduce the intermediate results DuckDB read in, make Q1 and Q3 significantly faster

@HarukiMoriarty HarukiMoriarty added the changelog/feature A new feature label Jul 2, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 2, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 1660 untouched benchmarks
⏩ 47 skipped benchmarks1


Comparing nemo/geo-bounds-zonemap (b443665) with develop (6b27c45)

Open in CodSpeed

Footnotes

  1. 47 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Base automatically changed from nemo/geo-native-pushdown to develop July 3, 2026 19:15
@robert3005 robert3005 requested a review from a team July 3, 2026 19:15
@HarukiMoriarty HarukiMoriarty force-pushed the nemo/geo-bounds-zonemap branch 3 times, most recently from 4274729 to bda1557 Compare July 13, 2026 17:22
@HarukiMoriarty HarukiMoriarty enabled auto-merge (squash) July 13, 2026 17:23
Comment thread vortex-array/src/aggregate_fn/plugin.rs Outdated
Comment on lines +33 to +34
/// The default per-chunk zone statistic to store for a column of `input_dtype`, or `None` if
/// this aggregate isn't one.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: this is kind of hard to understand, maybe say something like this or similar:

/// The default zone statistic (per-chunk) for a column of `input_dtype`,
/// or `None` if the dtype is not supported.
```rust

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.

What is this for?

@connortsui20 connortsui20 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems fine to me, but someone who has worked on aggregates and stats more should probably review this

…-filter pruning

Signed-off-by: Nemo Yu <zyu379@wisc.edu>
@HarukiMoriarty HarukiMoriarty force-pushed the nemo/geo-bounds-zonemap branch from bda1557 to 119fe60 Compare July 14, 2026 21:44
Comment thread vortex-array/src/aggregate_fn/session.rs
Comment thread vortex-geo/src/aggregate_fn/aabb.rs
Comment thread vortex-geo/src/aggregate_fn/bounds.rs Outdated
Comment thread vortex-geo/src/aggregate_fn/bounds.rs Outdated
Comment thread vortex-geo/src/prune.rs Outdated
Comment thread vortex-geo/src/prune.rs Outdated
Comment thread vortex-geo/src/prune.rs
let Some(radius) = expr.child(1).as_opt::<Literal>() else {
return Ok(None);
};
let Ok(radius) = f64::try_from(radius) else {

@joseph-isaacs joseph-isaacs Jul 15, 2026

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.

is this a bug? hitting this case

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  • A null literal (distance <= NULL), no value to reason about. Scanning is sound: the filter evaluates to null at scan time and drops every row anyway; we just missed a chance to skip the chunk early.
  • A non-primitive literal, mostly impossible in a typechecked plan (comparing f64 to a string fails planning), except extension-typed literals, which Binary's typecheck deliberately lets through.

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.

Did you test this I am not how f64::try_from and Option::<f64>::try_from differ?

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.

Also can you ever have a value that is not in fact f64 for a value typechecked pr

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Did you test this I am not how f64::try_from and Option::<f64>::try_from differ?

They differ on both axes: f64::try_from casts any primitive ptype (integer radius → 10.0), errors on null/non-primitive; Option::<f64>::try_from requires exactly F64 (an integer literal errors, no cast), null → Ok(None). So the Option form would stop integer radii from pruning. Wasn't tested before, I added tests to pin what we tolerate.

Also can you ever have a value that is not in fact f64 for a value typechecked pr

Through DuckDB, no. Its binder coerces to DOUBLE before pushdown. But Vortex itself never coerces filter expressions (coerce_expression exists but has no callers), and Binary's typecheck exempts extension-typed operands, so the rule assumes uncoerced input from any engine: cast what's numeric, decline the rest (never error). The new tests pin exactly that.

Comment thread vortex-geo/src/prune.rs Outdated
HarukiMoriarty and others added 2 commits July 15, 2026 11:37
…minology sweep

- Rename GeometryBounds -> GeometryAabb (id vortex.geo.bounds -> vortex.geo.aabb),
  BoundsPartial -> AabbPartial, aggregate_fn/bounds.rs -> aabb.rs: the statistic is
  specifically the axis-aligned bounding box, leaving room for other bbox kinds later
- Rename GeoDistanceBoundsPrune -> GeoDistancePrune; the AABB is an implementation
  detail carried by the doc comment
- Sweep docs, comments, and test names to axis-aligned bounding box (AABB) terminology
- Document what zone_stat_default is for and note zone_stat_defaults' linear registry
  scan (intended once per column at writer open)
- Present GeometryAabb as a plain aggregate whose zone-stat role is an application
- Collapse AabbPartial::merge to map_or
- Comment which other operators could prune in the future (==) or cannot (!=), and why
  the radius try_from fall-through is a sound decline rather than an error
- Extract test_harness::geo_session() to replace repeated session+initialize in tests

Signed-off-by: Nemo Yu <zyu379@wisc.edu>
Comment thread vortex-geo/src/prune.rs
@joseph-isaacs joseph-isaacs disabled auto-merge July 15, 2026 17:33

@joseph-isaacs joseph-isaacs 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.

LG, there are three or so optional comments

@HarukiMoriarty HarukiMoriarty merged commit 8a1f91c into develop Jul 15, 2026
95 checks passed
@HarukiMoriarty HarukiMoriarty deleted the nemo/geo-bounds-zonemap branch July 15, 2026 18:15
HarukiMoriarty added a commit that referenced this pull request Jul 15, 2026
…stic

- Add GeoIntersectsPrune: skips chunks whose AABB is strictly separated from
  the filter constant's bounding box (touching boxes scan, since touching
  geometries intersect); NOT-intersects and unprovable shapes fall through
  to the scan
- Restructure prune.rs into a directory module ready for future spatial
  predicates: mod.rs holds the soundness model and the shared vocabulary
  (geometry_and_constant, query_aabb, aabb_stat, min_dist_sq/max_dist_sq),
  one leaf module per predicate (distance.rs, intersects.rs), and tests in
  tests.rs with a shared zone-map harness
- Fold in the #8646 review follow-ups that missed its merge: the aabb_stat
  ctor (part of the restructure) and tests pinning the radius-literal
  tolerance set - an integer radius casts and prunes, null and
  extension-typed radii decline to a scan; the harness now takes any
  literal scalar

Signed-off-by: Nemo Yu <zyu379@wisc.edu>
HarukiMoriarty added a commit that referenced this pull request Jul 15, 2026
…stic

- Add GeoIntersectsPrune: skips chunks whose AABB is strictly separated from
  the filter constant's bounding box (touching boxes scan, since touching
  geometries intersect); NOT-intersects and unprovable shapes fall through
  to the scan
- Restructure prune.rs into a directory module ready for future spatial
  predicates: mod.rs holds the shared proof vocabulary (geometry_and_constant,
  query_aabb, aabb_stat, min_dist_sq/max_dist_sq), each predicate is a leaf
  module owning its own test cases (distance.rs, intersects.rs), and shared
  zone-map fixtures live in test_harness.rs
- Fold in the #8646 review follow-ups that missed its merge: the aabb_stat
  ctor (part of the restructure) and tests pinning the radius-literal
  tolerance set - an integer radius casts and prunes, null and
  extension-typed radii decline to a scan; the harness takes any literal
  scalar

Signed-off-by: Nemo Yu <zyu379@wisc.edu>
HarukiMoriarty added a commit that referenced this pull request Jul 15, 2026
…stics (#8775)

## Rationale for this change

Follow-up to #8646, which added the `GeometryAabb` zone-map statistic
and used it to prune `ST_Distance(geom, const) <op> r` filters. This PR
adds the second spatial predicate on the same statistic:
`ST_Intersects(geom, const)`. A chunk whose AABB is strictly separated
from the constant's bounding box cannot contain an intersecting row, so
the zone is skipped without any IO.

This PR also folds in two review follow-ups from #8646 that were pushed
minutes after it merged:
tests pinning the radius-literal tolerance set, and the `aabb_stat`
constructor requested in
review.

## What changes are included in this PR?

- **`GeoIntersectsPrune`** (`prune/intersects.rs`): The proof is
`min_dist_sq(chunk_aabb, query_aabb) > 0`, strictly: boxes that merely
touch must scan, since touching geometries intersect under OGC
semantics.
- **`prune/` restructure**.
- **#8646 review follow-ups**: the radius conversion's contract is
documented and pinned by tests.

Signed-off-by: Nemo Yu <zyu379@wisc.edu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants