Build a Graph RAG system with LangChain and GraphRetriever (Java)
Graph RAG is an enhancement to retrieval-augmented generation (RAG). Graph RAG uses vector search to find semantically similar documents, and then uses graph traversal to find connected documents through relationships like hyperlinks, citations, or references. This helps find documents that might not be semantically similar but are contextually connected. Similar to RAG, the found documents serve as context for a large language model (LLM).
In this tutorial, you will build a simple graph RAG system. First, you will build a graph from a small set of cross-linked HTML pages. Then, you will use the graph during the retrieval step of RAG to provide extended context to the LLM.
Prerequisites
-
An active Serverless (vector) database.
-
An application token with the Database Administrator role.
-
An OpenAI API key associated with a paid OpenAI account.
-
Java version 17 or later.
-
One of the following:
-
Apache Maven™ version 3.9 or later.
-
Gradle version 11 or later.
-
Install dependencies
-
Maven
-
Gradle
Add the dependencies used in this tutorial:
<dependencies>
<dependency>
<groupId>com.datastax.astra</groupId>
<artifactId>astra-db-java</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
<version>1.17.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.17.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
Add the dependencies used in this tutorial:
dependencies {
implementation 'com.datastax.astra:astra-db-java:2.3.2'
implementation 'dev.langchain4j:langchain4j-core:1.17.0'
implementation 'dev.langchain4j:langchain4j-open-ai:1.17.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
}
Store your credentials
For this tutorial, store your database’s Data API endpoint, application token, and OpenAI API key in environment variables:
- Linux or macOS
-
export API_ENDPOINT=API_ENDPOINT export APPLICATION_TOKEN=APPLICATION_TOKEN export OPENAI_API_KEY=OPENAI_API_KEY - Microsoft Windows
-
set API_ENDPOINT=API_ENDPOINTset APPLICATION_TOKEN=APPLICATION_TOKENset OPENAI_API_KEY=OPENAI_API_KEY
Build the graph
-
Download the graph_rag_dataset.json sample dataset. This dataset is a JSON array describing a small set of cross-linked HTML pages.
-
Copy the following code into a Java file, and replace the
PATH_TO_DATA_FILEplaceholder with the path to the JSON data file.This code processes the raw JSON dataset into a list of documents. Each document incudes a
metadata.hyperlinkfield, which lists the links from that document’s HTML content, and ametadata.urlfield, which contains the URL of the document. These fields are used to build the graph during retrieval in the next section.Then, the code creates a collection that uses Astra DB as the backend and OpenAI as the embedding service. Finally, the code inserts the processed documents into the vector store.
package com.example; import com.datastax.astra.client.DataAPIClient; import com.datastax.astra.client.collections.Collection; import com.datastax.astra.client.collections.definition.CollectionDefinition; import com.datastax.astra.client.collections.definition.documents.Document; import com.datastax.astra.client.core.vector.SimilarityMetric; import com.datastax.astra.client.databases.Database; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.openai.OpenAiEmbeddingModel; import dev.langchain4j.model.openai.OpenAiEmbeddingModelName; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; public class TutorialGraphRagCreateStore { // Helper to extract href values from HTML anchor tags private static final Pattern HREF_PATTERN = Pattern.compile("<a\\s+(?:[^>]*?\\s+)?href=\"([^\"]*)\"", Pattern.CASE_INSENSITIVE); // Helper to extract href values from an HTML string private static List<String> extractHyperlinks(String html) { List<String> links = new ArrayList<>(); Matcher matcher = HREF_PATTERN.matcher(html); while (matcher.find()) { links.add(matcher.group(1)); } return links; } public static void main(String[] args) throws Exception { String endpoint = System.getenv("API_ENDPOINT"); (1) String applicationToken = System.getenv("APPLICATION_TOKEN"); String openaiApiKey = System.getenv("OPENAI_API_KEY"); if (endpoint == null || endpoint.isBlank() || applicationToken == null || applicationToken.isBlank() || openaiApiKey == null || openaiApiKey.isBlank()) { throw new IllegalStateException( "Environment variables API_ENDPOINT, APPLICATION_TOKEN, OPENAI_API_KEY must be defined."); } // Initialize the Astra DB Data API Client DataAPIClient client = new DataAPIClient(applicationToken); Database database = client.getDatabase(endpoint); String dataFilePath = "modules/tutorials/attachments/graph_rag_dataset.json"; (2) // Read the JSON file and parse it into a list of data items ObjectMapper mapper = new ObjectMapper(); List<Map<String, String>> jsonData = mapper.readValue(new File(dataFilePath), new TypeReference<List<Map<String, String>>>() {}); // Build content strings and extract hyperlinks List<String> contents = new ArrayList<>(); List<List<String>> allHyperlinks = new ArrayList<>(); List<String> urls = new ArrayList<>(); for (Map<String, String> item : jsonData) { String htmlDoc = item.get("html_doc"); contents.add(htmlDoc); urls.add(item.get("url")); allHyperlinks.add(extractHyperlinks(htmlDoc)); } // Generate embeddings for the documents using LangChain4j OpenAI Embeddings System.out.println("Generating embeddings..."); OpenAiEmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder() .apiKey(openaiApiKey) .modelName(OpenAiEmbeddingModelName.TEXT_EMBEDDING_3_SMALL) .build(); List<TextSegment> segments = contents.stream().map(TextSegment::from).collect(Collectors.toList()); List<Embedding> embeddings = embeddingModel.embedAll(segments).content(); // Build Astra DB documents with content, metadata, and vector List<Document> documentsWithEmbeddings = new ArrayList<>(); for (int i = 0; i < jsonData.size(); i++) { Map<String, Object> metadata = new HashMap<>(); metadata.put("url", urls.get(i)); metadata.put("hyperlink", allHyperlinks.get(i)); Document doc = new Document() .append("content", contents.get(i)) .append("metadata", metadata) .vector(embeddings.get(i).vector()); documentsWithEmbeddings.add(doc); } // Create a new collection // (If a collection with the same name already exists, // that collection is used instead) System.out.println("Creating collection..."); Collection<Document> collection = database.createCollection( "graph_rag_tutorial", new CollectionDefinition().vector(1536, SimilarityMetric.COSINE)); // In case a collection with this name already existed, // delete any documents in the collection collection.deleteAll(); // Insert the documents into the collection System.out.println("Inserting documents..."); collection.insertMany(documentsWithEmbeddings); } }1 Store your database’s endpoint, application token, and OpenAI key in environment variables named API_ENDPOINT,APPLICATION_TOKEN, andOPENAI_API_KEY, as instructed in Store your credentials.2 Replace PATH_TO_DATA_FILEwith the path to the JSON data file. -
Execute the code. You should see printed messages indicating embedding generation, collection creation, and document insertion.
Define a graph retriever
Copy the following code into a Java file.
This code defines a GraphRetriever that uses vector search to find relevant documents and then uses graph traversal to explore their connections.
The code is imported in the next section.
package com.example;
import static com.datastax.astra.client.core.query.Filters.in;
import com.datastax.astra.client.collections.Collection;
import com.datastax.astra.client.collections.commands.options.CollectionFindOptions;
import com.datastax.astra.client.collections.definition.documents.Document;
import com.datastax.astra.client.core.query.Sort;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
// This retriever first uses vector search to find relevant documents,
// then uses graph traversal to explore their connections.
public class GraphRetriever {
private final Collection<Document> collection;
private final OpenAiEmbeddingModel embeddingModel;
private final int startK;
private final int selectK;
private final int maxDepth;
public GraphRetriever(
Collection<Document> collection,
OpenAiEmbeddingModel embeddingModel,
int startK,
int selectK,
int maxDepth) {
this.collection = collection;
this.embeddingModel = embeddingModel;
this.startK = startK;
this.selectK = selectK;
this.maxDepth = maxDepth;
}
public List<Document> getRelevantDocuments(String query) {
// Generate embedding vector for the search query
Embedding queryEmbedding = embeddingModel.embed(query).content();
// 1. Native vector search for initial seed documents
List<Document> seedDocs =
collection
.find(
new CollectionFindOptions()
.sort(Sort.vector(queryEmbedding.vector()))
.limit(startK))
.toList();
List<Document> retrievedDocs = new ArrayList<>(seedDocs);
Set<String> retrievedUrls = new HashSet<>();
for (Document doc : retrievedDocs) {
String url = getMetadataString(doc, "url");
if (url != null) {
retrievedUrls.add(url);
}
}
// 2. Graph Traversal
int currentDepth = 0;
List<Document> frontier = new ArrayList<>(seedDocs);
while (currentDepth < maxDepth && retrievedDocs.size() < selectK && !frontier.isEmpty()) {
// Extract all hyperlinks from the current frontier
Set<String> linksToFetch = new HashSet<>();
for (Document doc : frontier) {
List<String> links = getMetadataStringList(doc, "hyperlink");
for (String link : links) {
if (!retrievedUrls.contains(link)) {
linksToFetch.add(link);
}
}
}
if (linksToFetch.isEmpty()) {
break;
}
// Fetch documents matching these URLs from Astra DB
// Query the collection directly to perform metadata lookup
String[] linksArray = linksToFetch.toArray(new String[0]);
List<Document> fetchedDocs = collection.find(in("metadata.url", linksArray)).toList();
// Add to retrieved list and prepare next frontier
List<Document> nextFrontier = new ArrayList<>();
for (Document doc : fetchedDocs) {
if (retrievedDocs.size() >= selectK) {
break;
}
String url = getMetadataString(doc, "url");
if (url != null && !retrievedUrls.contains(url)) {
retrievedUrls.add(url);
retrievedDocs.add(doc);
nextFrontier.add(doc);
}
}
frontier = nextFrontier;
currentDepth++;
}
return retrievedDocs;
}
@SuppressWarnings("unchecked")
private static String getMetadataString(Document doc, String key) {
Object metaObj = doc.get("metadata");
if (metaObj instanceof Map) {
Object val = ((Map<String, Object>) metaObj).get(key);
return val instanceof String ? (String) val : null;
}
return null;
}
@SuppressWarnings("unchecked")
private static List<String> getMetadataStringList(Document doc, String key) {
List<String> result = new ArrayList<>();
Object metaObj = doc.get("metadata");
if (metaObj instanceof Map) {
Object val = ((Map<String, Object>) metaObj).get(key);
if (val instanceof List) {
for (Object item : (List<?>) val) {
if (item instanceof String) {
result.add((String) item);
}
}
}
}
return result;
}
}
Use the graph for retrieval and generation
-
Copy the following code into a Java file.
This code uses the graph retriever from the previous section to perform a vector search to find the documents that are most similar to a given string, then traverses the graph to find connected documents. The documents found by the graph retriever are passed along with the original question to the LLM.
package com.example; import com.datastax.astra.client.DataAPIClient; import com.datastax.astra.client.collections.Collection; import com.datastax.astra.client.collections.definition.documents.Document; import com.datastax.astra.client.databases.Database; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.openai.OpenAiChatModelName; import dev.langchain4j.model.openai.OpenAiEmbeddingModel; import dev.langchain4j.model.openai.OpenAiEmbeddingModelName; import java.util.List; public class TutorialGraphRagRetrieval { // Helper function to format the retrieved documents for LLM context private static String formatDocs(List<Document> docs) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < docs.size(); i++) { if (i > 0) sb.append("\n\n"); Object content = docs.get(i).get("content"); sb.append(content != null ? content.toString() : ""); } return sb.toString(); } public static void main(String[] args) { String endpoint = System.getenv("API_ENDPOINT"); (1) String applicationToken = System.getenv("APPLICATION_TOKEN"); String openaiApiKey = System.getenv("OPENAI_API_KEY"); if (endpoint == null || endpoint.isBlank() || applicationToken == null || applicationToken.isBlank() || openaiApiKey == null || openaiApiKey.isBlank()) { throw new IllegalStateException( "Environment variables API_ENDPOINT, APPLICATION_TOKEN, OPENAI_API_KEY must be defined."); } // Initialize the Astra DB Data API Client and get the collection DataAPIClient client = new DataAPIClient(applicationToken); Database database = client.getDatabase(endpoint); Collection<Document> collection = database.getCollection("graph_rag_tutorial"); // Initialize LangChain4j OpenAI Embeddings and LLM OpenAiEmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder() .apiKey(openaiApiKey) .modelName(OpenAiEmbeddingModelName.TEXT_EMBEDDING_3_SMALL) .build(); OpenAiChatModel llm = OpenAiChatModel.builder() .apiKey(openaiApiKey) .modelName(OpenAiChatModelName.GPT_4_O) .build(); // Define the prompt template String template = "Answer the question based only on the following context:\n\n" + "%s\n\n" + "Question: %s"; // Initialize GraphRetriever. // This retriever first uses vector search to find relevant documents, // then uses graph traversal to explore their connections. GraphRetriever retriever = new GraphRetriever( (2) collection, embeddingModel, // Number of documents to fetch via vector search for starting the traversal 3, // Maximum total documents to retrieve during traversal 10, // Maximum traversal depth. // A value of 0 only performs vector search, but does not do any graph traversal. 1); // Try these questions to explore the knowledge graph: String question = "What is close to the Space Needle?"; // Alternative questions: // - "What is in the Lower Queen Anne neighborhood?" // - "What is in the same neighborhood as the Space Needle?" // - "What connects the 1962 World's Fair to modern Seattle?" // - "Where is Chihuly Garden and Glass?" System.out.println("\nQuestion: " + question + "\n"); try { // Build the RAG chain: // 1. Retrieve relevant documents via vector search and graph traversal List<Document> docs = retriever.getRelevantDocuments(question); // 2. Format the retrieved documents into a single context string String context = formatDocs(docs); // 3. Inject the context and question into the prompt template String prompt = String.format(template, context, question); // 4. Pass the formatted prompt to the LLM and print the response String response = llm.chat(prompt); System.out.println("Answer:"); System.out.println(response); } catch (Exception e) { System.err.println("Error during RAG query: " + e); } } }1 Store your database’s endpoint, application token, and OpenAI key in environment variables named API_ENDPOINT,APPLICATION_TOKEN, andOPENAI_API_KEY, as instructed in Store your credentials.2 This is the GraphRetrieverdefined in the previous section. -
Execute the code. You should see the question print to the console, followed by the answer from the LLM.
Next steps
-
Ask different questions to see how the graph retriever performs.
-
Tune the
startK,selectK, andmaxDepthparameters to see how this affects the results. Note that increasing these values will increase the number of documents retrieved and passed to the LLM, which will increase the cost of the operation.-
startKis the number of documents to retrieve via vector search for starting the graph traversal.Increasing
startKcan help with questions that might match multiple documents. -
selectKis the number of documents to retrieve during graph traversal.Increasing
selectKcan help with questions that require broad context. -
maxDepthis the maximum traversal depth.Increasing
maxDepthcan help with questions that require more distant connections, but might also retrieve too many loosely related documents.
-
-
Try using a larger dataset.