Property graphs let retrieval applications represent named entities and the relationships between them instead of relying only on text similarity. A LlamaIndex PropertyGraphIndex keeps those graph paths connected to their source text, which makes service dependencies and other linked facts available for graph-aware retrieval.

The default extraction path can ask an LLM to infer triples from unstructured text. This local build uses explicit EntityNode and Relation objects with ImplicitPathExtractor, so every expected relationship is known before indexing and can be checked without an external model or API key.

The completed script stores three entities and two directed relationships, persists the index, then reloads and checks both paths from the recovered graph store. MockLLM and MockEmbedding keep the smoke test offline; production applications can retain the same graph construction flow while selecting their own models and property graph store.

Steps to build a LlamaIndex property graph index:

  1. Create property_graph_index.py with the imports and offline model settings.
    $ cat > property_graph_index.py <<'PY'
    from pathlib import Path
     
    from llama_index.core import (
        PropertyGraphIndex,
        Settings,
        StorageContext,
        load_index_from_storage,
    )
    from llama_index.core.embeddings import MockEmbedding
    from llama_index.core.graph_stores import EntityNode, Relation
    from llama_index.core.graph_stores.types import KG_NODES_KEY, KG_RELATIONS_KEY
    from llama_index.core.indices.property_graph import ImplicitPathExtractor
    from llama_index.core.llms import MockLLM
    from llama_index.core.schema import TextNode
     
     
    storage_dir = Path("property_graph_storage")
     
    Settings.llm = MockLLM(max_tokens=32)
    Settings.embed_model = MockEmbedding(embed_dim=8)
    PY
  2. Define the three graph entities after the model settings.
    $ cat >> property_graph_index.py <<'PY'
     
    service = EntityNode(name="CheckoutService", label="service")
    database = EntityNode(name="PostgreSQL", label="database")
    cache = EntityNode(name="Redis", label="cache")
    PY
  3. Attach the known entities and relationships to one source text node.
    $ cat >> property_graph_index.py <<'PY'
     
    source_node = TextNode(
        text="CheckoutService writes orders to PostgreSQL and reads carts from Redis.",
        id_="checkout-service-note",
        metadata={
            KG_NODES_KEY: [service, database, cache],
            KG_RELATIONS_KEY: [
                Relation(label="WRITES_TO", source_id=service.id, target_id=database.id),
                Relation(label="READS_FROM", source_id=service.id, target_id=cache.id),
            ],
        },
    )
    PY

    ImplicitPathExtractor reads graph metadata already attached to the source node. SimpleLLMPathExtractor or a schema-based extractor is appropriate when relationships must be inferred from unstructured text.

  4. Append property graph construction to the script.
    $ cat >> property_graph_index.py <<'PY'
     
    index = PropertyGraphIndex(
        nodes=[source_node],
        kg_extractors=[ImplicitPathExtractor()],
        embed_model=Settings.embed_model,
        llm=Settings.llm,
        show_progress=False,
    )
    PY
  5. Append the storage round-trip section to the script.
    $ cat >> property_graph_index.py <<'PY'
     
    index.storage_context.persist(persist_dir=str(storage_dir))
    loaded_index = load_index_from_storage(
        StorageContext.from_defaults(persist_dir=str(storage_dir))
    )
    PY
  6. Append reloaded graph assertions and output to the script.
    $ cat >> property_graph_index.py <<'PY'
     
    loaded_store = loaded_index.property_graph_store
    loaded_nodes = loaded_store.get()
    loaded_entities = sorted(
        graph_node.name
        for graph_node in loaded_nodes
        if graph_node.label != "text_chunk"
    )
    loaded_service = next(
        graph_node
        for graph_node in loaded_nodes
        if isinstance(graph_node, EntityNode)
        and graph_node.name == "CheckoutService"
    )
    loaded_paths = sorted(
        f"{source.name} -[{relation.label}]-> {target.name}"
        for source, relation, target in loaded_store.get_rel_map(
            [loaded_service], depth=1
        )
    )
     
    assert loaded_entities == ["CheckoutService", "PostgreSQL", "Redis"]
    assert loaded_paths == [
        "CheckoutService -[READS_FROM]-> Redis",
        "CheckoutService -[WRITES_TO]-> PostgreSQL",
    ]
    assert len(loaded_nodes) == 4
     
    print(f"reloaded entity nodes: {', '.join(loaded_entities)}")
    print("reloaded service paths:")
    for path in loaded_paths:
        print(f"- {path}")
    print(f"reloaded graph nodes: {len(loaded_nodes)}")
    PY
  7. Run the completed script to verify the stored relationships and reloaded graph.
    $ python3 property_graph_index.py
    reloaded entity nodes: CheckoutService, PostgreSQL, Redis
    reloaded service paths:
    - CheckoutService -[READS_FROM]-> Redis
    - CheckoutService -[WRITES_TO]-> PostgreSQL
    reloaded graph nodes: 4

    The reloaded count includes the source text chunk and the three entity nodes. A missing entity, relationship, or persisted node raises AssertionError instead of printing the success transcript.