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.*'}}) # or {'=~': '^A.*'}
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)
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 (O(1))
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.(city, age)'],
# '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)