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

<!--category-- AI, LLM, Embeddings, Vector Database, C#, AI-Article, mostlylucid.blogllm -->
<datetime class="hidden">2025-11-12T22:45</datetime>

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

<img src="https://media1.tenor.com/m/_rQc7PIEqwQAAAAd/cat-hello-cat-peek.gif" height="300px" />
## Introducción

¡Bienvenidos a la Parte 3![Tenemos nuestra pila de GPU trabajando (](/blog/building-a-lawyer-gpt-for-your-blog-part2)Parte 2[), y entendemos la arquitectura (](/blog/building-a-lawyer-gpt-for-your-blog-part1)Parte 1**[](https://www.sbert.net/)**Ahora es el momento de sumergirse en la magia que hace posible la búsqueda semántica:**incrustaciones**.

> y

bases de datos vectoriales

[TOC]

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

```mermaid
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

```mermaid
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

```csharp
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.0`Medición de la similitud
- `0.0`Usamos
- `-1.0`similaridad de coseno

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

```csharp
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

```mermaid
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 empotrado**Hay 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**:

```bash
pip install optimum[exporters]
```

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

```bash
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#.

```bash
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.Gpu`Convertir modelo BGE
- `Microsoft.ML.Tokenizers`Esto crea:

### Descarga si no quieres convertir

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

```csharp
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 modelo**Tokens especiales: 101 =
   
   - `1`CLS], 102 =
   - `0`SEP]

```mermaid
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

```csharp
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: truncate**Má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

```mermaid
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

```csharp
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**:

```csharp
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 uso**Producto

¡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

```mermaid
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

Croma**Mi elección: Qdrant**Excelente cliente C# (

## Implementación sencilla de Docker

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

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

**Gran documentación**:

- `6333`Desarrollo activo
- `6334`Alternativa: pgvector

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

### Para esta serie, usaré

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

```csharp
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.**:

- `Size`Configuración de Qdrant
- `Distance`Despliegue Docker
  - `Distance.Cosine`Puertos
  - `Distance.Euclid`- API REST
  - `Distance.Dot`- API de GRPC (más rápido, usaremos esto)

### Almacenamiento

```csharp
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:

```csharp
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
- `filter`Insertar vectores

### Carga útil explicada

```csharp
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

```csharp
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)

```csharp
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!

```sql
CREATE EXTENSION vector;
```

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

```sql
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

```csharp
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

```csharp
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](/blog/building-a-lawyer-gpt-for-your-blog-part4)**Si quieres mantener todo en PostgreSQL:

- Instalar pgvector Extension
- Crear tabla
- Insertar desde C#
- Búsqueda con pgvector[operador](https://qdrant.tech/)
- : Distancia de coseno (más baja es más similar)
- : Convertir en puntuación de similitud (más alto es mejor)

Resumen

## Cubrimos:

- [¡Ahora tenemos la tecnología principal para encontrar contenido de blog semánticamente similar!](/blog/building-a-lawyer-gpt-for-your-blog-part1)
- [¿Qué sigue?](/blog/building-a-lawyer-gpt-for-your-blog-part2)
- **In**Parte 4: Construcción del oleoducto de ingestión
- [, construiremos la tubería de ingestión:](/blog/building-a-lawyer-gpt-for-your-blog-part4)
- [Lectura y análisis de los archivos de marca (ya lo hacemos para el blog!)](/blog/building-a-lawyer-gpt-for-your-blog-part5)
- [Estrategias inteligentes de troceado](/blog/building-a-lawyer-gpt-for-your-blog-part6)
- [Generación de incrustaciones para todo el contenido](/blog/building-a-lawyer-gpt-for-your-blog-part7)
- [Inserción a granel en](/blog/building-a-lawyer-gpt-for-your-blog-part8)

## Qdrant

- [Manejo de actualizaciones y supresiones](https://www.sbert.net/)
- [Seguimiento de los progresos](https://qdrant.tech/documentation/)
- [¡Procesaremos todo nuestro corpus de blog y tendremos una base de conocimiento buscable!](https://github.com/pgvector/pgvector)
- [Navegación en serie](https://onnxruntime.ai/)
- [Parte 1: Introducción y Arquitectura](https://www.nuget.org/packages/Microsoft.ML.Tokenizers/)

Parte 2: Configuración de la GPU y CUDA en C#[Parte 3: Comprender los embebidos y las bases de datos vectoriales](/blog/building-a-lawyer-gpt-for-your-blog-part4)!