Repository navigation
perf(csr): filter in-memory relationship columns per chunk - #100
Merged
Merged
Conversation
A named hop with a relationship filter, such as IC5's `(friend)<-[membership:hasMember]-(forum) WHERE membership.joinDate > $minDate`, switched to a mask over the whole relationship once it had expanded 1/256 of its edges. One partition built that mask while the others waited on its lock. The switch pays off against scattered Lance reads, but for in-memory tables each partition can gather its own chunk's filter column from arrays already loaded, in parallel. In-memory relationship tables now never switch; Lance datasets keep the 1/256 threshold. On GDC SF1 in memory, IC5 drops from 97.1 to 89.1 ms (0.4.0: 77.6 ms). Lazy mode is unchanged. Part of #92.
prrao87
marked this pull request as ready for review
October 31, 2026 03:31
prrao87
added a commit
that referenced
this pull request
Oct 31, 2026
A patch release: nothing since 0.5.0 breaks the Python or Rust API, so RELEASE.md's rule gives a patch bump. It ships the two-hop count rewrite (#97), `optimize_csr()` (#98), per-chunk in-memory relationship filters (#100) and the node lookup after grouping by id (#101). Also refresh the version in the three lockfiles, which bump-my-version does not update.
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.
Why
IC5 got about 20% slower in memory mode when #87 moved named relationship hops onto the CSR (#92). Profiling by stage showed two causes. This PR fixes the first, the relationship filter on IC5's
hasMemberhop, and is part of #92 rather than closing it.(friend)<-[membership:hasMember]-(forum) WHERE membership.joinDate > $minDateexpands about 824K edges. Once it had expanded 1/256 of the relationship,CsrExpandExecswitched to a mask over all 1.6M edges, built by one partition while the others waited on its lock. 0.4.0 instead filtered the edge table in parallel and hash-joined. The switch exists for Lance datasets, where scattered reads of the filter column cost about a full read each. For in-memory tables it only serialized the work.What changed
IC5's stages in memory mode (median ms, release wheels):
mainhasMemberhop with itsjoinDatefilterWITH forum, collect(friend)IC5's plan computes the hop and its grouping twice (once for the
collect, once to seed theOPTIONAL MATCH), so each saving counts double.What we learned from a second step, not included
The remaining gap is the grouping after the hop. I tried grouping a node by its id and carrying its other columns with
first_value:forum.titleis. Carrying a string through a grouping of 800K rows costs about the same 7 to 9 ms whichever way it's done (max,head(collect()), or grouping by it), and today's grouping by all columns is already the cheapest.first_valueonly has a grouped accumulator when it has anORDER BY. Without one, IC5 went to 130 ms.So the fix is late materialization: group by the id and fetch the node's properties after the grouping, for 77K forums instead of 800K rows. That needs node ids known to be unique, and is tracked in #99.
Validation
cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace,make test(1660 passed) andmake lintpass locally, as do the strict docs build andtypos. CI is pending.csr_expand.rs, andtest_relationship_filter_over_many_batches, which compares against the join path over many batches in memory and from Lance.Benchmarks
Apple M5 (10 cores), 24 GB, macOS 26.6.2, other apps closed. Release wheels built from
mainat 29f3dcf and from this branch, loaded withuv run --with;mainand branch alternated twice per mode, each run started below a 1-minute load of 3. Benchmark repositories: graph-benchmark-ldbc 5f528da, graph-benchmark-ldbc-complex f3e63fd. Every result check passed.mainmainrun gapIC5 in memory mode: 97.1 to 89.1 ms (-8.2%). Every other change is within run-to-run noise. Lazy mode doesn't run the changed code, so its ±4% movements (IC5 -4.1%, IC1 +3.4%) are noise too.
Every query, both suites and modes (mean ms)
graph-benchmark-ldbc: memory
main(ms)mainrun gapGeomean branch/main 1.003; median main run gap 2.6%
graph-benchmark-ldbc: lazy
main(ms)mainrun gapGeomean branch/main 1.007; median main run gap 2.4%
graph-benchmark-ldbc-complex: memory
main(ms)mainrun gapGeomean branch/main 0.979; median main run gap 2.5%
graph-benchmark-ldbc-complex: lazy
main(ms)mainrun gapGeomean branch/main 1.000; median main run gap 1.0%
🤖 Generated with Claude Code