Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 129 additions & 4 deletions crates/lance-csr/examples/embedded_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
//!
//! Writes a small social graph as two Lance datasets in a temporary directory, opens
//! them in a CSR-backed engine, runs a two-hop query, appends an edge and picks it up
//! with `refresh()`, runs a query under a deadline, and repeats the home page's in-memory
//! Arrow example. The documentation includes this
//! file section by section (the `--8<--` markers), so keep it compiling and its printed
//! with `refresh()`, runs a query under a deadline, and repeats the in-memory Arrow
//! examples of the home page and the getting started guide. The documentation includes
//! this file section by section (the `--8<--` markers), so keep it compiling and its printed
//! output in step.
//!
//! ```bash
Expand All @@ -20,7 +20,7 @@ use arrow_array::{ArrayRef, Int64Array, RecordBatch, RecordBatchIterator, String
use arrow_schema::{DataType, Field, Schema};
use lance::dataset::{Dataset, WriteMode, WriteParams};
use lance_csr::query::with_execution_deadline;
use lance_csr::{CypherEngine, EngineSource, GraphConfig};
use lance_csr::{CypherEngine, CypherQuery, EngineSource, GraphConfig};
use tokio::time::{timeout_at, Instant};

fn knows(src: Vec<i64>, dst: Vec<i64>) -> lance_csr::Result<RecordBatch> {
Expand Down Expand Up @@ -178,5 +178,130 @@ async fn main() -> lance_csr::Result<()> {
// | Carol |
// +--------+
// --8<-- [end:quickstart]

getting_started().await
}

/// The Rust tabs of the getting started guide, one section per Python snippet.
async fn getting_started() -> lance_csr::Result<()> {
// --8<-- [start:gs_datasets]
let int = |values: Vec<i64>| Arc::new(Int64Array::from(values)) as ArrayRef;
let text = |values: Vec<&str>| Arc::new(StringArray::from(values)) as ArrayRef;

let datasets = HashMap::from([
(
"Person".to_string(),
RecordBatch::try_from_iter([
("id", int(vec![1, 2, 3, 4])),
("name", text(vec!["Alice", "Bob", "Carol", "David"])),
("age", int(vec![28, 34, 29, 42])),
])?,
),
(
"City".to_string(),
RecordBatch::try_from_iter([
("id", int(vec![10, 20])),
("name", text(vec!["London", "Sydney"])),
])?,
),
(
"KNOWS".to_string(),
RecordBatch::try_from_iter([
("src", int(vec![1, 1, 2, 3])),
("dst", int(vec![2, 3, 4, 4])),
])?,
),
(
"LIVES_IN".to_string(),
RecordBatch::try_from_iter([
("src", int(vec![1, 2, 3, 4])),
("dst", int(vec![10, 10, 20, 20])),
])?,
),
]);
// --8<-- [end:gs_datasets]

// --8<-- [start:gs_config]
let config = GraphConfig::builder()
.node_table("Person", "id")
.node_table("City", "id")
.rel_table("KNOWS", "Person", "Person", "src", "dst")
.rel_table("LIVES_IN", "Person", "City", "src", "dst")
.build()?;
// --8<-- [end:gs_config]

// --8<-- [start:gs_query]
// The engine reads each dataset as an `EngineSource`; `false` leaves the CSR off.
let sources: HashMap<_, _> = datasets
.iter()
.map(|(name, batch)| (name.clone(), EngineSource::Arrow(batch.clone())))
.collect();
let engine = CypherEngine::new(config.clone(), sources.clone(), false).await?;

let result = engine
.execute(
"MATCH (p:Person)-[:LIVES_IN]->(c:City {name: 'London'})
RETURN p.name AS name, p.age AS age
ORDER BY age",
HashMap::new(),
)
.await?;
print_batches(&[result])?;
// +-------+-----+
// | name | age |
// +-------+-----+
// | Alice | 28 |
// | Bob | 34 |
// +-------+-----+
// --8<-- [end:gs_query]

// --8<-- [start:gs_params]
let older = engine
.execute(
"MATCH (p:Person) WHERE p.age >= $min_age RETURN p.name AS name ORDER BY name",
HashMap::from([("min_age".to_string(), serde_json::json!(30))]),
)
.await?;
print_batches(&[older])?;
// +-------+
// | name |
// +-------+
// | Bob |
// | David |
// +-------+
// --8<-- [end:gs_params]

// --8<-- [start:gs_csr]
let engine = CypherEngine::new(config.clone(), sources, true).await?;

let friends_of_friends = engine
.execute(
"MATCH (a:Person {name: 'Alice'})-[:KNOWS]->()-[:KNOWS]->(c:Person)
RETURN DISTINCT c.name AS friend_of_friend",
HashMap::new(),
)
.await?;
print_batches(&[friends_of_friends])?;
// +------------------+
// | friend_of_friend |
// +------------------+
// | David |
// +------------------+
// --8<-- [end:gs_csr]

// --8<-- [start:gs_one_off]
let result =
CypherQuery::new("MATCH (p:Person) WHERE p.age > 30 RETURN p.name AS name ORDER BY name")?
.with_config(config)
.execute(datasets, None)
.await?;
print_batches(&[result])?;
// +-------+
// | name |
// +-------+
// | Bob |
// | David |
// +-------+
// --8<-- [end:gs_one_off]
Ok(())
}
47 changes: 47 additions & 0 deletions docs/csr.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,53 @@ flowchart TD
The 25% threshold is fixed: lookups pay a little for every pending delta edge, and a rebuild clears them.
`python/benchmarks/csr_delta_accumulated.py` measures that trade-off.

### How to keep the CSR fresh

The engine doesn't watch your datasets for changes. Keeping the CSR in step with the data is up to you:

1. **Write with any Lance writer**: `pylance`, LanceDB or another process.
2. **Call `refresh()` after the writes your queries should see.** When a writer updates several tables, call it
after all of them commit (see [snapshots are per dataset](lance.md#lazy-version-pinned-reads)). A refresh
with nothing new reuses the existing CSR, so calling it on a timer or before each batch of queries is fine.
3. **Check what it did with [`csr_stats()`](#csr_stats)**: `update` says whether the CSR was `reused`, updated
with a `delta` or fully rebuilt (`rebuild`).

=== "Python"

```python
import lance

lance.write_dataset(datasets["Person"], "person.lance")
lance.write_dataset(datasets["KNOWS"], "knows.lance")
engine = CypherEngine(
config,
{"Person": lance.dataset("person.lance"), "KNOWS": lance.dataset("knows.lance")},
use_csr=True,
)

# David now knows Alice. The engine doesn't see the write until refresh().
lance.write_dataset(pa.table({"src": [4], "dst": [1]}), "knows.lance", mode="append")
engine.refresh()

stats = engine.csr_stats()["KNOWS"]
print(stats["edges"], stats["delta_edges"], stats["update"]) # 5 1 delta
```

=== "Rust"

This continues the on-disk example from [Using from Rust](rust.md), where Bob gains an edge to Alice.

```rust
--8<-- "crates/lance-csr/examples/embedded_query.rs:refresh"
```

A refresh can only apply a delta when it can compare the version the engine is pinned to with the latest one.
If you overwrite a relationship dataset, or remove the pinned version's files (for example with
`cleanup_old_versions()`) before refreshing, the next refresh rebuilds that CSR in full. Refresh more often
than you clean up old versions to keep updates incremental.

In-memory PyArrow tables never change. To query new in-memory data, build a new engine.

## Should I turn it on?

| Situation | Recommendation |
Expand Down
Loading
Loading