Build a Graph RAG system with LangChain and GraphRetriever (TypeScript)

query_builder 20 min

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

Install dependencies

Install the dependencies used in this tutorial. For example:

npm install @datastax/astra-db-ts langchain @langchain/core @langchain/openai

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_ENDPOINT
set APPLICATION_TOKEN=APPLICATION_TOKEN
set OPENAI_API_KEY=OPENAI_API_KEY

Build the graph

  1. Download the graph_rag_dataset.json sample dataset. This dataset is a JSON array describing a small set of cross-linked HTML pages.

  2. Copy the following code into a typescript file, and replace the PATH_TO_DATA_FILE placeholder 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.hyperlink field, which lists the links from that document’s HTML content, and a metadata.url field, 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.

    import * as fs from "fs";
    import { DataAPIClient, SomeDoc } from "@datastax/astra-db-ts";
    import { OpenAIEmbeddings } from "@langchain/openai";
    
    const endpoint = process.env.API_ENDPOINT; (1)
    const applicationToken = process.env.APPLICATION_TOKEN;
    const openaiApiKey = process.env.OPENAI_API_KEY;
    
    if (!endpoint || !applicationToken || !openaiApiKey) {
      throw new Error(
        "Environment variables API_ENDPOINT, APPLICATION_TOKEN, OPENAI_API_KEY must be defined.",
      );
    }
    
    // Initialize the Astra DB Data API Client
    const client = new DataAPIClient();
    const database = client.db(endpoint, {
      token: applicationToken,
    });
    
    const dataFilePath = "PATH_TO_DATA_FILE"; (2)
    
    // Read the JSON file and parse it into a JSON array
    const rawData = fs.readFileSync(dataFilePath, "utf8");
    const jsonData = JSON.parse(rawData);
    
    // Helper to extract all hyperlinks from HTML anchor tags
    function extractHyperlinks(html: string): string[] {
      const hrefRegex = /<a\s+(?:[^>]*?\s+)?href="([^"]*)"/gi;
      const links: string[] = [];
      let match;
      while ((match = hrefRegex.exec(html)) !== null) {
        links.push(match[1]);
      }
      return links;
    }
    
    (async function () {
      // Convert the JSON array into documents with extracted hyperlinks
      const documents: SomeDoc[] = jsonData.map(
        (data: { html_doc: string; url: string }) => {
          const links = extractHyperlinks(data.html_doc);
          return {
            content: data.html_doc,
            url: data.url,
            hyperlink: links,
          };
        },
      );
    
      // Generate embeddings for the documents using LangChain OpenAI Embeddings
      console.log("Generating embeddings...");
      const embedder = new OpenAIEmbeddings({ apiKey: openaiApiKey });
      const contents = documents.map((doc) => doc.content);
      const vectors = await embedder.embedDocuments(contents);
      const documentsWithEmbeddings = documents.map((document, i) => ({
        content: document.content,
        metadata: {
          url: document.url,
          hyperlink: document.hyperlink,
        },
        $vector: vectors[i],
      }));
    
      // Create a new collection
      // (If a collection with the same name already exists,
      //   that collection is used instead)
      console.log("Creating collection...");
      const collection = await database.createCollection("graph_rag_tutorial", {
        vector: {
          dimension: 1536,
          metric: "cosine",
        },
      });
    
      // In case a collection with this name already existed,
      // delete any documents in the collection
      await collection.deleteMany({});
    
      // Insert the documents into the collection
      console.log("Inserting documents...");
      await collection.insertMany(documentsWithEmbeddings);
    })();
    1 Store your database’s endpoint, application token, and OpenAI key in environment variables named API_ENDPOINT, APPLICATION_TOKEN, and OPENAI_API_KEY, as instructed in Store your credentials.
    2 Replace PATH_TO_DATA_FILE with the path to the JSON data file.
  3. Execute the code. You should see printed messages indicating embedding generation, collection creation, and document insertion.

