Querying (Fluent API)¶
For most queries, prefer Cypher. The fluent API is for building reusable query chains or when you need
explain()and selection-based workflows.
Filtering¶
graph.select('Product').where({'price': 999.99})
graph.select('Product').where({'price': {'<': 500.0}, 'stock': {'>': 50}})
graph.select('Product').where({'id': {'in': [101, 103]}})
graph.select('Product').where({'category': {'is_null': True}})
# Regex matching
graph.select('Person').where({'name': {'regex': '^A.*'}}) # searches the value
graph.select('Person').where({'name': {'=~': 'A.*'}}) # matches the whole value
graph.select('Person').where({'name': {'regex': '(?i)^alice'}}) # case-insensitive
# Negated conditions
graph.select('Person').where({'city': {'not_in': ['Oslo', 'Bergen']}})
graph.select('Person').where({'name': {'not_contains': 'test'}})
graph.select('Person').where({'name': {'not_regex': '^[A-C].*'}})
# OR logic — where_any keeps nodes matching ANY condition set
graph.select('Person').where_any([
{'city': 'Oslo'},
{'city': 'Bergen'},
])
# Connection existence — filter without changing the selection target
graph.select('Person').where_connected('KNOWS') # any direction
graph.select('Person').where_connected('KNOWS', direction='outgoing') # outgoing only
# Orphan nodes (no connections)
graph.where_orphans(include_orphans=True)
Sorting and Pagination¶
graph.select('Product').sort('price')
graph.select('Product').sort('price', ascending=False)
graph.select('Product').sort([('stock', False), ('price', True)])
# Pagination with offset + limit
graph.select('Person').sort('name').offset(20).limit(10) # page 3 of 10
Traversing the Graph¶
alice = graph.select('User').where({'title': 'Alice'})
alice_products = alice.traverse('PURCHASED', direction='outgoing')
# Filter and sort traversal targets
expensive = alice.traverse(
'PURCHASED',
where={'price': {'>=': 500.0}},
sort_target='price',
limit=10
)
# Get connection information
alice.connections(include_node_properties=True)
Comparison Operations¶
For spatial, semantic, or clustering operations — where nodes are related
by proximity rather than explicit edges — use compare():
# Spatial: find wells inside structure polygons
graph.select('Structure').compare('Well', 'contains')
# Distance: wells within 5km of each platform
graph.select('Platform').compare('Well', {'type': 'distance', 'max_m': 5000})
# Semantic: similar documents by embedding
graph.select('Doc').compare('Doc',
{'type': 'text_score', 'property': 'summary', 'threshold': 0.7})
See the Traversal Hierarchy guide for details on multi-level chains, property enrichment, and grouped collection.
Grouped Results¶
After traversal, collect_grouped() groups leaf nodes by a parent type:
grouped = graph.select('Field').traverse('HAS_WELL') \
.collect_grouped('Field')
# → {'TROLL': [{...}, ...], 'EKOFISK': [{...}, ...]}
Enriching with add_properties()¶
Copy or aggregate properties from ancestor levels onto leaf nodes:
from kglite import Agg, Spatial
graph.select('Structure').compare('Well', 'contains') \
.add_properties({
'Structure': {'struct_name': 'name'}, # copy + rename
'Well': {'n_wells': Agg.count()}, # aggregate
})
See the Traversal Hierarchy guide for the full enrichment API.
Set Operations¶
n3 = graph.select('Prospect').where({'geoprovince': 'N3'})
m3 = graph.select('Prospect').where({'geoprovince': 'M3'})
n3.union(m3) # all nodes from both (OR)
n3.intersection(m3) # nodes in both (AND)
n3.difference(m3) # nodes in n3 but not m3
n3.symmetric_difference(m3) # nodes in exactly one (XOR)
Both operands must be selections over the same immutable graph view. Sibling selections derived from one graph are compatible. Copies, unrelated graphs, and selections retained across a copy-on-write mutation or vacuum are separate views because their physical node slots are not interchangeable.
Retrieving Results¶
people = graph.select('Person')
# Lightweight (no property materialization)
people.len() # → 3
people.indices() # → [0, 1, 2]
people.ids() # → [1, 2, 3]
# Medium (partial materialization)
people.titles() # → ['Alice', 'Bob', 'Charlie']
people.get_properties(['age', 'city']) # → [(28, 'Oslo'), (35, 'Bergen'), (42, 'Oslo')]
# Full materialization
people.collect() # → [{'type': 'Person', 'title': 'Alice', 'id': 1, 'age': 28, ...}, ...]
people.to_df() # → DataFrame with columns type, title, id, age, city, ...
# Single node lookup through the identity index
graph.node('Person', 1) # → {'type': 'Person', 'title': 'Alice', ...} or None
Schema Introspection¶
Methods for exploring graph structure — what types exist, what properties they have, and how they connect.
schema() — Full graph overview¶
s = graph.schema()
# {
# 'node_types': {
# 'Person': {'count': 500, 'properties': {'age': 'Int64', 'city': 'String'}},
# 'Company': {'count': 50, 'properties': {'founded': 'Int64'}},
# },
# 'connection_types': {
# 'KNOWS': {'count': 1200, 'source_types': ['Person'], 'target_types': ['Person']},
# 'WORKS_AT': {'count': 500, 'source_types': ['Person'], 'target_types': ['Company']},
# },
# 'indexes': ['Person.city', 'Person.(age, city)'],
# 'node_count': 550,
# 'edge_count': 1700,
# }
properties(node_type) — Property details¶
graph.properties('Person')
# {
# 'type': {'type': 'str', 'non_null': 500, 'unique': 1, 'values': ['Person']},
# 'title': {'type': 'str', 'non_null': 500, 'unique': 500},
# 'id': {'type': 'int', 'non_null': 500, 'unique': 500},
# 'city': {'type': 'str', 'non_null': 500, 'unique': 3, 'values': ['Bergen', 'Oslo', 'Stavanger']},
# 'age': {'type': 'int', 'non_null': 500, 'unique': 45},
# }
neighbors_schema(node_type) — Connection topology¶
graph.neighbors_schema('Person')
# {
# 'outgoing': [
# {'connection_type': 'KNOWS', 'target_type': 'Person', 'count': 1200},
# {'connection_type': 'WORKS_AT', 'target_type': 'Company', 'count': 500},
# ],
# 'incoming': [
# {'connection_type': 'KNOWS', 'source_type': 'Person', 'count': 1200},
# ],
# }
sample(node_type, n=5) — Quick data peek¶
result = graph.sample('Person', n=3)
result[0] # {'type': 'Person', 'title': 'Alice', 'id': 1, 'age': 28, 'city': 'Oslo'}
result.to_list() # all rows as list[dict]
result.to_df() # as DataFrame
describe() — AI agent context¶
Progressive-disclosure schema description designed for AI agents. See AI Agents for details.
Debugging Selections¶
result = graph.select('User').where({'id': 1001})
print(result.explain())
# SELECT User (1000 nodes) -> WHERE (1 nodes)
Pattern Matching¶
For simpler pattern-based queries without full Cypher clause support:
results = graph.match_pattern(
'(p:Play)-[:HAS_PROSPECT]->(pr:Prospect)-[:BECAME_DISCOVERY]->(d:Discovery)'
)
for match in results:
print(f"Play: {match['p']['title']}, Discovery: {match['d']['title']}")
# With property conditions
graph.match_pattern('(u:User)-[:PURCHASED]->(p:Product {category: "Electronics"})')
# Limit results for large graphs
graph.match_pattern('(a:Person)-[:KNOWS]->(b:Person)', max_matches=100)