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:

  1. Connect to Memgraph and load the Game of Thrones dataset

  2. Generate the YAML schema and the Python entity classes

  3. Initialize propagation properties

  4. Create a Document → Section → Page group with create_group()

  5. 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:

  • _propagate on edges with cascade delete enabled;

  • is_weak on the nodes at the child end of those edges;

  • parent_relation: the edge type linking parent → child;

  • _weak_init_done on 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()