Getting started¶
This page takes you from an empty environment to a working graph query. You'll install lance-csr, describe a small graph, and query it with Cypher.
Install¶
lance-csr is published on PyPI as lance-csr. Use uv:
Prebuilt wheels cover Linux x86-64 (glibc 2.28+: RHEL 8, Ubuntu 20.04 and later) and Apple Silicon
(macOS 11+), for CPython 3.12, 3.13 and 3.14. Installing a wheel needs no Rust toolchain. Other platforms
build from the source distribution, which needs Rust and protoc (see Contributing).
The package depends on pyarrow>=14 and pylance 13.x, which uv installs for you.
Docs track the main branch
These pages describe the code on main. The node_table and rel_table builder methods arrived in
0.2.0; on 0.1.0 use with_node_label and with_relationship (see
Graph schema).
Check the install:
Your first graph¶
A graph in lance-csr is a set of ordinary tables plus a GraphConfig that says which tables are nodes,
which are relationships, and which columns link them. Here are people, the cities they live in, and who
knows whom:
import pyarrow as pa
datasets = {
"Person": pa.table({
"id": [1, 2, 3, 4],
"name": ["Alice", "Bob", "Carol", "David"],
"age": [28, 34, 29, 42],
}),
"City": pa.table({"id": [10, 20], "name": ["London", "Sydney"]}),
"KNOWS": pa.table({"src": [1, 1, 2, 3], "dst": [2, 3, 4, 4]}),
"LIVES_IN": pa.table({"src": [1, 2, 3, 4], "dst": [10, 10, 20, 20]}),
}
Each key of datasets is a node label or relationship type. Now describe how they connect:
from lance_csr import GraphConfig
config = (
GraphConfig.builder()
.node_table("Person", "id") # (1)!
.node_table("City", "id")
.rel_table("KNOWS", "Person", "Person", "src", "dst") # (2)!
.rel_table("LIVES_IN", "Person", "City", "src", "dst")
.build()
)
- A node table: the label
Person, read from the dataset namedPerson, identified by itsidcolumn. - A relationship table:
KNOWSedges go fromPersontoPerson; thesrccolumn holds the source node'sidanddstthe target's.
Run a query¶
CypherEngine registers the datasets once and runs as many queries as you like against them:
from lance_csr import CypherEngine
engine = CypherEngine(config, datasets)
result = engine.execute("""
MATCH (p:Person)-[:LIVES_IN]->(c:City {name: 'London'})
RETURN p.name AS name, p.age AS age
ORDER BY age
""")
print(result.to_pylist())
# [{'name': 'Alice', 'age': 28}, {'name': 'Bob', 'age': 34}]
Results come back as a pyarrow.Table,
so to_pylist(), to_pydict(), to_pandas() and Polars' pl.from_arrow() all work.
Pass values with $parameters instead of formatting them into the query string:
older = engine.execute(
"MATCH (p:Person) WHERE p.age >= $min_age RETURN p.name AS name ORDER BY name",
{"min_age": 30},
)
print(older.to_pydict()) # {'name': ['Bob', 'David']}
Go multi-hop with the CSR¶
Pass use_csr=True and the engine builds a CSR adjacency index for each relationship type. Multi-hop
patterns then become array lookups instead of joins:
engine = CypherEngine(config, datasets, use_csr=True)
friends_of_friends = engine.execute("""
MATCH (a:Person {name: 'Alice'})-[:KNOWS]->()-[:KNOWS]->(c:Person)
RETURN DISTINCT c.name AS friend_of_friend
""")
print(friends_of_friends.to_pylist()) # [{'friend_of_friend': 'David'}]
When should I turn the CSR on?
Turn it on when your queries traverse relationships, and especially for two or more hops. Node keys and edge endpoints must be integer columns to benefit; anything else quietly uses joins. Read The CSR index for the full picture.
One-off queries¶
For a single query you don't plan to repeat, CypherQuery skips the engine:
from lance_csr import CypherQuery
result = (
CypherQuery("MATCH (p:Person) WHERE p.age > 30 RETURN p.name AS name ORDER BY name")
.with_config(config)
.execute(datasets)
)
print(result.to_pydict()) # {'name': ['Bob', 'David']}
CypherQuery rebuilds its context on every execute() call, so prefer CypherEngine whenever you run more
than one query against the same data.
Next steps¶
- Graph schema: every option of
GraphConfig. - Querying with Cypher: parameters, streaming,
explain()and the supported Cypher. - Lance datasets: query data on disk without loading it, and pick up new writes with
refresh(). - The
examples/directory has runnable scripts.