lance-format / lance-format/lance-graph
Factorized intermediate results for native multi-hop traversal
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 179
- Forks
- 33
- PR merge metrics
- No merged PRs in 30d
Description
The problem
Our native multi-hop traversal materializes intermediate results in flat (fully enumerated) form. expand_batch flattens the CSR adjacency immediately — for each input row it emits one output row per neighbor and take()s every carried column once per neighbor:
// crates/lance-graph/src/lance_native_planner/csr_expand.rs
for &n in csr.neighbors(src.value(row)) {
parent_idx.push(row_u32); // repeat the parent row
neighbors.push(n); // once per neighbor
}
// ...then take() duplicates every carried column once per neighbor
For a chain like (a)-[:KNOWS]->(b)-[:LIKES]->(c), the a columns get duplicated once per (b, c) path, so intermediate size grows as O(N^k) for k hops. This is the classic many-to-many blowup.
Background
KuzuDB (and the factorized-database line of work it builds on) avoids this with factorized intermediate results: keep intermediates in a compressed, Cartesian-product form so projections and aggregations push through without materializing the explosion. See What is KuzuDB.
Why this fits lance-graph well
CSR already produces the factorized group for free: csr.neighbors(src) returns a contiguous slice, which is exactly the shape of an Arrow ListArray (offset array + values), i.e. a nested child group. Today we take that naturally-nested output and flatten it. A factorized expand simply keeps the nesting — emit (parent values once) + (list of neighbors) instead of (parent values × neighbors).
It also belongs entirely in LanceNativePlanner. DataFusion's RecordBatch model is flat-relational, so factorization lives in the native operator tree, not the DataFusion fallback. This reinforces the native-planner direction in #159.
Scope and caveats
- Survives only inside a native-operator chain. Handing a batch back to a DataFusion operator (cross-table join, sort, filter on a cross-hop expression) requires unfolding to flat. Value is proportional to how long a native chain we can keep.
- Payoff needs consumers that push through nested form — aggregations (
count,EXISTS,collect),LIMIT, degree-style queries, heavily-filtered deep traversals. ForRETURN a, b, cwith no aggregation the final output is the flat product anyway; factorization still saves intermediate memory/CPU but the last step enumerates. - Multi-hop only. Single-hop expand gets nothing (its output is just the neighbor set). This is gated on Phase 3 (multi-hop /
VariableLengthExpand) in #159.
Proposed approach
- Step 1 — nested expand output (correctness-preserving)
- Have native expand emit a nested neighbor column (Arrow
ListArray) instead of flattening - Add a
Flatten/Unfoldoperator at the native → DataFusion boundary - Wire it so any plan can always unfold to flat (no behavior change, just a new option)
- Have native expand emit a nested neighbor column (Arrow
- Step 2 — factorized consumers
- Factorized
count/EXISTSthat consume the nested form without unfolding - Push projections through the factorized representation
-
LIMITshort-circuit over nested groups
- Factorized
- Step 3 — multi-hop chains
- Keep factorization across a chain of native expands (Phase 3
VariableLengthExpand) - Unfold only at the final boundary or when an operator can't consume nested input
- Keep factorization across a chain of native expands (Phase 3
- Step 4 — benchmarks
- Many-to-many multi-hop workload (flat vs. factorized) on intermediate size, memory, latency
Related
- #159 (CSR native traversal) — this is a Phase 3 sub-track of that effort
- Possible follow-up: worst-case optimal joins (WCOJ) for cyclic patterns (triangles), complementary to factorization
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with crates/lance-graph/src/lance_native_planner/csr_expand.rs and read the Phase 3 native traversal work in #159 to understand the current flattening path. Break the proposal into nested expand output, an unfold boundary, factorized consumers, and multi-hop chaining; completion requires those stages plus the listed benchmarks, but the issue names no specific tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100