Build a Graph RAG system with LangChain and GraphRetriever (C#)

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:

dotnet add package DataStax.AstraDB.DataApi
dotnet add package LangChain.Core --version 0.17.1
dotnet add package LangChain.Providers.OpenAI --version 0.17.0
dotnet add package tryAGI.OpenAI --version 4.0.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_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 C# 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.

    using System.Text.Json;
    using System.Text.Json.Serialization;
    using System.Text.RegularExpressions;
    using DataStax.AstraDB.DataApi;
    using DataStax.AstraDB.DataApi.Core;
    using LangChain.Providers;
    using LangChain.Providers.OpenAI;
    using AstraDocument = DataStax.AstraDB.DataApi.Collections.Document;
    
    namespace TutorialGraphRag;
    
    public class DataItem
    {
      [JsonPropertyName("html_doc")]
      public string HtmlDoc { get; set; } = string.Empty;
    
      [JsonPropertyName("url")]
      public string Url { get; set; } = string.Empty;
    }
    
    public class TutorialGraphRagCreateStore
    {
      // Helper to extract all hyperlinks from HTML anchor tags
      private static readonly Regex HrefRegex = new(
        @"<a\s+(?:[^>]*?\s+)?href=""([^""]*)""",
        RegexOptions.IgnoreCase | RegexOptions.Compiled
      );
    
      private static List<string> ExtractHyperlinks(string html)
      {
        var matches = HrefRegex.Matches(html);
        var links = new List<string>();
        foreach (Match match in matches)
        {
          if (match.Groups.Count > 1)
          {
            links.Add(match.Groups[1].Value);
          }
        }
        return links;
      }
    
      public static async Task Main(string[] args)
      {
        string? endpoint = Environment.GetEnvironmentVariable("API_ENDPOINT"); (1)
        string? applicationToken = Environment.GetEnvironmentVariable(
          "APPLICATION_TOKEN"
        );
        string? openaiApiKey = Environment.GetEnvironmentVariable(
          "OPENAI_API_KEY"
        );
    
        if (
          string.IsNullOrEmpty(endpoint)
          || string.IsNullOrEmpty(applicationToken)
          || string.IsNullOrEmpty(openaiApiKey)
        )
        {
          throw new InvalidOperationException(
            "Environment variables API_ENDPOINT, APPLICATION_TOKEN, OPENAI_API_KEY must be defined."
          );
        }
    
        // Initialize the Astra DB Data API Client
        var client = new DataAPIClient();
        var database = client.GetDatabase(endpoint, applicationToken);
    
        const string dataFilePath = "PATH_TO_DATA_FILE"; (2)
    
        // Read the JSON file and parse it into a DataItem list
        string rawData = await File.ReadAllTextAsync(dataFilePath);
        var jsonData =
          JsonSerializer.Deserialize<List<DataItem>>(rawData)
          ?? new List<DataItem>();
    
        // Convert the JSON data into documents with extracted hyperlinks
        var documents =
          new List<(string Content, string Url, List<string> Hyperlinks)>();
        foreach (var item in jsonData)
        {
          var links = ExtractHyperlinks(item.HtmlDoc);
          documents.Add((item.HtmlDoc, item.Url, links));
        }
    
        // Generate embeddings for the documents using LangChain OpenAI Embeddings
        Console.WriteLine("Generating embeddings...");
        var provider = new OpenAiProvider(openaiApiKey);
        var embedder = new OpenAiEmbeddingModel(
          provider,
          "text-embedding-3-small"
        );
    
        var contents = documents.ConvertAll(d => d.Content);
        var embeddingResponse = await embedder.CreateEmbeddingsAsync(
          new EmbeddingRequest { Strings = contents }
        );
        var vectors = embeddingResponse.Values;
    
        var documentsWithEmbeddings = new List<AstraDocument>();
        for (int i = 0; i < documents.Count; i++)
        {
          var doc = new AstraDocument
          {
            ["content"] = documents[i].Content,
            ["metadata"] = new Dictionary<string, object>
            {
              ["url"] = documents[i].Url,
              ["hyperlink"] = documents[i].Hyperlinks,
            },
            ["$vector"] = vectors[i],
          };
          documentsWithEmbeddings.Add(doc);
        }
    
        // Create a new collection
        // (If a collection with the same name already exists,
        //   that collection is used instead)
        Console.WriteLine("Creating collection...");
        var definition = new CollectionDefinition
        {
          Vector = new VectorOptions
          {
            Dimension = 1536,
            Metric = SimilarityMetric.Cosine,
          },
        };
        var collection = await database.CreateCollectionAsync(
          "graph_rag_tutorial",
          definition
        );
    
        // In case a collection with this name already existed,
        // delete any documents in the collection
        await collection.DeleteManyAsync(
          Builders<AstraDocument>.CollectionFilter.Empty()
        );
    
        // Insert the documents into the collection
        Console.WriteLine("Inserting documents...");
        await collection.InsertManyAsync(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

Copy the following code into a C# 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.

using DataStax.AstraDB.DataApi.Core;
using DataStax.AstraDB.DataApi.Core.Query;
using LangChain.Providers;
using LangChain.Providers.OpenAI;
using AstraCollection = DataStax.AstraDB.DataApi.Collections.Collection<DataStax.AstraDB.DataApi.Collections.Document>;
using AstraDocument = DataStax.AstraDB.DataApi.Collections.Document;
using LangChainDocument = LangChain.DocumentLoaders.Document;

namespace TutorialGraphRag;

public class GraphRetrieverOptions
{
  public required AstraCollection Collection { get; set; }
  public required OpenAiEmbeddingModel Embedder { get; set; }
  public int StartK { get; set; } = 3;
  public int SelectK { get; set; } = 10;
  public int MaxDepth { get; set; } = 1;
}

// This retriever first uses vector search to find relevant documents,
// then uses graph traversal to explore their connections.
public class GraphRetriever
{
  private readonly AstraCollection _collection;
  private readonly OpenAiEmbeddingModel _embedder;
  private readonly int _startK;
  private readonly int _selectK;
  private readonly int _maxDepth;

  public GraphRetriever(GraphRetrieverOptions options)
  {
    _collection = options.Collection;
    _embedder = options.Embedder;
    _startK = options.StartK;
    _selectK = options.SelectK;
    _maxDepth = options.MaxDepth;
  }

  public async Task<List<LangChainDocument>> GetRelevantDocumentsAsync(
    string query
  )
  {
    // Generate embedding vector for the search query
    var embeddingResponse = await _embedder.CreateEmbeddingsAsync(
      new EmbeddingRequest { Strings = new[] { query } }
    );
    var queryVector = embeddingResponse.Values[0];

    // 1. Native vector search for initial seed documents
    var seedDocsCursor = _collection.Find(
      new CollectionFindOptions<AstraDocument>
      {
        Sort = Builders<AstraDocument>.CollectionSort.Vector(queryVector),
        Limit = _startK,
      }
    );

    var seedDocs = new List<LangChainDocument>();
    await foreach (var doc in seedDocsCursor)
    {
      seedDocs.Add(DocToLangChainDocument(doc));
    }

    var retrievedDocs = new List<LangChainDocument>(seedDocs);
    var retrievedUrls = new HashSet<string>(
      retrievedDocs
        .Select(d => GetMetadataString(d, "url"))
        .Where(url => !string.IsNullOrEmpty(url))!
    );

    // 2. Graph Traversal
    int currentDepth = 0;
    var frontier = new List<LangChainDocument>(seedDocs);

    while (
      currentDepth < _maxDepth
      && retrievedDocs.Count < _selectK
      && frontier.Count > 0
    )
    {
      var nextFrontier = new List<LangChainDocument>();

      // Extract all hyperlinks from the current frontier
      var linksToFetch = new HashSet<string>();
      foreach (var doc in frontier)
      {
        var links = GetMetadataStringList(doc, "hyperlink");
        foreach (var link in links)
        {
          if (!retrievedUrls.Contains(link))
          {
            linksToFetch.Add(link);
          }
        }
      }

      if (linksToFetch.Count == 0)
      {
        break;
      }

      // Fetch documents matching these URLs from Astra DB
      // Query the collection directly to perform metadata lookup
      var fetchedDocs = new List<LangChainDocument>();
      var linksArray = linksToFetch.ToArray();

      var cursor = _collection.Find(
        Builders<AstraDocument>.CollectionFilter.In(
          "metadata.url",
          linksArray
        )
      );

      await foreach (var doc in cursor)
      {
        fetchedDocs.Add(DocToLangChainDocument(doc));
      }

      // Add to retrieved list and prepare next frontier
      foreach (var doc in fetchedDocs)
      {
        if (retrievedDocs.Count >= _selectK)
        {
          break;
        }

        var url = GetMetadataString(doc, "url");
        if (!string.IsNullOrEmpty(url) && !retrievedUrls.Contains(url))
        {
          retrievedUrls.Add(url);
          retrievedDocs.Add(doc);
          nextFrontier.Add(doc);
        }
      }

      frontier = nextFrontier;
      currentDepth++;
    }

    return retrievedDocs;
  }

  private static LangChainDocument DocToLangChainDocument(
    AstraDocument doc
  )
  {
    string content = "";
    if (
      doc.TryGetValue("content", out var contentVal)
      && contentVal is string s
    )
    {
      content = s;
    }

    var metadata = new Dictionary<string, object>();
    if (doc.TryGetValue("metadata", out var metaVal))
    {
      if (metaVal is Dictionary<string, object> dict)
      {
        metadata = dict;
      }
      else if (
        metaVal is System.Text.Json.JsonElement elem
        && elem.ValueKind == System.Text.Json.JsonValueKind.Object
      )
      {
        foreach (var prop in elem.EnumerateObject())
        {
          metadata[prop.Name] = prop.Value;
        }
      }
    }

    return new LangChainDocument(content, metadata);
  }

  private static string? GetMetadataString(
    LangChainDocument doc,
    string key
  )
  {
    if (doc.Metadata.TryGetValue(key, out var val))
    {
      if (val is string s)
        return s;
      if (
        val is System.Text.Json.JsonElement elem
        && elem.ValueKind == System.Text.Json.JsonValueKind.String
      )
      {
        return elem.GetString();
      }
    }
    return null;
  }

  private static List<string> GetMetadataStringList(
    LangChainDocument doc,
    string key
  )
  {
    var list = new List<string>();
    if (doc.Metadata.TryGetValue(key, out var val))
    {
      if (val is IEnumerable<string> strSeq)
      {
        list.AddRange(strSeq);
      }
      else if (val is IEnumerable<object> objSeq)
      {
        foreach (var item in objSeq)
        {
          if (item is string str)
            list.Add(str);
          else if (
            item is System.Text.Json.JsonElement elem
            && elem.ValueKind == System.Text.Json.JsonValueKind.String
          )
          {
            var strVal = elem.GetString();
            if (strVal != null)
              list.Add(strVal);
          }
        }
      }
      else if (
        val is System.Text.Json.JsonElement elem
        && elem.ValueKind == System.Text.Json.JsonValueKind.Array
      )
      {
        foreach (var item in elem.EnumerateArray())
        {
          if (item.ValueKind == System.Text.Json.JsonValueKind.String)
          {
            var strVal = item.GetString();
            if (strVal != null)
              list.Add(strVal);
          }
        }
      }
    }
    return list;
  }
}

Use the graph for retrieval and generation

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

    using DataStax.AstraDB.DataApi;
    using LangChain.Providers;
    using LangChain.Providers.OpenAI;
    using LangChainDocument = LangChain.DocumentLoaders.Document;
    
    namespace TutorialGraphRag;
    
    public class TutorialGraphRagRetrieval
    {
      // Helper function to format the retrieved documents for LLM context
      private static string FormatDocs(List<LangChainDocument> docs)
      {
        return string.Join("\n\n", docs.Select(doc => doc.PageContent));
      }
    
      public static async Task Main(string[] args)
      {
        string? endpoint = Environment.GetEnvironmentVariable("API_ENDPOINT"); (1)
        string? applicationToken = Environment.GetEnvironmentVariable(
          "APPLICATION_TOKEN"
        );
        string? openaiApiKey = Environment.GetEnvironmentVariable(
          "OPENAI_API_KEY"
        );
    
        if (
          string.IsNullOrEmpty(endpoint)
          || string.IsNullOrEmpty(applicationToken)
          || string.IsNullOrEmpty(openaiApiKey)
        )
        {
          throw new InvalidOperationException(
            "Environment variables API_ENDPOINT, APPLICATION_TOKEN, OPENAI_API_KEY must be defined."
          );
        }
    
        // Initialize the Astra DB Data API Client and get the collection
        var client = new DataAPIClient();
        var database = client.GetDatabase(endpoint, applicationToken);
        var collection = database.GetCollection("graph_rag_tutorial");
    
        // Initialize LangChain OpenAI Provider, Embeddings, and LLM
        var provider = new OpenAiProvider(openaiApiKey);
        var embedder = new OpenAiEmbeddingModel(
          provider,
          "text-embedding-3-small"
        );
        var llm = new OpenAiChatModel(provider, "gpt-4o");
    
        // Define the prompt template
        const string template = """
          Answer the question based only on the following context:
    
          {0}
    
          Question: {1}
          """;
    
        // Initialize GraphRetriever
        // This retriever first uses vector search to find relevant documents,
        // then uses graph traversal to explore their connections.
        var retriever = new GraphRetriever( (2)
          new GraphRetrieverOptions
          {
            Collection = collection,
            Embedder = 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,
          }
        );
    
        // Try these questions to explore the knowledge graph:
        const 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?"
    
        Console.WriteLine($"\nQuestion: {question}\n");
    
        try
        {
          // Build the RAG chain:
          // 1. Retrieve relevant documents via vector search and graph traversal
          var docs = await retriever.GetRelevantDocumentsAsync(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
          var response = await llm.GenerateAsync(prompt);
    
          Console.WriteLine("Answer:");
          Console.WriteLine(response.LastMessageContent);
        }
        catch (Exception error)
        {
          Console.Error.WriteLine($"Error during RAG query: {error}");
        }
      }
    }
    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 This is the GraphRetriever defined in the previous section. Update the import path as needed.
  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