GoodMem
How-To GuidesBuilding a basic RAG Agent with GoodMem

Java

Building a basic RAG Agent with GoodMem

View source on Github Run in Codespace

Building a Basic RAG Agent with GoodMem in Java

Overview

This tutorial will guide you through building a complete Retrieval-Augmented Generation (RAG) system using GoodMem's vector memory capabilities with Java. 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)
  • Java 1.8+ installed
  • Maven 3.8.3+ or Gradle 7.2+ for dependency management
  • API key for your GoodMem instance

Installation & Setup

First, let's set up the dependencies. In a real project, you would add these to your pom.xml (Maven) or build.gradle (Gradle):

%%loadFromPOM
<dependency>
  <groupId>ai.pairsys.goodmem</groupId>
  <artifactId>goodmem-client-java</artifactId>
  <version>1.0.5</version>
</dependency>

<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.10.1</version>
</dependency>

Authentication & Configuration

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

import ai.pairsys.goodmem.client.ApiClient;
import ai.pairsys.goodmem.client.Configuration;
import ai.pairsys.goodmem.client.auth.ApiKeyAuth;
import ai.pairsys.goodmem.client.ApiException;

// Import the API classes we'll use
import ai.pairsys.goodmem.client.api.SpacesApi;
import ai.pairsys.goodmem.client.api.MemoriesApi;
import ai.pairsys.goodmem.client.api.EmbeddersApi;

import ai.pairsys.goodmem.client.StreamingClient;
import ai.pairsys.goodmem.client.StreamingClient.*;

// Import model classes
import ai.pairsys.goodmem.client.model.*;

import java.util.*;
import java.util.stream.Stream;
import java.util.Base64;
import java.io.*;
import java.nio.file.*;

// Configuration - Update these values for your setup
String GOODMEM_HOST = System.getenv().getOrDefault("GOODMEM_HOST", "http://localhost:8080");
String GOODMEM_API_KEY = System.getenv().getOrDefault("GOODMEM_API_KEY", "your-api-key-here");

System.out.println("GoodMem Host: " + GOODMEM_HOST);
System.out.println("API Key configured: " + (!"your-api-key-here".equals(GOODMEM_API_KEY) ? "Yes" : "No - Please update"));

// Create and configure API client
ApiClient defaultClient = Configuration.getDefaultApiClient();
defaultClient.setBasePath(GOODMEM_HOST);
defaultClient.addDefaultHeader("X-API-Key", GOODMEM_API_KEY);

// Set up authentication
ApiKeyAuth apiKeyAuth = (ApiKeyAuth) defaultClient.getAuthentication("ApiKeyAuth");
apiKeyAuth.setApiKey(GOODMEM_API_KEY);

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

System.out.println("✅ GoodMem client configured successfully!");
// Test connection by listing existing spaces
try {
    ListSpacesResponse response = spacesApi.listSpaces(null, null, null, null, null, null, null);
    
    System.out.println("✅ Successfully connected to GoodMem!");
    List<Space> spaces = response.getSpaces();
    if (spaces != null) {
        System.out.println("   Found " + spaces.size() + " existing spaces");
    } else {
        System.out.println("   Found 0 existing spaces");
    }
    
} catch (ApiException e) {
    System.out.println("❌ Error connecting to GoodMem: " + e.getMessage());
    System.out.println("   Please check your API key and host configuration");
    System.out.println("   Response code: " + e.getCode());
} catch (Exception e) {
    System.out.println("❌ Unexpected error: " + e.getMessage());
    e.printStackTrace();
}

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 ArrayList<>();
EmbedderResponse defaultEmbedder = null;

