Repository navigation
feat(csr)!: keep named relationships on the csr with edge rows - #87
Merged
Merged
Conversation
Store each edge's row (Lance row address, or row index for in-memory tables) next to its neighbour in the CSR. A hop that names its relationship now stays on the CSR and fetches only the relationship columns the query reads, by row. WHERE conditions on those columns move into the expansion: small hops filter each batch, large hops read the filter columns once and skip failing edges. Refresh matches deleted, compacted and rewritten rows by address, so it no longer reads the old dataset version and keeps applying deltas after cleanup_old_versions(). Also fix count(r) on relationships, which failed on both execution paths, and mark the Rust crates as experimental while their API settles. BREAKING CHANGE: Rust API changes in CsrIndexBuilder::add_edge, Adjacency::build and with_changes, EdgeChanges and CsrExpandNode::try_new. Closes #73
Deploying lance-csr with
|
| Latest commit: |
5ee7774
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://05d29fd5.lance-csr.pages.dev |
| Branch Preview URL: | https://feat-csr-edge-rows.lance-csr.pages.dev |
prrao87
marked this pull request as ready for review
October 30, 2026 02:28
This was referenced Oct 30, 2026
prrao87
added a commit
that referenced
this pull request
Oct 30, 2026
* chore: bump version 0.4.0 → 0.5.0 0.5.0 rather than 0.4.1: #87 changed the Rust CSR API and #88 removed the deprecated with_node_label and with_relationship builder methods. Also refresh the version in the three lockfiles, which bump-my-version does not update. * ci: build and test macOS wheels on macos-15 runners GitHub retires the macos-14 image on 2026-11-02, and a scheduled brownout cancelled the 0.5.0 dry run's macOS build. macos-15 runners are also Apple Silicon, and MACOSX_DEPLOYMENT_TARGET stays 11.0, so the wheel tag is unchanged.
This was referenced Oct 31, 2026
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
A CSR slot held only the neighbour key, so it couldn't point back to the edge row it came from. Any hop that named its relationship (
-[k:KNOWS]->) fell back to hash joins, even to read one property. Refresh also had to read deleted rows from the old dataset version, so once that version was cleaned up, the engine rebuilt the whole CSR.This is Stage 1 of #72 and closes #73: each CSR slot now records its edge's row, which keeps named hops on the CSR and lets refresh work without the old version's files. Stage 2 (persisting the CSR) builds on it in a later PR.
What changed
Edge rows in the CSR. Each neighbour now carries the row its edge came from: the Lance row address, or the row index for in-memory tables. Reading these rows is compiled out of hops that don't need them.
Named relationships stay on the CSR. A hop such as
(a)-[k:KNOWS]->(b)fetches only the columns ofkthat the query reads, by row.explain()shows them asedge=k[k__since]. Parallel edges keep separate rows and properties, as on the join path. Inline property maps (-[k:KNOWS {since: 2020}]->) still use the join path.Filters on relationship columns run inside the expansion. A
WHEREcondition that reads onlyk's own columns moves into the CSR node (edge_filter=inexplain()), next to the existing target-filter pushdown:Refresh no longer needs the old dataset version. Removed, rewritten and newly deleted rows are matched against the rows the CSR stores, so only added fragments are read. Compaction followed by
cleanup_old_versions()now applies as a delta instead of forcing a rebuild.count(r)works on relationships. It failed withNo field named r__idon both the CSR and join paths. It now counts the rows wherermatched, so an unmatchedOPTIONAL MATCHcounts zero.count(DISTINCT r)compares all ofr's columns, so parallel edges with identical properties count once, the same rule relationship uniqueness already follows.Rust API marked experimental. The crate docs, crate READMEs and the Rust guide now say breaking changes are possible over the next few weeks. This PR itself changes
CsrIndexBuilder::add_edge,Adjacency::buildandwith_changes,EdgeChangesandCsrExpandNode::try_new. The Python API is unchanged.Costs. Peak memory while building the CSR goes from about 62 to about 94 bytes per edge (10M edges), and about 32 bytes per edge stay resident. The CSR build is roughly 1.35 times slower. The docs now say about 95 bytes per edge.
Validation
-D warningsandcargo fmt --check. New tests cover edge rows in the CSR, deltas by row, the per-batch filter, bitmaps from in-memory and multi-fragment Lance data, and thecount(r)rewrite.cleanup_old_versions()(this test rebuilds onmain), andcount(r)withOPTIONAL MATCH.typospass.Benchmarks. Release wheels from
main(1dc63aa) and this branch (0124f41), run with the harness inCONTRIBUTING.md: both suites, both modes,mainand branch alternating twice. All 88 runs passed against the expected answers. Apple M5, 24 GB, macOS 26.6.2, on AC power with no other heavy work running (load average 2.0 at the start, 4.6 at the end); graph-benchmark-ldbc-complex f3e63fd, graph-benchmark-ldbc b649bc1.main(geomean)mainrunsQueries that changed beyond the gap between
mainruns:main/ branchmain/ branchIC5 is the one regression. Its
hasMemberhop used to fall back to joins only because it namesmembership. On the CSR, its relationship filter now costs what a plain scan does (15.4 ms against 14.4 ms for the filtered count in lazy mode). What remains is fetching forum columns one batch of scattered rows at a time across about 800k expanded edges. Unnamed hops already have that weakness onmain, and #86 tracks the fix. The docs describe the case.IC3 doesn't name a relationship and is flat in lazy mode, but came out about 10% slower in memory mode in this run and the previous one. That is inside the 15 to 20% run-to-run range
CONTRIBUTING.mdtreats as noise; it's listed so a reviewer can watch for it.None of the 30 custom queries names a relationship; they stay within 2.2% of
mainin both modes.🤖 Generated with Claude Code