Bouwen van een "Advocaat GPT" voor uw blog - Deel 3: Begrijpen Inbeddingen & Vector Databanken (Nederlands (Dutch))

Bouwen van een "Advocaat GPT" voor uw blog - Deel 3: Begrijpen Inbeddingen & Vector Databanken

Wednesday, 12 November 2025

//

22 minute read

WAARSCHUWING: Deze zijn ontwerp-Posts die 'onthuld'.

Het is waarschijnlijk veel van wat hieronder zal niet werken; IK genereren deze als how-to voor MIJ en dan doen alle stappen en krijg de sample app werken...Je bent stiekem en ze gezien! ze zullen waarschijnlijk klaar zijn medio december.

## Inleiding

Welkom bij deel 3We hebben onze GPU stack werken (Deel 2), en we begrijpen de architectuur (Deel 1****Nu is het tijd om in de magie te duiken die semantisch zoeken mogelijk maakt:inbeddingen.

en

vectordatabases

OPMERKING: Dit maakt deel uit van mijn experimenten met AI (ondersteund opstellen) + mijn eigen bewerking.

Dezelfde stem, hetzelfde pragmatisme, gewoon snellere vingers.

Dit is waar dingen interessant worden.

We zullen begrijpen hoe we tekst als getallen kunnen representeren op een manier die betekenis vangt, niet alleen trefwoorden.

graph TD
    subgraph "2D Embedding Space (simplified)"
        A[cat: 0.8, 0.2]
        B[kitten: 0.7, 0.3]
        C[dog: 0.6, 0.1]
        D[puppy: 0.5, 0.2]
        E[car: -0.5, 0.8]
        F[vehicle: -0.6, 0.7]
        G[database: 0.1, -0.7]
        H[SQL: 0.2, -0.8]
    end

    class A,B cats
    class C,D dogs
    class E,F vehicles
    class G,H tech

    classDef cats stroke:#333
    classDef dogs stroke:#333
    classDef vehicles stroke:#333
    classDef tech stroke:#333

Het is het verschil tussen het vinden van berichten die het woord "docker" bevatten vs het vinden van berichten die semantisch over containerization concepten.

  • Wat zijn inbeddingen?
  • Een inbedding is een numerieke weergave van tekst (of afbeeldingen, audio, enz.) als vector - in wezen een lijst van getallen.
  • De intuïtie
  • Stel je voor dat je woorden plant in een 2D ruimte gebaseerd op hun betekenis:

**Soortgelijke concepten clusteren samen:**Huisdieren (rood/groen) zijn dicht bij elkaar

Voertuigen (blauw) zijn gegroepeerd

Technische termen (geel) cluster

  • Niet-gerelateerde concepten liggen ver uit elkaar
  • Echte inbeddingen
graph LR
    A[Text: 'Docker container'] --> B[Embedding Model]
    B --> C[Vector: 384 floats]

    D[Text: 'containerization'] --> B
    B --> E[Vector: 384 floats]

    C -.Similar.-> E

    F[Text: 'chocolate cake'] --> B
    B --> G[Vector: 384 floats]

    C -.Very Different.-> G

    class B model
    class C,E similar
    class G different

    classDef model stroke:#333,stroke-width:4px
    classDef similar stroke:#333
    classDef different stroke:#333

gebruik 384 tot 1536 afmetingen (niet alleen 2!), het vastleggen van veel meer nuance.

Hoe het werkt**Een inbedding model is een neuraal netwerk getraind om tekst in kaart te brengen naar vectoren die:**Soortgelijke betekenissen → vectoren sluiten

public static float CosineSimilarity(float[] vectorA, float[] vectorB)
{
    if (vectorA.Length != vectorB.Length)
        throw new ArgumentException("Vectors must have same length");

    // Dot product: sum of element-wise multiplication
    float dotProduct = 0;
    for (int i = 0; i < vectorA.Length; i++)
    {
        dotProduct += vectorA[i] * vectorB[i];
    }

    // Magnitude of each vector: sqrt(sum of squares)
    float magnitudeA = 0;
    float magnitudeB = 0;
    for (int i = 0; i < vectorA.Length; i++)
    {
        magnitudeA += vectorA[i] * vectorA[i];
        magnitudeB += vectorB[i] * vectorB[i];
    }
    magnitudeA = MathF.Sqrt(magnitudeA);
    magnitudeB = MathF.Sqrt(magnitudeB);

    // Cosine similarity: dot product / (magnitude_a * magnitude_b)
    return dotProduct / (magnitudeA * magnitudeB);
}

Verschillende betekenissen → verre vectoren:

  • 1.0Gelijksoortigheid meten
  • 0.0We gebruiken
  • -1.0Cosinus-vergelijkbaarheid

om te meten hoe dicht twee vectoren zijn::

var dockerEmbed = new float[] { 0.5f, 0.3f, -0.2f, 0.8f };  // "Docker container"
var containerEmbed = new float[] { 0.45f, 0.35f, -0.18f, 0.75f };  // "containerization"
var cakeEmbed = new float[] { -0.7f, 0.1f, 0.9f, -0.3f };  // "chocolate cake"

Console.WriteLine(CosineSimilarity(dockerEmbed, containerEmbed));  // ~0.95 (very similar!)
Console.WriteLine(CosineSimilarity(dockerEmbed, cakeEmbed));  // ~0.15 (unrelated)

Cosinus overeenkomst varieert van -1 tot 1

= identieke betekenis

= niet gerelateerd

= tegenovergestelde betekenis (zeldzaam in de praktijk)

  • Voorbeeld
  • Waarom inbeddingen perfect zijn voor ons gebruik geval
  • Onthoud, we bouwen een schrijfassistent.
  • Als ik begin met schrijven:

**"In deze post zal ik laten zien hoe entiteit Framework te gebruiken met..."**Het systeem zou eerder berichten moeten vinden over:

Entiteitskader

graph TB
    subgraph "Traditional Keyword Search"
        A1[Query: 'Docker setup'] --> B1[Find: 'Docker' OR 'setup']
        B1 --> C1[❌ Misses: 'containerization guide']
        B1 --> D1[❌ Misses: 'running containers']
        B1 --> E1[✅ Finds: 'Docker setup tutorial']
    end

    subgraph "Embedding-Based Semantic Search"
        A2[Query: 'Docker setup'] --> B2[Generate embedding]
        B2 --> C2[Find similar embeddings]
        C2 --> D2[✅ Finds: 'containerization guide']
        C2 --> E2[✅ Finds: 'running containers']
        C2 --> F2[✅ Finds: 'Docker setup tutorial']
    end

    class B2,C2 semantic

    classDef semantic stroke:#333,stroke-width:2px

Toegangspatronen voor databases

ORM-configuraties

  1. Gerelateerde ASP.NET kernthema'sNiet alleen het sleutelwoord komt overeen!
  2. **Het moet begrijpen dat "EF Core migraties" gerelateerd is, ook al staat er geen "Entity Framework."**Inbeddingen vs Traditioneel zoeken
  3. Een inbeddingsmodel kiezenEr zijn veel inbeddingsmodellen.
  4. **We hebben er één nodig die:**Loopt lokaal

(privacy + snelheid)

Werkt met ONNX Runtime |-------|------------|------|---------|-------| | (zodat we onze GPU kunnen gebruiken) | 384 | 80MB | Good | Very Fast ⚡⚡⚡ | | Goede kwaliteit | 768 | 420MB | Better | Fast ⚡⚡ | | (nauwkeurig semantisch begrip) | 384 | 133MB | Better | Very Fast ⚡⚡⚡ | | Juiste grootte | 768 | 436MB | Best | Fast ⚡⚡ | | (384-768 afmetingen is goed evenwicht) | 1536 | N/A (API) | Excellent | Slow (network) ⚡ |

Populaire opties: Model Afmetingen Grootte Kwaliteit Snelheid

  • all-MiniLM-L6-v2
  • all-mpnet-base-v2
  • bge-small-en-v1.5
  • bge-base-en-v1.5

OpenAI text-embedding-ada-002

Mijn aanbeveling

bge-base-en-v1.5:

pip install optimum[exporters]

State-of-the-art open source model:

optimum-cli export onnx --model BAAI/bge-base-en-v1.5 --task feature-extraction bge-base-en-onnx/

768 afmetingen (goed evenwicht)

bge-base-en-onnx/
    model.onnx           # The neural network
    tokenizer.json       # Text → tokens converter
    tokenizer_config.json
    special_tokens_map.json
    config.json

Werkt geweldig met ONNX Runtime: Vrij en loopt lokaal

Omzetten naar ONNX-formaat

De meeste modellen zijn in PyTorch formaat.

We hebben ONNX nodig voor C#.

mkdir EmbeddingTest
cd EmbeddingTest
dotnet new console
dotnet add package Microsoft.ML.OnnxRuntime.Gpu --version 1.16.3
dotnet add package Microsoft.ML.Tokenizers --version 0.1.0-preview.23511.1

Optimum installeren (Python-bibliotheek voor conversie)

  • OnnxRuntime.GpuOmzetten BGE model
  • Microsoft.ML.TokenizersDit creëert:

Downloaden als u niet wilt converteren

Veel modellen zijn voorgeconverteerd en beschikbaar op Hugging Face met "onnx" in de naam.

using Microsoft.ML.Tokenizers;
using System;
using System.Linq;

public class SimpleTokenizer
{
    private readonly Tokenizer _tokenizer;

    public SimpleTokenizer(string tokenizerPath)
    {
        // Load the tokenizer.json file
        _tokenizer = Tokenizer.CreateTokenizer(tokenizerPath);
    }

    public (long[] InputIds, long[] AttentionMask) Tokenize(string text, int maxLength = 512)
    {
        // Tokenize the text
        var encoding = _tokenizer.Encode(text);

        // Get token IDs
        var ids = encoding.Ids.Select(i => (long)i).ToArray();

        // Pad or truncate to maxLength
        var inputIds = new long[maxLength];
        var attentionMask = new long[maxLength];

        int length = Math.Min(ids.Length, maxLength);

        // Copy actual tokens
        Array.Copy(ids, inputIds, length);

        // Set attention mask (1 = real token, 0 = padding)
        for (int i = 0; i < length; i++)
        {
            attentionMask[i] = 1;
        }

        return (inputIds, attentionMask);
    }
}

Inbeddingen in C# gebruiken

  1. **Laten we een praktische inbedding generator bouwen met behulp van ONNX Runtime.**Projectopstelling[101, 8667, 2088, 102]

    • Waarom deze pakketten?
      • Start het model op GPU[- Tekst omzetten naar token-ID's (modelinvoerformaat)[Tokenization first
  2. **Voor inbedding moeten we token (tekst omzetten naar getallen):**Wat gebeurt hier?

    • Codering
      • "Hallo world" →
  3. Elk nummer is een token ID uit de woordenschat van het modelSpeciale penningen: 101 =

    • 1CLS], 102 =
    • 0SEP]
