GoodMem
How-To GuidesBuilding a basic RAG Agent with GoodMem

.NET

Building a basic RAG Agent with GoodMem

View source on Github Run in Codespace

Building a Basic RAG Agent with GoodMem in C#

Overview

This tutorial will guide you through building a complete Retrieval-Augmented Generation (RAG) system using GoodMem's vector memory capabilities with C#. By the end of this guide, you'll have a functional Q&A system that can:

  • 🔍 Semantically search through your documents
  • 📝 Generate contextual answers using retrieved information
  • 🏗️ Scale to handle large document collections

What is RAG?

RAG combines the power of retrieval (finding relevant information) with generation (creating natural language responses). This approach allows AI systems to provide accurate, context-aware answers by:

  1. Retrieving relevant documents from a knowledge base
  2. Augmenting the query with this context
  3. Generating a comprehensive answer using both the query and retrieved information

Why GoodMem for RAG?

GoodMem provides enterprise-grade vector storage with:

  • Multiple embedder support for optimal retrieval accuracy
  • Streaming APIs for real-time responses
  • Advanced post-processing with reranking and summarization
  • Scalable architecture for production workloads

Prerequisites

Before starting, ensure you have:

  • GoodMem server running (install with: curl -s https://get.goodmem.ai | bash)
  • .NET 6.0+ SDK installed
  • NuGet package manager for dependency management
  • API key for your GoodMem instance

Installation & Setup

First, let's install the required NuGet packages:

// Install required NuGet packages
#r "nuget: Pairsystems.Goodmem.Client, 1.0.5"
#r "nuget: Newtonsoft.Json, 13.0.2"

Console.WriteLine("📦 Packages installed:");
Console.WriteLine("   - Pairsystems.Goodmem.Client");
Console.WriteLine("   - Newtonsoft.Json");
Console.WriteLine("\n💡 Make sure .NET 6.0+ SDK is installed");

Authentication & Configuration

Let's configure our GoodMem client and test the connection:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Pairsystems.Goodmem.Client;
using Pairsystems.Goodmem.Client.Api;
using Pairsystems.Goodmem.Client.Client;
using Pairsystems.Goodmem.Client.Model;

// Configuration - Update these values for your setup
var GOODMEM_HOST = Environment.GetEnvironmentVariable("GOODMEM_HOST") ?? "http://localhost:8080";
var GOODMEM_API_KEY = Environment.GetEnvironmentVariable("GOODMEM_API_KEY") ?? "your-api-key-here";

Console.WriteLine($"GoodMem Host: {GOODMEM_HOST}");
Console.WriteLine($"API Key configured: {(GOODMEM_API_KEY != "your-api-key-here" ? "Yes" : "No - Please update")}");

// Create and configure API client
var config = new Configuration();
config.BasePath = GOODMEM_HOST;
config.DefaultHeaders["X-API-Key"] = GOODMEM_API_KEY;

// Create API instances
var spacesApi = new SpacesApi(config);
var memoriesApi = new MemoriesApi(config);
var embeddersApi = new EmbeddersApi(config);

Console.WriteLine("✅ GoodMem client configured successfully!");

Test Connection

Let's verify we can connect to the GoodMem server:

// Test connection by listing existing spaces
try
{
    var response = await spacesApi.ListSpacesAsync();
    
    Console.WriteLine("✅ Successfully connected to GoodMem!");
    var spaces = response.Spaces ?? new List<Space>();
    Console.WriteLine($"   Found {spaces.Count} existing spaces");
}
catch (ApiException e)
{
    Console.WriteLine($"❌ Error connecting to GoodMem: {e.Message}");
    Console.WriteLine("   Please check your API key and host configuration");
    Console.WriteLine($"   Response code: {e.ErrorCode}");
}
catch (Exception e)
{
    Console.WriteLine($"❌ Unexpected error: {e.Message}");
}

Creating Your First Space

In GoodMem, a Space is a logical container for organizing memories. Each space has:

  • Associated embedders for generating vector representations
  • Access controls (public/private)
  • Metadata labels for organization

Let's create a space for our RAG demo:

// First, let's see what embedders are available
List<EmbedderResponse> availableEmbedders = new List<EmbedderResponse>();
EmbedderResponse defaultEmbedder = null;

try
{
    var embeddersResponse = await embeddersApi.ListEmbeddersAsync();
    availableEmbedders = embeddersResponse.Embedders?.ToList() ?? new List<EmbedderResponse>();
    
    Console.WriteLine($"📋 Available Embedders ({availableEmbedders.Count}):");
    for (int i = 0; i < availableEmbedders.Count; i++)
    {
        var embedder = availableEmbedders[i];
        Console.WriteLine($"   {i + 1}. {embedder.DisplayName} - {embedder.ProviderType}");
        Console.WriteLine($"      Model: {embedder.ModelIdentifier ?? "N/A"}");
        Console.WriteLine($"      ID: {embedder.EmbedderId}");
        Console.WriteLine();
    }
    
    if (availableEmbedders.Any())
    {
        defaultEmbedder = availableEmbedders[0];
        Console.WriteLine($"🎯 Using embedder: {defaultEmbedder.DisplayName}");
    }
    else
    {
        Console.WriteLine("⚠️  No embedders found. You may need to configure an embedder first.");
        Console.WriteLine("   Refer to the documentation: https://docs.goodmem.ai/docs/reference/cli/goodmem_embedder_create/");
    }
}
catch (ApiException e)
{
    Console.WriteLine($"❌ Error listing embedders: {e.Message}");
}
// Create a space for our RAG demo
var SPACE_NAME = "RAG Demo Knowledge Base (C#)";
Space demoSpace = null;

// Define chunking configuration that we'll reuse throughout the tutorial
var recursiveConfig = new RecursiveChunkingConfiguration(
    chunkSize: 256,
    chunkOverlap: 25,
    separators: new List<string> { "\n\n", "\n", ". ", " ", "" },
    keepStrategy: SeparatorKeepStrategy.KEEPEND,
    separatorIsRegex: false,
    lengthMeasurement: LengthMeasurement.CHARACTERCOUNT
);

var DEMO_CHUNKING_CONFIG = new ChunkingConfiguration(
    recursive: recursiveConfig
);

Console.WriteLine("📋 Demo Chunking Configuration:");
Console.WriteLine($"   Chunk Size: {DEMO_CHUNKING_CONFIG.Recursive.ChunkSize} characters");
Console.WriteLine($"   Overlap: {DEMO_CHUNKING_CONFIG.Recursive.ChunkOverlap} characters");
Console.WriteLine($"   Strategy: {DEMO_CHUNKING_CONFIG.Recursive.KeepStrategy}");
Console.WriteLine("   💡 This chunking config will be reused for all memory creation!");
Console.WriteLine();

try
{
    // Check if space already exists
    var existingSpaces = await spacesApi.ListSpacesAsync();
    
    if (existingSpaces.Spaces != null)
    {
        foreach (var space in existingSpaces.Spaces)
        {
            if (space.Name == SPACE_NAME)
            {
                Console.WriteLine($"📁 Space '{SPACE_NAME}' already exists");
                Console.WriteLine($"   Space ID: {space.SpaceId}");
                Console.WriteLine("   To remove existing space, see https://docs.goodmem.ai/docs/reference/cli/goodmem_space_delete/");
                demoSpace = space;
                break;
            }
        }
    }
    
    // Create space if it doesn't exist
    if (demoSpace == null)
    {
        var spaceEmbedders = new List<SpaceEmbedderConfig>();
        if (defaultEmbedder != null)
        {
            var embedderConfig = new SpaceEmbedderConfig(
                embedderId: defaultEmbedder.EmbedderId,
                defaultRetrievalWeight: 1.0
            );
            spaceEmbedders.Add(embedderConfig);
        }
        
        var createRequest = new SpaceCreationRequest(
            name: SPACE_NAME,
            labels: new Dictionary<string, string>
            {
                ["purpose"] = "rag-demo",
                ["environment"] = "tutorial",
                ["content-type"] = "documentation",
                ["language"] = "csharp"
            },
            spaceEmbedders: spaceEmbedders,
            publicRead: false,
            defaultChunkingConfig: DEMO_CHUNKING_CONFIG
        );
        
        demoSpace = await spacesApi.CreateSpaceAsync(createRequest);
        
        Console.WriteLine($"✅ Created space: {demoSpace.Name}");
        Console.WriteLine($"   Space ID: {demoSpace.SpaceId}");
        Console.WriteLine($"   Embedders: {demoSpace.SpaceEmbedders?.Count ?? 0}");
        Console.WriteLine($"   Labels: {string.Join(", ", demoSpace.Labels.Select(kv => $"{kv.Key}={kv.Value}"))}");
        Console.WriteLine($"   Chunking Config Saved: {DEMO_CHUNKING_CONFIG.Recursive.ChunkSize} chars with {DEMO_CHUNKING_CONFIG.Recursive.ChunkOverlap} overlap");
    }
}
catch (ApiException e)
{
    Console.WriteLine($"❌ Error creating space: {e.Message}");
    Console.WriteLine($"   Response code: {e.ErrorCode}");
}
catch (Exception e)
{
    Console.WriteLine($"❌ Unexpected error: {e.Message}");
}
// Verify our space configuration
if (demoSpace != null)
{
    try
    {
        var spaceDetails = await spacesApi.GetSpaceAsync(demoSpace.SpaceId);
        
        Console.WriteLine("🔍 Space Configuration:");
        Console.WriteLine($"   Name: {spaceDetails.Name}");
        Console.WriteLine($"   Owner ID: {spaceDetails.OwnerId}");
        Console.WriteLine($"   Public Read: {spaceDetails.PublicRead}");
        Console.WriteLine($"   Created: {DateTimeOffset.FromUnixTimeMilliseconds(spaceDetails.CreatedAt).DateTime}");
        Console.WriteLine($"   Labels: {string.Join(", ", spaceDetails.Labels.Select(kv => $"{kv.Key}={kv.Value}"))}");
        
        Console.WriteLine("\n🤖 Associated Embedders:");
        if (spaceDetails.SpaceEmbedders != null && spaceDetails.SpaceEmbedders.Any())
        {
            foreach (var embedderAssoc in spaceDetails.SpaceEmbedders)
            {
                Console.WriteLine($"   Embedder ID: {embedderAssoc.EmbedderId}");
                Console.WriteLine($"   Retrieval Weight: {embedderAssoc.DefaultRetrievalWeight}");
            }
        }
        else
        {
            Console.WriteLine("   No embedders configured");
        }
    }
    catch (ApiException e)
    {
        Console.WriteLine($"❌ Error getting space details: {e.Message}");
    }
}
else
{
    Console.WriteLine("⚠️  No space available for the demo");
}

Adding Documents to Memory

Now let's add some sample documents to our space. GoodMem will automatically:

  • Chunk the documents into optimal sizes
  • Generate embeddings using the configured embedders
  • Index the content for fast retrieval

We'll use sample company documents that represent common business use cases:

// Helper class to hold document information
public class DocumentInfo
{
    public string Filename { get; set; }
    public string Description { get; set; }
    public string Content { get; set; }
}

// Load our sample documents
async Task<List<DocumentInfo>> LoadSampleDocuments()
{
    var documents = new List<DocumentInfo>();
    var sampleDir = "sample_documents";
    
    var docFiles = new Dictionary<string, string>
    {
        ["company_handbook.txt"] = "Employee handbook with policies and procedures",
        ["technical_documentation.txt"] = "API documentation and technical guides",
        ["product_faq.txt"] = "Frequently asked questions about products",
        ["security_policy.txt"] = "Information security policies and procedures"
    };
    
    foreach (var (filename, description) in docFiles)
    {
        var filepath = Path.Combine(sampleDir, filename);
        
        try
        {
            if (File.Exists(filepath))
            {
                var content = await File.ReadAllTextAsync(filepath);
                documents.Add(new DocumentInfo
                {
                    Filename = filename,
                    Description = description,
                    Content = content
                });
                Console.WriteLine($"📄 Loaded: {filename} ({content.Length:N0} characters)");
            }
            else
            {
                Console.WriteLine($"⚠️  File not found: {filepath}");
            }
        }
        catch (Exception e)
        {
            Console.WriteLine($"❌ Error reading file {filename}: {e.Message}");
        }
    }
    
    return documents;
}

// Load the documents
var sampleDocs = await LoadSampleDocuments();
Console.WriteLine($"\n📚 Total documents loaded: {sampleDocs.Count}");
// Create the first memory individually to demonstrate single memory creation
async Task<Memory> CreateSingleMemory(string spaceId, DocumentInfo document)
{
    try
    {
        var memoryRequest = new MemoryCreationRequest(
            spaceId: spaceId,
            originalContent: document.Content,
            contentType: "text/plain",
            chunkingConfig: DEMO_CHUNKING_CONFIG,
            metadata: new Dictionary<string, string>
            {
                ["filename"] = document.Filename,
                ["description"] = document.Description,
                ["source"] = "sample_documents",
                ["document_type"] = document.Filename.Split('_')[0],
                ["ingestion_method"] = "single"
            }
        );
        
        var memory = await memoriesApi.CreateMemoryAsync(memoryRequest);
        
        Console.WriteLine($"✅ Created single memory: {document.Filename}");
        Console.WriteLine($"   Memory ID: {memory.MemoryId}");
        Console.WriteLine($"   Status: {memory.ProcessingStatus}");
        Console.WriteLine($"   Content Length: {document.Content.Length} characters");
        Console.WriteLine();
        
        return memory;
    }
    catch (ApiException e)
    {
        Console.WriteLine($"❌ Error creating memory for {document.Filename}: {e.Message}");
        return null;
    }
    catch (Exception e)
    {
        Console.WriteLine($"❌ Unexpected error with {document.Filename}: {e.Message}");
        return null;
    }
}

Memory singleMemory = null;
if (demoSpace != null && sampleDocs.Any())
{
    var firstDoc = sampleDocs[0];
    Console.WriteLine("📝 Creating first document using CreateMemory API:");
    Console.WriteLine($"   Document: {firstDoc.Filename}");
    Console.WriteLine("   Method: Individual memory creation");
    Console.WriteLine();
    
    singleMemory = await CreateSingleMemory(demoSpace.SpaceId, firstDoc);
    
    if (singleMemory != null)
    {
        Console.WriteLine("🎯 Single memory creation completed successfully!");
    }
    else
    {
        Console.WriteLine("⚠️  Single memory creation failed");
    }
}
else
{
    Console.WriteLine("⚠️  Cannot create memory: missing space or documents");
}
// Demonstrate retrieving a memory by ID using GetMemory
if (singleMemory != null)
{
    try
    {
        Console.WriteLine("📖 Retrieving memory details using GetMemory API:");
        Console.WriteLine($"   Memory ID: {singleMemory.MemoryId}");
        Console.WriteLine();
        
        // Retrieve the memory without content
        var retrievedMemory = await memoriesApi.GetMemoryAsync(singleMemory.MemoryId, false);
        
        Console.WriteLine("✅ Successfully retrieved memory:");
        Console.WriteLine($"   Memory ID: {retrievedMemory.MemoryId}");
        Console.WriteLine($"   Space ID: {retrievedMemory.SpaceId}");
        Console.WriteLine($"   Status: {retrievedMemory.ProcessingStatus}");
        Console.WriteLine($"   Content Type: {retrievedMemory.ContentType}");
        Console.WriteLine($"   Created At: {DateTimeOffset.FromUnixTimeMilliseconds(retrievedMemory.CreatedAt).DateTime}");
        Console.WriteLine($"   Updated At: {DateTimeOffset.FromUnixTimeMilliseconds(retrievedMemory.UpdatedAt).DateTime}");
        
        if (retrievedMemory.Metadata != null)
        {
            Console.WriteLine("\n   📋 Metadata:");
            var metadata = retrievedMemory.Metadata as Dictionary<string, string>;
            if (metadata != null)
            {
                foreach (var kvp in metadata)
                {
                    Console.WriteLine($"      {kvp.Key}: {kvp.Value}");
                }
            }
        }
        
        // Now retrieve with content included
        Console.WriteLine("\n📖 Retrieving memory with content:");
        var retrievedWithContent = await memoriesApi.GetMemoryAsync(singleMemory.MemoryId, true);
        
        if (retrievedWithContent.OriginalContent != null)
        {
            // Get the content as string (it may be FileParameter or string)
            string base64Content = retrievedWithContent.OriginalContent.ToString();
            
            // Decode the base64 encoded content
            var decodedBytes = Convert.FromBase64String(base64Content);
            var decodedContent = System.Text.Encoding.UTF8.GetString(decodedBytes);
            
            Console.WriteLine("✅ Content retrieved and decoded:");
            Console.WriteLine($"   Content length: {decodedContent.Length} characters");
            var preview = decodedContent.Length > 200 ? decodedContent.Substring(0, 200) + "..." : decodedContent;
            Console.WriteLine($"   First 200 chars: {preview}");
        }
        else
        {
            Console.WriteLine("⚠️  No content available");
        }
    }
    catch (ApiException e)
    {
        Console.WriteLine($"❌ Error retrieving memory: {e.Message}");
        Console.WriteLine($"   Status code: {e.ErrorCode}");
    }
    catch (Exception e)
    {
        Console.WriteLine($"❌ Unexpected error: {e.Message}");
    }
}
else
{
    Console.WriteLine("⚠️  No memory available to retrieve");
}
// Create the remaining documents using batch memory creation
async Task CreateBatchMemories(string spaceId, List<DocumentInfo> documents)
{
    var memoryRequests = documents.Select(doc => new MemoryCreationRequest(
        spaceId: spaceId,
        originalContent: doc.Content,
        contentType: "text/plain",
        chunkingConfig: DEMO_CHUNKING_CONFIG,
        metadata: new Dictionary<string, string>
        {
            ["filename"] = doc.Filename,
            ["description"] = doc.Description,
            ["source"] = "sample_documents",
            ["document_type"] = doc.Filename.Split('_')[0],
            ["ingestion_method"] = "batch"
        }
    )).ToList();
    
    try
    {
        var batchRequest = new BatchMemoryCreationRequest(
            requests: memoryRequests
        );
        
        Console.WriteLine($"📦 Creating {memoryRequests.Count} memories using BatchCreateMemory API:");
        
        await memoriesApi.BatchCreateMemoryAsync(batchRequest);
        
        Console.WriteLine("✅ Batch creation request submitted successfully");
    }
    catch (ApiException e)
    {
        Console.WriteLine($"❌ Error during batch creation: {e.Message}");
        Console.WriteLine($"   Response code: {e.ErrorCode}");
    }
    catch (Exception e)
    {
        Console.WriteLine($"❌ Unexpected error during batch creation: {e.Message}");
    }
}

if (demoSpace != null && sampleDocs.Count > 1)
{
    var remainingDocs = sampleDocs.Skip(1).ToList();
    await CreateBatchMemories(demoSpace.SpaceId, remainingDocs);
    
    Console.WriteLine("\n📋 Total Memory Creation Summary:");
    Console.WriteLine("   📄 Single CreateMemory: 1 document");
    Console.WriteLine($"   📦 Batch CreateMemory: {remainingDocs.Count} documents submitted");
    Console.WriteLine("   ⏳ Check processing status in the next cell");
}
else
{
    Console.WriteLine("⚠️  Cannot create batch memories: insufficient documents or missing space");
}
// List all memories in our space to verify they're ready
if (demoSpace != null)
{
    try
    {
        var memoriesResponse = await memoriesApi.ListMemoriesAsync(demoSpace.SpaceId);
        var memories = memoriesResponse.Memories ?? new List<Memory>();
        
        Console.WriteLine($"📚 Memories in space '{demoSpace.Name}':");
        Console.WriteLine($"   Total memories: {memories.Count}");
        Console.WriteLine();
        
        for (int i = 0; i < memories.Count; i++)
        {
            var memory = memories[i];
            var metadata = ((Newtonsoft.Json.Linq.JObject)memory.Metadata).ToObject<Dictionary<string, string>>();
            var filename = metadata.ContainsKey("filename") ? metadata["filename"] : "Unknown";
            var description = metadata.ContainsKey("description") ? metadata["description"] : "No description";
            
            Console.WriteLine($"   {i + 1}. {filename}");
            Console.WriteLine($"      Status: {memory.ProcessingStatus}");
            Console.WriteLine($"      Description: {description}");
            Console.WriteLine($"      Created: {DateTimeOffset.FromUnixTimeMilliseconds(memory.CreatedAt).DateTime}");
            Console.WriteLine();
        }
    }
    catch (ApiException e)
    {
        Console.WriteLine($"❌ Error listing memories: {e.Message}");
    }
}
// Monitor processing status for all created memories
async Task<bool> WaitForProcessingCompletion(string spaceId, int maxWaitSeconds = 120)
{
    Console.WriteLine("⏳ Waiting for document processing to complete...");
    Console.WriteLine("   💡 Note: Batch memories are processed asynchronously, so we check by listing all memories in the space");
    Console.WriteLine();
    
    var startTime = DateTime.Now;
    var maxWait = TimeSpan.FromSeconds(maxWaitSeconds);
    
    while (DateTime.Now - startTime < maxWait)
    {
        try
        {
            var memoriesResponse = await memoriesApi.ListMemoriesAsync(spaceId);
            var memories = memoriesResponse.Memories ?? new List<Memory>();
            
            // Check processing status
            var statusCounts = memories
                .GroupBy(m => m.ProcessingStatus)
                .ToDictionary(g => g.Key, g => g.Count());
            
            var statusStr = string.Join(", ", statusCounts.Select(kv => $"{kv.Key}: {kv.Value}"));
            Console.WriteLine($"📊 Processing status: {{{statusStr}}} (Total: {memories.Count} memories)");
            
            // Check if all are completed
            if (memories.All(m => m.ProcessingStatus == "COMPLETED"))
            {
                Console.WriteLine("✅ All documents processed successfully!");
                return true;
            }
            
            // Check for any failures
            var failedCount = memories.Count(m => m.ProcessingStatus == "FAILED");
            if (failedCount > 0)
            {
                Console.WriteLine($"❌ {failedCount} memories failed processing");
                return false;
            }
            
            await Task.Delay(5000); // Wait 5 seconds
        }
        catch (ApiException e)
        {
            Console.WriteLine($"❌ Error checking processing status: {e.Message}");
            return false;
        }
    }
    
    Console.WriteLine($"⏰ Timeout waiting for processing (waited {maxWaitSeconds}s)");
    return false;
}

if (demoSpace != null)
{
    var processingComplete = await WaitForProcessingCompletion(demoSpace.SpaceId);
    
    if (processingComplete)
    {
        Console.WriteLine("🎉 Ready for semantic search and retrieval!");
        Console.WriteLine("📈 Batch API benefit: Multiple documents submitted in a single API call");
        Console.WriteLine("🔧 Consistent chunking: All memories use DEMO_CHUNKING_CONFIG");
    }
    else
    {
        Console.WriteLine("⚠️  Some documents may still be processing. You can continue with the tutorial.");
    }
}
else
{
    Console.WriteLine("⚠️  Skipping processing check - no space available");
}

Semantic Search & Retrieval

Now comes the exciting part! Let's perform semantic search using GoodMem's streaming API. This will:

  • Find relevant chunks based on semantic similarity
  • Stream results in real-time
  • Include relevance scores for ranking
  • Return structured data for easy processing
// Helper class to hold search results
public class SearchResult
{
    public string ChunkText { get; set; }
    public double RelevanceScore { get; set; }
    public int MemoryIndex { get; set; }
    public string ResultSetId { get; set; }
    public int ChunkSequence { get; set; }
}

// Perform semantic search using GoodMem's streaming API
async Task<List<SearchResult>> SemanticSearchStreaming(string query, string spaceId, int maxResults = 5)
{
    Console.WriteLine($"🔍 Streaming search for: '{query}'");
    Console.WriteLine($"📁 Space ID: {spaceId}");
    Console.WriteLine($"📊 Max results: {maxResults}");
    Console.WriteLine(new string('-', 50));
    
    try
    {
        var streamingClient = new Pairsystems.Goodmem.Client.StreamingClient(config);
        var request = new MemoryStreamRequest
        {
            Message = query,
            SpaceIds = new List<string> { spaceId },
            RequestedSize = maxResults,
            FetchMemory = true,
            FetchMemoryContent = false,
            Format = "ndjson"
        };

        var retrievedChunks = new List<SearchResult>();
        var eventCount = 0;
        
        var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
        
        await foreach (var streamingEvent in streamingClient.RetrieveMemoryStreamAsync(request, cancellationTokenSource.Token))
        {
            eventCount++;

            if (streamingEvent.RetrievedItem?.Chunk != null)
            {
                var chunkRef = streamingEvent.RetrievedItem.Chunk;
                var chunkData = chunkRef.Chunk;
                
                var chunkText = chunkData.ContainsKey("chunkText") ? chunkData["chunkText"]?.ToString() : "";
                var chunkSequence = ((System.Text.Json.JsonElement)chunkData["chunkSequenceNumber"]).GetInt32();

                retrievedChunks.Add(new SearchResult
                {
                    ChunkText = chunkText,
                    RelevanceScore = chunkRef.RelevanceScore,
                    MemoryIndex = chunkRef.MemoryIndex,
                    ResultSetId = chunkRef.ResultSetId,
                    ChunkSequence = chunkSequence
                });

                Console.WriteLine($"\n{retrievedChunks.Count}. Relevance: {chunkRef.RelevanceScore:F3}");
                var preview = chunkText.Length > 100 ? chunkText.Substring(0, 100) + "..." : chunkText;
                Console.WriteLine($"   {preview}");

            }
            else if (streamingEvent.ResultSetBoundary != null)
            {
                Console.WriteLine($"🔄 {streamingEvent.ResultSetBoundary.Kind}: {streamingEvent.ResultSetBoundary.StageName}");
            }
        }
        
        Console.WriteLine($"✅ Streaming search completed: {retrievedChunks.Count} chunks found, {eventCount} events processed");
        return retrievedChunks;
    }
    catch (Pairsystems.Goodmem.Client.StreamingException ex)
    {
        Console.WriteLine($"❌ Streaming error: {ex.Message}");
        return new List<SearchResult>();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"❌ Unexpected error: {ex.Message}");
        return new List<SearchResult>();
    }
}