try {
    ListEmbeddersResponse embeddersResponse = embeddersApi.listEmbedders(null, null, null);
    availableEmbedders = embeddersResponse.getEmbedders();
    

    System.out.println("📋 Available Embedders (" + availableEmbedders.size() + "):");
    for (int i = 0; i < availableEmbedders.size(); i++) {
        EmbedderResponse embedder = availableEmbedders.get(i);
        System.out.println("   " + (i + 1) + ". " + embedder.getDisplayName() + " - " + embedder.getProviderType());
        System.out.println("      Model: " + (embedder.getModelIdentifier() != null ? embedder.getModelIdentifier() : "N/A"));
        System.out.println("      ID: " + embedder.getEmbedderId());
        System.out.println();
    }
    
    if (!availableEmbedders.isEmpty()) {
        defaultEmbedder = availableEmbedders.get(0);
        System.out.println("🎯 Using embedder: " + defaultEmbedder.getDisplayName());
    } else {
        System.out.println("⚠️  No embedders found. You may need to configure an embedder first.");
        System.out.println("   Refer to the documentation: See https://docs.goodmem.ai/docs/reference/cli/goodmem_embedder_create/");
    }
    
} catch (ApiException e) {
    System.out.println("❌ Error listing embedders: " + e.getMessage());
    defaultEmbedder = null;
}
// Create a space for our RAG demo
String SPACE_NAME = "RAG Demo Knowledge Base (Java)";
Space demoSpace = null;

// Define chunking configuration that we'll reuse throughout the tutorial
// Using fromJson for easier construction
String chunkingConfigJson = """
{
  "recursive": {
    "chunkSize": 256,
    "chunkOverlap": 25,
    "separators": ["\\n\\n", "\\n", ". ", " ", ""],
    "keepStrategy": "KEEP_END",
    "separatorIsRegex": false,
    "lengthMeasurement": "CHARACTER_COUNT"
  }
}
""";

ChunkingConfiguration demoChunkingConfig = ChunkingConfiguration.fromJson(chunkingConfigJson);

System.out.println("📋 Demo Chunking Configuration:");
System.out.println("   Chunk Size: " + demoChunkingConfig.getRecursive().getChunkSize() + " characters");
System.out.println("   Overlap: " + demoChunkingConfig.getRecursive().getChunkOverlap() + " characters");
System.out.println("   Strategy: " + demoChunkingConfig.getRecursive().getKeepStrategy());
System.out.println("   💡 This chunking config will be reused for all memory creation!");
System.out.println();

