Module 07: Advanced RAG Patterns Lesson 20 / 41
30–35 min read Level: Advanced
Part 2 — Knowledge Retrieval & Production RAG

Lesson 16.20: GraphRAG & Advanced Knowledge Retrieval

Knowledge Graph, Entity Relationships, Graph Search & Multi-Hop Question Answering: overcoming flat vector search limitations with structured entity-relationship webs, answering multi-hop compound questions, and unifying vector search with graph traversal in enterprise systems.

What You Will Learn

  • 1. Vector search limitations and why Knowledge Graphs are vital for multi-hop relational logic.
  • 2. Enterprise Graph Data Modeling uniting Nodes, Relationships, and Properties.
  • 3. Relational Databases (SQL/JOIN) vs. Graph Databases (Cypher/Traversal) distinctions.
  • 4. Extracting entities and relationships from unstructured text with LLMs and managing extraction errors.
  • 5. Neo4j setup, Python Driver integration, and parameterized Cypher query patterns.
  • 6. Multi-hop retrieval traversing multiple entity and relationship boundaries to resolve queries.
  • 7. Source Provenance: mapping every graph relationship back to source document chunks.
  • 8. Entity Resolution: eliminating duplicate nodes while strictly preventing cross-tenant leakage.
  • 9. Hybrid Graph Retrieval: combining vector entrypoints with localized graph traversals.
  • 10. Microsoft GraphRAG (Local Search vs. Global Search) and Neo4j GraphRAG frameworks.

01 The Dilemma: Can Vector Search Answer Every Inquiry?

In previous lessons, we built a highly sophisticated RAG pipeline: text extraction, chunking, embeddings, vector databases, hybrid retrieval, reranking, and corrective evaluation. This pipeline excels at answering direct factual inquiries:

"What is our standard annual leave allowance?" → Vector search directly locates the exact HR handbook chunk.

Now examine a realistic, multi-hop enterprise supply chain inquiry:

"Who is the official supplier of keyboards belonging to the product category managed by the Procurement Department, and what warranty covers them?"

To answer this question, no single document chunk suffices. The necessary facts are fragmented across disparate entities and corporate records:

flowchart LR D["Department: Procurement"] -->|"MANAGES"| C["Category: Computer Accessories"] C -->|"CONTAINS"| P["Product: BYT-KB-1042"] S["Supplier: Example Supplies"] -->|"SUPPLIES"| P P -->|"COVERED_BY"| W["Policy: 12-Month Warranty"]

Vector search operates on Semantic Similarity across isolated text blocks. It has no structural mechanism to navigate from a department to a category, from a category to a product SKU, and from a SKU to an authorized supplier. When facts reside in separated chunks, vector search cannot assemble this relational chain.

Vector Search Flat Paradigm vs. Relational Traversals:

In vector space, chunks exist as isolated mathematical points without explicit relationship edges connecting them. To resolve multi-hop logic chains, systems require a relational network representation: a Knowledge Graph.

02 What is a Knowledge Graph? Nodes, Relationships, and Properties

A Knowledge Graph (KG) is a graph-structured data model that represents real-world entities and their conceptual relationships as an interconnected network of nodes and edges.

A labeled property graph consists of three fundamental primitives:

1. Node (Entity)

Represents an entity, concept, or document record. For example: (:Person), (:Company), (:Product).

2. Relationship (Edge)

A directed, semantic bond connecting two nodes. For example: -[:WORKS_IN]->, -[:SUPPLIES]->, -[:MANAGES]->.

3. Property (Attributes)

Key-value metadata attached to nodes or edges, such as sku: "BYT-KB-1042" or edge metadata since: "2024-01-01".

For instance, in our Byteion Assistant knowledge model, a factual relationship triplet (Subject-Predicate-Object) is structured as:

(:Department {name: "Procurement", tenant_id: "tenant_123"})
  -[:MANAGES {effective_year: 2026}]->
