Back to "Costruire un "GPT avvocato" per il tuo blog - Parte 3: Comprendere le basi di dati e i vettori"

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

Costruire un "GPT avvocato" per il tuo blog - Parte 3: Comprendere le basi di dati e i vettori

Wednesday, 12 November 2025

ATTENZIONE: Questi sono i progetti di POST che sono "scaparnati."

E 'probabile che gran parte di ciò che è qui sotto non funzionerà; Io generare questi come come come-per me e poi fare tutti i passi e ottenere il campione di lavoro app ... Sei stato subdolo e li hai visti! saranno probabilmente pronti a metà dicembre.

## Introduzione

Benvenuti alla terza parteAbbiamo il nostro stack GPU funzionante (Parte 2), e comprendiamo l'architettura (Parte 1****Ora è il momento di tuffarsi nella magia che rende possibile la ricerca semantica:Incorporazioni.

e

banche dati vettoriali

NOTA: Questo fa parte dei miei esperimenti con l'AI (elaborazione assistita) + il mio editing.

Stessa voce, stesso pragmatismo, solo dita piu' veloci.

E' qui che le cose si fanno interessanti.

Stiamo per capire come rappresentare il testo come numeri in un modo che cattura significato, non solo parole chiave.

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

E 'la differenza tra trovare post che contengono la parola "docker" vs trovare post che sono semanticamente sui concetti di containerizzazione.

  • Cosa sono gli Embeddings?
  • Un'integrazione è una rappresentazione numerica di testo (o immagini, audio, ecc.) come vettore - essenzialmente una lista di numeri.
  • L'intuizione
  • Immaginate di complottare parole in uno spazio 2D in base al loro significato:

**Concetti simili si raggruppano insieme:**Animali domestici (rosso/verde) sono vicini l'uno all'altro

I veicoli (blu) sono raggruppati

Cluster di termini tecnologici (giallo)

  • Concetti non correlati sono molto distanti
  • Inserzioni reali
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

utilizzare 384-1536 dimensioni (non solo 2!), catturando molto più sfumature.

Come funziona**Un modello di integrazione è una rete neurale addestrato per mappare il testo a vettori tali che:**Significati simili → vettori ravvicinati

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

Significati diversi → vettori distanti:

  • 1.0Similarità di misurazione
  • 0.0Noi usiamo
  • -1.0somiglianza del coseno

per misurare la vicinanza di due vettori::

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 somiglianza del coseno varia da -1 a 1

= significato identico

= non collegato

= significato opposto (raro in pratica)

  • Esempio
  • Perché le inserzioni sono perfette per il nostro caso di utilizzo
  • Ricorda, stiamo costruendo un assistente di scrittura.
  • Quando inizio a scrivere:

**"In questo post, mostrerò come usare Entity Framework con..."**Il sistema dovrebbe trovare post precedenti su:

Quadro dell'entità

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

Modelli di accesso alle banche dati

Configurazioni ORM

  1. Argomenti principali correlati ASP.NETNon solo le parole chiave corrispondono!
  2. **Dovrebbe capire che le "migrazioni core dell'EF" sono correlate anche se non dicono "Framework Entity."**Imbeddings vs Ricerca tradizionale
  3. Scegliere un modello di integrazioneCi sono molti modelli di incorniciamento.
  4. **Abbiamo bisogno di uno che:**Esegue localmente

(privacy + velocità)

Funziona con ONNX Runtime |-------|------------|------|---------|-------| | (così possiamo usare la nostra GPU) | 384 | 80MB | Good | Very Fast ⚡⚡⚡ | | Buona qualità | 768 | 420MB | Better | Fast ⚡⚡ | | (accurata comprensione semantica) | 384 | 133MB | Better | Very Fast ⚡⚡⚡ | | Dimensione destra | 768 | 436MB | Best | Fast ⚡⚡ | | (384-768 dimensioni è un buon equilibrio) | 1536 | N/A (API) | Excellent | Slow (network) ⚡ |

