My IBM Log in Subscribe

Implementing graph RAG using knowledge graphs

06 February, 2025

 

Joshua Noble

Data Scientist

Graph retrieval augmented generation (Graph RAG) is emerging as a powerful technique for generative AI applications to use domain-specific knowledge and relevant information. Graph RAG is an alternative to vector search methods that use a vector database. Knowledge graphs are knowledge systems where graph databases such as Neo4j or Amazon Neptune can represent structured data. In a knowledge graph, the relationships between data points, called edges, are as meaningful as the connections between data points, called vertices or sometimes nodes. A knowledge graph makes it easy to traverse a network and process complex queries about connected data. Knowledge graphs are especially well suited for use cases involving chatbots, identity resolution, network analysis, recommendation engines, customer 360 and fraud detection.

A Graph RAG approach leverages the structured nature of graph databases to give greater depth and context of retrieved information about networks or complex relationships. When a graph database is paired with a large language model (LLM), a developer can automate significant parts of the graph creation process from unstructured data like text. An LLM can process text data and identify entities, understand their relationships and represent them in a graph structure.

There are many ways to create a Graph RAG application, for instance Microsoft’s GraphRAG, or pairing GPT4 with LlamaIndex. For this tutorial you’ll use Memgraph, an open source graph database solution to create a rag system by using Meta’s Llama-3 on watsonx. Memgraph uses Cypher, a declarative query language. It shares some similarities with SQL but focuses on nodes and relationships rather than tables and rows. You’ll have Llama 3 both create and populate your graph database from unstructured text and query information in the database.

Step 1

While you can choose from several tools, this tutorial walks you through how to set up an IBM account to use a Jupyter Notebook.

Log in to watsonx.ai™ using your IBM Cloud® account.

Create a watsonx.ai project.

You get your project ID from within your project. Click the Manage tab. Then, copy the project ID from the Details section of the General page. You need this Project ID for this tutorial.

Next, associate your project with the watsonx.ai Runtime

a.  Create a watsonx.ai Runtime service instance (choose the Lite plan, which is a free instance).

b.  Generate an API Key in watsonx.ai Runtime. Save this API key for use in this tutorial.

c.  Go to your project and select the Manage tab

d.  In the left tab, select Services and Integrations

e.  Select IBM services

f. Select Associate service and pick watsonx.ai Runtime.

g. Associate the watsonx.ai Runtime to the project that you created in watsonx.ai

Step 2

Now, you'll need to install Docker from https://www.docker.com/products/docker-desktop/

Once you've installed Docker, install Memgraph using their Docker container. On OSX or Linux, you can use this command in a terminal:

curl https://install.memgraph.com | sh

On a Windows computer use:

iwr https://windows.memgraph.com | iex

Follow the installation steps to get the Memgraph engine and Memgraph lab up and running.

On your computer, create a fresh virtualenv for this project:

virtualenv kg_rag --python=python3.12

In the Python environment for your notebook, install the following Python libraries:

./kg_rag/bin/pip install langchain langchain-openai langchain_experimental langchain-community==0.3.15 neo4j langchain_ibm jupyterlab json-repair getpass4

Now you're ready to connect to Memgraph.

Step 3

If you've configured Memgraph to use a username and password, set them here, otherwise you can use the defaults of having neither. It's not good practice for a production database but for a local development environment that doesn't store sensitive data, it's not an issue.

import os
from langchain_community.chains.graph_qa.memgraph import MemgraphQAChain
from langchain_community.graphs import MemgraphGraph

url = os.environ.get("MEMGRAPH_URI", "bolt://localhost:7687")
username = os.environ.get("MEMGRAPH_USERNAME", "")
password = os.environ.get("MEMGRAPH_PASSWORD", "")

#initialize memgraph connection
graph = MemgraphGraph(
    url=url, username=username, password=password, refresh_schema=True
)

Now create a sample string that describes a dataset of relationships that you can use to test the graph generating capabilities of your LLM system. You can use more complex data sources but this simple example helps us demonstrate the algorithm.

graph_text = """
John's title is Director of the Digital Marketing Group.
John works with Jane whose title is Chief Marketing Officer.
Jane works in the Executive Group.
Jane works with Sharon whose title is the Director of Client Outreach.
Sharon works in the Sales Group.
"""

Enter the watsonx API key that you created in the first step:

from getpass import getpass

watsonx_api_key = getpass()
os.environ["WATSONX_APIKEY"] = watsonx_api_key
watsonx_project_id = getpass()
os.environ["WATSONX_PROJECT_ID"] = watsonx_project_id

Now configure a WatsonxLLM instance to generate text. The temperature should be fairly low and the number of tokens high to encourage the model to generate as much detail as possible without hallucinating entities or relationships that aren't present.

from langchain_ibm import WatsonxLLM
from ibm_watsonx_ai.metanames import GenTextParamsMetaNames

graph_gen_parameters = {   
    GenTextParamsMetaNames.DECODING_METHOD: "sample",
    GenTextParamsMetaNames.MAX_NEW_TOKENS: 1000,
    GenTextParamsMetaNames.MIN_NEW_TOKENS: 1,
    GenTextParamsMetaNames.TEMPERATURE: 0.3,
    GenTextParamsMetaNames.TOP_K: 10,
    GenTextParamsMetaNames.TOP_P: 0.8
}
watsonx_llm = WatsonxLLM(
    model_id="meta-llama/llama-3-3-70b-instruct",
    url="https://us-south.ml.cloud.ibm.com",
    project_id=os.getenv("WATSONX_PROJECT_ID"),
    params=graph_gen_parameters,
)

The LLMGraphTransformer allows you to set what kinds of nodes and relationships you'd like the LLM to generate. In your case, the text describes employees at a company, the groups they work in and their job titles. Restricting the LLM to just those entities makes it more likely that you'll get a good representation of the knowledge in a graph.

The call to convert_to_graph_documents has the LLMGraphTransformer create a knowledge graph from the text. This step generates the correct Neo4j syntax to insert the information into the graph database to represent the relevant context and relevant entities.

from langchain_experimental.graph_transformers.llm import LLMGraphTransformer
from langchain_core.documents import Document

llm_transformer = LLMGraphTransformer(
    llm=watsonx_llm,
    allowed_nodes=["Person", "Title", "Group"],
    allowed_relationships=["TITLE", "COLLABORATES", "GROUP"]
)
documents = [Document(page_content=graph_text)]
graph_documents = llm_transformer.convert_to_graph_documents(documents)

Now clear any old data out of the Memgraph database and insert the new nodes and edges.

# make sure the database is empty
graph.query("STORAGE MODE IN_MEMORY_ANALYTICAL")
graph.query("DROP GRAPH")
graph.query("STORAGE MODE IN_MEMORY_TRANSACTIONAL")

# create knowledge graph
graph.add_graph_documents(graph_documents)

The generated Cypher syntax is stored in the graph_documents objects. You can inspect it simply by printing it as a string.

print(f"{graph_documents}")

The schema and data types created by the Cypher can be seen in the graphs `get_schema` property.

graph.refresh_schema()
print(graph.get_schema)

This prints out:

Node labels and properties (name and type) are:
- labels: (:Title)
properties:
- id: string
- labels: (:Group)
properties:
- id: string
- labels: (:Person)
properties:
- id: string

Nodes are connected with the following relationships:
(:Person)-[:COLLABORATES]->(:Person)
(:Person)-[:GROUP]->(:Group)
(:Person)-[:TITLE]->(:Title)

You can also see the graph structure in the Memgraph labs viewer:

 

The LLM has done a reasonable job of creating the correct nodes and relationships. Now it's time to query the knowledge graph.

Step 4

Prompting the LLM correctly requires some prompt engineering. LangChain provides a FewShotPromptTemplate that can be used to give examples to the LLM in the prompt to ensure that it writes correct and succinct Cypher syntax. The following code gives several examples of questions and queries that the LLM should use. It also shows constraining the output of the model to only the query. An overly chatty LLM might add in extra information that would lead to invalid Cypher queries, so the prompt template asks the model to output only the query itself.

Adding an instructive prefix also helps to constrain the model behavior and makes it more likely that the LLM will output correct Cypher syntax.

from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate

examples = [
{
"question": "<|begin_of_text|>What group is Charles in?<|eot_id|>",
"query": "<|begin_of_text|>MATCH (p:Person {{id: 'Charles'}})-[:GROUP]->(g:Group) RETURN g.id<|eot_id|>",
},
{
"question": "<|begin_of_text|>Who does Paul work with?<|eot_id|>",
"query": "<|begin_of_text|>MATCH (a:Person {{id: 'Paul'}})-[:COLLABORATES]->(p:Person) RETURN p.id<|eot_id|>",
},
{
"question": "What title does Rico have?<|eot_id|>",
"query": "<|begin_of_text|>MATCH (p:Person {{id: 'Rico'}})-[:TITLE]->(t:Title) RETURN t.id<|eot_id|>",
}
]

example_prompt = PromptTemplate.from_template(
"<|begin_of_text|>{query}<|eot_id|>"
)

prefix = """
Instructions:
- Respond with ONE and ONLY ONE query.
- Use provided node and relationship labels and property names from the
schema which describes the database's structure. Upon receiving a user
question, synthesize the schema to craft a precise Cypher query that
directly corresponds to the user's intent.
- Generate valid executable Cypher queries on top of Memgraph database.
Any explanation, context, or additional information that is not a part
of the Cypher query syntax should be omitted entirely.
- Use Memgraph MAGE procedures instead of Neo4j APOC procedures.
- Do not include any explanations or apologies in your responses. Only answer the question asked.
- Do not include additional questions. Only the original user question.
- Do not include any text except the generated Cypher statement.
- For queries that ask for information or functionalities outside the direct
generation of Cypher queries, use the Cypher query format to communicate
limitations or capabilities. For example: RETURN "I am designed to generate Cypher queries based on the provided schema only."

Here is the schema information

{schema}

With all the above information and instructions, generate Cypher query for the
user question.

The question is:

{question}

Below are a number of examples of questions and their corresponding Cypher queries."""

cypher_prompt = FewShotPromptTemplate(
    examples=examples,
    example_prompt=example_prompt,
    prefix=prefix,
    suffix="User input: {question}\nCypher query: ",
    input_variables=["question", "schema"],
)

Next, you'll create a prompt to control how the LLM answers the question with the information returned from Memgraph. We'll give the LLM several examples and instructions on how to respond once it has context information back from the graph database.

 

qa_examples = [
    {
        "question": "<|begin_of_text|>What group is Charles in?<|eot_id|>",
        "context": "[{{'g.id': 'Executive Group'}}]",
        "response": "Charles is in the Executive Group<|eot_id|>"
    },
    {
        "question": "<|begin_of_text|>Who does Paul work with?<|eot_id|>",
        "context": "[{{'p.id': 'Greg'}}, {{'p2.id': 'Norma'}}]",
        "response": "Paul works with Greg and Norma<|eot_id|>"
    },
    {
        "question": "What title does Rico have?<|eot_id|>",
        "context": "[{{'t.id': 'Vice President of Sales'}}]",
        "response": "Vice President of Sales<|eot_id|>"
    }
]

qa_template = """
Use the provided question and context to create an answer.Question: {question}

Context: {context}
Use only names departments or titles contained within {question} and {context}.
"""
qa_example_prompt = PromptTemplate.from_template("")

qa_prompt = FewShotPromptTemplate(
    examples=qa_examples,
    prefix=qa_template,
    input_variables=["question", "context"],
    example_prompt=qa_example_prompt,
    suffix=" "
)

Now it's time to create the question answering chain. The MemgraphQAChain allows you to set which LLM you'd like to use, the graph schema to be used and information about debugging. Using a temperature of 0 and a length penalty encourages the LLM to keep the Cypher prompt short and straightforward.

query_gen_parameters = {
    GenTextParamsMetaNames.DECODING_METHOD: "sample",
    GenTextParamsMetaNames.MAX_NEW_TOKENS: 100,
    GenTextParamsMetaNames.MIN_NEW_TOKENS: 1,
    GenTextParamsMetaNames.TEMPERATURE: 0.0,
    GenTextParamsMetaNames.TOP_K: 1,
    GenTextParamsMetaNames.TOP_P: 0.9,
    GenTextParamsMetaNames.LENGTH_PENALTY: {'decay_factor': 1.2, 'start_index': 20}
}

chain = MemgraphQAChain.from_llm(
        llm = WatsonxLLM(
        model_id="meta-llama/llama-3-3-70b-instruct",
        url="https://us-south.ml.cloud.ibm.com",
        project_id="dfe8787b-1f6f-4e18-b36a-e22c00f141d1",
        params=query_gen_parameters
    ),
    graph = graph,
    allow_dangerous_requests = True,
    verbose = True,
    return_intermediate_steps = True, # for debugging
    cypher_prompt=cypher_prompt,
    qa_prompt=qa_prompt
)