(:Category {name: "Computer Accessories", category_id: "cat_01"})

03 Knowledge Graph vs. Relational Database (SQL)

Coming from relational ecosystems like PostgreSQL, MySQL, and Laravel, engineers must understand the architectural distinction between relational tables and graph models:

Dimension Relational Database (PostgreSQL/MySQL) Graph Database (Neo4j)
Data Structure Tables, Rows (Records), Columns Nodes (Vertices), Relationships (Edges), Properties
Relationship Storage Foreign Keys computed at query time via JOIN operations Direct memory pointers (Index-free Adjacency)
Query Language SQL (Structured Query Language) Cypher (Graph Query Language)
Multi-Hop Scaling 5+ table JOINs degrade exponentially in memory and CPU latency Traversal scales with the size of the subgraph, not the whole DB
Schema Flexibility Rigid schemas requiring DDL migrations Dynamic and semantically expressive property models
Ideal Workload ACID financial ledgers, transactional orders, and user tables Complex knowledge discovery, multi-hop RAG, fraud graphs

Enterprise Coexistence: In modern architectures, relational databases are never abandoned. User authentication and financial ledgers reside in PostgreSQL, embeddings and chunks live in Qdrant, and organizational relational networks operate in Neo4j.

04 What is GraphRAG and How Does it Work?

GraphRAG is an advanced retrieval-augmented generation paradigm that leverages a structured Knowledge Graph alongside semantic vector search to augment language models with relational context.

Standard Vector RAG vs. GraphRAG Architecture

graph TD subgraph VectorRAG ["Standard Vector RAG (Flat Chunks)"] V_Q["User Query"] --> V_EMB["Query Embedding"] V_EMB --> V_SIM["Cosine Similarity Search"] V_SIM --> V_DOCS["Top-K Isolated Chunks"] V_DOCS --> V_LLM["LLM Answer Generation"] end subgraph GraphRAG ["GraphRAG (Structured Relations & Traversals)"] G_Q["User Query"] --> G_ENT["Entity & Intent Extraction"] G_ENT --> G_TRAV["Multi-Hop Graph Traversal"] G_ENT --> G_VEC["Vector Search on Chunks"] G_TRAV --> G_JOIN["Entities + Relationships + Source Evidence"] G_VEC --> G_JOIN G_JOIN --> G_PACK["Structured Knowledge Pack"] G_PACK --> G_LLM["Grounded Relational LLM Answer"] end

05 When Should You Implement GraphRAG?

Introducing GraphRAG prematurely into a simple RAG application is an expensive anti-pattern. Evaluate the query complexity before opting for graph modeling:

Query Archetype Concrete Example Recommended Approach
Specific Factual Lookup "What is the sick leave allowance in our handbook?" Standard Hybrid RAG
Multi-Hop Relational Search "Who supplies products managed by the Procurement department?" GraphRAG (Local Traversal)
Organizational Dependency "Which downstream microservices fail if the Billing API crashes?" GraphRAG (Dependency Graph)
Corpus-Wide Thematic Insights "What are the recurring contractual liabilities across all vendor agreements?" Microsoft Global GraphRAG

06 How GraphRAG Works: Construction & Indexing Pipeline

A complete GraphRAG architecture decouples into two distinct lifecycles: 1. Indexing-Time Graph Construction and 2. Query-Time Retrieval.

Step 1 (Ingest & Chunk): Parse corporate documents into clean, overlapping token segments.
Step 2 (Entity Extraction): Use an LLM to identify standardized entity nodes from text chunks.
Step 3 (Relationship Extraction): Extract directed predicate actions connecting identified entities.
Step 4 (Entity Resolution): Merge aliases (e.g., "ABC Corp" and "ABC Corporation") into canonical IDs.
Step 5 (Schema Validation): Filter out invalid relationship types that violate the domain ontology.
Step 6 (Graph Persistence): Upsert validated nodes, edges, and provenance metadata into Neo4j.

07 Extracting Entities & Relationships with Pydantic

To transform unstructured text into validated knowledge triplets, we use Pydantic v2 schemas and OpenAI Structured Outputs:

# app/entity_extractor.py
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
from openai import OpenAI
import json

class ExtractedEntity(BaseModel):
    model_config = ConfigDict(extra="forbid")
    name: str = Field(description="Normalized canonical entity name")
    label: Literal["Company", "Department", "Category", "Product", "Supplier", "Policy"]
    description: str

class ExtractedRelationship(BaseModel):
    model_config = ConfigDict(extra="forbid")
    source_entity: str
    target_entity: str
    relation_type: Literal["HAS_DEPARTMENT", "MANAGES", "CONTAINS", "SUPPLIES", "COVERED_BY"]
    evidence_quote: str = Field(description="Exact sentence fragment supporting this relation")

class GraphExtractionResult(BaseModel):
    model_config = ConfigDict(extra="forbid")
    entities: list[ExtractedEntity]
    relationships: list[ExtractedRelationship]

def extract_knowledge_graph_triplets(client: OpenAI, model: str, chunk_text: str) -> GraphExtractionResult:
    system_prompt = (
        "You are an enterprise ontology extraction specialist. Extract strictly factual entities "
        "and relationships matching our predefined schema. Do NOT invent claims not explicitly in the text. "
        "Every relationship must include an exact evidence_quote from the input."
    )
    
    response = client.responses.parse(
        model=model,
        input=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": f"DOCUMENT CHUNK:\n{chunk_text}"}
        ],
        text_format=GraphExtractionResult
    )
    return response.output_parsed
Extraction Hazard: Hallucinated Relationships:

If text reads: "Example Supplies distributes hardware in this category," an unconstrained LLM might hallucinate "SOLE_SUPPLIER". This misleads business audits. Requiring exact evidence_quote fragments is mandatory for verifiable provenance.

08 Enterprise Knowledge Graph Schema Modeling

Allowing an LLM to invent arbitrary node labels or edge types produces an unmaintainable graph. One extraction might yield MANAGES, another CONTROLS, and another OPERATES. A strict Domain Schema is non-negotiable:

Approved Enterprise Schema Constraints:

(:Tenant)-[:OWNS]->(:Company)
(:Company)-[:HAS_DEPARTMENT]->(:Department)
(:Department)-[:MANAGES]->(:Category)
(:Category)-[:CONTAINS]->(:Product)
(:Supplier)-[:SUPPLIES]->(:Product)
(:Product)-[:COVERED_BY]->(:Policy)

09 Neo4j Database Setup & Python Driver Configuration

The industry standard for property graph databases is Neo4j, powered by the declarative query language Cypher. Install the official driver:

pip install neo4j python-dotenv
# app/graph_db.py
import os
from neo4j import GraphDatabase
from dotenv import load_dotenv

load_dotenv()

URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
USER = os.getenv("NEO4J_USERNAME", "neo4j")
PASSWORD = os.getenv("NEO4J_PASSWORD")

if not PASSWORD:
    raise RuntimeError("NEO4J_PASSWORD environment variable is missing!")

# Connection pool driver instance
graph_driver = GraphDatabase.driver(URI, auth=(USER, PASSWORD), max_connection_lifetime=3600)
graph_driver.verify_connectivity()
print("✓ Neo4j Connection Established Successfully.")

10 Populating Knowledge Graphs with Cypher MERGE

Executing CREATE in Cypher produces duplicate nodes on every ingestion run. Instead, production architectures use MERGE, which locates existing records or initializes new entities idempotently:

