Back to "Att bygga en "Lawyer GPT" för din blogg | - | Del |3: | Understanding Embeddings | МSK4 | Vektordatabas"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

AI AI-Article C# Embeddings LLM mostlylucid.blogllm Vector Database

Att bygga en "Lawyer GPT" för din blogg | - | Del |3: | Understanding Embeddings | МSK4 | Vektordatabas

Wednesday, 12 November 2025

Inleiding

Välkomna till del 3! Vi' har fått vår GPU-stack igång (Del 2), och vi förstår arkitekturenDel 1). Nu är det tid att dyka in i magin som gör det semantiska att söka inbäddar och vektordatabaser.

NOTET: Det här är en del av mina experiment med AI ( assisterad redigering ) + mitt eget redigerande M SK4 Samma röst, , samma pragmatism,; bara snabbare fingrar

Det är här som saker blir intressanta.., vi kommer att förstå hur man representerar text som siffror på ett sätt som fångar meningen.,, inte bara nyckeltal, ., det är skillnaden mellan att hitta meddelanden som innehåller ordet "" " docker "", " och att hitta nyheter som handlar semantiskt om containeriseringskonzepter.

Vad är inbäddar?

En inbäddar är en numerisk representation av text ( eller bilder , ljud ♫ ♫ , ♫ etc ♫

Intuitionen

Föreställ dig att rita ord i ett 2D utrymme baserat på deras mening.

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

Samma koncept samlas i en kluster

  • Hundar (rött/ gröntM SK2 är nära varandra
  • Transporter (blueM SK1 är grouperade
  • tekniska termer (yellow) kluster
  • Orelaterade koncept är långt från varandra

Verkliga inbäddar använda 384 till | 1536 | dimensioner | ( | inte bara | МSK3 | för att ta upp mycket mer nyanser

Hur det fungerar

En inbäddad modell är ett neuronnätverk som tränas att kartlägga text till vektorer som att:

  • liknande betydelser → närma vektorer
  • Olika betydelser → avlägsna vektorer
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

mäta likhet

Vi använder kosine likhet för att mäta hur nära två vektorer är:

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);
}

Kosine likheter varierar från -1 till 1:

  • 1.0 = samma mening
  • 0.0 = orelaterade
  • -1.0 = motsatsen (ovanlig i praktiken

exempel:

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)

Varför är inbäddar perfekta för vårt användande

Kom ihåg att vi bygger en skrivassistent när jag börjar skriva.

"I det här постetM SK1 Jag ' ska visa hur man använder "" Entity Framework "" med

Systemet borde hitta tidigare Beiträge om:

  • Enhetens ram
  • Tillämpningsmönster för databaser
  • ORM-konfigurationer
  • Related ASP.NET Core topics

Inte bara Keyword matches! Den borde förstå att "EF Core- migrationer " är besläktade även om den inte säger '

Embeddings mot traditionella sökningar

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

Att välja en inbäddad modell

Det finns många inbäddade modeller.

  1. kör lokalt ( integritet + hastighet
  2. Fungerar med ONNX Runtime ( så vi kan använda vår GPU
  3. Bra kvalitet ( korrekt semantisk förståelse
  4. högra storlek (384-768 dimensioner är bra balans

Beliebta alternativ

Modell Dimensioner Storleken
alla-MiniLM 384 ♫ ♫ ♫
alla-mpnet 768 ♫ ♫ ♫
bge- 384 ♫ ♫ ♫
bge-bas 768 ♫ ♫ ♫
OpenAI text 1536 ♫ ♫ N ♫

Min suositus: bge-bas

  • "Status-" av - och - av den nya öppen källkodsmodellen
  • 768 dimensioner ( bra balans
  • Det fungerar jättebra med ONNX Runtime
  • fri och kör lokalt

Omvandling till ONNX format

De flesta modeller är i PyTorch format.

Installera Optimum (Python bibliotek för konvertering:

pip install optimum[exporters]

Konvertera BGE-modellen:

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

Detta skapar:

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

ladda ner om du inte vill konvertera: Många modeller är förvandlade innan "-", och är tillgängliga på "" Hugging Face "" med "",' onnx ', ",' under namnet

Att använda inbäddar i C#

Låt oss bygga en praktisk embedding-generator med hjälp av ONNX Runtime

Projektuppsättning

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

Varför dessa paket?

  • OnnxRuntime.Gpu - kör modellen på GPU
  • Microsoft.ML.Tokenizers - Konvertera text till token-IDs modellinputsformat

Markering först

Innan vi lägger in , måste vi tokenisera ( omvandla text till siffror

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);
    }
}

Vad händer här?

  1. Kodering - "Hallo i världen [101, 8667, 2088, 102]

    • Varje tal är en token ID från modellens ordbok
    • Speciella tokens: M SK1 | = [CLS], 102 = [SEP]
  2. Belagning - Modeller förväntar sig ett fixerat intryck

    • Om text är kort: platta med noll
    • Om texten är lång: förkorta
  3. uppmärksamhetsmask - Berättar för modellen vilka tokens som är riktiga jämfört med padding

    • 1 = processa den här token
    • 0 = ignorera
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

Embedding Generator klass

Nu kommer fulla embedding-rörsledningar

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();
    }
}

Kodfördelning:

  1. Modelllagning - skapar en ONNX sessie med GPU support
  2. Tokenisering - Konverterar text till token IDs
  3. Tensor skapande - Former data för modellinput
  4. Inferens - kör nervnätverket
  5. Kollning - Föreställer tokenbeläggningar → en enda meningsbeläggning
  6. Normalisering - gör att vektorlängden 1 | | | förenklar likhetsberäkningar

Meranpoolning förklarad

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

Vi räknar genomsnittet eftersom

  • Varje token har sin egen inbäddning
  • Vi behöver ONE inbäddar för hela meningen.
  • Föreställande fångar hela meningen

Till exempel:

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;
    }
}

Utgång:

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

Skönt! Den semantiskt liknande innehållet har hög likhet , den orelaterade innehållen är låga .

Vektordatabaser

Nu har vi inbäddar.

Problemet

Låt oss säga att vi har "1000", blogposter, , , och de är uppdelade i "10".

Enkelt sökande:

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;
    }
}

Problemet: Det här är OM SK1n) - vi ser varenda inbäddning

För 10,000 inbäddar | × | | 768 | varje dimension

  • ~30 miljoner floating point operationer
  • ~50-100ms även på snabb CPU

Vi behöver något snabbare.

Vektordatabaslösning

Vektordatabaser använder smarta datastrukturer (liknande HNSW | - | Hierarkiska navigerabla små världar | ) | för att hitta närmaste grannar i O

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

Vergleich av hastighet:

  • Enkelt sökande: 50-100ms för 10K vektorer
  • Vektor DB (HNSW): 1-5ms för 10K-vektorer
  • 10-50x snabbareM SK1

Och den skalar : miljoner vektorer fortfarande bara tar ~10-20msM SK2

Att välja en vektordatabas

För vårt C-projekt, #, behöver vi

  1. Bra C# klientbibliotek
  2. Lika att utsätta (Docker)
  3. Bra prestationer
  4. Gratis / öppen källkod
Databas ♫ ♫ C ♫ МSK2 ♫ Support ♫ ♫ Deployment ♫
Qdrant ✅ Utmärkt Docker Mycket snabb Apache 2.0
pgvector ♫ ✅ ♫ ♫ МSK2 ♫ via Npgsql ♫
Veva ✅ Bra Docker Väldigt snabbt BSD
Milvus ⚠️ Limiterad Docker МSK3 K 8 Mycket snabb Apache
Chroma ❌ Python-first Docker ♫ ♫ Fast ♫ {\displaystyle } Apache ♫

Mitt val: Qdrant

  • Utmärkt C# klient (Qdrant.Client)
  • Enkel Dockerimplementering
  • Utformad specifikt för vektorsökningar ( inte bostad på)
  • Lysande dokumentation
  • Aktiv utveckling

Alternativt: pgvektor

  • Vi' använder redan PostgreSQL för bloggen
  • kunde hålla allt i en enda databas
  • Lite mindre fungerande men enklare arkitektur

För den här serien ska jag använda Qdrant eftersom det är konstruerat och lättare att förstå konceptet. Men jag ska visa pgvector som ett alternativ.

Att sätta upp Qdrant

Dockerimplementering

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

Porter:

  • 6333 - REST API
  • 6334 - gRPC API snabbare

Storage: Håller data till ./qdrant_storage på hemma

C# Client Setup

dotnet add package Qdrant.Client --version 1.7.0

Att skapa en samling

En samling är som en tabell - den har vektorer av en specifik dimension .

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");
    }
}

Nyckelparameter:

  • Size - måste matcha din inbäddade modell 768 för bge
  • Distance - Hur man mäter likhet
    • Distance.Cosine - Kosineliknande vanligaste
    • Distance.Euclid - Euklidiska avstånd
    • Distance.Dot - Dot-produkt

Insering av vektorer

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");
    }
}

Belöningslast förklarad:

  • Som metadata som fästes vid varje vektor
  • Kan lagra vad som helst
  • sökbart och filtrerat!

Batchinsertion:

  • Mycket snabbare än en
  • Qdrant hanterar lager av 100-1000 effektivt

Jag letar efter vektorer

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; }
}

Sökfunktionen:

  • limit - Kom tillbaka till toppen K mest lik
  • scoreThreshold - Bara återvänder om likhet överskrider tröskel
  • filter - Filtera efter metadata e.gM SK3 sök bara specifika postituser

Ett komplett exempel: sökkanal

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();
        }
    }
}

Utgång:

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...

Perfekt! Det fann relevanta poster om Docker och ASP

Performanceoptimering

Batch Embedding Generation

Är du tvungen att generera inbäddar? En, en, ett, två, ett

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;
    }
}

Varför slå??

  • GPU-användning: håller GPU upptagna
  • Memory efficiency: återanvänder buffer
  • Framsteg tracking: användarfeedback

Att kaska inbäddar

Don' inte regenerera inbäddar för den orändrade innehållet !

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);
    }
}

pgvector Alternative

Om du vill behålla allt i PostgreSQL:

Installera pgvector-extension

CREATE EXTENSION vector;

Att skapa en tabell

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);

Insert från C#

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();
}

söka med pgvector

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;
}

<=> -operatör: Kosineavstånd ( lägre är mer likt 1 - distance: Konvertera till likvärdighetstest ( högre är bättre

sammanfattning

Vi' har täckt

  1. ✅ Vad är inbäddar och varför de möjliggör semantisk sök
  2. ✅ Hur man väljer och konverterar en inbäddad modell till ONNX
  3. ✅ Generera inbäddar i C# med ONNX Runtime
  4. ✅ Vektordatabasbeståndsdelar
  5. ✅ Att använda Qdrant för vektorlagning och sökande
  6. ✅ Alternativt: pgvector i PostgreSQL
  7. ✅ Performanceoptimering (batching, cachingCSK3

Vi har nu kärnan i tekniken för att hitta semantiskt liknande blogginhoud

Vad's Nästa?

I Del 4: Byggnaden av införingsledning, vi ' byggr inspegeringskanaln

  • Att läsa och analysera markerdown-filer ( vi gör det redan för bloggen!)
  • Intelligenta skalningsstrategier
  • Genererar inbäddar för alla innehåll
  • Mängt som insätts i Qdrant
  • Att hantera uppdaterade och raderade saker
  • Att övervaka utvecklingen

Vi processar hela vår bloggkorpus och har en sökbar kunskapsbas.

Seri Navigation

resurser

Se er i Del 4!

logo

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