// Test semantic search with a sample query
if (demoSpace != null)
{
    var sampleQuery = "What is the vacation policy for employees?";
    var searchResults = await SemanticSearchStreaming(sampleQuery, demoSpace.SpaceId);
}
else
{
    Console.WriteLine("⚠️  No space available for search");
}
// Let's try a few different queries to see how streaming semantic search works
async Task TestMultipleStreamingQueries(string spaceId)
{
    var testQueries = new List<string>
    {
        "How do I reset my password?",
        "What are the security requirements for remote work?",
        "API authentication and rate limits",
        "Employee benefits and health insurance",
        "How much does the software cost?"
    };
    
    for (int i = 0; i < testQueries.Count; i++)
    {
        var query = testQueries[i];
        Console.WriteLine($"\n🔍 Test Query {i + 1}: {query}");
        Console.WriteLine(new string('=', 60));
        
        await SemanticSearchStreaming(query, spaceId, 3);
        
        Console.WriteLine("\n" + new string('-', 60));
    }
}

if (demoSpace != null)
{
    await TestMultipleStreamingQueries(demoSpace.SpaceId);
    Console.WriteLine("\n✅ All queries completed");
}
else
{
    Console.WriteLine("⚠️  No space available for testing multiple streaming queries");
}

Next Steps & Advanced Features

Congratulations! 🎉 You've successfully built a semantic search system using GoodMem. Here's what you've accomplished:

✅ What You Built

  • Document ingestion pipeline with automatic chunking and embedding
  • Semantic search system with relevance scoring
  • Streaming retrieval using GoodMem's real-time API

🚀 Next Steps for Advanced Implementation

1. Multiple Embedders & Reranking

  • Coming Soon
  • Coming Soon

3. Advanced Post-Processing

  • Coming Soon

📚 Additional Resources

GoodMem Documentation: