Documentation

Data Modeling

Data Modeling in JetGraph

Core Concepts

JetGraph organizes data into two primitives:

Each node and edge has a type (also called a label) and an optional set of properties (key-value pairs).

Graph Diagram — Fraud Detection Domain

:CARD external_id TRANSACTS_AT :MERCHANT external_id, mcc USES_DEVICE :DEVICE fingerprint USES_IP :IP_ADDRESS address, country OWNED_BY :ACCOUNT external_id, risk

Schema Registration

Before any data can be written, you must declare your node types and edge types. This is done once at startup via the CALL db.* system procedures, then finalized with db.finalizeSchema().

bash — full schema setup
# Register node types (second arg is the ID type: "string" or "integer") curl -sS -X POST http://localhost:8080/cypher \ -H 'Content-Type: application/json' \ -d '{"query":"CALL db.registerNodeType(\"CARD\", \"string\") YIELD node_type_id RETURN node_type_id","parameters":{}}' curl -sS -X POST http://localhost:8080/cypher \ -H 'Content-Type: application/json' \ -d '{"query":"CALL db.registerNodeType(\"MERCHANT\", \"string\") YIELD node_type_id RETURN node_type_id","parameters":{}}' curl -sS -X POST http://localhost:8080/cypher \ -H 'Content-Type: application/json' \ -d '{"query":"CALL db.registerNodeType(\"DEVICE\", \"string\") YIELD node_type_id RETURN node_type_id","parameters":{}}' # Register an edge type curl -sS -X POST http://localhost:8080/cypher \ -H 'Content-Type: application/json' \ -d '{"query":"CALL db.registerEdgeType({name:\"TRANSACTS_AT\",from_node_type:\"CARD\",to_node_type:\"MERCHANT\"}) YIELD edge_type_id RETURN edge_type_id","parameters":{}}' curl -sS -X POST http://localhost:8080/cypher \ -H 'Content-Type: application/json' \ -d '{"query":"CALL db.registerEdgeType({name:\"USES_DEVICE\",from_node_type:\"CARD\",to_node_type:\"DEVICE\"}) YIELD edge_type_id RETURN edge_type_id","parameters":{}}' # Finalize — must be called after all types are registered curl -sS -X POST http://localhost:8080/cypher \ -H 'Content-Type: application/json' \ -d '{"query":"CALL db.finalizeSchema() YIELD schema_version RETURN schema_version","parameters":{}}'
⚠️
Schema is immutable after finalization. Plan your node types and edge types carefully. In development you can wipe everything with CALL db.resetGraph() when ENABLE_ADMIN_RESET=true is set, or restart the container with a fresh volume.

Edge Types, Histograms & Activity Windows

Edge types define both the graph relationship (CARD → MERCHANT) and the feature storage kept for every edge pair. The storage layout is chosen once, when the edge type is registered. For ML/GNN use cases, the two most important read paths are: graph.histogram for node-level bucketed counts and graph.edgeState for edge-pair state.

ConceptScopeWhat it storesTypical use
Compact edge payload One (src, dst, edge_type) pair tx_count, approx_sum, last_seen, activity bitmap, optional 8-bin amount histogram, optional bool flag Edge features such as count, amount sum, recency, velocity.
Activity bitmap One edge pair 21 recent time ticks, 3 bits each. Each tick count saturates at 7. Fast edge-level velocity windows: last 5 min, 10 min, 1 hour, etc.
Node histogram One (node, edge_type) side Two ring buffers: hourly slots and daily slots. Each slot has 8 amount/value buckets. Node-level behaviour: amount distribution for a card over last 1h, 24h, 7d.

Registering a transaction edge with full numeric features

To get the full compact payload (numeric bins + approximate sum), register the edge type with bin_boundaries. The seven boundaries define eight buckets. tracked_property names the numeric value from ingest that is binned and summed, usually "amount" for payments.

cypher — PAYMENT edge with amount bins, ticks, and node histograms
CALL db.registerEdgeType({ name: "PAYMENT", from_node_type: "CARD", to_node_type: "MERCHANT", state_ttl_secs: 7776000, // Full CompactEdgePayload: 7 thresholds → 8 amount buckets bin_boundaries: [5, 25, 50, 100, 250, 500, 1000], tracked_property: "amount", // Edge-level velocity bitmap: 21 ticks × 5 minutes = 105 minutes max lookback activity_bitmap: { tick_size_secs: 300 }, // Node-level rolling histograms. Each slot stores 8 bucket counts. node_histogram: { enabled_for_src: true, enabled_for_dst: false, hourly_slots: 24, daily_slots: 7 } }) YIELD edge_type_id RETURN edge_type_id
Registration fieldMeaning
bin_boundariesSeven numeric thresholds. They create eight buckets: <5, 5–25, 25–50, …, ≥1000.
tracked_propertyThe numeric input field that feeds approx_sum and the bucket counters. For payments this is usually amount.
activity_bitmap.tick_size_secsThe duration of one edge-level activity tick. With 300, [1, 2, 12] means last 5 min, 10 min, and 1 hour.
node_histogram.hourly_slotsHow many hourly histogram slots to keep. 24 keeps 24 hours of hourly detail.
node_histogram.daily_slotsHow many daily histogram slots to keep. 7 keeps 7 days of daily detail.

Reading node histograms

graph.histogram returns aggregated bucket counts for one node and one edge type. It is node-level: for CARD → PAYMENT, it counts all PAYMENT edges from that card, not a single merchant edge.

cypher — last 24 hours and last 7 days
MATCH (c:card {external_id: "card-velocity-09"}) // Use the hourly ring and sum the most recent 24 hourly slots. CALL graph.histogram(c, "PAYMENT", 24) YIELD buckets, counts AS hourly // Use the daily ring and sum the most recent 7 daily slots. CALL graph.histogram(c, "PAYMENT", null, 7) YIELD counts AS days RETURN c.node_id, buckets, hourly, days

Example output counts = [0, 2, 12, 2, 0, 0, 0, 0] means: 0 events below 5, 2 events in 5–25, 12 events in 25–50, 2 events in 50–100, and none in the higher buckets.

Reading edge state

graph.edgeState reads one edge pair. It is the edge-level complement to graph.histogram. Use it for per-edge features such as tx_count, approx_sum, last_seen, boolean flags, and activity windows.

cypher — specific card → merchant edge state
MATCH (c:card {external_id: "card-velocity-09"}) CALL graph.edgeState( c, "merchant:merchant-uk-10", "PAYMENT", [1, 2, 12] ) YIELD tx_count, approx_sum, last_seen, bool_flag, activity_counts RETURN c.node_id, tx_count, approx_sum, last_seen, bool_flag, activity_counts

If PAYMENT.activity_bitmap.tick_size_secs = 300, then [1, 2, 12] asks for counts over the last 5 minutes, 10 minutes, and 1 hour. The bitmap holds at most 21 ticks, so a 5-minute tick gives about 105 minutes of edge-level activity history. Longer windows should come from node histograms.

💡
For embeddings: combine graph.histogram for node features (count distributions over 1h/24h/7d) with graph.edgeState for edge features (pair count, amount sum, recency, and short velocity windows).

Data Modeling Best Practices