Propagation Properties — Full Memgraph Demo
The workflow of the Neo4j demo (propagation_demo.ipynb) on Memgraph. MemgraphGraph has the same API and propagation behaviour as Neo4jGraph and speaks the same Cypher, so the code only changes where the graph is created.
Steps:
Connect to Memgraph and load the Game of Thrones dataset
Generate the YAML schema and the Python entity classes
Initialize propagation properties
Create a Document → Section → Page group with
create_group()Run Cypher queries, get subdocuments, and explore the graph in the viewer
Prerequisites
A running Memgraph server, for example in Docker (Memgraph Community is distributed under the Business Source License, see the README):
docker run -p 7687:7687 memgraph/memgraph
The notebook reads MEMGRAPH_URL (default bolt://localhost:7687), MEMGRAPH_USER and MEMGRAPH_PASSWORD (default: no authentication).
Warning: Step 1.5 deletes everything in the database.
[ ]:
import importlib.util
# cvcdocdb with the [notebook] extra (anywidget) for the graph viewer at the end.
if (importlib.util.find_spec("cvcdocdb") is None
or importlib.util.find_spec("cvcdocdb.viewer") is None
or importlib.util.find_spec("anywidget") is None):
print("⚠️ Installing cvcdocdb[notebook]...")
%pip install -q --upgrade "cvcdocdb[notebook]"
print("✅ Installation complete. Restart the kernel if an import below fails.")
else:
print("✅ cvcdocdb and anywidget are already installed.")
[ ]:
import json
import os
import tempfile
from pathlib import Path
from cvcdocdb import MemgraphGraph
from cvcdocdb.exemples import load_got_characters
from cvcdocdb.schema_gen import generate_classes
def section(title):
"""Print a section header."""
print(f"\n{'=' * 60}")
print(f" {title}")
print(f"{'=' * 60}")
def sub_section(title):
"""Print a subsection header."""
print(f"\n--- {title} ---")
# Generated files (schema and entity classes) go to a fresh temporary directory.
output_dir = Path(tempfile.mkdtemp(prefix="cvcdocdb_got_"))
Step 1: Connect to Memgraph
[ ]:
url = os.environ.get("MEMGRAPH_URL", "bolt://localhost:7687")
graph = MemgraphGraph(
url,
os.environ.get("MEMGRAPH_USER", ""),
os.environ.get("MEMGRAPH_PASSWORD", ""),
)
print(f"Connected to {url} ({graph.server_edition} edition)")
total = graph.query("MATCH (n) RETURN count(n) AS total")[0]["total"]
print(f"Nodes in database: {total}")
Step 1.5: Clear existing data
Delete all nodes and relationships before loading fresh data.
[ ]:
graph.query("MATCH (n) DETACH DELETE n")
total = graph.query("MATCH (n) RETURN count(n) AS total")[0]["total"]
print(f"✅ Database cleared. Nodes remaining: {total}")
Step 2: Load the Game of Thrones dataset
Load character and house data from thronesapi.com. Falls back to bundled data if the API is unavailable.
[ ]:
stats = load_got_characters(graph, limit=25)
print("Loaded:", json.dumps(stats, indent=2))
sub_section("Nodes by label")
for row in graph.query("MATCH (n) RETURN labels(n)[0] AS label, count(n) AS cnt ORDER BY cnt DESC"):
print(f" {row['label']}: {row['cnt']}")
sub_section("Relations by type")
for row in graph.query("MATCH ()-[r]->() RETURN type(r) AS rel_type, count(r) AS cnt ORDER BY cnt DESC"):
print(f" {row['rel_type']}: {row['cnt']}")
Step 3: Generate the YAML schema
Introspect the graph and generate a YAML schema description: labels, properties, relationships and counts.
[ ]:
yaml_content = graph.schema_yaml(db_name="got")
yaml_path = output_dir / "got_schema.yaml"
yaml_path.write_text(yaml_content)
print(f"Written: {yaml_path} ({len(yaml_content)} bytes)")
print("\nSchema preview:")
for line in yaml_content.split("\n")[:20]:
print(f" {line}")
print(" ...")
Step 4: Generate Python entity classes
Generate Node, WeakNode, Relation and WeakRelation subclasses for each label and relationship type in the schema.
[ ]:
code = generate_classes(yaml_path.read_text())
py_path = output_dir / "got_entities.py"
py_path.write_text(code)
print(f"Written: {py_path} ({len(code)} bytes)")
print("\nGenerated code preview:")
for line in code.split("\n")[:25]:
print(f" {line}")
print(" ...")
Step 5: Initialize propagation properties
init_propagation() scans the graph and initialises the propagation properties:
_propagateon edges with cascade delete enabled;is_weakon the nodes at the child end of those edges;parent_relation: the edge type linking parent → child;_weak_init_doneon the nodes already initialised.
The Game of Thrones data has no WeakNodes, so the counts only change after the group of Step 6.
[ ]:
COUNTS = ("MATCH (n) RETURN count(n) AS total, count(n.is_weak) AS weak_count, "
"count(n._weak_init_done) AS done_count")
def show_counts(title):
sub_section(title)
row = graph.query(COUNTS)[0]
print(f" Total nodes: {row['total']}")
print(f" Nodes with is_weak: {row['weak_count']}")
print(f" Nodes with _weak_init_done: {row['done_count']}")
edges = graph.query("MATCH ()-[r]->() WHERE r._propagate = TRUE RETURN count(r) AS c")[0]["c"]
print(f" Edges with _propagate: {edges}")
show_counts("Before init_propagation()")
print("\nRunning init_propagation()...", graph.init_propagation())
show_counts("After init_propagation()")
Step 6: Transactional group creation
create_group() creates a strong node with its WeakNodes and WeakRelations in a single transaction: if any step fails, nothing is written. We create a Document → Section → Page hierarchy.
[ ]:
from cvcdocdb.base import Node, WeakNode
doc = Node(
pk={"doc_id": "GOT-DEMO-001"},
main_label="Document",
title="Game of Thrones — Episode Guide",
author="Demo Script",
)
section1 = WeakNode(parent=doc, parent_relation="HAS_SECTION", pk={"section": "intro"},
main_label="Section", title="Introduction", order=1)
section2 = WeakNode(parent=doc, parent_relation="HAS_SECTION", pk={"section": "characters"},
main_label="Section", title="Main Characters", order=2)
page1 = WeakNode(parent=section1, parent_relation="HAS_PAGE", pk={"page": 1}, main_label="Page",
content="Welcome to the Game of Thrones episode guide.", order=1)
page2 = WeakNode(parent=section2, parent_relation="HAS_PAGE", pk={"page": 1}, main_label="Page",
content="Jon Snow, Daenerys Targaryen, Tyrion Lannister...", order=1)
doc_id = graph.create_group(strong_node=doc, weak_nodes=[section1, section2, page1, page2])
print(f"✅ Group created (strong node id: {doc_id}): Document, 2 Sections, 2 Pages")
Step 7: Verify the group
[ ]:
sub_section("Section and Page nodes")
rows = graph.query(
"MATCH (d:Document {doc_id: 'GOT-DEMO-001'})-[*1..2]->(n) "
"RETURN labels(n)[0] AS label, coalesce(n.title, n.content) AS text, "
"n.is_weak AS is_weak ORDER BY label, text"
)
for row in rows:
print(f" {row['label']}: {row['text'][:45]!r} is_weak={row['is_weak']}")
Step 8: Sample queries
[ ]:
sub_section("Characters by House")
rows = graph.query(
"MATCH (c:Character)-[:MEMBER_OF]->(h:House) "
"RETURN h.name AS house, collect(c.name) AS characters ORDER BY house"
)
for row in rows:
names = row["characters"]
print(f" {row['house']}: {', '.join(names[:5])}" + (f" ... and {len(names) - 5} more" if len(names) > 5 else ""))
sub_section("Edges with _propagate")
rows = graph.query(
"MATCH (a)-[r]->(b) WHERE r._propagate = TRUE "
"RETURN labels(a)[0] AS from_label, type(r) AS rel_type, labels(b)[0] AS to_label, count(*) AS count "
"ORDER BY count DESC"
)
for row in rows:
print(f" {row['from_label']} -[{row['rel_type']}]-> {row['to_label']}: {row['count']}")
Step 9: Get subdocuments
get_subdocuments() returns every node reachable from a strong node through _propagate edges: the document’s sections and pages.
[ ]:
strong_doc = Node(pk={"doc_id": "GOT-DEMO-001"}, main_label="Document")
subdocuments = graph.get_subdocuments(strong_doc)
print(f"Found {len(subdocuments)} subdocuments:")
for sd in subdocuments:
print(f" - {sd['label']}: {sd['pk']}") # a WeakNode's key includes its parents' keys
Step 10: Explore the graph
The graph viewer works on every backend. Click a node to see its properties, double-click it to expand its neighbours (try the Document), and search or filter from the sidebar.
[ ]:
from cvcdocdb.viewer import show
show(graph, load_all=True)
Cleanup
Closing the graph disconnects the viewer: run this once you are done exploring. The data stays in Memgraph.
[ ]:
# graph.close()