DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Securing AI/ML Workloads in the Cloud: Integrating DevSecOps with MLOps
  • Chat Completion Models vs OpenAI Assistants API
  • Scholcast: Generating Academic Paper Summaries With AI-Driven Audio
  • Graph Database Pruning for Knowledge Representation in LLMs

Trending

  • Engineering Production Agentic Systems: Part 1: The Pipeline
  • The Dark Side of Big Data: Pseudo-Science & Fooled By Randomness
  • Implementing Asynchronous Communication Between Microservices Using Kafka and Spring Boot
  • The Twelve-Factor Agents: Building Production-Ready LLM Applications
  1. DZone
  2. Data Engineering
  3. AI/ML
  4. Build Your First Knowledge Graph From Unstructured Documents Using Python

Build Your First Knowledge Graph From Unstructured Documents Using Python

Learn how to convert a small set of unstructured engineering documents into a searchable knowledge graph using Python, spaCy, and NetworkX.

By 
Sriharsha Makineni user avatar
Sriharsha Makineni
·
Aug. 06, 26 · Tutorial
Likes (0)
Comment
Save
Tweet
Share
23 Views

Join the DZone community and get the full member experience.

Join For Free

Many engineering teams currently face a knowledge challenge.

Information does exist; however, the information is distributed across various documentation formats such as design documents, runbooks, architectural notes, deployment guides, and incident reports. In general, a developer is aware of which services depend on each other (the Checkout Service depends upon the Payment API), the database or technology stack being used by the dependent services (the Payment API utilizes PostgreSQL), and who owns/operates the dependent service (Platform Team owns and operates the Payment API), however, these pieces of information typically reside in separate locations.

A traditional search capability can locate documents with references to those terms. Using a retrieval-augmented generation (RAG) solution allows retrieval of relevant fragments/chunks based on contextually relevant keywords provided to the RAG model, which can then be passed along to a large language model (LLM). That strategy is effective for answering most types of questions.

However, there are certain types of questions that are not simply about identifying the content of one document; they are about relating concepts. 

For instance:

Plain Text
 
Which team is owns the service that 'Checkout' relies upon?


Relating all applicable data points is necessary when answering this type of question. To aid in the process of relating all applicable data points, a knowledge graph can become helpful.

This article describes building a very basic knowledge graph from unformatted text using Python. 

As described above, keeping the example as simple as possible, but again, this is essentially how you would implement your own GraphRAG system: Identify entities from text, determine how those entities relate to each other, represent those relations as edges within a graph structure, and query that graph for relevant data prior to generating an answer.

What We Are Building

We will start with a few short engineering notes:

Plain Text
 
Checkout Service depends on Payment API.
Payment API uses PostgreSQL.
Platform Team owns Payment API.
Recommendation Service calls Catalog API.
Catalog API uses Elasticsearch.
Search Team owns Catalog API.


From those notes, we want to build a graph like this:

Plain Text
 
Checkout Service 		--DEPENDS_ON--> 	Payment API
Payment API 	 		--USES--> 		PostgreSQL
Platform Team 	 		--OWNS--> 		Payment API
Recommendation Service  	--CALLS--> 		Catalog API
Catalog API			--USES--> 		Elasticsearch
Search Team 			--OWNS--> 		Catalog API


Once we have that structure, we can answer questions by traversing the graph instead of scanning raw text.

Project Setup

Create a new folder:

Shell
 
mkdir python-knowledge-graph
cd python-knowledge-graph


Create a requirements.txt file:

Plain Text
 
networkx==3.3
spacy==3.7.5


Install the dependencies:

Shell
 
pip install -r requirements.txt
python -m spacy download en_core_web_sm


We will use:

  • spaCy for basic Natural Language Processing (NLP)
  • NetworkX for building and querying the graph

For this first example, we will not use a database. Keeping everything in memory makes the workflow easier to understand.

Step 1: Identify The Input Documents

Create a new file called build_graph.py.

Python
 
documents =  ["Checkout Service depends on Payment API.",

    "Payment API uses PostgreSQL.",

    "Platform Team owns Payment API.",

    "Recommendation Service calls Catalog API.",

    "Catalog API uses Elasticsearch.",

    "Search Team owns Catalog API.",
]


