Typed Traversal and Sampling

Typed traversal uses edge.properties["type"]. When an edge has a type, GestaltDB stores typed adjacency records for efficient directional scans.

Create a Typed Graph

from gestaltdb.graphdb import Edge, GraphDB, Node
from gestaltdb.kvstores import LMDBStore
from gestaltdb.serializers import PickleSerializer

graph_db = GraphDB(LMDBStore(path="typed_graph_lmdb"), PickleSerializer())

for node_id, kind in [
    ("drug-1", "drug"),
    ("protein-1", "protein"),
    ("protein-2", "protein"),
    ("disease-1", "disease"),
]:
    graph_db.put_node(Node(node_id=node_id, properties={"kind": kind}))

graph_db.put_edges_bulk([
    Edge(edge_id="d1-p1", source="drug-1", target="protein-1", properties={"type": "drug-to-protein"}),
    Edge(edge_id="d1-p2", source="drug-1", target="protein-2", properties={"type": "drug-to-protein"}),
    Edge(edge_id="p1-dis1", source="protein-1", target="disease-1", properties={"type": "protein-to-disease"}),
])

Query Typed Neighbors

proteins = graph_db.neighbors_by_edge_type(
    "drug-1",
    "drug-to-protein",
    direction="out",
)
print(proteins)

Query Typed Edges

edge_ids = graph_db.edges_by_edge_type(
    "drug-1",
    "drug-to-protein",
    direction="out",
)

Sample Neighbors

sample_neighbors uses reservoir sampling, so memory is bounded by sample_size instead of by node degree.

import random

sample = graph_db.sample_neighbors(
    "drug-1",
    "drug-to-protein",
    direction="out",
    sample_size=1,
    rng=random.Random(7),
)

Object-Based Sampling Patterns

Use SamplingHop and SamplingPattern for validated, documented sampling configuration objects.

import random

from gestaltdb.sampling import SamplingHop, SamplingPattern

pattern = SamplingPattern([
    SamplingHop("drug-to-protein", direction="out", sample_size=2),
    SamplingHop("protein-to-disease", direction="out", sample_size=1),
])

paths = graph_db.sample_typed_paths(
    seed_ids=["drug-1"],
    pattern=pattern,
    rng=random.Random(3),
)

Dictionary-Based Sampling Patterns

Existing dictionary configurations are still supported.

pattern = [
    {"edge_type": "drug-to-protein", "direction": "out", "sample_size": 2},
    {"edge_type": "protein-to-disease", "direction": "out", "sample_size": 1},
]

paths = graph_db.sample_typed_paths(["drug-1"], pattern)

Cypher Sampling Procedure

The Cypher API exposes multi-hop typed path sampling through a GestaltDB-specific procedure call. This delegates to GraphDB.sample_typed_paths and returns one path value per sampled path.

result = graph_db.query(
    'CALL pg.sample_typed_paths(["drug-1"], '
    '[{"edge_type": "drug-to-protein", "direction": "out", "sample_size": 2}, '
    '{"edge_type": "protein-to-disease", "direction": "out", "sample_size": 1}]) '
    'YIELD path RETURN path'
)

for record in result:
    print(record["path"])

Sample a Materialized Subgraph

subgraph = graph_db.sample_typed_subgraph(
    seed_ids=["drug-1"],
    pattern=pattern,
)

print(subgraph["nodes"].keys())
print(subgraph["edges"].keys())
print(subgraph["paths"])

Rebuild Typed Adjacency

If edge records already exist but typed adjacency indexes are missing, rebuild them from stored edges.

rebuilt = graph_db.rebuild_typed_adjacency()
print(f"rebuilt {rebuilt} typed adjacency records")

Temporal Sampler Snapshots

Format-v2 snapshots can preserve the edge history visible at one system-time horizon. Each compact edge row has an edge-version ID, half-open valid interval, open-end mask, commit ID, and system time. The temporal outgoing and incoming CSR indexes are ordered by endpoint, relation, and valid-time start.

snapshot = graph_db.build_sampler_snapshot(
    "snapshots/history",
    temporal=True,
    system_time="2026-01-01T00:00:00Z",
    time_bucket="day",
)

assert snapshot.temporal
print(snapshot.edge_version_ids)
print(snapshot.valid_from_us, snapshot.valid_to_us, snapshot.valid_to_open)

The build captures exactly one authenticated GraphReadView. Corrections and retractions are resolved at its system horizon, including splitting a surviving version into multiple edge rows when only part of its validity is retracted. temporal_candidates(node, relation, valid_time, direction=...) uses the start-time ordering to return a candidate superset. SamplerEngine then applies exact half-open validity filtering before fanout and random selection:

from gestaltdb.sampling import SamplerEngine
from gestaltdb.temporal import TemporalContext

engine = SamplerEngine.load(snapshot.path, mode="memmap", seed=7)
sampled = engine.sample_neighbors(
    [alice_id],
    fanout=20,
    direction="out",
    relations=[works_for_id],
    temporal=TemporalContext.as_of(cutoff),
)

Point selection honors exclusive valid-to boundaries. For finite windows, window_policy="overlap" selects edge intervals with a non-empty intersection, while window_policy="contained" requires the complete edge interval to lie inside the window (open-ended edges therefore cannot be contained). sample_multihop and sample_subgraph accept the same options. Set causal_policy to "nondecreasing" or "nonincreasing" to compare edge valid-start instants hop by hop along each sampled path. Temporal, relation, direction, and causal filters all run before fanout selection.

Temporal SampledNeighbors and SampledSubgraphBatch values expose edge_version_ids, valid_from_us, valid_to_us, and valid_to_open aligned with their sampled edge arrays. Supplying a temporal filter or causal policy to a non-temporal/legacy snapshot raises ValueError. PyG dictionaries and DGL graphs retain string edge_version_ids as Python metadata because their edge-feature stores only accept numeric tensors.

Time-Aware Hard Negatives

Temporal snapshots also contain authenticated positive-triple history and node availability indexes. is_positive can test all known history, one example instant, or a query window. Hard-negative sampling applies endpoint type and relation constraints first, temporal candidate availability second, and positive-history rejection last:

from gestaltdb.sampling import HardNegativeConfig

config = HardNegativeConfig(
    negatives_per_positive=8,
    temporal_positive_policy="at_positive_time",
    temporal_candidate_window_days=90,
    exhaustion_policy="raise",
)
batch = engine.sample_subgraph(
    seed_edges,
    fanouts=[15, 10],
    negative_config=config,
    temporal=TemporalContext.as_of(cutoff),
)

temporal_positive_policy is "any_time" (the compatibility default), "at_positive_time", or "window". Window rejection uses temporal_positive_window_policy="overlap" or "contained". A candidate window is trailing: candidates and relation neighborhoods must have been available during the configured number of days ending at the example time. Without a candidate window they must be available at the example time. Future candidates are excluded unless allow_future_candidates=True.

sample_hard_negatives accepts aligned positive_times_us and optionally returns deterministic rejection counters with return_diagnostics=True. Direct calls on temporal snapshots must supply positive_times_us or a temporal context unless allow_future_candidates=True; this prevents an ambiguous default call from drawing nodes that only become available later. SampledSubgraphBatch carries positive_time_us, negative_time_us, and negative_diagnostics. exhaustion_policy="raise" fails when unique negatives are exhausted; "repeat" deterministically reuses an accepted negative while preserving temporal-positive rejection. RAM and memmap engines produce the same seeded result.

V2 publication is atomic: arrays and canonical metadata are written in a sibling staging directory, checksummed, and followed by completion.json before the directory is renamed into place. Loading in RAM or memmap mode validates the completion record, artifact catalog, SHA-256 checksums, dtypes, shapes, aligned lengths, CSR bounds, interval invariants, and source-provenance token. Existing format-v1 snapshots still load, but have no integrity or temporal guarantees. Rebuild v1 snapshots from their source database to migrate to v2; snapshots are immutable and have no in-place upgrade path. Unknown format versions and corrupt v2 artifacts fail closed.