Back to "Bâtir un "avocat GPT" pour votre blog - Partie 3: Compréhension des embeddings et des bases de données vectorielles"

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

Bâtir un "avocat GPT" pour votre blog - Partie 3: Compréhension des embeddings et des bases de données vectorielles

Wednesday, 12 November 2025

AVERTISSEMENT: CES PROJETS DE POSTES QUI 'ESCAPÉ'.

Il est probable qu'une grande partie de ce qui est ci-dessous ne fonctionnera pas; je les génère comme comment faire pour MOI, puis faire toutes les étapes et obtenir le travail de l'application d'échantillon ... Vous avez été sournois et les a vus! ils seront probablement prêts à la mi-décembre.

## Présentation

Bienvenue dans la troisième partieNotre pile GPU fonctionne (Deuxième partie), et nous comprenons l'architecture (Première partie****Maintenant, il est temps de plonger dans la magie qui rend possible la recherche sémantique:encastrements.

et

bases de données vectorielles

REMARQUE: Ceci fait partie de mes expériences avec l'IA (couture assistée) + mon propre montage.

Même voix, même pragmatisme, juste des doigts plus rapides.

C'est là que les choses deviennent intéressantes.

Nous allons comprendre comment représenter le texte en tant que nombres d'une manière qui capture le sens, pas seulement les mots-clés.

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

C'est la différence entre trouver des messages qui contiennent le mot « docker » par rapport à trouver des messages qui sont sémantiques sur les concepts de conteneurisation.

  • Qu'est-ce que sont les emblèmes?
  • L'intégration est une représentation numérique du texte (ou des images, audio, etc.) en tant que vecteur - essentiellement une liste de nombres.
  • L'intuition
  • Imaginez tracer des mots dans un espace 2D en fonction de leur signification:

**Les concepts similaires regroupent :**Animaux de compagnie (rouge/vert) sont proches l'un de l'autre

Les véhicules (bleu) sont groupés

Cluster de termes techniques (jaune)

  • Les concepts non liés sont très différents
  • Véritables emboîtements
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

utiliser 384 à 1536 dimensions (pas seulement 2!), capturer beaucoup plus de nuance.

Comment ça marche**Un modèle d'intégration est un réseau neuronal formé pour cartographier le texte vers des vecteurs tels que:**Significations similaires → fermer les 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);
}

Différentes significations → vecteurs éloignés:

  • 1.0Mesure de la similarité
  • 0.0Nous utilisons
  • -1.0similarité de la cosine

pour mesurer la proximité de deux vecteurs::

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)

La similarité de la cosine varie de -1 à 1

= signification identique

= sans rapport

= sens opposé (rare dans la pratique)

  • Exemple
  • Pourquoi les accessoires sont parfaits pour notre cas d'utilisation
  • Rappelez-vous, nous construisons un assistant d'écriture.
  • Quand je commence à écrire :

**"Dans ce billet, je vais montrer comment utiliser le cadre d'entité avec..."**Le système devrait trouver des posts sur:

Cadre des entités

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

Modèles d'accès à la base de données

Configurations ORM

  1. Sujets de base connexes ASP.NETPas seulement des correspondances de mots clés !
  2. **Il devrait comprendre que les « migrations de base de l'EF » sont liées même si elles ne disent pas « cadre d'entité ».**Embeddings vs la recherche traditionnelle
  3. Choisir un modèle d'assemblageIl y a beaucoup de modèles d'intégration.
  4. **Il nous en faut un qui :**Exécute localement

(privacité + vitesse)

Fonctionne avec ONNX Runtime |-------|------------|------|---------|-------| | (pour que nous puissions utiliser notre GPU) | 384 | 80MB | Good | Very Fast ⚡⚡⚡ | | Bonne qualité | 768 | 420MB | Better | Fast ⚡⚡ | | (compréhension sémantique exacte) | 384 | 133MB | Better | Very Fast ⚡⚡⚡ | | Taille appropriée | 768 | 436MB | Best | Fast ⚡⚡ | | (384-768 dimensions est un bon équilibre) | 1536 | N/A (API) | Excellent | Slow (network) ⚡ |