In a real-world deployment, the input document could have originated from a variety of sources (e.g., Markdown files, Confluence pages, GitHub repositories, service catalogs, incident reports). In this case, a couple of lines of example text should be sufficient to illustrate the concept.

Step 2: Determine Relationship Triples

Knowledge graphs typically store information in triple format (the subject, its relationship with another entity, and that other entity):

Plain Text
 
subject   ---Relationship--->   object


An example would be:

Plain Text
 
Checkout Service  --DEPENDS_ON-->  Payment API


In general, relationship detection in a large-scale application is often performed by a trained machine learning model. For demonstration purposes in this post, we'll utilize a simple rule-based detector to keep things straightforward. 

Add the following to build_graph.py:

Python
 
import re


RELATION_PATTERNS = [
    (r"(.+?) depends on (.+?)\.", "DEPENDS_ON"),
    (r"(.+?) uses (.+?)\.", "USES"),
    (r"(.+?) owns (.+?)\.", "OWNS"),
    (r"(.+?) calls (.+?)\.", "CALLS"),
]


def extract_triples(text):
    triples = []

    for pattern, relation in RELATION_PATTERNS:
        match = re.match(pattern, text, re.IGNORECASE)
        if match:
            subject = normalize_entity(match.group(1))
            object_ = normalize_entity(match.group(2))
            triples.append((subject, relation, object_))

    return triples


def normalize_entity(value):
    return value.strip()


This function is intentionally simple. It looks for a small set of verbs and converts each sentence into a graph-friendly structure.

Try it:

Python
 
for doc in documents:
    print(extract_triples(doc))


Expected output:

Plain Text
 
[('Checkout Service', 'DEPENDS_ON', 'Payment API')]
[('Payment API', 'USES', 'PostgreSQL')]
[('Platform Team', 'OWNS', 'Payment API')]
[('Recommendation Service', 'CALLS', 'Catalog API')]
[('Catalog API', 'USES', 'Elasticsearch')]
[('Search Team', 'OWNS', 'Catalog API')]


This is the first useful step. We have converted unstructured text into structured facts.

Step 3: Create a Graph With NetworkX

We can now add those triplets into a directed graph.

Python
 
import networkx as nx


def build_knowledge_graph(documents):
    graph = nx.DiGraph()

    for doc in documents:
        triples = extract_triples(doc)

        for subject, relation, object_ in triples:
            graph.add_node(subject)
            graph.add_node(object_)
            graph.add_edge(subject, object_, relation=relation, source_text=doc)

    return graph


The use of a directed graph makes sense when dealing with relations that are directional.

This:

Plain Text
 
Checkout Service  --DEPENDS_ON-->  Payment API


does not mean the same thing as this:

Python
 
Payment API  --DEPENDS_ON-->  Checkout Service


Direction matters for dependency analysis, ownership lookup, impact analysis, and retrieval.

Step 4: Print the Graph

Add a helper function:

Python
 
def print_graph(graph):
    for source, target, data in graph.edges(data=True):
        relation = data["relation"]
        print(f"{source} --{relation}--> {target}")


Now run the full flow:

Python
 
if __name__ == "__main__":
    graph = build_knowledge_graph(documents)
    print_graph(graph)


Output:

Plain Text
 
Checkout Service --DEPENDS_ON--> Payment API
Payment API --USES--> PostgreSQL
Platform Team --OWNS--> Payment API
Recommendation Service --CALLS--> Catalog API
Catalog API --USES--> Elasticsearch
Search Team --OWNS--> Catalog API


At this point, we have a working knowledge graph.

It is small, but it already gives us something normal keyword search does not: explicit relationships.

Step 5: Query the Graph

Let’s answer a practical question:

Plain Text
 
Who owns the API that Checkout Service depends on?


That requires two hops:

Plain Text
 
Checkout Service -> Payment API -> Platform Team


The first hop finds the dependency. The second hop finds the owner.

Add this function:

Python
 
def find_owner_of_dependency(graph, service_name):
    results = []

    for dependency in graph.successors(service_name):
        edge_data = graph.get_edge_data(service_name, dependency)

        if edge_data["relation"] != "DEPENDS_ON":
            continue

        for possible_owner in graph.predecessors(dependency):
            owner_edge = graph.get_edge_data(possible_owner, dependency)

            if owner_edge["relation"] == "OWNS":
                results.append(
                    {
                        "service": service_name,
                        "dependency": dependency,
                        "owner": possible_owner,
                    }
                )

    return results


