Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

DEV Community

Rudolf Šimo
Rudolf Šimo

Posted on

Implementing Simple RAG in local environment /w .NET (C#).

This tutorial demonstrates how to build a simple Retrieval-Augmented Generation (RAG) solution using:

  • Ollama LLM (Gemma 3)
  • mxbai large embedding model
  • PostgreSQL with the pgvector extension
  • Pokémon dataset

Overview

This solution demonstrates how to:

  1. Generate embeddings for Pokémon data using the mxbai large embedding model.
  2. Store embeddings in a PostgreSQL database with pgvector.
  3. Perform similarity searches to retrieve relevant Pokémon data.
  4. Use Ollama LLM to generate responses based on user input and retrieved data.

Prerequisites

  1. Environment Setup:

  2. Dependencies:

    Add the following NuGet packages to your project:

    • Microsoft.SemanticKernel
    • Microsoft.SemanticKernel.Connectors.Postgres
    • IdeaTech.SemanticKernel.Connectors.Ollama
    • Npgsql
  3. Database Setup:

    Ensure your PostgreSQL database is running and has the pgvector extension installed. Use the connection string:

   Host=localhost;Port=5433;Username=postgres;Password=admin;Database=postgres;
Enter fullscreen mode Exit fullscreen mode

Step 1: Define Pokémon Data

Create a Data class to hold Pokémon records and generate sample data.

internal class Data
{
    private static List<Pokemon> myRecords;

    public Data()
    {
        myRecords = new List<Pokemon>();
        CreateRecords();
    }

    private void CreateRecords()
    {
        myRecords = new List<Pokemon>
        {
            new Pokemon { Id = 1, Name = "Bulbasaur", Description = "A Grass/Poison type Pokémon." },
            new Pokemon { Id = 2, Name = "Charmander", Description = "A Fire type Pokémon." },
            new Pokemon { Id = 3, Name = "Squirtle", Description = "A Water type Pokémon." },
            // Add more Pokémon as needed
        };
    }

    public List<Pokemon> RetrieveRecords()
    {
        return myRecords;
    }
}

internal class Pokemon : IEmbeddable
{
    [VectorStoreRecordKey(StoragePropertyName = "id")]
    public long Id { get; set; }

    [VectorStoreRecordData(StoragePropertyName = "name")]
    public string Name { get; set; }

    [VectorStoreRecordData(StoragePropertyName = "description")]
    public string Description { get; set; }

    [VectorStoreRecordVector(Dimensions: 1024, DistanceFunction.CosineDistance, StoragePropertyName = "descriptionembedding")]
    public ReadOnlyMemory<float> DescriptionEmbedding { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Initialize the Database

Create a DatabaseInitializer class to set up the PostgreSQL database.

For the embedding VECTOR(1024) size, in this case 1024, can be found as embedding_length, embedding_size, vector size, output_dim, hidden size...

For mxbai-embed-large Embedding Model, the dimensions were found here:

BERT embedding length of 1024 in MBAIX Open Large Embed model

internal class DatabaseInitializer
{
    private readonly string _connectionString;

    public DatabaseInitializer(string connectionString)
    {
        _connectionString = connectionString;
    }

    public async Task InitializeDatabaseAsync<T>() where T : IEmbeddable
    {
        using var connection = new NpgsqlConnection(_connectionString);
        await connection.OpenAsync();

        string createTableQuery = @"
            CREATE TABLE IF NOT EXISTS pokemons (
                id BIGINT PRIMARY KEY,
                name TEXT,
                description TEXT,
                descriptionembedding VECTOR(1024)
            );";

        using var command = new NpgsqlCommand(createTableQuery, connection);
        await command.ExecuteNonQueryAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Generate and Store Embeddings

Use the TextEmbedding class to generate embeddings for Pokémon descriptions and store them in the database.

internal class TextEmbedding
{
    public async Task GenerateEmbeddingAndUpsertAsync<T>(
        ITextEmbeddingGenerationService textEmbeddingGenerationService,
        IVectorStoreRecordCollection<long, T> vectorStoreRecordCollection,
        T objectToEmbed) where T : IEmbeddable
    {
        string description = objectToEmbed.Description;

        ReadOnlyMemory<float> searchEmbedding = await textEmbeddingGenerationService.GenerateEmbeddingAsync(description);
        objectToEmbed.DescriptionEmbedding = searchEmbedding;

        await vectorStoreRecordCollection.UpsertAsync(objectToEmbed);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Perform Similarity Search

Retrieve the most relevant Pokémon data based on user input.

public async Task<string> SearchAsync<T>(
    ITextEmbeddingGenerationService textEmbeddingGenerationService,
    IVectorStoreRecordCollection<long, T> vectorStoreRecordCollection,
    string query) where T : IEmbeddable
{
    ReadOnlyMemory<float> queryEmbedding = await textEmbeddingGenerationService.GenerateEmbeddingAsync(query);
    VectorSearchResults<T> results = await vectorStoreRecordCollection.VectorizedSearchAsync(queryEmbedding);
    var resultItems = await results.Results.ToListAsync();

    var topResults = resultItems.Take(3).ToList();

    StringBuilder resultString = new StringBuilder();
    foreach (var result in topResults)
    {
        if (result.Record is Pokemon pokemon)
        {
            resultString.AppendLine($"{pokemon.Name}: {pokemon.Description}");
        }
    }

    return resultString.ToString();
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Integrate Ollama LLM

Use Ollama LLM to generate responses based on user input and retrieved Pokémon data.

internal class LLMCommunicator
{
    private TextEmbedding myTextEmbedding;
    private Kernel myKernel;
    private PostgresVectorStoreRecordCollection<long, Pokemon> myVectorStoreRecordCollection;

    public LLMCommunicator()
    {
        myKernel = InitializeKernel();
        myTextEmbedding = new TextEmbedding();
    }

    public Kernel InitializeKernel()
    {
        IKernelBuilder kernelBuilder = Kernel.CreateBuilder();
        kernelBuilder.Services
            .AddOllamaChatCompletion("gemma3", new Uri("http://localhost:11434"))
            .AddOllamaTextEmbeddingGeneration("mxbai-embed-large", new Uri("http://localhost:11434"));

        return kernelBuilder.Build();
    }

    public async Task AddRecords()
    {
        var ollamaEmbedding = myKernel.Services.GetService<ITextEmbeddingGenerationService>();
        Data data = new Data();

        var databaseInitializer = new DatabaseInitializer("Host=localhost;Port=5433;Username=postgres;Password=admin;Database=postgres;");
        await databaseInitializer.InitializeDatabaseAsync<Pokemon>();

        NpgsqlDataSourceBuilder dataSourceBuilder = new("Host=localhost;Port=5433;Username=postgres;Password=admin;Database=postgres;");
        dataSourceBuilder.UseVector();
        var dataSource = dataSourceBuilder.Build();
        myVectorStoreRecordCollection = new PostgresVectorStoreRecordCollection<long, Pokemon>(dataSource, "pokemons");

        foreach (var record in data.RetrieveRecords())
        {
            await myTextEmbedding.GenerateEmbeddingAndUpsertAsync(ollamaEmbedding, myVectorStoreRecordCollection, record);
        }
    }

    public async Task Run(string userInput)
    {
        var ollamaEmbedding = myKernel.Services.GetService<ITextEmbeddingGenerationService>();
        string mostSimilarDocument = await myTextEmbedding.SearchAsync(ollamaEmbedding, myVectorStoreRecordCollection, userInput);

        var chatCompletionService = myKernel.Services.GetService<IChatCompletionService>();
        string prompt = $"You are a bot that makes Pokémon recommendations. Recommended Pokémon: {mostSimilarDocument}. User input: {userInput}.";

        var settings = new OllamaPromptExecutionSettings { MaxTokens = 4096 };
        var history = new ChatHistory(prompt);
        var responses = await chatCompletionService.GetChatMessageContentsAsync(history, settings, myKernel);

        foreach (var response in responses)
        {
            Console.WriteLine(response.Content);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Run the Solution

In your Program.cs, initialize the LLMCommunicator and run the solution.

internal class Program
{
    static async Task Main(string[] args)
    {
        LLMCommunicator communicator = new LLMCommunicator();
        await communicator.AddRecords();

        Console.WriteLine("Enter your query:");
        string userInput = Console.ReadLine();
        await communicator.Run(userInput);
    }
}
Enter fullscreen mode Exit fullscreen mode

Try it

Now try asking: Could you recommend me a Fire type Pokemon? (For the best results, try to create a Non Existing pokémon, e.g. NonExistant /w Description "A Non Existing Pokemon". Then try: Could you recommend me a Non Existing Pokemon?

Conclusion

This tutorial demonstrates how to build a simple RAG solution using Ollama LLM, mxbai embeddings, pgvector, and Pokémon data. You can extend this solution by adding more data, improving the prompt, or integrating additional features.

Sources

Original sample used (Without Embeding model and vector database): https://learnbybuilding.ai/tutorial/rag-from-scratch/

Top comments (0)