# app/graph_writer.py
def upsert_enterprise_graph(driver, data: dict):
    """Upserts entities and relationships under tenant isolation."""
    query = """
    MERGE (company:Company {tenant_id: $tenant_id, company_id: $company_id})
    SET company.name = $company_name

    MERGE (dept:Department {tenant_id: $tenant_id, department_id: $dept_id})
    SET dept.name = $dept_name

    MERGE (cat:Category {tenant_id: $tenant_id, category_id: $cat_id})
    SET cat.name = $cat_name

    MERGE (prod:Product {tenant_id: $tenant_id, sku: $sku})
    SET prod.name = $prod_name

    MERGE (supp:Supplier {tenant_id: $tenant_id, supplier_id: $supp_id})
    SET supp.name = $supp_name

    MERGE (company)-[:HAS_DEPARTMENT]->(dept)
    MERGE (dept)-[:MANAGES]->(cat)
    MERGE (cat)-[:CONTAINS]->(prod)
    MERGE (supp)-[:SUPPLIES]->(prod)

    RETURN prod.sku AS sku
    """
    with driver.session() as session:
        result = session.run(query, data)
        return result.single()["sku"]

11 & 12 Multi-Hop Retrieval: Answering Complex Queries with Graph Traversals

We can now resolve our core compound question: "Which suppliers provide products within categories managed by the Procurement department?"

# app/graph_retriever.py
def multi_hop_procurement_query(driver, tenant_id: str, dept_name: str) -> list[dict]:
    query = """
    MATCH (dept:Department {tenant_id: $tenant_id, name: $dept_name})
          -[:MANAGES]->(cat:Category {tenant_id: $tenant_id})
          -[:CONTAINS]->(prod:Product {tenant_id: $tenant_id})
    MATCH (supp:Supplier {tenant_id: $tenant_id})-[:SUPPLIES]->(prod)
    RETURN 
        dept.name AS department,
        cat.name AS category,
        prod.name AS product,
        prod.sku AS sku,
        supp.name AS supplier
    """
    with driver.session() as session:
        records = session.run(query, {"tenant_id": tenant_id, "dept_name": dept_name})
        return [record.data() for record in records]
Returned Traversal Results:
[
  {
    "department": "Procurement",
    "category": "Computer Accessories",
    "product": "Mechanical Keyboard",
    "sku": "BYT-KB-1042",
    "supplier": "Example Supplies"
  }
]

A single Cypher traversal navigates 3 distinct relationship hops, connecting previously fragmented facts that vector search could never reliably link.

13 Source Provenance: Grounding Graph Facts to Document Chunks

A fundamental vulnerability of knowledge graphs is that relationships can become detached from their original citations. When an auditor asks: "What verified document proves that Example Supplies manufactures this keyboard?", the system must produce the exact source citation.

We resolve this by embedding Provenance Tracking directly into the graph:

(:Product)-[:MENTIONED_IN]->(:DocumentChunk {chunk_id: "chk_091", source: "vendor_contract_2026.pdf", page: 14})
(:Supplier)-[:MENTIONED_IN]->(:DocumentChunk {chunk_id: "chk_091"})

Every relationship stores source_chunk_id and extraction confidence properties, empowering language models to produce auditable page citations.

14 Entity Resolution: Eliminating Duplicate Nodes & Tenant Leaks

Corporate documents reference entities using diverse names: "ABC Corp", "ABC Corporation Ltd", and "ABC". Treating these as three separate nodes fragments the knowledge graph.

Entity Resolution Techniques:
  • Canonical IDs: Deterministic tax or ERP registration identifiers.
  • Levenshtein / Fuzzy: Lexical edit distance thresholds.
  • Embedding Similarity: Semantic vector proximity across entity descriptions.
Critical Cross-Tenant Leakage Warning!

Never merge "ABC Corp" from Tenant A with "ABC Corp" from Tenant B into a global node! Merging entities across tenant boundaries creates catastrophic data leaks. All resolution logic must be strictly partitioned by tenant_id.

15 The Neo4j GraphRAG Python Package & SimpleKGPipeline