Define a graph retriever

  1. Copy the following code into a typescript file.

    This code defines a GraphRetriever class that uses vector search to find relevant documents and then uses graph traversal to explore their connections.

    The class is imported in the next section.

    import { Collection } from "@datastax/astra-db-ts";
    import { Document } from "@langchain/core/documents";
    import {
      BaseRetriever,
      type BaseRetrieverInput,
    } from "@langchain/core/retrievers";
    import { OpenAIEmbeddings } from "@langchain/openai";
    
    interface GraphRetrieverInput extends BaseRetrieverInput {
      collection: Collection;
      embedder: OpenAIEmbeddings;
      startK: number;
      selectK: number;
      maxDepth: number;
    }
    
    // This retriever first uses vector search to find relevant documents,
    // then uses graph traversal to explore their connections.
    export class GraphRetriever extends BaseRetriever {
      lc_namespace = ["langchain", "retrievers"];
    
      private collection: Collection;
      private embedder: OpenAIEmbeddings;
      private startK: number;
      private selectK: number;
      private maxDepth: number;
    
      constructor(fields: GraphRetrieverInput) {
        super(fields);
        this.collection = fields.collection;
        this.embedder = fields.embedder;
        this.startK = fields.startK;
        this.selectK = fields.selectK;
        this.maxDepth = fields.maxDepth;
      }
    
      async _getRelevantDocuments(query: string): Promise<Document[]> {
        // Generate embedding vector for the search query
        const queryVector = await this.embedder.embedQuery(query);
    
        // 1. Native vector search for initial seed documents
        const seedDocsCursor = this.collection.find(
          {},
          {
            sort: { $vector: queryVector },
            limit: this.startK,
          },
        );
    
        const seedDocs: Document[] = [];
        for await (const doc of seedDocsCursor) {
          seedDocs.push(
            new Document({
              pageContent: doc.content || "",
              metadata: doc.metadata || {},
            }),
          );
        }
    
        const retrievedDocs = [...seedDocs];
        const retrievedUrls = new Set<string>(
          retrievedDocs.map((doc) => doc.metadata.url).filter(Boolean),
        );
    
        // 2. Graph Traversal
        let currentDepth = 0;
        let frontier = [...seedDocs];
    
        while (
          currentDepth < this.maxDepth &&
          retrievedDocs.length < this.selectK &&
          frontier.length > 0
        ) {
          const nextFrontier: Document[] = [];
    
          // Extract all hyperlinks from the current frontier
          const linksToFetch = new Set<string>();
          for (const doc of frontier) {
            const links: string[] = doc.metadata.hyperlink || [];
            for (const link of links) {
              if (!retrievedUrls.has(link)) {
                linksToFetch.add(link);
              }
            }
          }
    
          if (linksToFetch.size === 0) {
            break;
          }
    
          // Fetch documents matching these URLs from Astra DB
          const fetchedDocs: Document[] = [];
          const linksArray = Array.from(linksToFetch);
    
          // Query the collection directly to perform metadata lookup
          const cursor = this.collection.find({
            "metadata.url": { $in: linksArray },
          });
    
          for await (const doc of cursor) {
            fetchedDocs.push(
              new Document({
                pageContent: doc.content || "",
                metadata: doc.metadata || {},
              }),
            );
          }
    
          // Add to retrieved list and prepare next frontier
          for (const doc of fetchedDocs) {
            if (retrievedDocs.length >= this.selectK) {
              break;
            }
    
            const url = doc.metadata.url;
            if (url && !retrievedUrls.has(url)) {
              retrievedUrls.add(url);
              retrievedDocs.push(doc);
              nextFrontier.push(doc);
            }
          }
    
          frontier = nextFrontier;
          currentDepth++;
        }
    
        return retrievedDocs;
      }
    }