Call it:

Python
 
owners = find_owner_of_dependency(graph, "Checkout Service")

for item in owners:
    print(
        f"{item['owner']} owns {item['dependency']}, "
        f"which is used by {item['service']}."
    )


Output:

Plain Text
 
Platform Team owns Payment API, which is used by Checkout Service.


This is a simple example, but it shows the main value of graph-based retrieval. We did not search for similar text. We followed relationships.

Step 6. Save the Graph

When building a small prototype, you can often save the graph as JSON. 

Here’s how to do that.

Python
 
import json 

def export_graph(graph, output_path): 
    data = { 
        "nodes": list(graph.nodes()), 
        "edges": [ 
            { 
                "source": source, 
                "target": target, 
                "relation": edge_data["relation"], 
                "source_text": edge_data["source_text"], 
            } 
            for source, target, edge_data in graph.edges(data=True) 
        ], 
    } 
    with open(output_path, "w", encoding="utf-8") as file: 
        json.dump(data, file, indent=2) 

export_graph(graph, "graph.json")


The output looks like this:

JSON
 
{
  "nodes": [
    "Checkout Service",
    "Payment API",
    "PostgreSQL",
    "Platform Team",
    "Recommendation Service",
    "Catalog API",
    "Elasticsearch",
    "Search Team"
  ],
  "edges": [
    {
      "source": "Checkout Service",
      "target": "Payment API",
      "relation": "DEPENDS_ON",
      "source_text": "Checkout Service depends on Payment API."
    }
  ]
}


Keeping the original source_text when saving the graph is important. This is because if you were to plug this graph into either a RAG or GraphRAG pipeline, there needs to be some way for the LLM to see not just the relationship within the graph but also have access to the actual supporting text for each one.

Where spaCy Fits In

Regular expressions were used in the previous example because of how simple your examples were (you know exactly what will appear). However, most real-world documentation is not as straightforward.

This is why we use spaCy. It can find all types of named entities; e.g., organization, product, person, etc.

Below is a small sample:

Python
 
import spacy 


nlp = spacy.load("en_core_web_sm") 
text = "Platform Team owns Payment API, which uses PostgreSQL." 
doc = nlp(text) 
for entity in doc.ents: 
    print(entity.text, entity.label_)


As with many things with regard to NLP, depending upon your specific model and the input data you provide, spaCy may automatically find some entities. However, generally speaking, you will need to either develop custom rules or train your own model to extract certain types of entities that pertain to specific domains of interest (e.g., engineering-related terms such as internal services, APIs, teams, etc.).

In practice, one common method to take advantage of this is to use a combination of approaches:

Plain Text
 
Use spaCy for general entity extraction.
Use rule-based patterns for known engineering relationships.
Use an LLM only when the relationship cannot be extracted reliably with simpler methods.


Using a combination of these methods provides a way to keep costs and complexity at reasonable levels.

How This Connects to GraphRAG Pipeline

GraphRAG (graph retrieval-augmented generation) uses a graph during the retrieval phase prior to generating an answer using an LLM.

Markdown
 
Documents
   |
Entity and relationship extraction
   |
Knowledge graph
   |
Graph traversal
   |
Supporting text
   |
Large Language Model
   |
Answer


This article implements the graph construction and traversal steps only; LLM integration is outside the scope of this example.

The graph is used instead of traditional RAG in many cases. However, it is used most often when the question relates to entities that have some form of relationship to one another.

Good fit:

Plain Text
 
Which services are indirectly affected by a Payment API outage?


Usually not worth the extra complexity:

Plain Text
 
What is the timeout value for Payment API?


For simple fact lookup, vector search may be enough. For dependency, ownership, impact analysis, and multi-hop questions, graph retrieval can add real value.

Complete Example

Here is the complete script:

Python
 
import json
import re
import networkx as nx


documents = [
    "Checkout Service depends on Payment API.",
    "Payment API uses PostgreSQL.",
    "Platform Team owns Payment API.",
    "Recommendation Service calls Catalog API.",
    "Catalog API uses Elasticsearch.",
    "Search Team owns Catalog API.",
]