Options populaires: Modèle Dimensions Taille Qualité Vitesse

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

OpenAI text-embeddding-ada-002

Ma recommandation

bge-base-en-v1.5:

pip install optimum[exporters]

Modèle open source ultramoderne:

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

768 dimensions (bon équilibre)

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

Fonctionne bien avec ONNX Runtime: Gratuit et fonctionne localement

Conversion en format ONNX

La plupart des modèles sont au format PyTorch.

Nous avons besoin d'ONNX pour C#.

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

Installer Optimum (bibliothèque Python pour la conversion)

  • OnnxRuntime.GpuConvertir le modèle BGE
  • Microsoft.ML.TokenizersCela crée :

Télécharger si vous ne voulez pas convertir

De nombreux modèles sont pré-convertis et disponibles sur Hugging Face avec "onnx" dans le nom.

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

Utilisation des embeddings dans C#

  1. **Construisons un générateur d'intégration pratique à l'aide d'ONNX Runtime.**Mise en place du projet[101, 8667, 2088, 102]

    • Pourquoi ces paquets ?
      • Exécutez le modèle sur GPU[- Convertir le texte en identifiants de jeton (format d'entrée modèle)[Première tokenisation
  2. **Avant d'intégrer, nous devons tokenize (convertir le texte en nombres):**Qu'est-ce qui se passe ici ?

    • Encodage
      • "Bonjour" →
  3. Chaque numéro est un identifiant de jeton à partir du vocabulaire du modèleJetons spéciaux: 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

Padding

  • Les modèles s'attendent à une entrée de durée fixe
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();
    }
}

Si le texte est court : pad avec zéros:

  1. Si le texte est long: tronquerMasque d'attention
  2. - Dit le modèle que les jetons sont réels vs rembourrage= traiter ce jeton
  3. **= ignorer (c'est le rembourrage)**Classe de générateur d'assemblage
  4. **Maintenant le pipeline d'intégration complète:**Ventilation par code
  5. Chargement du modèle- Crée la session ONNX avec le support GPU
  6. Tokenisation- Convertit le texte en identifiants token

Création de Tensor

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

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

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

    class A input
    class F process
    class G output

    classDef input stroke:#333
    classDef process stroke:#333,stroke-width:2px
    classDef output stroke:#333,stroke-width:2px
  • Données de formes pour l'entrée du modèle

  • Inférence

    • Exécute le réseau neuronal
  • Mise en commun moyenne

- Moyennes d'intégration de jetons → une phrase d'intégration

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

Normalisation:

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
  • Fait la longueur du vecteur 1 (simplifie le calcul de la similarité)

Poolage moyen expliqué

Nous avons une moyenne parce que :

Chaque jeton a son propre emboîtement

Nous avons besoin d'UNE intégration pour toute la phrase

La moyenne saisit la signification globale:

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

Exemple d'utilisationProduit

Magnifique !

  • Le contenu sémantiquement similaire a une forte similitude, le contenu non lié est faible.
  • Bases de données vectorielles

Maintenant, nous avons des emboîtements.

Nous devons stocker des millions d'entre eux et rechercher efficacement.

Le problème

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

Disons que nous avons 1000 billets de blog, chacun découpé en 10 pièces = 10 000 emboîtements.:

  • Recherche naïve
  • Problème
  • : C'est O(n) - nous vérifions chaque intégration.

Lentement !

Pour 10 000 emboîtements × 768 dimensions chacun:

~30 millions d'opérations ponctuelles flottantes

  1. ~50-100ms même sur CPU rapide
  2. Il nous faut quelque chose de plus rapide.
  3. Solution de base de données vectorielle
  4. Les bases de données vectorielles utilisent des structures de données intelligentes (comme HNSW - Diagrammes Hiérarchique Navigable Small World) pour trouver les voisins les plus proches à l'heure O(log n).

Comparaison de vitesse |----------|------------|------------|-------------|---------| | Recherche naïve : 50-100ms pour vecteurs 10K | ✅ Excellent | Docker | Very Fast | Apache 2.0 | | Vecteur DB (HNSW): 1-5ms pour les vecteurs 10K | ✅ (via Npgsql) | Postgres extension | Fast | PostgreSQL License | | 10-50x plus vite ! | ✅ Good | Docker | Very Fast | BSD-3 | | Et il s'échelle: millions de vecteurs ne prend encore que ~10-20ms. | ⚠️ Limited | Docker/K8s | Very Fast | Apache 2.0 | | Choisir une base de données vectorielle | ❌ Python-first | Docker | Fast | Apache 2.0 |

Pour notre projet C#, nous avons besoin :

  • Bonne bibliothèque client C#Qdrant.Client)
  • Facile à déployer (Docker)
  • Bonne performance
  • Libre / open source
  • Base de données de C# Support de Déploiement Performance de License de License

Qdurant

  • Pgvector
  • Façonné
  • Milvus

ChromaMon choix : QdurantExcellent client C# (

Déploiement simple de Docker

Construit spécifiquement pour la recherche vectorielle (non boulonnée)

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

Excellente documentation:

  • 6333Développement actif
  • 6334Variante: pgvector

**Nous utilisons déjà PostgreSQLTM pour le blog!**Pourrait tout garder dans une seule base de données./qdrant_storageLégèrement moins performant mais plus simple architecture

Pour cette série, j'utiliserai

dotnet add package Qdrant.Client --version 1.7.0

Qdurant

parce qu'il est conçu à dessein et plus facile à comprendre les concepts.

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

Mais je vais montrer le pgvector comme une alternative.:

  • SizeConfiguration de Qdurant
  • DistanceDéploiement Docker
    • Distance.CosinePorts
    • Distance.Euclid- API REST
    • Distance.Dot- API gRPC (plus rapide, on va utiliser ça)

Stockage

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

: Persists données à:

  • sur l'hôte
  • C# Configuration du client
  • Création d'une collection

Une collection est comme une table - elle contient des vecteurs d'une dimension spécifique.:

  • Paramètres clés
    • Doit correspondre à votre modèle d'intégration (768 pour bge-base)

- Comment mesurer la similarité:

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

- Similarité de cosinus (le plus fréquent):

  • limit- Distance euclidienne
  • scoreThreshold- Produit pointillé
  • filterInsérer des vecteurs

Charge utile expliquée

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

Comme les métadonnées attachées à chaque vecteur:

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

Peut stocker n'importe quoi: titre de publication, texte en morceaux, date, catégories

Recherche et filtrage !

Insertion par lots

Beaucoup plus rapide qu'un par un

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

Qdurant manipule efficacement des lots de 100-1000

  • Recherche de vecteurs
  • Fonctions de recherche
    • Retour en haut K les plus similaires

- Retourne seulement si la similitude dépasse le seuil

  • Filtrer par métadonnées (par exemple, uniquement les messages spécifiques à la recherche)
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);
    }
}

Exemple complet : Pipeline de recherche

Produit

Parfait !

CREATE EXTENSION vector;

Il a trouvé des messages pertinents sur Docker et ASP.NET Core, même si la phrase exacte n'est pas apparue.

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

Optimisation des performances

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

Génération d'assemblage de lots

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

**<=>Ne génère pas d'intégrations un par un.**Emballez-les ! **1 - distance**Pourquoi faire des lots ?

Utilisation du GPU : maintient le GPU occupé

Efficacité de la mémoire: réutilise les tampons

  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)

Suivi des progrès: retour d'information des utilisateurs

Embedures de cache

Ne régénérez pas d'intégrations pour un contenu inchangé!**Pgvector Alternative**Si vous voulez tout garder dans PostgreSQLTM :

  • Installer l'extension pgvector
  • Créer un tableau
  • Insérer à partir de C#
  • Rechercher avec pgvectoropérateur
  • : Distance de cosinus (la plus basse est plus similaire)
  • : Convertir en score de similarité (plus haut c'est mieux)

Résumé

Nous avons couvert :

Qdurant

Partie 2: Configuration GPU & CUDA en C#Partie 3: Compréhension des intégrations et des bases de données vectorielles!

logo

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