Skip to content

feat(csr): fold the delta into a new base with optimize_csr() - #98

Merged
prrao87 merged 1 commit into
mainfrom
feat/csr-optimize
Oct 31, 2026
Merged

prrao87 merged 1 commit into
mainfrom
feat/csr-optimize

Conversation

@prrao87

@prrao87 prrao87 commented Oct 31, 2026

Copy link
Copy Markdown
Member

Why

refresh() applies Lance writes to the CSR as a delta over its base, and rebuilds on its own only once pending changes pass 25% of the base. Below that threshold every lookup also reads the delta, and there was no way to fold it in short of rebuilding the engine. #93 added the reporting half of #75 (base_version, unindexed_edges); this adds the explicit optimize step, so users choose when to rebuild. Closes #75.

What changed

  • optimize_csr(), in Python and Rust (engine.optimize_csr().await), rebuilds every CSR that carries a delta at the version the engine is pinned to. Afterwards csr_stats() reports update "rebuild", delta_edges, tombstones and unindexed_edges at 0, and base_version equal to version. A CSR without a delta, node indexes and everything else are reused, and without use_csr=True it does nothing. Query results never change.
  • It doesn't move to newer versions; refresh() stays the call for that. Both share one state-building path, and optimize_csr() only adds a flag that sends delta-carrying CSRs straight to a full build. It takes refresh()'s lock, so the two run one at a time, and the replacement is built in full before it's swapped in.
  • Why a separate call: refresh() is meant to be cheap enough to run on a timer, while a rebuild scans every edge's endpoint columns again. Keeping them apart follows Lance, where picking up new data and optimize_indices() are separate steps.

The CSR guide's new "Folding the delta in" section says this mirrors Lance's index design and maps one onto the other:

Lance index CSR
The fragments an index was built from The base CSR, at base_version
Unindexed rows, scanned alongside the index The delta, read by every lookup
num_unindexed_rows in index statistics unindexed_edges in csr_stats()
optimize_indices() optimize_csr()

It links Lance's indexing guide and index format, runs an example, and adds a tip on when to reach for optimize_csr() rather than refresh(): after a bulk load, before a latency-sensitive run of queries, or on a schedule, not after every refresh. The update field description, the "How to keep the CSR fresh" steps, the Python and Rust comparison table and the docstring (which feeds the API reference) mention it too.

User impact: one new method on CypherEngine in Python and Rust. Existing calls behave as before.

Validation

  • Rust: an append that stays a delta, then optimize_csr(), checking every stats field and the query results, and that a second call reuses the CSR.
  • Python: mixed appends and deletes folded, with results equal to a fresh join-path engine and a fresh CSR engine; an unrefreshed write stays unseen after optimize_csr(); no-op without use_csr.
  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace, make test (1660 passed) and make lint pass locally, as do docs/check_snippets.py (the new example prints rebuild 0), zensical build --strict and typos. CI is pending.
  • No benchmark runs: no query path changes, and the new code runs only when optimize_csr() is called.

🤖 Generated with Claude Code

refresh() applies Lance writes as a delta over the base CSR and rebuilds
on its own only once pending changes pass 25% of the base. Below that,
every lookup carries the delta and users had no way to fold it in. This
is the optimize step #75 asks for, the CSR's counterpart of Lance's
optimize_indices().

optimize_csr() (Rust and Python) rebuilds every CSR that carries a delta,
at the version the engine is pinned to, and reuses everything else. It
shares refresh()'s state building, with a flag that sends delta-carrying
CSRs straight to a full build, and takes the same lock, so the two run
one at a time and the replacement is validated before it's swapped in.

The CSR guide maps the base CSR, delta, unindexed_edges and
optimize_csr() onto Lance's index coverage, num_unindexed_rows and
optimize_indices(), links Lance's indexing guide and index format, and
adds a tip on when to call it rather than refresh().

Closes #75.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying lance-csr with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8ee2ca3
Status: ✅  Deploy successful!
Preview URL: https://d679b183.lance-csr.pages.dev
Branch Preview URL: https://feat-csr-optimize.lance-csr.pages.dev

View logs

@prrao87
prrao87 marked this pull request as ready for review October 31, 2026 02:10
@prrao87
prrao87 merged commit 29f3dcf into main Oct 31, 2026
8 checks passed
@prrao87
prrao87 deleted the feat/csr-optimize branch October 31, 2026 02:12
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Report CSR freshness in Lance index terms and add an optimize step

1 participant