Now you can invoke the chain with a natural language question (note that your responses might be slightly different because LLMs are not purely deterministic).

chain.invoke("What is Johns title?")

This will output:

> Entering new MemgraphQAChain chain...
Generated Cypher:
 MATCH (p:Person {id: 'John'})-[:TITLE]->(t:Title) RETURN t.id
Full Context:
[{'t.id': 'Director of the Digital Marketing Group'}]

> Finished chain.
{'query': 'What is Johns title?',
 'result': ' \nAnswer: Director of the Digital Marketing Group.',
 'intermediate_steps': [{'query': " MATCH (p:Person {id: 'John'})-[:TITLE]->(t:Title) RETURN t.id"},
  {'context': [{'t.id': 'Director of the Digital Marketing Group'}]}]}

In the next question, ask the chain a slightly more complex question:

chain.invoke("Who does John collaborate with?")

This should return:

> Entering new MemgraphQAChain chain...
Generated Cypher:
MATCH (p:Person {id: 'John'})-[:COLLABORATES]->(c:Person) RETURN c
Full Context:
[{'c': {'id': 'Jane'}}]

> Finished chain.
{'query': 'Who does John collaborate with?',
'result': ' \nAnswer: John collaborates with Jane.',
'intermediate_steps': [{'query': " MATCH (p:Person {id: 'John'})-[:COLLABORATES]->(c:Person) RETURN c"},
{'context': [{'c': {'id': 'Jane'}}]}]}

The correct answer is contained in the response. In some cases there may be extra text that you would want to remove before returning the answer to an end user.

You can ask the Memgraph chain about Group relationships:

chain.invoke("What group is Jane in?")

This will return:

> Entering new MemgraphQAChain chain...
Generated Cypher:
MATCH (p:Person {id: 'Jane'})-[:GROUP]->(g:Group) RETURN g.id
Full Context:
[{'g.id': 'Executive Group'}]

> Finished chain.
{'query': 'What group is Jane in?',
'result': 'Jane is in Executive Group.',
'intermediate_steps': [{'query': " MATCH (p:Person {id: 'Jane'})-[:GROUP]->(g:Group) RETURN g.id"},
{'context': [{'g.id': 'Executive Group'}]}]}

This is the correct answer.

Finally, ask the chain a question with two outputs:

chain.invoke("Who does Jane collaborate with?")

This should output:

> Entering new MemgraphQAChain chain...
Generated Cypher:
MATCH (p:Person {id: 'Jane'})-[:COLLABORATES]->(c:Person) RETURN c
Full Context:
[{'c': {'id': 'Sharon'}}]

> Finished chain.
{'query': 'Who does Jane collaborate with?',
'result': ' Jane collaborates with Sharon.',
'intermediate_steps': [{'query': " MATCH (p:Person {id: 'Jane'})-[:COLLABORATES]->(c:Person) RETURN c"},
{'context': [{'c': {'id': 'Sharon'}}]}]}

The chain correctly identifies both of the collaborators.

Conclusion

In this tutorial, you built a Graph RAG application using Memgraph and watsonx to generate the graph data structures and query them. Using an LLM through watsonx you extracted node and edge information from natural language source text and generated Cypher query syntax to populate a graph database. You then used watsonx to turn natural language questions about that source text into Cypher queries that extracted information from the graph database. Using prompt engineering the LLM turned the results from the Memgraph database into natural language responses.

Related solutions

Related solutions

IBM watsonx.ai

Train, validate, tune and deploy generative AI, foundation models and machine learning capabilities with IBM watsonx.ai, a next-generation enterprise studio for AI builders. Build AI applications in a fraction of the time with a fraction of the data.

Discover watsonx.ai
Artificial intelligence solutions

Put AI to work in your business with IBM’s industry-leading AI expertise and portfolio of solutions at your side.

Explore AI solutions
AI consulting and services

Reinvent critical workflows and operations by adding AI to maximize experiences, real-time decision-making and business value.

Explore AI services
Take the next step

Get one-stop access to capabilities that span the AI development lifecycle. Produce powerful AI solutions with user-friendly interfaces, workflows and access to industry-standard APIs and SDKs.

Explore watsonx.ai Book a live demo