feat: indexed nearest-by join over Lance — Spark 4.2 SQL Catalyst - #14
Open
sezruby wants to merge 3 commits into
Open
feat: indexed nearest-by join over Lance — Spark 4.2 SQL Catalyst#14sezruby wants to merge 3 commits into
sezruby wants to merge 3 commits into
Conversation
Add `lance-spark-knn-4.2_2.13`: a Catalyst integration that transparently accelerates Spark 4.2's `APPROX NEAREST k BY DISTANCE ...` (the `NearestByJoin` operator from SPARK-56395) over a Lance scan. It rewrites the operator onto a no-shuffle, per-partition native probe against a Lance vector index instead of Spark's default O(|L|x|R|) cross-product + MaxMinByK rewrite. How it works: - `IndexedNearestByJoinRule` is a postHocResolutionRule — the only injection point that sees the unrewritten `NearestByJoin`, since Spark's own `RewriteNearestByJoin` runs first in the FinishAnalysis batch. It rewrites the operator to a single `LanceKnnJoinLogicalPlan`; `LanceKnnJoinStrategy` lowers that to `LanceKnnJoinExec`, which drives one `mapPartitions` -> per-row `LanceProbe` (native ANN/exact search) -> bounded top-K -> late materialize by row address. No shuffle, no broadcast. - Opt-in behind `spark.lance.knn.indexedNearestByJoin.enabled` (off by default); a disabled query falls through to Spark's built-in rewrite. - Right-side WHERE is pushed to Lance as a prefilter, or the rewrite is refused when the predicate cannot be pushed in full (never silently drops a residual). Metrics: L2, cosine, dot. The module carries its own probe core (LanceKnnJoinStage / LanceProbe / TopKHeap / Metric); it depends on lance-spark-base for LanceRuntime and on the Spark 4.2 connector at test scope. Tests: SQL end-to-end (parser -> rule -> strategy -> exec against a real Lance dataset, oracle-checked, incl. WHERE pushdown and opt-in gating), an IVF-PQ approximate-recall suite driven through the SQL path, plus rule-unit and probe/heap unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Test The end-to-end (no-index/exact) and IVF-PQ recall tests are the same kind of test — full SQL execution through the Catalyst chain against a real Lance dataset — and duplicated SparkSession setup, schema builders, vector generation, the oracle top-K, and l2/randomVector. Folding them into one class (exact + approximate sections) shares that scaffolding: 653 -> 481 lines, no loss of coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ters translateFilter's asRightColumn only matched a bare top-level Attribute, so a right-side WHERE on a nested struct field (e.g. meta.category = 'A') refused the rewrite and fell back to Spark's brute-force cross-product. Make asRightColumn recurse through GetStructField and render a dotted path (col.field, arbitrarily deep). Lance's scan filter planner runs with enable_relations = false, so a dotted identifier is interpreted as a nested column path — this maps 1:1. The recursion still gates on the root resolving to a right-side attribute, so a left-side or foreign root refuses. Array/map element access (col[i]) remains unsupported and refuses via the catch-all. Adds accept + refuse unit cases to IndexedNearestByJoinRuleTest's translator tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a single module,
lance-spark-knn-4.2_2.13, that transparently accelerates Spark 4.2's nativeAPPROX NEAREST k BY DISTANCE ...join (theNearestByJoinoperator from SPARK-56395) over a Lance scan.What it does
Spark's default rewrite lowers
NearestByJointo an O(|L|×|R|) cross-product +MaxMinByK. This module rewrites it onto a no-shuffle, per-partition native probe against a Lance vector index instead.IndexedNearestByJoinRule— a post-hoc resolution rule (the only injection point that sees the unrewrittenNearestByJoin; Spark's ownRewriteNearestByJoinruns first, in theFinishAnalysisbatch) rewrites the operator into a singleLanceKnnJoinLogicalPlan.LanceKnnJoinStrategylowers that toLanceKnnJoinExec, which drives onemapPartitions→ per-rowLanceProbe(native ANN/exact search) → bounded top-K → late materialize by row address. No shuffle, no broadcast.spark.lance.knn.indexedNearestByJoin.enabled(off by default). When disabled, the query falls through to Spark's built-in rewrite.WHEREis translated to a Lance prefilter, or the rewrite is refused when the predicate can't be pushed in full — never a silently dropped residual.nprobes/refineFactorare tunable viaspark.lance.knn.*configs.The module carries its own probe core (
LanceKnnJoinStage/LanceProbe/TopKHeap/Metric); it depends onlance-spark-baseforLanceRuntimeand on the Spark 4.2 connector at test scope.Tests
WHEREprefilter pushdown and the opt-in gating.refineFactorhelps).Notes
spark.ymlrunsmvn test -pl lance-spark-<spark>_<scala> -am) builds the connector modules and their upstream deps only; this module is downstream, so it is not built by that matrix. Verifying it needs a reactor build (e.g../mvnw test -pl lance-spark-knn-4.2_2.13 -am).df.kNearestJoinDataFrame API is deferred — it can be added later as a Spark-version-agnostic base module when there's demand.🤖 Generated with Claude Code