graph LR
    A["Text: 'Docker setup'"] --> B[Tokenizer]
    B --> C[Token IDs:<br/>101, 12849, 12229, 102]
    C --> D[Pad to 512]
    D --> E[Input IDs:<br/>101, 12849, 12229, 102, 0, 0,...]
    D --> F[Attention Mask:<br/>1, 1, 1, 1, 0, 0,...]

    E --> G[Feed to Model]
    F --> G

    class B,G process

    classDef process stroke:#333,stroke-width:2px

Opvulling

  • Modellen verwachten invoer met vaste lengte
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using System;
using System.Collections.Generic;
using System.Linq;

public class EmbeddingGenerator : IDisposable
{
    private readonly InferenceSession _session;
    private readonly SimpleTokenizer _tokenizer;
    private readonly int _embeddingDimension;

    public EmbeddingGenerator(string modelPath, string tokenizerPath, bool useGpu = true)
    {
        // Setup session options
        var options = new SessionOptions();
        if (useGpu)
        {
            options.AppendExecutionProvider_CUDA(0);
        }

        // Load model
        _session = new InferenceSession(modelPath, options);

        // Load tokenizer
        _tokenizer = new SimpleTokenizer(tokenizerPath);

        // Get embedding dimension from model output shape
        var outputMetadata = _session.OutputMetadata["last_hidden_state"];
        _embeddingDimension = outputMetadata.Dimensions[2]; // Usually 768 for base models
    }

    public float[] GenerateEmbedding(string text)
    {
        // Step 1: Tokenize
        var (inputIds, attentionMask) = _tokenizer.Tokenize(text);

        // Step 2: Create input tensors
        var inputIdsTensor = new DenseTensor<long>(inputIds, new[] { 1, inputIds.Length });
        var attentionMaskTensor = new DenseTensor<long>(attentionMask, new[] { 1, attentionMask.Length });

        var inputs = new List<NamedOnnxValue>
        {
            NamedOnnxValue.CreateFromTensor("input_ids", inputIdsTensor),
            NamedOnnxValue.CreateFromTensor("attention_mask", attentionMaskTensor)
        };

        // Step 3: Run inference
        using var results = _session.Run(inputs);

        // Step 4: Extract embeddings from output
        var outputTensor = results.First().AsTensor<float>();

        // Output shape is [batch_size, sequence_length, embedding_dim]
        // We want [batch_size, embedding_dim] by mean pooling

        return MeanPooling(outputTensor, attentionMask);
    }

    private float[] MeanPooling(Tensor<float> outputTensor, long[] attentionMask)
    {
        int seqLength = outputTensor.Dimensions[1];
        int embeddingDim = outputTensor.Dimensions[2];

        var embedding = new float[embeddingDim];
        int tokenCount = 0;

        // Average across all non-padded tokens
        for (int seq = 0; seq < seqLength; seq++)
        {
            if (attentionMask[seq] == 0) continue; // Skip padding

            tokenCount++;
            for (int dim = 0; dim < embeddingDim; dim++)
            {
                embedding[dim] += outputTensor[0, seq, dim];
            }
        }

        // Divide by count to get mean
        for (int dim = 0; dim < embeddingDim; dim++)
        {
            embedding[dim] /= tokenCount;
        }

        // Normalize to unit length (common practice)
        return Normalize(embedding);
    }

    private float[] Normalize(float[] vector)
    {
        float magnitude = 0;
        foreach (var val in vector)
        {
            magnitude += val * val;
        }
        magnitude = MathF.Sqrt(magnitude);

        var normalized = new float[vector.Length];
        for (int i = 0; i < vector.Length; i++)
        {
            normalized[i] = vector[i] / magnitude;
        }

        return normalized;
    }

    public void Dispose()
    {
        _session?.Dispose();
    }
}

Als de tekst kort is: pad met nullen:

  1. Als de tekst lang is: afgekortAttentiemasker
  2. - Vertelt het model welke tokens echt zijn vs vulling= dit token verwerken
  3. **= negeren (opvulling)**Generatorklasse inbedding
  4. **Nu de volledige inbedding pijplijn:**Opsplitsing van de code
  5. Modelbelasting- Creëert ONNX-sessie met GPU-ondersteuning
  6. Tokenisatie- Converteert tekst naar token ID's

Aanmaken van tensor

graph TB
    A[Model Output:<br/>Token Embeddings] --> B["Token 0 (CLS):<br/>(0.1, 0.5, -0.3, ...)"]
    A --> C["Token 1 (Docker):<br/>(0.4, 0.2, -0.1, ...)"]
    A --> D["Token 2 (setup):<br/>(0.3, 0.6, -0.2, ...)"]
    A --> E["Token 3 (SEP):<br/>(0.2, 0.3, -0.4, ...)"]

    B --> F[Average]
    C --> F
    D --> F
    E --> F

    F --> G["Sentence Embedding:<br/>(0.25, 0.4, -0.25, ...)"]

    class A input
    class F process
    class G output

    classDef input stroke:#333
    classDef process stroke:#333,stroke-width:2px
    classDef output stroke:#333,stroke-width:2px
  • Vormen van gegevens voor modelinvoer

  • Conclusie

    • Hij leidt het neurale netwerk.
  • Gemiddelde pooling

- Gemiddelden token inbeddingen → enkele zin inbedding

using System;

class Program
{
    static void Main(string[] args)
    {
        using var embedder = new EmbeddingGenerator(
            modelPath: "bge-base-en-onnx/model.onnx",
            tokenizerPath: "bge-base-en-onnx/tokenizer.json",
            useGpu: true
        );

        // Generate embeddings
        var embedding1 = embedder.GenerateEmbedding("Docker containerization tutorial");
        var embedding2 = embedder.GenerateEmbedding("Setting up containers with Docker");
        var embedding3 = embedder.GenerateEmbedding("Baking a chocolate cake");

        Console.WriteLine($"Embedding dimension: {embedding1.Length}");
        Console.WriteLine($"First 5 values: {string.Join(", ", embedding1.Take(5).Select(f => f.ToString("F4")))}");

        // Calculate similarities
        float sim12 = CosineSimilarity(embedding1, embedding2);
        float sim13 = CosineSimilarity(embedding1, embedding3);

        Console.WriteLine($"\nSimilarity (Docker vs Containers): {sim12:F4}");  // ~0.85
        Console.WriteLine($"Similarity (Docker vs Cake): {sim13:F4}");  // ~0.10
    }

    static float CosineSimilarity(float[] a, float[] b)
    {
        // Since vectors are normalized, dot product = cosine similarity
        float dot = 0;
        for (int i = 0; i < a.Length; i++)
        {
            dot += a[i] * b[i];
        }
        return dot;
    }
}

Normalisatie:

Embedding dimension: 768
First 5 values: 0.0123, -0.0456, 0.0789, -0.0234, 0.0567

Similarity (Docker vs Containers): 0.8542
Similarity (Docker vs Cake): 0.1023
  • Maakt vectorlengte 1 (vereenvoudigt overeenkomst berekening)

Gemiddelde pooling uitgelegd

We gemiddelden omdat:

Elke token heeft zijn eigen inbedding

We hebben een inbedding nodig voor de hele zin.

Averaging vangt de algemene betekenis op:

float bestSimilarity = -1;
int bestIndex = -1;

for (int i = 0; i < 10000; i++)
{
    float sim = CosineSimilarity(queryEmbedding, storedEmbeddings[i]);
    if (sim > bestSimilarity)
    {
        bestSimilarity = sim;
        bestIndex = i;
    }
}

GebruiksvoorbeeldUitvoer

Prachtig!

  • Semantisch vergelijkbare inhoud heeft een hoge gelijkenis, niet-verbonden inhoud is laag.
  • Vectordatabases

Nu hebben we inbeddingen.

We moeten er miljoenen opslaan en efficiënt zoeken.

Het probleem

graph TB
    A[Query Embedding] --> B[Vector Database]
    B --> C{HNSW Index}

    C --> D[Layer 2:<br/>Coarse Search]
    D --> E[Layer 1:<br/>Refined Search]
    E --> F[Layer 0:<br/>Exact Search]

    F --> G[Top K Results]

    H[10,000 vectors] -.Indexed.-> C

    class B db
    class C index
    class G results

    classDef db stroke:#333,stroke-width:4px
    classDef index stroke:#333,stroke-width:2px
    classDef results stroke:#333,stroke-width:2px

Laten we zeggen dat we 1000 blog posts hebben, elk in stukken verdeeld in 10 stuks = 10.000 inbeddingen.:

  • Naïef zoeken
  • Probleem
  • : Dit is O(n) - we controleren elke inbedding.

Langzaam!

Voor 10.000 inbeddingen × 768 afmetingen elk:

~30 miljoen transacties met drijvende punten

  1. ~50-100ms zelfs op snelle CPU
  2. We hebben iets sneller nodig.
  3. Vectordatabase-oplossing
  4. Vector databases maken gebruik van slimme data structuren (zoals HNSW - Hierarchical Navigable Small World grafieken) om de dichtstbijzijnde buren in O(log n) tijd te vinden.

Snelheidsvergelijking |----------|------------|------------|-------------|---------| | Naïef zoeken: 50-100ms voor 10K vectoren | ✅ Excellent | Docker | Very Fast | Apache 2.0 | | Vector DB (HNSW): 1-5ms voor 10K vectoren | ✅ (via Npgsql) | Postgres extension | Fast | PostgreSQL License | | 10-50x sneller! | ✅ Good | Docker | Very Fast | BSD-3 | | En het schalen: miljoen vectoren nog steeds slechts ~10-20ms. | ⚠️ Limited | Docker/K8s | Very Fast | Apache 2.0 | | Een vectordatabase kiezen | ❌ Python-first | Docker | Fast | Apache 2.0 |

Voor ons C# project hebben we:

  • Goede C#-clientbibliotheekQdrant.Client)
  • Gemakkelijk in te zetten (Dokter)
  • Goede prestaties
  • Vrije / open bron
  • Database C# Support Deployment Performance License

Qdrant

  • pgvector
  • Weaviate
  • Milvus

ChromaMijn keuze: QdrantUitstekende C# client (

Eenvoudige inzet van Docker

Speciaal gebouwd voor vector zoeken (niet aangekoppeld)

docker run -p 6333:6333 -p 6334:6334 \
    -v $(pwd)/qdrant_storage:/qdrant/storage \
    qdrant/qdrant

Geweldige documentatie:

  • 6333Actieve ontwikkeling
  • 6334Alternatief: pgvector

**We gebruiken PostgreSQL al voor de blog!**Kan alles in één database bewaren./qdrant_storageIets minder performant maar eenvoudiger architectuur

Voor deze serie, zal ik gebruiken

dotnet add package Qdrant.Client --version 1.7.0

Qdrant

omdat het is ontworpen en gemakkelijker om de concepten te begrijpen.

using Qdrant.Client;
using Qdrant.Client.Grpc;

public class QdrantSetup
{
    private readonly QdrantClient _client;

    public QdrantSetup(string host = "localhost", int port = 6334)
    {
        _client = new QdrantClient(host, port);
    }

    public async Task CreateCollectionAsync(string collectionName, ulong vectorSize)
    {
        // Check if collection exists
        var collections = await _client.ListCollectionsAsync();
        if (collections.Any(c => c.Name == collectionName))
        {
            Console.WriteLine($"Collection '{collectionName}' already exists");
            return;
        }

        // Create collection
        await _client.CreateCollectionAsync(
            collectionName: collectionName,
            vectorsConfig: new VectorParams
            {
                Size = vectorSize,  // 768 for bge-base
                Distance = Distance.Cosine  // Cosine similarity
            }
        );

        Console.WriteLine($"Created collection '{collectionName}' with {vectorSize} dimensions");
    }
}

Maar ik zal pgvector laten zien als alternatief.:

  • SizeQdrant instellen
  • DistanceDocker Implementatie
    • Distance.CosineHavens
    • Distance.Euclid- REST API
    • Distance.Dot- gRPC API (sneller, we zullen dit gebruiken)

Opslag

using Qdrant.Client.Grpc;
using System.Collections.Generic;

public class QdrantInserter
{
    private readonly QdrantClient _client;

    public QdrantInserter(QdrantClient client)
    {
        _client = client;
    }

    public async Task InsertBlogChunkAsync(
        string collectionName,
        ulong id,
        float[] embedding,
        string blogPostSlug,
        string chunkText,
        int chunkIndex)
    {
        var point = new PointStruct
        {
            Id = id,
            Vectors = embedding,
            Payload =
            {
                ["blog_post_slug"] = blogPostSlug,
                ["chunk_text"] = chunkText,
                ["chunk_index"] = chunkIndex,
                ["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
            }
        };

        await _client.UpsertAsync(collectionName, new[] { point });
    }

    public async Task InsertBatchAsync(
        string collectionName,
        List<(ulong id, float[] embedding, Dictionary<string, object> payload)> points)
    {
        var qdrantPoints = points.Select(p => new PointStruct
        {
            Id = p.id,
            Vectors = p.embedding,
            Payload = { p.payload }
        }).ToList();

        // Batch insert for efficiency
        await _client.UpsertAsync(collectionName, qdrantPoints);

        Console.WriteLine($"Inserted {points.Count} points");
    }
}

: Persist data to:

  • op host
  • C# Client-instellingen
  • Een verzameling aanmaken

Een verzameling is als een tabel - het bevat vectoren van een specifieke dimensie.:

  • Belangrijkste parameters
    • Moet overeenkomen met uw inbedding model (768 voor bge-base)

- Hoe de gelijkenis te meten:

public class QdrantSearcher
{
    private readonly QdrantClient _client;

    public QdrantSearcher(QdrantClient client)
    {
        _client = client;
    }

    public async Task<List<SearchResult>> SearchAsync(
        string collectionName,
        float[] queryEmbedding,
        int topK = 10)
    {
        var searchResult = await _client.SearchAsync(
            collectionName: collectionName,
            vector: queryEmbedding,
            limit: (ulong)topK,
            scoreThreshold: 0.7f  // Only return if similarity > 0.7
        );

        return searchResult.Select(r => new SearchResult
        {
            Id = r.Id.Num,
            Score = r.Score,
            BlogPostSlug = r.Payload["blog_post_slug"].StringValue,
            ChunkText = r.Payload["chunk_text"].StringValue,
            ChunkIndex = (int)r.Payload["chunk_index"].IntegerValue
        }).ToList();
    }

    public async Task<List<SearchResult>> SearchWithFilterAsync(
        string collectionName,
        float[] queryEmbedding,
        string blogPostSlug,  // Only search within this post
        int topK = 5)
    {
        var filter = new Filter
        {
            Must =
            {
                new Condition
                {
                    Field = new FieldCondition
                    {
                        Key = "blog_post_slug",
                        Match = new Match { Keyword = blogPostSlug }
                    }
                }
            }
        };

        var searchResult = await _client.SearchAsync(
            collectionName: collectionName,
            vector: queryEmbedding,
            filter: filter,
            limit: (ulong)topK
        );

        return searchResult.Select(r => new SearchResult
        {
            Id = r.Id.Num,
            Score = r.Score,
            BlogPostSlug = r.Payload["blog_post_slug"].StringValue,
            ChunkText = r.Payload["chunk_text"].StringValue,
            ChunkIndex = (int)r.Payload["chunk_index"].IntegerValue
        }).ToList();
    }
}

public class SearchResult
{
    public ulong Id { get; set; }
    public float Score { get; set; }
    public string BlogPostSlug { get; set; }
    public string ChunkText { get; set; }
    public int ChunkIndex { get; set; }
}

- Cosinus overeenkomst (meest voorkomende):

  • limit- Euclidische afstand
  • scoreThreshold- Puntproduct
  • filterVectoren invoegen

Payload uitgelegd

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        // Setup
        var embedder = new EmbeddingGenerator(
            "bge-base-en-onnx/model.onnx",
            "bge-base-en-onnx/tokenizer.json",
            useGpu: true
        );

        var client = new QdrantClient("localhost", 6334);
        var searcher = new QdrantSearcher(client);

        // User query
        string query = "How do I set up Docker with ASP.NET Core?";

        // Generate query embedding
        Console.WriteLine($"Searching for: {query}");
        var queryEmbedding = embedder.GenerateEmbedding(query);

        // Search
        var results = await searcher.SearchAsync(
            collectionName: "blog_embeddings",
            queryEmbedding: queryEmbedding,
            topK: 5
        );

        // Display results
        Console.WriteLine($"\nFound {results.Count} results:\n");

        foreach (var result in results)
        {
            Console.WriteLine($"Score: {result.Score:F4}");
            Console.WriteLine($"Post: {result.BlogPostSlug}");
            Console.WriteLine($"Chunk: {result.ChunkText.Substring(0, Math.Min(100, result.ChunkText.Length))}...");
            Console.WriteLine();
        }
    }
}

Zoals metadata verbonden aan elke vector:

Searching for: How do I set up Docker with ASP.NET Core?

Found 5 results:

Score: 0.8923
Post: dockercomposedevdeps
Chunk: In this post, I'll show you how to set up a development environment using Docker Compose. This is p...

Score: 0.8654
Post: dockercompose
Chunk: Docker Compose is a tool for defining and running multi-container Docker applications. With Compose...

Score: 0.8102
Post: addingentityframeworkforblogpostspt1
Chunk: You can set it up either as a windows service or using Docker as I presented in a previous post on...

Score: 0.7891
Post: imagesharpwithdocker
Chunk: When running ASP.NET Core applications in Docker containers, you may encounter issues with ImageSha...

Score: 0.7654
Post: selfhostingseq
Chunk: I use Docker Compose to run all my services. Here's the relevant part of my docker-compose.yml file...

Kan alles opslaan: posttitel, brok tekst, datum, categorieën

Doorzoekbaar en filterbaar!

Partij invoegen

Veel sneller dan een-voor-een

public class BatchEmbeddingGenerator
{
    private readonly EmbeddingGenerator _embedder;

    public BatchEmbeddingGenerator(EmbeddingGenerator embedder)
    {
        _embedder = embedder;
    }

    public List<float[]> GenerateBatch(List<string> texts, int batchSize = 32)
    {
        var embeddings = new List<float[]>();

        for (int i = 0; i < texts.Count; i += batchSize)
        {
            var batch = texts.Skip(i).Take(batchSize).ToList();

            foreach (var text in batch)
            {
                embeddings.Add(_embedder.GenerateEmbedding(text));
            }

            Console.WriteLine($"Processed {Math.Min(i + batchSize, texts.Count)} / {texts.Count}");
        }

        return embeddings;
    }
}

Qdrant verwerkt batches van 100-1000 efficiënt

  • Zoeken naar vectoren
  • Zoekfuncties
    • Geef boven K het meest gelijkaardig terug

- Alleen retourneren als de overeenkomst de drempel overschrijdt

  • Filteren op metadata (bv. alleen zoeken naar specifieke berichten)
using System.Security.Cryptography;
using System.Text;

public class EmbeddingCache
{
    private readonly Dictionary<string, float[]> _cache = new();

    public float[] GetOrGenerate(string text, Func<string, float[]> generator)
    {
        string hash = ComputeHash(text);

        if (_cache.TryGetValue(hash, out var cached))
        {
            return cached;
        }

        var embedding = generator(text);
        _cache[hash] = embedding;

        return embedding;
    }

    private string ComputeHash(string text)
    {
        using var sha256 = SHA256.Create();
        var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(text));
        return Convert.ToBase64String(bytes);
    }
}

Compleet voorbeeld: Search Pipeline

Uitvoer

Perfect!

CREATE EXTENSION vector;

Het vond relevante berichten over Docker en ASP.NET Core, hoewel de exacte zin niet verscheen.

CREATE TABLE blog_embeddings (
    id SERIAL PRIMARY KEY,
    blog_post_slug VARCHAR(255),
    chunk_text TEXT,
    chunk_index INT,
    embedding VECTOR(768)  -- 768 dimensions
);

-- Create HNSW index for fast search
CREATE INDEX ON blog_embeddings USING hnsw (embedding vector_cosine_ops);

Optimalisatie van de prestaties

using Npgsql;
using Pgvector;

public async Task InsertEmbeddingAsync(
    string slug,
    string chunkText,
    int chunkIndex,
    float[] embedding)
{
    await using var conn = new NpgsqlConnection(connectionString);
    await conn.OpenAsync();

    await using var cmd = new NpgsqlCommand(
        "INSERT INTO blog_embeddings (blog_post_slug, chunk_text, chunk_index, embedding) VALUES ($1, $2, $3, $4)",
        conn
    )
    {
        Parameters =
        {
            new() { Value = slug },
            new() { Value = chunkText },
            new() { Value = chunkIndex },
            new() { Value = new Vector(embedding) }
        }
    };

    await cmd.ExecuteNonQueryAsync();
}

Batch-inbeddingsgeneratie

public async Task<List<SearchResult>> SearchAsync(float[] queryEmbedding, int topK = 10)
{
    await using var conn = new NpgsqlConnection(connectionString);
    await conn.OpenAsync();

    await using var cmd = new NpgsqlCommand(
        @"SELECT blog_post_slug, chunk_text, chunk_index,
                 1 - (embedding <=> $1) as similarity
          FROM blog_embeddings
          ORDER BY embedding <=> $1
          LIMIT $2",
        conn
    )
    {
        Parameters =
        {
            new() { Value = new Vector(queryEmbedding) },
            new() { Value = topK }
        }
    };

    var results = new List<SearchResult>();

    await using var reader = await cmd.ExecuteReaderAsync();
    while (await reader.ReadAsync())
    {
        results.Add(new SearchResult
        {
            BlogPostSlug = reader.GetString(0),
            ChunkText = reader.GetString(1),
            ChunkIndex = reader.GetInt32(2),
            Score = reader.GetFloat(3)
        });
    }

    return results;
}

**<=>Genereer geen inbeddingen één voor één.**Batch ze! **1 - distance**Waarom?

GPU-gebruik: houdt GPU bezig

Geheugenefficiëntie: hergebruikt buffers

  1. ✅ What embeddings are and why they enable semantic search
  2. ✅ How to choose and convert an embedding model to ONNX
  3. ✅ Generating embeddings in C# with ONNX Runtime
  4. ✅ Vector database concepts (HNSW, similarity search)
  5. ✅ Using Qdrant for vector storage and search
  6. ✅ Alternative: pgvector in PostgreSQL
  7. ✅ Performance optimization (batching, caching)

Voortgangstracking: feedback van de gebruiker

Caching Inbeddingen

Inbeddingen niet regenereren voor ongewijzigde inhoud!**pgvector alternatief**Als je alles in PostgreSQL wilt houden:

  • Pgvector-extensie installeren
  • Tabel aanmaken
  • Invoegen vanuit C#
  • Zoeken met pgvectorexploitant
  • : Cosinusafstand (lager is meer vergelijkbaar)
  • : Converteren naar gelijkenis score (hoger is beter)

Samenvatting

We hebben het over:

Qdrant

Deel 2: GPU-instellingen & CUDA in C#Deel 3: Inbeddingen en vectordatabases begrijpen!

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.