Opzioni popolari: | Modello | Dimensioni | Dimensioni | Qualità | Velocità |

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

OpenAI text-embedding-ada-002

La mia raccomandazione

bge-base-en-v1.5:

pip install optimum[exporters]

Modello open source all'avanguardia:

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

768 dimensioni (buono equilibrio)

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

Funziona alla grande con ONNX Runtime: Libero ed eseguibile localmente

Conversione in formato ONNX

La maggior parte dei modelli sono in formato PyTorch.

Ci serve ONNX per 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

Installa Optimum (libreria Python per la conversione)

  • OnnxRuntime.GpuConverti modello BGE
  • Microsoft.ML.TokenizersQuesto crea:

Scarica se non vuoi convertire

Molti modelli sono pre-convertiti e disponibili su Hugging Face con "onnx" nel nome.

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

Utilizzo di Embeddings in C#

  1. **Costruiamo un generatore di embedding pratico usando ONNX Runtime.**Configurazione progetto[101, 8667, 2088, 102]

    • Perche' questi pacchetti?
      • Eseguire il modello su GPU[- Convertire il testo in ID token (formato di input del modello)[Tokenization First
  2. **Prima di embedding, dobbiamo tokenize (convertire il testo ai numeri):**Che sta succedendo qui?

    • Codifica
      • "Ciao mondo" →
  3. Ogni numero è un ID token dal vocabolario del modelloToken speciali: 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

PaddingCity name (optional, probably does not need a translation)

  • I modelli prevedono input a lunghezza fissa
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();
    }
}

Se il testo è corto: pad con zero:

  1. Se il testo è lungo: troncaMaschera di attenzione
  2. - Dice al modello quali gettoni sono reali vs imbottitura= processa questo token
  3. **= ignorare (è imbottitura)**Classe generatore di integrazione
  4. **Ora l'intera pipeline di integrazione:**Ripartizione del codice
  5. Caricamento del modello- Crea sessione ONNX con supporto GPU
  6. TokenizationCity name (optional, probably does not need a translation)- Converte il testo in ID token

Creazione del tensore

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
  • Dati delle forme per l'ingresso del modello

  • Inferenza

    • Gestisce la rete neurale
  • Media messa in comune

- Embeddings token medie → embedding singola frase

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

Normalizzazione:

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
  • Rende la lunghezza vettoriale 1 (semplifica il calcolo della somiglianza)

Media Pooling spiegato

Siamo in media perché:

Ogni gettone ha la sua inserzione

Abbiamo bisogno di uno per l'intera frase

Averaging cattura il significato generale:

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

Esempio di utilizzoOutput

Bellissimo!

  • Il contenuto semanticamente simile ha un'elevata somiglianza, il contenuto non correlato è basso.
  • Database vettoriali

Ora abbiamo delle incastonate.

Dobbiamo memorizzarne milioni e cercarli in modo efficiente.

Il problema

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

Diciamo che abbiamo 1000 post sul blog, ciascuno diviso in 10 pezzi = 10.000 embeddings.:

  • Ricerca naive
  • Problema
  • : Questo è O(n) - controlliamo TUTTO l'inserzione.

Piano!

Per 10.000 inserzioni × 768 dimensioni ciascuna:

~30 milioni di operazioni in virgola mobile

  1. ~50-100m anche su CPU veloce
  2. Ci serve qualcosa di piu' veloce.
  3. Soluzione per database vettoriali
  4. I database vettoriali utilizzano strutture dati intelligenti (come i grafici HNSW - Hierarchical Navigable Small World) per trovare i vicini più vicini nel tempo O(log n).

Confronto della velocità |----------|------------|------------|-------------|---------| | Ricerca naive: 50-100m per vettori 10K | ✅ Excellent | Docker | Very Fast | Apache 2.0 | | Vettore DB (HNSW): 1-5ms per vettori 10K | ✅ (via Npgsql) | Postgres extension | Fast | PostgreSQL License | | 10-50x più veloce! | ✅ Good | Docker | Very Fast | BSD-3 | | E scala: milioni di vettori ancora prende solo ~10-20m. | ⚠️ Limited | Docker/K8s | Very Fast | Apache 2.0 | | Scegliere un database vettoriale | ❌ Python-first | Docker | Fast | Apache 2.0 |