Use the graph for retrieval and generation

  1. Copy the following code into a typescript 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.

    import { DataAPIClient } from "@datastax/astra-db-ts";
    import { Document } from "@langchain/core/documents";
    import { StringOutputParser } from "@langchain/core/output_parsers";
    import { ChatPromptTemplate } from "@langchain/core/prompts";
    import {
      RunnablePassthrough,
      RunnableSequence,
    } from "@langchain/core/runnables";
    import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
    import { GraphRetriever } from "./GraphRetriever"; (1)
    
    const endpoint = process.env.API_ENDPOINT; (2)
    const applicationToken = process.env.APPLICATION_TOKEN;
    const openaiApiKey = process.env.OPENAI_API_KEY;
    
    if (!endpoint || !applicationToken || !openaiApiKey) {
      throw new Error(
        "Environment variables API_ENDPOINT, APPLICATION_TOKEN, OPENAI_API_KEY must be defined.",
      );
    }
    
    // Initialize the Astra DB Data API Client and get the collection
    const client = new DataAPIClient();
    const database = client.db(endpoint, {
      token: applicationToken,
    });
    const collection = database.collection("graph_rag_tutorial");
    
    // Initialize LangChain Embeddings
    const embedder = new OpenAIEmbeddings({ apiKey: openaiApiKey });
    
    // Initialize the LLM
    const llm = new ChatOpenAI({
      model: "gpt-4o",
      apiKey: openaiApiKey,
    });
    
    // Define the prompt template
    const template = `Answer the question based only on the following context:
    
    {context}
    
    Question: {question}
    `;
    const prompt = ChatPromptTemplate.fromTemplate(template);
    
    // Initialize GraphRetriever
    // This retriever first uses vector search to find relevant documents,
    // then uses graph traversal to explore their connections.
    const retriever = new GraphRetriever({
      collection,
      embedder,
      // Number of documents to fetch via vector search for starting the traversal
      startK: 3,
      // Maximum total documents to retrieve during traversal
      selectK: 10,
      // Maximum traversal depth
      // A value of 0 only performs vector search, but does not do any graph traversal
      maxDepth: 1,
    });
    
    // Helper function to format the retrieved documents for LLM context
    function formatDocs(docs: Document[]): string {
      return docs.map((doc) => doc.pageContent).join("\n\n");
    }
    
    // Build the RAG chain:
    // 1. Take the input question, retrieve relevant documents from a vector store,
    //   and format the documents into a string.
    //   Create a dictionary with the formatted documents and the original question.
    // 2. Create the prompt by injecting the dictionary from step 1 into the prompt template.
    // 3. Pass the formatted prompt to the LLM.
    // 4. Clean and return the LLM output.
    const chain = RunnableSequence.from([
      {
        context: retriever.pipe(formatDocs),
        question: new RunnablePassthrough(),
      },
      prompt,
      llm,
      new StringOutputParser(),
    ]);
    
    // Try these questions to explore the knowledge graph:
    const 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?"
    
    (async function () {
      console.log(`\nQuestion: ${QUESTION}\n`);
      try {
        const response = await chain.invoke(QUESTION);
        console.log("Answer:");
        console.log(response);
      } catch (error) {
        console.error("Error during RAG query:", error);
      }
    })();
    1 This is the GraphRetriever defined in the previous section. Update the import path as needed.
    2 Store your database’s endpoint, application token, and OpenAI key in environment variables named API_ENDPOINT, APPLICATION_TOKEN, and OPENAI_API_KEY, as instructed in Store your credentials.
  2. 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, and maxDepth parameters 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.

    • startK is the number of documents to retrieve via vector search for starting the graph traversal.

      Increasing startK can help with questions that might match multiple documents.

    • selectK is the number of documents to retrieve during graph traversal.

      Increasing selectK can help with questions that require broad context.

    • maxDepth is the maximum traversal depth.

      Increasing maxDepth can help with questions that require more distant connections, but might also retrieve too many loosely related documents.

  • Try using a larger dataset.

Was this helpful?

Give Feedback

How can we improve the documentation?

© Copyright IBM Corporation 2026 | Privacy policy | Terms of use Manage Privacy Choices

Apache, Apache Cassandra, Cassandra, Apache Tomcat, Tomcat, Apache Lucene, Apache Solr, Apache Hadoop, Hadoop, Apache Pulsar, Pulsar, Apache Spark, Spark, Apache TinkerPop, TinkerPop, Apache Kafka and Kafka are either registered trademarks or trademarks of the Apache Software Foundation or its subsidiaries in Canada, the United States and/or other countries. Kubernetes is the registered trademark of the Linux Foundation.

General Inquiries: Contact IBM