Construyendo un "Abogado GPT" para su blog - Parte 3: Comprensión de Incrustaciones y Bases de Datos de Vectores (Español (Spanish))

Construyendo un "Abogado GPT" para su blog - Parte 3: Comprensión de Incrustaciones y Bases de Datos de Vectores

Wednesday, 12 November 2025

//

22 minute read

ATENCIÓN: Estos son los proyectos de mensajes que se han "espaciado".

Es probable que gran parte de lo que está a continuación no funcione; Yo genera estos como cómo para MÍ y luego hacer todos los pasos y hacer que la aplicación de muestra funcione...¡Has sido astuto y los has visto!Probablemente estarán listos a mediados de diciembre.

## Introducción

¡Bienvenidos a la Parte 3Tenemos nuestra pila de GPU trabajando (Parte 2), y entendemos la arquitectura (Parte 1****Ahora es el momento de sumergirse en la magia que hace posible la búsqueda semántica:incrustaciones.

y

bases de datos vectoriales

NOTA: Esto es parte de mis experimentos con IA (redacción asistida) + mi propia edición.

La misma voz, el mismo pragmatismo; sólo los dedos más rápidos.

Aquí es donde las cosas se ponen interesantes.

Vamos a entender cómo representar el texto como números de una manera que captura el significado, no sólo palabras clave.

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

Es la diferencia entre encontrar posts que contienen la palabra "docker" vs encontrar posts que son semánticamente sobre conceptos de contenedorización.

  • ¿Qué son los empotrados?
  • Una incrustación es una representación numérica de texto (o imágenes, audio, etc.) como vector - esencialmente una lista de números.
  • La intuición
  • Imagine trazar palabras en un espacio 2D basado en su significado:

**Conceptos similares agrupados:**Las mascotas (rojas/verdes) están cerca unas de otras

Se agrupan los vehículos (azul)

Términos técnicos (amarillo) cluster

  • Los conceptos no relacionados están muy separados
  • Inserciones reales
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

utilizar 384 a 1536 dimensiones (no sólo 2!), capturando mucho más matices.

Cómo funciona**Un modelo de incrustación es una red neuronal entrenada para mapear textos a vectores de tal manera que:**Significados similares → cerrar vectores

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

Diferentes significados → vectores distantes:

  • 1.0Medición de la similitud
  • 0.0Usamos
  • -1.0similaridad de coseno

para medir lo cerca que están dos vectores::

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 similitud del coseno varía de -1 a 1

= idéntico significado

= no relacionado

= significado opuesto (rara en la práctica)

  • Ejemplo
  • Por qué las incrustaciones son perfectas para nuestro caso de uso
  • Recuerda, estamos construyendo un asistente de escritura.
  • Cuando empiezo a escribir:

**"En este post, voy a mostrar cómo utilizar Entity Framework con..."**El sistema debería encontrar publicaciones pasadas sobre:

Marco de las entidades

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

Patrones de acceso a la base de datos

Configuraciones ORM

  1. Temas básicos relacionados con ASP.NET¡No sólo coincidencias de palabras clave!
  2. **Debería entender que las "migraciones del núcleo de la FE" están relacionadas a pesar de que no dice "Entity Framework".**Embellecimientos vs Búsqueda Tradicional
  3. Elegir un modelo de empotradoHay muchos modelos de incrustación.
  4. **Necesitamos uno que:**Ejecuta localmente

(privacidad + velocidad)

Funciona con ONNX Runtime |-------|------------|------|---------|-------| | (así que podemos usar nuestra GPU) | 384 | 80MB | Good | Very Fast ⚡⚡⚡ | | Buena calidad | 768 | 420MB | Better | Fast ⚡⚡ | | (comprensión semántica exacta) | 384 | 133MB | Better | Very Fast ⚡⚡⚡ | | Tamaño correcto | 768 | 436MB | Best | Fast ⚡⚡ | | (384-768 dimensiones es un buen equilibrio) | 1536 | N/A (API) | Excellent | Slow (network) ⚡ |

Opciones populares: # Modelo # # Dimensiones # Tamaño # # Calidad # # Velocidad #

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

OpenAI text-embedding-ada-002

Mi recomendación

bge-base-en-v1.5:

pip install optimum[exporters]

Modelo de código abierto de última generación:

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

768 dimensiones (buen equilibrio)

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

Funciona muy bien con ONNX Runtime: Libre y funciona localmente

Convertir a formato ONNX

La mayoría de los modelos están en formato PyTorch.

Necesitamos ONNX para 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

Instalar Optimum (biblioteca de Python para conversión)

  • OnnxRuntime.GpuConvertir modelo BGE
  • Microsoft.ML.TokenizersEsto crea:

Descarga si no quieres convertir

Muchos modelos están pre-convertidos y disponibles en Hugging Face con "onnx" en el nombre.

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

Usando incrustaciones en C#

  1. **Construyamos un práctico generador de incrustación usando ONNX Runtime.**Configuración del proyecto[101, 8667, 2088, 102]

    • ¿Por qué estos paquetes?
      • Ejecutar el modelo en GPU[- Convertir texto a IDs token (formato de entrada de modelo)[Tokenización en primer lugar
  2. Antes de incrustar, debemos tokenizar (convertir el texto en números):¿Qué está pasando aquí?

    • Codificación
      • "Hola mundo" →
  3. Cada número es una identificación simbólica del vocabulario del modeloTokens especiales: 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

Relleno

  • Los modelos esperan entrada de longitud fija
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 el texto es corto: bloc con ceros:

  1. Si el texto es largo: truncateMáscara de atención
  2. - Le dice al modelo qué fichas son reales vs relleno= procesar este token
  3. **= ignorar (es relleno)**Clase de generador de empotrado
  4. **Ahora la tubería de incrustación completa:**Desglose por código
  5. Carga del modelo- Crea sesión de ONNX con soporte GPU
  6. Tokenización- Convierte texto a IDs token

Creación 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
  • Datos de formas para la entrada del modelo

  • Inferencia

    • Maneja la red neuronal.
  • Media de la agrupación

- Medias token embeddings → inserción de una sola 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;
    }
}

Normalización:

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
  • Hace que la longitud del vector 1 (simplifica el cálculo de la similitud)

Explicación de la agrupación media

Promedimos porque:

Cada token tiene su propia incrustación

Necesitamos ONE incrustado para toda la frase

Average capta el significado general:

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

Ejemplo de usoProducto

¡Hermoso!

  • El contenido semánticamente similar tiene una gran similitud, el contenido no relacionado es bajo.
  • Bases de datos vectoriales

Ahora tenemos incrustaciones.

Tenemos que almacenar millones de ellos y buscar de manera eficiente.

El 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

Digamos que tenemos 1000 posts de blog, cada uno dividido en 10 piezas = 10.000 incrustaciones.:

  • Búsqueda inocente
  • Problema
  • : Este es O(n) - revisamos CADA incrustación.

¡Despacio!

Para 10.000 incrustaciones × 768 dimensiones cada una:

~30 millones de operaciones en punto flotante

  1. ~50-100ms incluso en la CPU rápida
  2. Necesitamos algo más rápido.
  3. Solución de base de datos vectorial
  4. Las bases de datos vectoriales utilizan estructuras de datos inteligentes (como HNSW - Hierarchical Navigable Small World graphs) para encontrar vecinos más cercanos en el tiempo O(log n).

Comparación de la velocidad |----------|------------|------------|-------------|---------| | Naive búsqueda: 50-100ms para vectores 10K | ✅ Excellent | Docker | Very Fast | Apache 2.0 | | Vector DB (HNSW): 1-5ms para vectores 10K | ✅ (via Npgsql) | Postgres extension | Fast | PostgreSQL License | | ¡10-50x más rápido! | ✅ Good | Docker | Very Fast | BSD-3 | | Y escala: millones de vectores todavía sólo toma ~10-20ms. | ⚠️ Limited | Docker/K8s | Very Fast | Apache 2.0 | | Elegir una base de datos vectorial | ❌ Python-first | Docker | Fast | Apache 2.0 |

Para nuestro proyecto C#, necesitamos:

  • Buena biblioteca de clientes C#Qdrant.Client)
  • Fácil de implementar (Docker)
  • Buen rendimiento
  • Free / open source
  • Base de datos Base de datos Soporte Implementación Rendimiento Licencia

Qdrant

  • pgvector
  • Tejido
  • Milvus

CromaMi elección: QdrantExcelente cliente C# (

Implementación sencilla de Docker

Construido específicamente para la búsqueda de vectores (no atornillado)

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

Gran documentación:

  • 6333Desarrollo activo
  • 6334Alternativa: pgvector

**¡Ya estamos usando PostgreSQL para el blog!**Podría guardar todo en una base de datos./qdrant_storageUn poco menos performant pero arquitectura más simple

Para esta serie, usaré

dotnet add package Qdrant.Client --version 1.7.0

Qdrant

porque tiene un propósito y es más fácil de entender los conceptos.

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

Pero le mostraré a Pgvector como una alternativa.:

  • SizeConfiguración de Qdrant
  • DistanceDespliegue Docker
    • Distance.CosinePuertos
    • Distance.Euclid- API REST
    • Distance.Dot- API de GRPC (más rápido, usaremos esto)

Almacenamiento

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

: Persisten los datos a:

  • en el host
  • Configuración del cliente C#
  • Crear una colección

Una colección es como una tabla - tiene vectores de una dimensión específica.:

  • Parámetros clave
    • Debe coincidir con su modelo de incrustación (768 para bge-base)

- Cómo medir la similitud:

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

- Similitud de coseno (más frecuente):

  • limit- Distancia euclidiana
  • scoreThreshold- Producto de punto
  • filterInsertar vectores

Carga útil explicada

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

Como metadatos conectados a cada vector:

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

Puede almacenar cualquier cosa: título de publicación, texto en trozos, fecha, categorías

¡Buscable y filtrable!

Inserción de lotes

Mucho más rápido que uno por 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 maneja lotes de 100-1000 de manera eficiente

  • Búsqueda de vectores
  • Características de búsqueda
    • Volver arriba K más similar

- Sólo retorno si la similitud supera el umbral

  • Filtrar por metadatos (por ejemplo, buscar sólo publicaciones específicas)
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);
    }
}

Ejemplo completo: Pipeline de búsqueda

Producto

¡Perfecto!

CREATE EXTENSION vector;

Encontró publicaciones relevantes sobre Docker y ASP.NET Core, a pesar de que la frase exacta no apareció.

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

Optimización del rendimiento

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

Generación de empotrado por lotes

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

<=>No genere incrustaciones una por una.¡Cógelos! 1 - distance¿Por qué lotes?

Utilización de la GPU: mantiene ocupada la GPU

Eficiencia de memoria: reutiliza buffers

  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)

Seguimiento del progreso: retroalimentación de los usuarios

Incrustaciones en caché

No regenerar incrustaciones para el contenido sin cambios!**pgvector Alternativa**Si quieres mantener todo en PostgreSQL:

  • Instalar pgvector Extension
  • Crear tabla
  • Insertar desde C#
  • Búsqueda con pgvectoroperador
  • : Distancia de coseno (más baja es más similar)
  • : Convertir en puntuación de similitud (más alto es mejor)

Resumen

Cubrimos:

Qdrant

Parte 2: Configuración de la GPU y CUDA en C#Parte 3: Comprender los embebidos y las bases de datos vectoriales!

Finding related posts...
logo

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