Per il nostro progetto C# abbiamo bisogno di:

  • Libreria client C# buonaQdrant.Client)
  • Facile da distribuire (Docker)
  • Buona prestazione
  • Libero / open source
  • | Database | Supporto C# | Distribuzione | Prestazioni | Licenza |

QdrantCity name (optional, probably does not need a translation)

  • pgvector
  • WeaviateCity name (optional, probably does not need a translation)
  • MilvusCity name (optional, probably does not need a translation)

ChromaCity name (optional, probably does not need a translation)La mia scelta: QdrantEccellente client C# (

Semplice distribuzione di Docker

Costruito appositamente per la ricerca vettoriale (non bullonato)

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

Ottima documentazione:

  • 6333Sviluppo attivo
  • 6334Alternativa: pgvector

**Stiamo già usando PostgreSQL per il blog!**Potrebbe tenere tutto in un database./qdrant_storageArchitettura leggermente meno performante ma più semplice

Per questa serie, userò

dotnet add package Qdrant.Client --version 1.7.0

QdrantCity name (optional, probably does not need a translation)

perché è costruito con lo scopo e più facile da capire i concetti.

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

Ma mostrerò Pgvector come alternativa.:

  • SizeImpostazione di Qdrant
  • DistanceDocker Deployment
    • Distance.CosinePorti
    • Distance.Euclid- API REST
    • Distance.Dot- API GRPC (più veloce, useremo questo)

Stoccaggio

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

: Persiste dati a:

  • sull'host
  • Impostazione client C#
  • Creazione di una collezione

Una collezione è come una tabella - contiene vettori di una dimensione specifica.:

  • Parametri chiave
    • Deve corrispondere al modello di inserimento (768 per bge-base)

- Come misurare la somiglianza:

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

- Similità del coseno (più comune):

  • limit- Distanza euclidea
  • scoreThreshold- Prodotto a pois
  • filterInserimento vettori

Payload spiegato

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

Come i metadati collegati ad ogni vettore:

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

Può memorizzare qualsiasi cosa: titolo post, testo pezzo, data, categorie

Cercabile e filtrabile!

Inserimento del lotto

Molto piu' veloce di uno per uno.

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 gestisce lotti di 100-1000 in modo efficiente

  • Ricerca vettori
  • Cerca le caratteristiche
    • Ritorna in alto K più simile

- Ritorno solo se la somiglianza supera la soglia

  • Filtrare per metadati (ad esempio, solo messaggi di ricerca specifici)
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);
    }
}

Esempio completo: Condotto di ricerca

Output

Perfetto!

CREATE EXTENSION vector;

Ha trovato post rilevanti su Docker e ASP.NET Core, anche se la frase esatta non è apparsa.

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

Ottimizzazione delle prestazioni

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

Generazione inserimento lotti

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

**<=>Non generare inserzioni uno per uno.**Batch them! **1 - distance**Perche' fare il batch?

Utilizzo della GPU: mantiene la GPU occupata

Efficienza della memoria: riutilizza i buffer

  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)

Monitoraggio dei progressi: feedback dell'utente

Caching Embeddings

Non rigenerare le inserzioni per contenuti immutati!**pgvector Alternative**Se si desidera mantenere tutto in PostgreSQL:

  • Installa l'estensione pgvector
  • Crea tabella
  • Inserisci da C#
  • Cerca con pgvectoroperatore
  • : Distanza del coseno (più bassa è simile)
  • : Converti in punteggio di somiglianza (più alto è meglio)

Sommario

Abbiamo coperto:

QdrantCity name (optional, probably does not need a translation)

Parte 2: Configurazione GPU & CUDA in C#Parte 3: Comprensione delle basi di dati e dei vettori!

logo

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