Établissement d’un "Lawyer GPT" pour votre blog - Partie SSK3 Comprendre les embeddings & Bases de données vectorielles (Français (French))

Établissement d’un "Lawyer GPT" pour votre blog - Partie SSK3 Comprendre les embeddings & Bases de données vectorielles

Wednesday, 12 November 2025

//

23 minute read

Introduction

Bienvenue à la Partie 3! Nous ' avons fait fonctionner notre plate-forme GPUPartie 2), et nous comprenons l’architecturePartie 1). Maintenant, il est temps de se plonger dans la magie qui rend possible une recherche sémantique. intégrations et Bases de données des vecteurs.

NOTE: C'est une partie des expériences que j'ai réalisées avec l'A.I. M SK1 rédaction assistée) + ma propre éditionMST4 La même voixM ST5 le même pragmatismeM st6 quelques doigts plus rapidesMst7

C'est là où les choses se font intéressantes. Nous' allons comprendre comment représenter le texte comme des chiffres d'une manière qui capture la signification , non seulement les mots clésM SK3 IlMSC4 est la différence entre trouver des articles qui contiennent le mot MNK5dockerMRK6 et trouver des postes qui sont sémantiquement sur les concepts de conteneurisation MNK7

Qu'est-ce que les embeddings?

Un emplacement est une représentation numérique du texte (ou d'imagesM SK1 audio, etcMSC3 en tant que vecteur - essentiellement une liste de numéros

L'intuition

Imaginez de dessiner des mots dans un espace 2D en fonction de leur significationM SK1

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

Les concepts similaires regroupent ensemble:

  • Les animaux domestiques (redM SK1green) sont proches l'un de l'autre
  • Les véhicules (blueM SK1 sont groupés
  • Termes techniques (yellow) cluster
  • Les concepts non liés sont loin d'être les mêmes.

Emplacements réels Utiliser 384 à 1536 dimensions ( pas seulement 2!), saisir beaucoup plus de nuances

Comment cela fonctionne

Un modèle embedding est un réseau neuronal formé pour cartographier le texte à des vecteurs tels que:

  • Membres similaires → vecteurs fermés
  • Différentes significations → vecteurs éloignés
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

Mesure de la similitude

Nous utilisons similitude des cosine pour mesurer la proximité de deux vecteurs:

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

La similitude des cosine varie de -1 à 1:

  • 1.0 = signification identique
  • 0.0 = non lié
  • -1.0 = sens opposé ( rare dans la pratique

Exemple ::

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)

Pourquoi les emplacements sont parfaits pour notre cas d'utilisation

Rappelez-vous que nous construisons un assistant de rédaction lorsque je commence à écrire.

"Dans ce post , IM SK2je montrerai comment utiliser le cadre d’entités avec..."

Le système devrait trouver des articles antérieurs sur:

  • Cadre d'entité
  • Méthodes d'accès à la base de données
  • Configurations de l'ORM
  • Autres sujets connexes ASP.NET

Non seulement les mots clés correspondent! Il devrait comprendre que "EF Migrations de base" est liée même s’il n’y a pas d’interprétation de ce qui précède.

Embeddings vs Recherche traditionnelle

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

Choisir un modèle d'emplacement

Il y a de nombreux modèles embeddings.

  1. Fonctionne localement (privéeté + vitesseM SK2
  2. Travaille avec ONNX Runtime (so que nous puissions utiliser notre GPU)
  3. Bonne qualité (compréhension sémantique exacte
  4. Taille droite (384-768 les dimensions sont bien équilibrées

Options populaires

Modèle
all-MiniLMM SK1L 6-v2 384 ≥ ≤80MB MSC4 Bon MSC5 Très rapide SMC6 ±MSC7
all-mpnetM SK1base-vMSC3 768
bge-smallM SK1en -v1.5 384
bge-baseMSC1en-vM SK3 768 2 3 MB 4 Meilleure 5 Rapide 6 7
Texte de l’OpenAI-embedding-adaM SK2 1536

Ma recommandation: bge-baseMSC1en-vM SK3

  • État-deM SK1du-modèle open source
  • 768 dimensions ( bon soldeM SK2
  • Il fonctionne très bien avec ONNX Runtime
  • Libre et fonctionne localement

Conversion au format ONNX

La plupart des modèles sont en format PyTorch. Nous avons besoin d’onNX pour C#.

Installer la bibliothèque Optimum (Python pour la conversion):

pip install optimum[exporters]

Convertre le modèle BGE:

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

Cela crée:

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

Télécharger si vous ne voulez pas convertir ': De nombreux modèles sont avant -convertis et disponibles sur Hugging Face avec "onnx" sous le nom de .

Utilisation des embeddings dans C#

Let's bâtir un générateur d'embedding pratique à l'aide de Runtime ONNX

Établissement du projet

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

Pourquoi ces paquets?

  • OnnxRuntime.Gpu - Faire fonctionner le modèle sur GPU
  • Microsoft.ML.Tokenizers - Convertir le texte en IDs de token format d'entrée modèleM SK2

Tokenisation Premièrement

Avant d’incorporer ,, nous devons tokeniser (convertir le texte en chiffres):

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

Qu’est-ce qui se passe ici?

  1. Encodage - "Welcome to the world [101, 8667, 2088, 102]

    • Chaque numéro est un ID de token du vocabulaire du modèle'.
    • Cotisations spéciales: M SK1 ≥= [TÉU, TPAC, TPMD, TM, TMÉU, TC: [TÉU, TPAC et TPMD
  2. Établissement de padding - Les modèles s’attendent à une entrée de longueur fixe

    • Si le texte est court: pad avec zéro
    • Si le texte est long: atténuer
  3. Maske d'attention - Dit au modèle quels tokens sont réels contre le padding

    • 1 = traiter ce token
    • 0 = ignorer (itM SK2s padding)
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

Classe des générateurs intégrés

Maintenant, le pipeline d’implantation complet:

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

Répartition des codes:

  1. Chargement du modèle - Crée une session ONNX avec support GPU
  2. Tokenisation - Convert le texte à des ID de token
  3. Création de tensors - Données de forme pour l'entrée du modèle
  4. Inférence - Conduit le réseau neuronal
  5. Répartition moyenne - Moyenne des emplacements de tokens → Emplacement d'une seule phrase
  6. Normalisation - Fait la longueur du vecteur 1 simplifie le calcul de la similitude

Répartition moyenne Expliqué

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

Nous sommes en moyenne parce:

  • Chaque token a son propre emplacement
  • Nous avons besoin d'un emplacement pour toute la phrase.
  • L'évaluation permet de saisir le sens global

Exemple d'utilisation

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

Résultats:

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

Très beau! Contenu sémantiquement similaire a une grande similaritéM SK1 Contenus non liés sont faibles.

Bases de données des vecteurs

Nous devons en stocker des millions et effectuer une recherche efficace.

Le problème

Let's disons que nous avons 1000 des articles de blogM SK2 chacun en morceaux 10 pieces

Recherche naïve:

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

Problème: C'est OM SK1n) - nous vérifions EVERY embedding

Pour 10,000 embeddings × 768 dimensions chacuneM SK3

  • ~30 millions d’opérations de point flottant
  • ~50-100ms même sur un CPU rapide

Nous avons besoin de quelque chose plus rapide.

Résolution de la base de données des vecteurs

Les bases de données vectorielles utilisent des structures de données intelligentes (comme l'HNSW - Graphiques hiérarchiques navigables du monde petitM SK2 pour trouver les voisins les plus proches dans O(log nMSC4 tempsMNK5

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

Comparaison de vitesse:

  • Recherche naïve: M SK1ms pour les vecteurs 10K
  • DB de vecteur (HNSWM SK1 1-5ms pour les vecteurs ≥10K
  • 10-50x plus rapidesM SK1

Et il s’élargit : millions de vecteurs encore seulement prend ~10-20ms.

Choix d'une base de données Vector

Pour notre projet C#, nous avons besoin de ,

  1. Bonne bibliothèque de clients C#
  2. Facile à déployer (Docker)
  3. Bon rendement
  4. Source libre /
Base de données
Qdrant ✅ Excellent MSC2 Docker MSC3 Très rapide SMC4 Apache SMC5 ≥MSC6
pgvector ✅ ≥(par l'entremise de Npgsql) ≤ Extension Postgres MSC5 Fast MSC6 Licence PostgreSQL SMC7
Favoriser ✅ Bon MSC2 Docker MSC3 Très rapide SMC4 BSDMSc5 ♫
Milvus ⚠️ Limitée Docker/KM SK4s M Très rapide P Apache MPSK7 ≥
Chrome ❌ Python-première Python3 Docker Fast

Mon choix: Qdrant

  • Excellent client C# (Qdrant.Client)
  • Déploiement simple du Docker
  • Élaboré spécialement pour la recherche par vecteur (non encadré sur)
  • Une excellente documentation
  • Développement actif

Solutions de rechange: pgvector

  • Nous utilisons déjà PostgreSQL pour le blog.
  • Peut tenir tout dans une seule base de données
  • Une architecture légèrement moins performante mais plus simple

Pour cette série, je vais utiliser Qdrant parce qu’il est constructif et plus facile à comprendre les concepts, mais je montrerai le pgvector comme alternative.

Établissement du Qdrant

Déploiement du docker

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

Ports:

  • 6333 - REST API
  • 6334 - gRPC API faster, nousM SK3nous utiliserons ce

Entreposage: Les données persistent à ./qdrant_storage sur l'hôte

Établissement du client C#

dotnet add package Qdrant.Client --version 1.7.0

Création d'une collection

Une collection est comme une table - elle contient des vecteurs d'une dimension spécifiqueM SK1

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

Paramètres clés:

  • Size - doit correspondre à votre modèle d'implantation 768 pour bge
  • Distance - Comment mesurer la similitudeM SK1
    • Distance.Cosine - Similarité cosine la plus communeM SK2
    • Distance.Euclid - Distance euclidienne
    • Distance.Dot - Produit ponctuel

Insertion des vecteurs

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

Charge de travail expliquée:

  • Comme les métadonnées attachées à chaque vecteur
  • Peut stocker quelque chose: titre d'affichage , texte en morceauxM SK2 date, catégories
  • Recherche et filtration!

Insertion de lots:

  • beaucoup plus rapide qu'un
  • Qdrant manipule efficacement les lots de 100-1000

Vektors de recherche

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

Fonctions de recherche:

  • limit - Retour en haut K le plus similaire
  • scoreThreshold - Retour seulement si la similitude dépasse le seuil
  • filter - Filtrer par métadonnées e.gM SK3 rechercher uniquement des postes spécifiquesMSC4

Exemple complète: Pipeline de recherche

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

Résultats:

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

Perfect! Il a trouvé des articles pertinents sur Docker et ASP.NET CoreM SK2 même si la phrase exacte n’apparaît pasMSC3

Optimisation du rendement

Établissement d'un emplacement par lots

Donn't generate embeddings one -by-oneM SK3 Batch themMSC4

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

Pourquoi battre?

  • Utilisation du GPU: maintient le GPU occupé
  • Efficacité de la mémoire: réutilise les tampons
  • Traçage des progrès: Rétroaction de l'utilisateur

Cachement des embeddings

Don'ne régénérer les embeddings pour le contenu inchangé!

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

Si vous voulez garder tout dans PostgreSQL:

Installer l'extension pgvector

CREATE EXTENSION vector;

Créer une table

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

Insérer de 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();
}

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

<=> opérateur: Distance de cosine ( inférieure est plus semblableM SK2 1 - distance: Convertre à la note de similitude (plus élevé est meilleurM SK2

Résumé

Nous'nous avons couvert:

  1. ✅ Quels sont les emplacements et pourquoi ils permettent la recherche sémantique
  2. ✅ Comment choisir et convertir un modèle d'implantation en ONNX
  3. ✅ Gérer des embeddings dans C# avec ONNX Runtime
  4. ✅ Concepts de bases de données vectorielles (HNSWM SK2 recherche de similitude)
  5. ✅ Utilisation de Qdrant pour l'entreposage et la recherche des vecteurs
  6. ✅ Solutions de rechange: pgvector dans PostgreSQL
  7. ✅ Optimisation du rendement (batching, cachingCSK3

Nous avons maintenant la technologie de base pour trouver des contenus blog sémantiquement similaires!

Qu’est-ce que's Suivant?

À l'intérieur **Partie 4: Établissement du pipeline d’ingestion**Nous construisons le pipeline d’ingestion.

  • Lecture et analyse des fichiers de marquage (nous faisons déjà cela pour le blog!)
  • Stratégies de regroupement intelligents
  • Gérer des embeddings pour tous les contenus
  • Insertion en vrac Qdrant
  • Traitement des mises à jour et des suppressions
  • Surveillance des progrès

Nous ' traiterons l’ensemble du corpus de notre blog et disposerons d’une base de connaissances recherchable.

Ressources

Nous vous ferons plaisir. Partie 4!

Finding related posts...
logo

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