The official neo4j-graphrag package streamlines building knowledge graphs from text and executing hybrid graph retrievals:

# app/neo4j_pipeline.py
import asyncio
from neo4j_graphrag.llm import OpenAILLM
from neo4j_graphrag.embeddings import OpenAIEmbeddings
from neo4j_graphrag.experimental.pipeline.kg_builder import SimpleKGPipeline
from app.graph_db import graph_driver

llm = OpenAILLM(model_name="gpt-4o-mini")
embedder = OpenAIEmbeddings(model="text-embedding-3-small")

async def ingest_document_to_graph(text: str):
    pipeline = SimpleKGPipeline(
        llm=llm,
        driver=graph_driver,
        embedder=embedder,
        from_file=False
    )
    result = await pipeline.run_async(text=text)
    return result

Note: SimpleKGPipeline is currently marked experimental. Production deployments must place strict schema validation and tenant resolution guards in front of the builder.

16 Unifying Vector Search and Graph Traversals

The most robust architecture in production is Hybrid Graph Retrieval. Its execution flow operates as follows:

1. User submits a query: "What is the warranty claim policy for BYT-KB-1042?"
2. Vector Search locates relevant document chunks and retrieves entrypoint Product nodes.
3. Graph Traversal expands from Product nodes via COVERED_BY edges to retrieve Policy nodes.
4. Context Assembly merges semantic text chunks with structured graph relationship triplets.

17 Microsoft GraphRAG: Local Search vs. Global Search

In their landmark framework, Microsoft introduced two complementary search strategies for GraphRAG:

Local Search (Entity-Centric)

Explores a specific target entity and its immediate 1-2 hop neighborhood to combine structured facts with raw chunk text.
Use Case: "What are the specifications and suppliers for BYT-KB-1042?"

Global Search (Community-Centric)

Clusters nodes into hierarchical communities using the Leiden algorithm and generates summaries to answer corpus-wide thematic questions.
Use Case: "What are the primary supply chain risks across all contracts?"

18 Operational Constraints & Maintenance Costs of GraphRAG

While immensely capable, GraphRAG introduces significant operational overhead:

  • High LLM Ingestion Costs: Extracting entity triplets across thousands of chunks requires substantial LLM compute.
  • Graph Mutation Overhead: Updating or deleting documents requires identifying and pruning obsolete edges.
  • The Open-World Assumption: The absence of an edge does not prove the relationship does not exist in reality.

Interactive Lab: GraphRAG Explorer & Resolution Sandbox

Test multi-hop graph traversals, compare vector search against graph reasoning, and simulate entity resolution live below.

Select a starting node to inspect how Cypher traverses multiple relationship hops to retrieve connected entities:
Generated Cypher Match:
MATCH (d:Department {name: 'Procurement'})-[:MANAGES]->(c:Category)-[:CONTAINS]->(p:Product)<-[:SUPPLIES]-(s:Supplier) RETURN d, c, p, s
Traversed Knowledge Chain:
Procurement → [:MANAGES] → Computer Accessories → [:CONTAINS] → BYT-KB-1042 ← [:SUPPLIES] ← Example Supplies

19 Practical Project: FastAPI & Neo4j GraphRAG Service

The complete production FastAPI router and automated unit test suite for the GraphRAG pipeline are presented below:

# app/routers/graphrag_api.py
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from app.auth import get_current_user, UserContext
from app.graph_db import graph_driver
from openai import OpenAI
import os

router = APIRouter(prefix="/v1/graphrag", tags=["GraphRAG"])
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

class MultiHopQueryRequest(BaseModel):
    department: str = Field(min_length=2, max_length=100)