RELATION_PATTERNS = [
    (r"(.+?) depends on (.+?)\.", "DEPENDS_ON"),
    (r"(.+?) uses (.+?)\.", "USES"),
    (r"(.+?) owns (.+?)\.", "OWNS"),
    (r"(.+?) calls (.+?)\.", "CALLS"),
]


def normalize_entity(value):
    return value.strip()

  
def extract_triples(text):
    triples = []
    
    for pattern, relation in RELATION_PATTERNS:
        match = re.match(pattern, text, re.IGNORECASE)
        if match:
            subject = normalize_entity(match.group(1))
            object_ = normalize_entity(match.group(2))
            triples.append((subject, relation, object_))
            
    return triples

  
def build_knowledge_graph(documents):
    graph = nx.DiGraph()
    
    for doc in documents:
        triples = extract_triples(doc)
        for subject, relation, object_ in triples:
            graph.add_node(subject)
            graph.add_node(object_)
            graph.add_edge(subject, object_, relation=relation, source_text=doc)
            
    return graph

  
def find_owner_of_dependency(graph, service_name):
    results = []
    
    for dependency in graph.successors(service_name):
        edge_data = graph.get_edge_data(service_name, dependency)
        if edge_data["relation"] != "DEPENDS_ON":
            continue
            
        for possible_owner in graph.predecessors(dependency):
            owner_edge = graph.get_edge_data(possible_owner, dependency)
            if owner_edge["relation"] == "OWNS":
                results.append(
                    {
                        "service": service_name,
                        "dependency": dependency,
                        "owner": possible_owner,
                    }
                )
                
    return results

  
def export_graph(graph, output_path):
    data = {
        "nodes": list(graph.nodes()),
        "edges": [
            {
                "source": source,
                "target": target,
                "relation": edge_data["relation"],
                "source_text": edge_data["source_text"],
            }
            for source, target, edge_data in graph.edges(data=True)
        ],
    }
    
    with open(output_path, "w", encoding="utf-8") as file:
        json.dump(data, file, indent=2)

        
if __name__ == "__main__":
    graph = build_knowledge_graph(documents)
    
    for source, target, data in graph.edges(data=True):
        print(f"{source} --{data['relation']}--> {target}")
        
    owners = find_owner_of_dependency(graph, "Checkout Service")
    
    for item in owners:
        print(
            f"{item['owner']} owns {item['dependency']}, "
            f"which is used by {item['service']}."
        )

    export_graph(graph, "graph.json")


Run it:

Shell
 
python build_graph.py


Production Considerations

This example is intentionally small. In a real system, the hardest part is not creating the graph. It is keeping the graph clean.

A few things matter quickly:

  1. Entity normalization: Payment API, payment-api, and Payments API may all refer to the same system.
  2. Relationship quality: Bad relationships are worse than missing relationships because they lead retrieval in the wrong direction.
  3. Source tracking: Every edge should preserve where it came from. This helps with debugging, trust, and answer citation.
  4. Incremental updates: Rebuilding the entire graph every time a document changes is usually wasteful.
  5. Storage choice: NetworkX is excellent for local prototypes. For larger graphs, use a graph database such as Neo4j or another persistent graph store.

Key Takeaways

A knowledge graph is a practical way to represent relationships hidden inside documents.

You do not need a complex architecture to get started. A small Python script can extract triples, build a graph, and answer multi-hop questions.

Graph-based retrieval is most useful when the answer depends on connections between entities. It is less useful for simple lookup questions where traditional search already works well.

The foundation of a good GraphRAG system is not the LLM prompt. It is the quality of the entities, relationships, and supporting evidence in the graph.

Try It Yourself

Add these two documents:

Plain Text
 
Checkout Service runs on Kubernetes.
Platform Team manages Kubernetes.


Then add a new relationship type called RUNS_ON.

Update the query function to answer:

Plain Text
 
Who manages the platform that Checkout Service runs on?


This small exercise will help you see why graph traversal becomes useful as relationships grow.

API Machine learning Build (game engine) Graph (Unix) Python (language) teams Knowledge Graph large language model

Opinions expressed by DZone contributors are their own.

Related

  • Securing AI/ML Workloads in the Cloud: Integrating DevSecOps with MLOps
  • Chat Completion Models vs OpenAI Assistants API
  • Scholcast: Generating Academic Paper Summaries With AI-Driven Audio
  • Graph Database Pruning for Knowledge Representation in LLMs

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook