"Lakimies GPT:n" rakentaminen blogiisi - Osa 3: Understanding Upbeddings & Vector Databases (Suomi (Finnish))

"Lakimies GPT:n" rakentaminen blogiisi - Osa 3: Understanding Upbeddings & Vector Databases

Wednesday, 12 November 2025

//

20 minute read

VAROITUS: NÄYTTELEVÄT NÄYTTEET, JOITA EI OLE KÄYTETTY.

Se on todennäköisesti suuri osa siitä, mikä alla on, ei toimi. Minä luon nämä minulle ja sitten teen kaikki askeleet ja saat näytesovelluksen toimimaan...Olet ollut ovela ja nähnyt ne! ne ovat todennäköisesti valmiita joulukuun puolivälissä.

## Johdanto

Tervetuloa 3. osaanMeillä on GPU-pino työn alla (2 osa), ja ymmärrämme arkkitehtuuria (Osa 1****Nyt on aika sukeltaa taikuuteen, joka mahdollistaa semanttisen etsinnän:sulautteet.

sekä

vektoritietokannat

HUOMAUTUS: Tämä on osa kokeilujani tekoälyllä (avustettu suunnittelu) + omalla muokkauksellani.

Sama ääni, sama pragmatismi, vain nopeammat sormet.

Täällä asiat muuttuvat mielenkiintoisiksi.

Me tulemme ymmärtämään, miten teksti esitetään numeroina tavalla, joka vangitsee merkityksen, ei vain avainsanoja.

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

Se on ero löytää virkoja, jotka sisältävät sanan "docker" vs. löytää virkoja, jotka ovat semanttisesti konttien käsitteitä.

  • Mitä siihen liittyy?
  • Upotus on numeerinen esitys tekstistä (tai kuvista, äänistä jne.) vektorina - lähinnä numeroluettelona.
  • Intuitio
  • Kuvittele, että piirrät sanoja 2D-tilassa, joka perustuu niiden merkitykseen:

**Vastaavat käsitteet ryhmittyvät yhteen:**Lemmikit (punaiset/vihreät) ovat lähellä toisiaan

Ajoneuvot (siniset) ryhmitellään

Tekniset termit (keltainen) klusteri

  • Aiheettomat käsitteet ovat kaukana toisistaan
  • Todelliset upotukset
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

käytä 384-1536 ulottuvuuksia (ei vain 2!), jotka vangitsevat paljon enemmän vivahteita.

Miten se toimii**Upotettava malli on hermoverkko, joka on koulutettu kartoittamaan tekstiä vektoreille siten, että:**Samanlaisia merkityksiä → Sulje vektorit

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

Eri merkitykset → kaukaiset vektorit:

  • 1.0Samankaltaisuuden mittaaminen
  • 0.0Käytämme
  • -1.0cosinen samankaltaisuus

mittaamaan, kuinka lähellä kaksi vektoria on::

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)

Cosinen samankaltaisuus vaihtelee -1:stä 1:een

= sama merkitys

= ei yhteyttä

= päinvastainen merkitys (harvinainen käytännössä)

  • Esimerkki
  • Miksi upotukset ovat täydellisiä käyttöömme
  • Muista, että rakennamme kirjoitusapulaista.
  • Kun alan kirjoittaa:

**"Tässä viestissä näytän, miten Entity Frameworkiä käytetään..."**Järjestelmän pitäisi löytää aiemmat virat seuraavista asioista:

"Yhteenkuuluvuuskehys"

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

Tietokannan käyttömallit

ORM-kokoonpanot

  1. Aiheeseen liittyvät ASP.NET-avainaiheetEi vain avainsana täsmää!
  2. **Sen pitäisi ymmärtää, että "EF Core Migrations" liittyy toisiinsa, vaikka siinä ei sanotakaan "Entity Framework".**Upotus perinteiseen hakuun
  3. Uppoavan mallin valitseminenOn olemassa monia upottavia malleja.
  4. **Tarvitsemme sellaisen, joka:**Toimii paikallisesti

(yksityisyys + nopeus)

Toimii ONNX-ajon kanssa |-------|------------|------|---------|-------| | (jotta voimme käyttää GPU:ta) | 384 | 80MB | Good | Very Fast ⚡⚡⚡ | | Hyvä laatu | 768 | 420MB | Better | Fast ⚡⚡ | | (tarkkaa semanttista ymmärrystä) | 384 | 133MB | Better | Very Fast ⚡⚡⚡ | | Oikea koko | 768 | 436MB | Best | Fast ⚡⚡ | | (384-768 ulottuvuuksia on hyvä tasapaino) | 1536 | N/A (API) | Excellent | Slow (network) ⚡ |

Suositut vaihtoehdot: Mallin mitat Koko Laatu Nopeus

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

OpenAI-tekstien upotus-ada-002

Suositukseni

bge-base-en-v1.5:

pip install optimum[exporters]

Huippuluokan avoimen lähdekoodin malli:

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

768 kokoa (hyvä tasapaino)

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

Toimii erinomaisesti ONNX-runtimessa: Vapaa ja toimii paikallisesti

Muunnetaan ONNX-muotoon

Useimmat mallit ovat PyTorch-muodossa.

Tarvitsemme ONNX:ää C#:hen.

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

Asenna Optimum (Python-kirjasto remontoitavaksi)

  • OnnxRuntime.GpuMuunna BGE-malli
  • Microsoft.ML.TokenizersNäin syntyy:

Lataa, jos et halua muuntaa

Monet mallit ovat esikonvertoituja ja saatavilla Hugging Facessa, jonka nimi on "onnx".

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

Liitännäisten käyttö C#:ssä

  1. **Tehdään ONNX-runtimella käytännöllinen upotusgeneraattori.**Projektin asetukset[101, 8667, 2088, 102]

    • Miksi nämä paketit?
      • Suorita malli GPU: lla[- Muunna teksti polettitunnuksiksi (mallin syöttömuoto)[Tokenisointi ensin
  2. **Ennen upottamista meidän on tokenisoitava (käännetään teksti numeroiksi):**Mitä täällä tapahtuu?

    • Koodaus
      • "Hei maailma" →
  3. Jokainen numero on malliston sanastosta kuponkitunnisteErikoismerkit: 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

Pähkinät

  • Mallit odottavat kiinteämittaista syöttöä
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();
    }
}

Jos teksti on lyhyt: sivu nollilla:

  1. Jos teksti on pitkä: tynkäHuomionaamari
  2. - Kertoo mallin, joka on aito vs. pehmuste= käsittele tämä merkki
  3. **= ei huomioi (pehmuste)**Upotusgeneraattorin luokka
  4. **Nyt täysi upotusputki:**Koodin jaottelu
  5. Mallin lataus- Luo ONNX-istunnon GPU-tuella
  6. Tokenisointi- Muuntaa tekstin symbolitunnuksiksi

Tensorin luominen

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
  • Muodostaa mallisyötteen tiedot

  • Johtopäätös

    • Johtaa hermoverkkoa.
  • Keskimääräiset yhteenlaskut

- Averages plake inbeddings → yksi lause upotettu

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

Normalisointi:

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
  • Tekee vektorin pituuden 1 (yksinkertaistaa samankaltaisuuslaskentaa)

Keskimääräistä yhdistämistä selitettiin

Keskimäärin, koska:

Jokaisessa kyltissä on oma upotuksensa

Koko lausetta varten tarvitaan YKSI

Keskimääräisyys kuvaa kokonaismerkitystä:

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

KäyttöesimerkkiTuloste

Kaunista!

  • Semanttisesti samankaltainen sisältö on hyvin samankaltaista, irrallinen sisältö on vähäistä.
  • Vektoritietokannat

Nyt meillä on upotuksia.

Niitä on varastoitava miljoonia ja etsittävä tehokkaasti.

Ongelma

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

Sanotaan, että meillä on 1000 blogikirjoitusta, joista jokainen on paloiteltu 10 osaan = 10 000 upotusta.:

  • Naiven etsintä
  • Ongelma
  • Tämä on O(n) - tarkistamme jokaisen upotuksen.

Hitaasti!

10 000 upotusta × 768 mitalle kukin:

~30 miljoonaa kelluntapistettä

  1. ~50-100 ms jopa nopealla prosessorilla
  2. Tarvitsemme jotain nopeampaa.
  3. Vektoritietokannan ratkaisu
  4. Vektoritietokannat käyttävät näppäriä tietorakenteita (kuten HNSW - Hierarkical Navigable Small World -kaavioita) löytääkseen lähimmät naapurit O(log n) -ajasta.

Nopeusvertailu |----------|------------|------------|-------------|---------| | Naishaku: 50-100ms 10K-vektoreille | ✅ Excellent | Docker | Very Fast | Apache 2.0 | | Vektori DB (HNSW): 1–5 ms 10K-vektoreille | ✅ (via Npgsql) | Postgres extension | Fast | PostgreSQL License | | 10-50x nopeammin! | ✅ Good | Docker | Very Fast | BSD-3 | | Ja se vaa'aa: miljoona vektoria vie yhä vain ~10-20 ms. | ⚠️ Limited | Docker/K8s | Very Fast | Apache 2.0 | | Vektoritietokannan valinta | ❌ Python-first | Docker | Fast | Apache 2.0 |

C#-projektiamme varten tarvitaan:

  • Hyvä C#-asiakaskirjastoQdrant.Client)
  • Helppo ottaa käyttöön (Docker)
  • Hyvä suoritus
  • Vapaa / avoin lähdekoodi
  • Tietokanta C# Tuki Käyttöönotto Performance Lisenssi

Qdrant

  • pgvector
  • Hellävarainen
  • Milvus

KromaMinun valintani: QdrantErinomainen C#-asiakas (

Yksinkertainen Dockerin käyttöönotto

Rakennettu nimenomaan vektorihakua varten (ei lukittu päälle)

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

Hienot asiakirjat:

  • 6333Aktiivinen kehitys
  • 6334Vaihtoehto: pgvector

**Käytämme blogissa jo PostgreSQL:ää!**Voisi pitää kaiken yhdessä tietokannassa./qdrant_storageHieman vähemmän performanssia mutta yksinkertaisempaa arkkitehtuuria

Tähän sarjaan käytän

dotnet add package Qdrant.Client --version 1.7.0

Qdrant

Koska se on tarkoituksella rakennettua ja helpompi ymmärtää käsitteitä.

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

Mutta näytän pgvectorin vaihtoehtona.:

  • SizeAsetetaan Qdrant
  • DistanceDockerin käyttöönotto
    • Distance.CosineSatamat
    • Distance.Euclid- REST API
    • Distance.Dot- GRPC API (nopeammin, käytämme tätä)

Varastointi

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

: Pysyy datassa:

  • isännöitsijällä
  • C#-asiakasasetukset
  • Kokoelman luominen

Kokoelma on kuin pöytä - siinä on tietyn ulottuvuuden vektoreita.:

  • Tärkeimmät parametrit
    • Soveltuu varmasti upotettavaan malliin (768 bge-base)

- Samankaltaisuuden mittaaminen:

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

- Cosinen samankaltaisuus (yleisin):

  • limit- Euklidinen etäisyys
  • scoreThreshold- Pistetuote
  • filterLisää vektoreita

Hyötykuorma selitetty

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

Kuten jokaiseen vektoriin liitetyt metatiedot:

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

Voi tallentaa mitä tahansa: postittaa otsikon, kokotekstin, päivämäärän, kategoriat

Etsittävä ja suodatettava!

Erän lisääminen

Paljon nopeammin kuin yksi kerrallaan

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 käsittelee 100-1000 erän tehokkaasti

  • Etsiviä vektoreita
  • Hakuominaisuudet
    • Paluu K: n kärkeen suurin piirtein

- Palautus vain, jos samankaltaisuus ylittää kynnyksen

  • Suodata metatietojen mukaan (esim. vain hakukohtaiset viestit)
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);
    }
}

Täydellinen esimerkki: Etsi putkistosta

Tuloste

Täydellistä!

CREATE EXTENSION vector;

Se löysi merkityksellisiä viestejä Dockerista ja ASP.NET Coresta, vaikka tarkkaa lausetta ei näkynyt.

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

Suorituskyvyn optimointi

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

Erän upottava sukupolvi

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

**<=>Älä luo upotuksia yksi kerrallaan.**Napatkaa heidät! **1 - distance**Miksi erkanevat?

GPU:n käyttö: pitää GPU:n kiireisenä

Muistitehokkuus: uudelleenkäyttöpuskurit

  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)

Edistymisen seuranta: käyttäjäpalaute

Välilyöntejä

Älä uudista upotuksia muuttumattomaan sisältöön!**pgvector Vaihtoehto**Jos haluat pitää kaiken PostgreSQL:ssä:

  • Asenna pgvectorin laajennus
  • Luo taulukko
  • Lisää kohtaan C#
  • Etsi pgvectorillatoiminnanharjoittaja
  • : Cosine etäisyys (alempi on enemmän samanlainen)
  • : Muunna samankaltaisuuspisteeksi (suurempi on parempi)

Yhteenveto

Olemme käsitelleet:

Qdrant

Osa 2: GPU:n asetukset ja CUDA C#-muodossaOsa 3: Understanding Upbeddings & Vector Databases!

Finding related posts...
logo

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