@router.post("/query-suppliers")
async def query_procurement_suppliers(
    request: MultiHopQueryRequest,
    user: UserContext = Depends(get_current_user)
):
    query = """
    MATCH (dept:Department {tenant_id: $tenant_id, name: $dept_name})
          -[:MANAGES]->(cat:Category {tenant_id: $tenant_id})
          -[:CONTAINS]->(prod:Product {tenant_id: $tenant_id})
    MATCH (supp:Supplier {tenant_id: $tenant_id})-[:SUPPLIES]->(prod)
    RETURN 
        dept.name AS department,
        cat.name AS category,
        prod.name AS product,
        prod.sku AS sku,
        supp.name AS supplier
    """
    try:
        with graph_driver.session() as session:
            records = session.run(query, {
                "tenant_id": user.tenant_id,
                "dept_name": request.department
            })
            results = [r.data() for r in records]
            
        if not results:
            return {"status": "empty", "message": "No related products or suppliers found.", "data": []}
            
        return {"status": "success", "tenant_id": user.tenant_id, "data": results}
    except Exception as exc:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Graph retrieval failed: {str(exc)}"
        )
# tests/test_graphrag.py
import pytest
from unittest.mock import MagicMock

def test_tenant_isolation_in_graph_query():
    mock_session = MagicMock()
    mock_driver = MagicMock()
    mock_driver.session.return_value.__enter__.return_value = mock_session
    
    tenant_id = "tenant_test_123"
    dept_name = "Procurement"
    
    # Simulate multi-hop call
    query = "MATCH (d:Department {tenant_id: $tenant_id})..."
    mock_session.run(query, {"tenant_id": tenant_id, "dept_name": dept_name})
    
    mock_session.run.assert_called_with(query, {"tenant_id": "tenant_test_123", "dept_name": "Procurement"})

20 Knowledge Check: Scenario-Based Assessment

Test your technical understanding with the following 8 scenario-based questions:

1. What are the three fundamental building blocks of a Knowledge Graph?

Explanation: Labeled property graphs are modeled using entities (nodes), semantic bonds (relationships), and metadata attributes (properties).

2. When is GraphRAG significantly superior to traditional Vector RAG?

Explanation: Vector similarity only compares isolated text fragments, whereas graphs follow multi-step relational pathways across different files.

3. What is the primary objective of Entity Resolution?

Explanation: Entity resolution prevents graph fragmentation by clustering alias variants under a unified canonical identity.

4. What catastrophic vulnerability must be avoided during multi-tenant entity resolution?

Explanation: Merging identically named companies across distinct tenant partitions exposes confidential data across customer boundaries.

5. Why is Cypher's MERGE preferred over CREATE during graph ingestion?

Explanation: MERGE matches or creates patterns idempotently, stopping repeated pipeline runs from duplicating nodes.

6. What conclusion must be drawn if no relationship edge exists between two nodes in the graph?

Explanation: Under the Open-World Assumption, missing graph edges simply reflect unextracted data, not guaranteed real-world absence.

7. For which query archetype is Microsoft GraphRAG's Global Search specifically designed?

Explanation: Global Search aggregates community summaries generated via graph clustering to synthesize high-level corpus themes.

8. Why is retaining Source Provenance (chunk links) on graph edges mandatory in enterprise AI?

Explanation: Linking extracted relationships back to raw document chunks makes the knowledge graph auditable and eliminates hallucination.

21 Lesson Summary & Next Steps

In this lesson, we evolved past the flat geometric boundaries of vector search into multidimensional knowledge graph networks. We explored how to extract structured entity triplets from raw text, execute multi-hop Cypher traversals in Neo4j, ground relationships with source provenance, and build an enterprise GraphRAG service.

Key Takeaways:

  • Vectors for Similarity, Graphs for Structure: Pair semantic embedding search with relational graph traversals.
  • Schema Integrity: Without strict domain ontology rules, extracted graphs degenerate into chaotic noise.
  • Multi-Hop Reasoning: Graph traversals cross multiple relationship edges to synthesize fragmented facts.
  • Tenant Isolation: Breaching tenant boundaries during entity resolution represents a severe data leak.