Property-Graph Filtering and Indexes
Filter attributed graphs by node labels, relationship types, exact property values, ranges, and Cypher predicates. Use indexed candidate sets for selective reads, then build a reusable filtered sampler snapshot when every training hop must stay within that graph. For sampling policies, see High-Performance Graph Sampling; for cost and benchmark guidance, see Performance and Benchmarks.
Choose Where to Filter
Stage |
API |
Meaning |
|---|---|---|
Select records or seeds |
|
Restricts returned records or starting IDs, not subsequent traversal. |
Define the sampled graph |
|
Build-time callbacks exclude nodes/edges before arrays are persisted. |
Restrict a sampling step |
Engine |
Selects eligible array candidates before the fanout draw. No arbitrary property callback runs in the sampling hot path. |
Selecting high-score seed nodes alone does not prevent a walk from reaching low-score nodes. Filtering samples after drawing them can reduce effective fanout and changes the intended sampling process. To sample from an eligible property-defined graph, filter the snapshot before sampling.
Automatic and Explicit Indexes
Node(labels=[...])maintains native label indexes automatically.Edge(properties={"type": ...})maintains relationship-type indexes and directional typed adjacency automatically.create_node_property_index("score")andcreate_edge_property_index("confidence")explicitly build property indexes over existing records and maintain them on subsequent writes.A configured property also gets composite label + property or relationship type + property index entries. These are not arbitrary multi-property composite indexes, and need no separate composite declaration.
Secondary indexes configured for the mutable graph are distinct from temporal history indexes and the sampler’s immutable CSR arrays.
Exact and Range Lookup Reference
Scope |
Exact |
Range |
|---|---|---|
Nodes |
|
|
Labeled nodes |
|
|
Edges |
|
|
Typed edges |
|
|
Ranges default to inclusive bounds. Use None for an unbounded endpoint;
include_start=False and include_end=False select exclusive bounds.
Scalar range encodings are type-aware: do not mix unrelated value categories
when defining a range. Immutable GestaltDB temporal scalars support configured
exact/range property indexes; these are different from bitemporal version reads.
When you only need candidate IDs, use the corresponding
iter_node_ids_by_* / iter_edge_ids_by_* helpers instead of hydrating
Node / Edge objects. They yield byte IDs; decode with
graph.key_to_string before comparing with external strings. The count_*
variants count indexed IDs without hydrating entities; they are not guaranteed
constant-time metadata lookups. Lookup order is not guaranteed.
Cypher Filtering
MATCH supports labels, relationship types, inline property maps, and
parameterized WHERE predicates. Eligible exact/range predicates use configured
indexes to select candidates; residual predicates are then evaluated for
correctness. This is not a promise that every expression, conjunction, or graph
expansion uses an index or intersects every applicable index.
Use label/type + property indexes for selective entry points, then typed
adjacency for traversal. String/regex predicates and computed expressions can
require candidate scans. See Cypher Queries for supported syntax and planner
behavior. A successful WHERE query by itself is not evidence that it used a
property index.
End-to-End: Indexed Cohort to Filtered Sampling
Install gestaltdb[lmdb] for this complete example. Only approved drugs,
high-score proteins, and high-confidence binding relationships enter the
snapshot. Edges with excluded endpoints are omitted even if their own properties
pass the edge filter. Retained nodes can be isolated. Snapshot edges must have
the configured relation property (edge_type_property="type" by default);
untyped edges are not included.
from tempfile import TemporaryDirectory
from gestaltdb.graphdb import Edge, GraphDB, Node
from gestaltdb.sampling import SamplerEngine
with TemporaryDirectory() as tmpdir:
graph = GraphDB.create(f"{tmpdir}/graph", backend="lmdb", serializer="json")
try:
graph.create_node_property_index("approved")
graph.create_node_property_index("score")
graph.create_edge_property_index("confidence")
graph.put_nodes([
Node("drug-1", labels=["Drug"], properties={"approved": True}),
Node("drug-2", labels=["Drug"], properties={"approved": False}),
Node("protein-1", labels=["Protein"], properties={"score": 0.9}),
Node("protein-2", labels=["Protein"], properties={"score": 0.95}),
Node("protein-3", labels=["Protein"], properties={"score": 0.5}),
])
graph.put_edges_bulk([
Edge("e1", "drug-1", "protein-1", properties={"type": "binds", "confidence": 0.95}),
Edge("e2", "drug-1", "protein-2", properties={"type": "binds", "confidence": 0.4}),
Edge("e3", "drug-1", "protein-3", properties={"type": "binds", "confidence": 0.99}),
Edge("e4", "drug-2", "protein-2", properties={"type": "binds", "confidence": 0.99}),
])
# ID-only index scans avoid hydrating entities just to select a cohort.
drug_ids = {
graph.key_to_string(key)
for key in graph.iter_node_ids_by_label_property("Drug", "approved", True)
}
protein_ids = {
graph.key_to_string(key)
for key in graph.iter_node_ids_by_label_property_range("Protein", "score", 0.8, None)
}
edge_ids = {
graph.key_to_string(key)
for key in graph.iter_edge_ids_by_type_property_range("binds", "confidence", 0.9, None)
}
cohort = drug_ids | protein_ids
result = graph.query(
'MATCH (d:Drug {approved: true})-[r:binds]->(p:Protein) '
'WHERE p.score >= $score AND r.confidence >= $confidence '
'RETURN d.id AS drug, p.id AS protein',
{"score": 0.8, "confidence": 0.9},
)
assert result.records == [{"drug": "drug-1", "protein": "protein-1"}]
snapshot = graph.build_sampler_snapshot(
f"{tmpdir}/sampler",
node_filter=lambda node: node.get_id in cohort,
edge_filter=lambda edge: edge.get_id in edge_ids,
edge_weight_property="confidence",
read_snapshot=True,
)
finally:
graph.close()
engine = SamplerEngine.load(snapshot.path, mode="memmap", seed=7)
sampled = engine.sample_neighbors_external(
["drug-1"], fanout=10, relations=["binds"], strategy="weighted",
)
assert sampled.to_external(engine.snapshot)["edge_ids"].tolist() == ["e1"]
assert set(engine.snapshot.external_node_ids.tolist()) == cohort
Build-Time and Runtime Boundaries
node_filter and edge_filter receive decoded graph objects during snapshot
construction. The builder still scans source nodes and edges; passing indexed
ID sets to these callbacks does not turn the build into an index-only or
predicate-pushdown plan. Callback work and cohort sets consume time and memory.
If compact edge arrays are already available upstream,
SamplerSnapshot.from_edge_arrays avoids graph-object hydration altogether.
The example has no concurrent writers. If cohort IDs are selected before a
snapshot build, they are not automatically pinned to its later source horizon.
Quiesce writers for the full selection/build workflow, or express the property
conditions directly in callbacks within a pinned build. read_snapshot=True
requires a verifiable coherent mutable source snapshot (supported by LMDB);
unsupported backends fail that explicit request rather than silently weakening
it. The default uses this guarantee automatically where available.
Snapshots do not track later graph writes. Rebuild to change the eligible graph
or its persisted weights. At runtime, use compact relation/type filters and
temporal policies supported by High-Performance Graph Sampling, not property callbacks.
node_type_property defaults to the kind property, not native node labels;
sample_nodes(node_types=...) uses encoded type IDs from snapshot metadata.
The example describes mutable-graph snapshots. For temporal history, valid/system horizons, node availability, and leakage-aware negatives, use Temporal Version History and the temporal sections of High-Performance Graph Sampling; do not substitute current-property cohorts for historical property evaluation.
Index Maintenance and Limits
Index configuration persists across database reopens.
Deferred ingestion marks affected secondary indexes stale. Use
rebuild_deferred_indexes()before index-backed reads, or ingest withIndexMaintenanceMode.DEFER_REBUILDto rebuild before returning.Encoded secondary/range keys have a 511-byte limit across backends. Oversized indexed IDs/values fail instead of being silently truncated.
Indexes trade write work and storage for selective reads. Arbitrary multi-property predicates may still require residual filtering. Benchmark selectivity and graph shape as well as total node/edge counts.