try {
    // Check if space already exists
    ListSpacesResponse existingSpaces = spacesApi.listSpaces(null, null, null, null, null, null, null);
    
    if (existingSpaces.getSpaces() != null) {
        for (Space space : existingSpaces.getSpaces()) {
            if (SPACE_NAME.equals(space.getName())) {
                System.out.println("📁 Space '" + SPACE_NAME + "' already exists");
                System.out.println("   Space ID: " + space.getSpaceId());
                System.out.println("   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) {
        // Configure space embedders if we have available embedders
        List<SpaceEmbedderConfig> spaceEmbedders = new ArrayList<>();
        if (defaultEmbedder != null) {
            SpaceEmbedderConfig embedderConfig = new SpaceEmbedderConfig();
            embedderConfig.setEmbedderId(defaultEmbedder.getEmbedderId());
            embedderConfig.setDefaultRetrievalWeight(1.0);
            spaceEmbedders.add(embedderConfig);
        }
        
        // Create space request with our saved chunking configuration
        SpaceCreationRequest createRequest = new SpaceCreationRequest();
        createRequest.setName(SPACE_NAME);
        
        Map<String, String> labels = new HashMap<>();
        labels.put("purpose", "rag-demo");
        labels.put("environment", "tutorial");
        labels.put("content-type", "documentation");
        labels.put("language", "java");
        createRequest.setLabels(labels);
        
        createRequest.setSpaceEmbedders(spaceEmbedders);
        createRequest.setPublicRead(false);  // Private space
        createRequest.setDefaultChunkingConfig(demoChunkingConfig);  // Use our saved config
        
        // Create the space
        demoSpace = spacesApi.createSpace(createRequest);
        
        System.out.println("✅ Created space: " + demoSpace.getName());
        System.out.println("   Space ID: " + demoSpace.getSpaceId());
        System.out.println("   Embedders: " + (demoSpace.getSpaceEmbedders() != null ? demoSpace.getSpaceEmbedders().size() : 0));
        System.out.println("   Labels: " + demoSpace.getLabels());
        System.out.println("   Chunking Config Saved: " + demoChunkingConfig.getRecursive().getChunkSize() + " chars with " + demoChunkingConfig.getRecursive().getChunkOverlap() + " overlap");
    }
    
} catch (ApiException e) {
    System.out.println("❌ Error creating space: " + e.getMessage());
    System.out.println("   Response code: " + e.getCode());
    demoSpace = null;
} catch (Exception e) {
    System.out.println("❌ Error parsing chunking configuration: " + e.getMessage());
    demoSpace = null;
}
// Verify our space configuration
if (demoSpace != null) {
    try {
        // Get detailed space information
        Space spaceDetails = spacesApi.getSpace(demoSpace.getSpaceId());
        
        System.out.println("🔍 Space Configuration:");
        System.out.println("   Name: " + spaceDetails.getName());
        System.out.println("   Owner ID: " + spaceDetails.getOwnerId());
        System.out.println("   Public Read: " + spaceDetails.getPublicRead());
        System.out.println("   Created: " + spaceDetails.getCreatedAt());
        System.out.println("   Labels: " + spaceDetails.getLabels());
        
        System.out.println("\n🤖 Associated Embedders:");
        if (spaceDetails.getSpaceEmbedders() != null) {
            for (SpaceEmbedder embedderAssoc : spaceDetails.getSpaceEmbedders()) {
                System.out.println("   Embedder ID: " + embedderAssoc.getEmbedderId());
                System.out.println("   Retrieval Weight: " + embedderAssoc.getDefaultRetrievalWeight());
            }
        } else {
            System.out.println("   No embedders configured");
        }
        
    } catch (ApiException e) {
        System.out.println("❌ Error getting space details: " + e.getMessage());
    }
} else {
    System.out.println("⚠️  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
class DocumentInfo {
    String filename;
    String description;
    String content;
    
    DocumentInfo(String filename, String description, String content) {
        this.filename = filename;
        this.description = description;
        this.content = content;
    }
}

// Load our sample documents
List<DocumentInfo> loadSampleDocuments() {
    List<DocumentInfo> documents = new ArrayList<>();
    String sampleDir = "sample_documents";
    
    // Document files and their descriptions
    Map<String, String> docFiles = new HashMap<>();
    docFiles.put("company_handbook.txt", "Employee handbook with policies and procedures");
    docFiles.put("technical_documentation.txt", "API documentation and technical guides");
    docFiles.put("product_faq.txt", "Frequently asked questions about products");
    docFiles.put("security_policy.txt", "Information security policies and procedures");
    
    for (Map.Entry<String, String> entry : docFiles.entrySet()) {
        String filename = entry.getKey();
        String description = entry.getValue();
        String filepath = sampleDir + "/" + filename;
        
        try {
            Path path = Paths.get(filepath);
            if (Files.exists(path)) {
                String content = new String(Files.readAllBytes(path), "UTF-8");
                documents.add(new DocumentInfo(filename, description, content));
                System.out.println("📄 Loaded: " + filename + " (" + String.format("%,d", content.length()) + " characters)");
            } else {
                System.out.println("⚠️  File not found: " + filepath);
            }
        } catch (IOException e) {
            System.out.println("❌ Error reading file " + filename + ": " + e.getMessage());
        }
    }
    
    return documents;
}

// Load the documents
List<DocumentInfo> sampleDocs = loadSampleDocuments();
System.out.println("\n📚 Total documents loaded: " + sampleDocs.size());
// Create the first memory individually to demonstrate single memory creation
Memory createSingleMemory(String spaceId, DocumentInfo document) {
    try {
        // Create memory request
        MemoryCreationRequest memoryRequest = new MemoryCreationRequest();
        memoryRequest.setSpaceId(spaceId);
        memoryRequest.setOriginalContent(document.content);
        memoryRequest.setContentType("text/plain");
        memoryRequest.setChunkingConfig(demoChunkingConfig);
        
        Map<String, String> metadata = new HashMap<>();
        metadata.put("filename", document.filename);
        metadata.put("description", document.description);
        metadata.put("source", "sample_documents");
        metadata.put("document_type", document.filename.split("_")[0]);
        metadata.put("ingestion_method", "single");  // Track how this was ingested
        memoryRequest.setMetadata(metadata);
        
        // Create the memory
        Memory memory = memoriesApi.createMemory(memoryRequest);
        
        System.out.println("✅ Created single memory: " + document.filename);
        System.out.println("   Memory ID: " + memory.getMemoryId());
        System.out.println("   Status: " + memory.getProcessingStatus());
        System.out.println("   Content Length: " + document.content.length() + " characters");
        System.out.println();
        
        return memory;
        
    } catch (ApiException e) {
        System.out.println("❌ Error creating memory for " + document.filename + ": " + e.getMessage());
        return null;
    } catch (Exception e) {
        System.out.println("❌ Unexpected error with " + document.filename + ": " + e.getMessage());
        return null;
    }
}

Memory singleMemory = null;
if (demoSpace != null && !sampleDocs.isEmpty()) {
    // Create the first document using single memory creation
    DocumentInfo firstDoc = sampleDocs.get(0);
    System.out.println("📝 Creating first document using CreateMemory API:");
    System.out.println("   Document: " + firstDoc.filename);
    System.out.println("   Method: Individual memory creation");
    System.out.println();
    
    singleMemory = createSingleMemory(demoSpace.getSpaceId(), firstDoc);
    
    if (singleMemory != null) {
        System.out.println("🎯 Single memory creation completed successfully!");
    } else {
        System.out.println("⚠️  Single memory creation failed");
    }
} else {
    System.out.println("⚠️  Cannot create memory: missing space or documents");
}
// Demonstrate retrieving a memory by ID using getMemory
if (singleMemory != null) {
    try {
        System.out.println("📖 Retrieving memory details using getMemory API:");
        System.out.println("   Memory ID: " + singleMemory.getMemoryId());
        System.out.println();
        
        // Retrieve the memory without content
        Memory retrievedMemory = memoriesApi.getMemory(singleMemory.getMemoryId(), false);
        
        System.out.println("✅ Successfully retrieved memory:");
        System.out.println("   Memory ID: " + retrievedMemory.getMemoryId());
        System.out.println("   Space ID: " + retrievedMemory.getSpaceId());
        System.out.println("   Status: " + retrievedMemory.getProcessingStatus());
        System.out.println("   Content Type: " + retrievedMemory.getContentType());
        System.out.println("   Created At: " + retrievedMemory.getCreatedAt());
        System.out.println("   Updated At: " + retrievedMemory.getUpdatedAt());
        
        if (retrievedMemory.getMetadata() != null) {
            System.out.println("\n   📋 Metadata:");
            Map<String, String> metadata = (Map<String, String>) retrievedMemory.getMetadata();
            for (Map.Entry<String, String> entry : metadata.entrySet()) {
                System.out.println("      " + entry.getKey() + ": " + entry.getValue());
            }
        }
        
        // Now retrieve with content included
        System.out.println("\n📖 Retrieving memory with content:");
        Memory retrievedWithContent = memoriesApi.getMemory(singleMemory.getMemoryId(), true);
        
        if (retrievedWithContent.getOriginalContent() != null) {
            // Decode the base64 encoded content
            byte[] decodedBytes = Base64.getDecoder().decode(retrievedWithContent.getOriginalContent());
            String decodedContent = new String(decodedBytes, "UTF-8");
            
            System.out.println("✅ Content retrieved and decoded:");
            System.out.println("   Content length: " + decodedContent.length() + " characters");
            String preview = decodedContent.length() > 200 ? 
                decodedContent.substring(0, 200) + "..." : decodedContent;
            System.out.println("   First 200 chars: " + preview);
        } else {
            System.out.println("⚠️  No content available");
        }
            
    } catch (ApiException e) {
        System.out.println("❌ Error retrieving memory: " + e.getMessage());
        System.out.println("   Status code: " + e.getCode());
    } catch (Exception e) {
        System.out.println("❌ Unexpected error: " + e.getMessage());
        e.printStackTrace();
    }
} else {
    System.out.println("⚠️  No memory available to retrieve");
}
// Create the remaining documents using batch memory creation
void createBatchMemories(String spaceId, List<DocumentInfo> documents) {
    
    // Prepare batch memory requests using our saved chunking configuration
    List<MemoryCreationRequest> memoryRequests = new ArrayList<>();
    for (DocumentInfo doc : documents) {
        
        // Create memory request with our saved chunking configuration
        MemoryCreationRequest memoryRequest = new MemoryCreationRequest();
        memoryRequest.setSpaceId(spaceId);
        memoryRequest.setOriginalContent(doc.content);
        memoryRequest.setContentType("text/plain");
        memoryRequest.setChunkingConfig(demoChunkingConfig);   // Reuse saved chunking configuration
        
        Map<String, String> metadata = new HashMap<>();
        metadata.put("filename", doc.filename);
        metadata.put("description", doc.description);
        metadata.put("source", "sample_documents");
        metadata.put("document_type", doc.filename.split("_")[0]);
        metadata.put("ingestion_method", "batch");
        memoryRequest.setMetadata(metadata);
        
        memoryRequests.add(memoryRequest);
    }
    
    try {
        // Create batch request
        BatchMemoryCreationRequest batchRequest = new BatchMemoryCreationRequest();
        batchRequest.setRequests(memoryRequests);
        
        System.out.println("📦 Creating " + memoryRequests.size() + " memories using BatchCreateMemory API:");
        
        // Execute batch creation - this may return void on success
        memoriesApi.batchCreateMemory(batchRequest);
        
        System.out.println("✅ Batch creation request submitted successfully");
        
    } catch (ApiException e) {
        System.out.println("❌ Error during batch creation: " + e.getMessage());
        System.out.println("   Response code: " + e.getCode());
    } catch (Exception e) {
        System.out.println("❌ Unexpected error during batch creation: " + e.getMessage());
        e.printStackTrace();
    }
}

if (demoSpace != null && sampleDocs.size() > 1) {
    // Create the remaining documents (skip the first one we already created)
    List<DocumentInfo> remainingDocs = sampleDocs.subList(1, sampleDocs.size());
    createBatchMemories(demoSpace.getSpaceId(), remainingDocs);
    
    System.out.println("\n📋 Total Memory Creation Summary:");
    System.out.println("   📄 Single CreateMemory: 1 document");
    System.out.println("   📦 Batch CreateMemory: " + remainingDocs.size() + " documents submitted");
    System.out.println("   ⏳ Check processing status in the next cell");
    
} else {
    System.out.println("⚠️  Cannot create batch memories: insufficient documents or missing space");
}
// List all memories in our space to verify they're ready
if (demoSpace != null) {
    try {
        MemoryListResponse memoriesResponse = memoriesApi.listMemories(demoSpace.getSpaceId(), null, null, null, null, null, null);
        List<Memory> memories = memoriesResponse.getMemories();
        
        System.out.println("📚 Memories in space '" + demoSpace.getName() + "':");
        System.out.println("   Total memories: " + (memories != null ? memories.size() : 0));
        System.out.println();
        
        if (memories != null) {
            for (int i = 0; i < memories.size(); i++) {
                Memory memory = memories.get(i);
                Map<String, String> metadata = (Map<String, String>) memory.getMetadata();
                String filename = metadata != null ? metadata.getOrDefault("filename", "Unknown") : "Unknown";
                String description = metadata != null ? metadata.getOrDefault("description", "No description") : "No description";

                System.out.println("   " + (i + 1) + ". " + filename);
                System.out.println("      Status: " + memory.getProcessingStatus());
                System.out.println("      Description: " + description);
                System.out.println("      Created: " + memory.getCreatedAt());
                System.out.println();
            }
        }
        
    } catch (ApiException e) {
        System.out.println("❌ Error listing memories: " + e.getMessage());
    }
}
// Monitor processing status for all created memories
boolean waitForProcessingCompletion(String spaceId, int maxWaitSeconds) {
    System.out.println("⏳ Waiting for document processing to complete...");
    System.out.println("   💡 Note: Batch memories are processed asynchronously, so we check by listing all memories in the space");
    System.out.println();
    
    long startTime = System.currentTimeMillis();
    long maxWaitMs = maxWaitSeconds * 1000L;
    
    while (System.currentTimeMillis() - startTime < maxWaitMs) {
        try {
            // List memories in our space
            MemoryListResponse memoriesResponse = memoriesApi.listMemories(spaceId, null, null, null, null, null, null);
            List<Memory> memories = memoriesResponse.getMemories();
            
            if (memories == null) {
                System.out.println("📊 No memories found in space");
                return false;
            }
            
            // Check processing status
            Map<String, Integer> statusCounts = new HashMap<>();
            for (Memory memory : memories) {
                String status = memory.getProcessingStatus();
                statusCounts.put(status, statusCounts.getOrDefault(status, 0) + 1);
            }
            
            System.out.println("📊 Processing status: " + statusCounts + " (Total: " + memories.size() + " memories)");
            
            // Check if all are completed
            boolean allCompleted = true;
            for (Memory memory : memories) {
                if (!"COMPLETED".equals(memory.getProcessingStatus())) {
                    allCompleted = false;
                    break;
                }
            }
            
            if (allCompleted) {
                System.out.println("✅ All documents processed successfully!");
                return true;
            }
                
            // Check for any failures
            int failedCount = statusCounts.getOrDefault("FAILED", 0);
            if (failedCount > 0) {
                System.out.println("❌ " + failedCount + " memories failed processing");
                return false;
            }
            
            Thread.sleep(5000);  // Wait 5 seconds before checking again
            
        } catch (ApiException e) {
            System.out.println("❌ Error checking processing status: " + e.getMessage());
            return false;
        } catch (InterruptedException e) {
            System.out.println("⏹️ Interrupted while waiting for processing");
            return false;
        }
    }
    
    System.out.println("⏰ Timeout waiting for processing (waited " + maxWaitSeconds + "s)");
    return false;
}

boolean processingComplete = false;
if (demoSpace != null) {
    // Wait for processing to complete for all memories (single + batch)
    // Since batchCreateMemory returns void, we monitor by listing all memories
    processingComplete = waitForProcessingCompletion(demoSpace.getSpaceId(), 120);
    
    if (processingComplete) {
        System.out.println("🎉 Ready for semantic search and retrieval!");
        System.out.println("📈 Batch API benefit: Multiple documents submitted in a single API call");
        System.out.println("🔧 Consistent chunking: All memories use demoChunkingConfig");
    } else {
        System.out.println("⚠️  Some documents may still be processing. You can continue with the tutorial.");
    }
} else {
    System.out.println("⚠️  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

Note: For this Java demo, we'll use the synchronous retrieval API since the Java SDK doesn't currently expose the streaming API directly.

// Helper class to hold search results
class SearchResult {
    String chunkText;
    Double relevanceScore;
    Integer memoryIndex;
    String resultSetId;
    Integer chunkSequence;
    Map<String, Object> memory;
    
    SearchResult(String chunkText, Double relevanceScore, Integer memoryIndex, String resultSetId, Integer chunkSequence, Map<String, Object> memory) {
        this.chunkText = chunkText;
        this.relevanceScore = relevanceScore;
        this.memoryIndex = memoryIndex;
        this.resultSetId = resultSetId;
        this.chunkSequence = chunkSequence;
        this.memory = memory;
    }
}

List<SearchResult> semanticSearchStreaming(String query, String spaceId, int maxResults) {
    /**
     * Perform semantic search using GoodMem's streaming retrieval API.
     * 
     * Args:
     *     query: The search query
     *     spaceId: ID of the space to search
     *     maxResults: Maximum number of results to return
     * 
     * Returns:
     *     List of search results with chunks and metadata
     */
    
    try {
        System.out.println("🔍 Streaming search for: '" + query + "'");
        System.out.println("📁 Space ID: " + spaceId);
        System.out.println("📊 Max results: " + maxResults);
        System.out.println("-".repeat(50));
        
        // // Create streaming client
        StreamingClient streamingClient = new StreamingClient(defaultClient);
        
        // // Create memory stream request
        MemoryStreamRequest streamRequest = new MemoryStreamRequest(query);
        streamRequest.setSpaceIds(List.of(spaceId));
        streamRequest.setRequestedSize(maxResults);
        streamRequest.setFetchMemory(true);
        streamRequest.setFetchMemoryContent(false);  // We don't need full content for this demo
        streamRequest.setFormat(StreamingClient.StreamingFormat.NDJSON);
        
        // // Get streaming results
        Stream<MemoryStreamResponse> stream = streamingClient.retrieveMemoryStream(streamRequest);
    
        List<SearchResult> retrievedChunks = new ArrayList<>();
        final int[] eventCount = {0};
        
        // Process streaming events
        stream.forEach(streamingEvent -> {
            eventCount[0]++;
            
            if (streamingEvent.getRetrievedItem() != null) {
                if (streamingEvent.getRetrievedItem().getChunk() != null) {
                    StreamChunkReference chunkRef = streamingEvent.getRetrievedItem().getChunk();
                    Map<String, Object> chunkData = chunkRef.getChunk();
                    
                    String chunkText = "";
                    Integer chunkSequence = 0;
                    
                    if (chunkData != null) {
                        chunkText = (String) chunkData.getOrDefault("chunkText", "");
                        Object seqObj = chunkData.get("chunkSequenceNumber");
                        if (seqObj instanceof Integer) {
                            chunkSequence = (Integer) seqObj;
                        } else if (seqObj instanceof Double) {
                            chunkSequence = ((Double) seqObj).intValue();
                        }
                    }
                    
                    SearchResult result = new SearchResult(
                        chunkText,
                        chunkRef.getRelevanceScore(),
                        chunkRef.getMemoryIndex(),
                        chunkRef.getResultSetId(),
                        chunkSequence,
                        null
                    );
                    
                    retrievedChunks.add(result);
                    
                    System.out.println("📄 Chunk " + retrievedChunks.size() + ":");
                    System.out.println("   Relevance: " + String.format("%.3f", chunkRef.getRelevanceScore()));
                    System.out.println("   Text: " + chunkText.substring(0, Math.min(chunkText.length(), 100)) + "...");
                    System.out.println();
                }
                else if (streamingEvent.getRetrievedItem().getMemory() != null) {
                    // Handle memory events if needed
                    Map<String, Object> memory = streamingEvent.getRetrievedItem().getMemory();
                    String memoryId = memory.containsKey("memoryId") ? memory.get("memoryId").toString() : "unknown";
                    System.out.println("💾 Memory: " + memoryId);
                }
            }
            else if (streamingEvent.getResultSetBoundary() != null) {
                System.out.println("🔄 " + streamingEvent.getResultSetBoundary().getKind() + 
                                 ": " + streamingEvent.getResultSetBoundary().getStageName());
            }
        });
        
        System.out.println("✅ Streaming search completed: " + retrievedChunks.size() + " chunks found");
        System.out.println("   Total streaming events: " + eventCount[0]);
        return retrievedChunks;
    } catch (StreamingClient.StreamError e) {
        System.out.println("❌ Streaming error during search: " + e.getMessage());
        System.out.println("   Status code: " + e.getStatusCode());
        return new ArrayList<>();
    } catch (Exception e) {
        System.out.println("❌ Unexpected error during streaming search: " + e.getMessage());
        e.printStackTrace();
        return new ArrayList<>();
    }
}

// Test semantic streaming search with a sample query
List<SearchResult> searchResults = new ArrayList<>();
if (demoSpace != null) {
    String sampleQuery = "What is the vacation policy for employees?";
    searchResults = semanticSearchStreaming(sampleQuery, demoSpace.getSpaceId(), 5);
} else {
    System.out.println("⚠️  No space available for search");
}
// Let's try a few different queries to see how streaming semantic search works
void testMultipleStreamingQueries(String spaceId) {
    /**
     * Test streaming semantic search with different types of queries.
     */
    
    String[] testQueries = {
        "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.length; i++) {
        String query = testQueries[i];
        System.out.println("\n🔍 Test Query " + (i + 1) + ": " + query);
        System.out.println("=".repeat(60));
        
        semanticSearchStreaming(query, spaceId, 3);
        
        System.out.println("\n" + "-".repeat(60));
    }
}

if (demoSpace != null) {
    testMultipleStreamingQueries(demoSpace.getSpaceId());
} else {
    System.out.println("⚠️  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
  • Simple Q&A system using GoodMem's vector capabilities

🚀 Next Steps for Advanced Implementation

1. Multiple Embedders & Reranking

  • Coming Soon
  • Coming Soon

3. Advanced Post-Processing

  • Coming Soon

📚 Additional Resources

GoodMem Documentation: