Établissement d’un "Lawyer GPT" pour votre blog - Partie SSK3 Comprendre les embeddings & Bases de données vectorielles
Introduction
Bienvenue à la Partie 3! Nous ' avons fait fonctionner notre plate-forme GPUPartie 2), et nous comprenons l’architecturePartie 1). Maintenant, il est temps de se plonger dans la magie qui rend possible une recherche sémantique. intégrations et Bases de données des vecteurs.
NOTE: C'est une partie des expériences que j'ai réalisées avec l'A.I. M SK1 rédaction assistée) + ma propre éditionMST4 La même voixM ST5 le même pragmatismeM st6 quelques doigts plus rapidesMst7
C'est là où les choses se font intéressantes. Nous' allons comprendre comment représenter le texte comme des chiffres d'une manière qui capture la signification , non seulement les mots clésM SK3 IlMSC4 est la différence entre trouver des articles qui contiennent le mot MNK5dockerMRK6 et trouver des postes qui sont sémantiquement sur les concepts de conteneurisation MNK7
Qu'est-ce que les embeddings?
Un emplacement est une représentation numérique du texte (ou d'imagesM SK1 audio, etcMSC3 en tant que vecteur - essentiellement une liste de numéros
L'intuition
Imaginez de dessiner des mots dans un espace 2D en fonction de leur significationM SK1
graph TD
subgraph "2D Embedding Space (simplified)"
A[cat: 0.8, 0.2]
B[kitten: 0.7, 0.3]
C[dog: 0.6, 0.1]
D[puppy: 0.5, 0.2]
E[car: -0.5, 0.8]
F[vehicle: -0.6, 0.7]
G[database: 0.1, -0.7]
H[SQL: 0.2, -0.8]
end
class A,B cats
class C,D dogs
class E,F vehicles
class G,H tech
classDef cats stroke:#333
classDef dogs stroke:#333
classDef vehicles stroke:#333
classDef tech stroke:#333
Les concepts similaires regroupent ensemble:
- Les animaux domestiques (redM SK1green) sont proches l'un de l'autre
- Les véhicules (blueM SK1 sont groupés
- Termes techniques (yellow) cluster
- Les concepts non liés sont loin d'être les mêmes.
Emplacements réels Utiliser 384 à 1536 dimensions ( pas seulement 2!), saisir beaucoup plus de nuances
Comment cela fonctionne
Un modèle embedding est un réseau neuronal formé pour cartographier le texte à des vecteurs tels que:
- Membres similaires → vecteurs fermés
- Différentes significations → vecteurs éloignés
graph LR
A[Text: 'Docker container'] --> B[Embedding Model]
B --> C[Vector: 384 floats]
D[Text: 'containerization'] --> B
B --> E[Vector: 384 floats]
C -.Similar.-> E
F[Text: 'chocolate cake'] --> B
B --> G[Vector: 384 floats]
C -.Very Different.-> G
class B model
class C,E similar
class G different
classDef model stroke:#333,stroke-width:4px
classDef similar stroke:#333
classDef different stroke:#333
Mesure de la similitude
Nous utilisons similitude des cosine pour mesurer la proximité de deux vecteurs:
public static float CosineSimilarity(float[] vectorA, float[] vectorB)
{
if (vectorA.Length != vectorB.Length)
throw new ArgumentException("Vectors must have same length");
// Dot product: sum of element-wise multiplication
float dotProduct = 0;
for (int i = 0; i < vectorA.Length; i++)
{
dotProduct += vectorA[i] * vectorB[i];
}
// Magnitude of each vector: sqrt(sum of squares)
float magnitudeA = 0;
float magnitudeB = 0;
for (int i = 0; i < vectorA.Length; i++)
{
magnitudeA += vectorA[i] * vectorA[i];
magnitudeB += vectorB[i] * vectorB[i];
}
magnitudeA = MathF.Sqrt(magnitudeA);
magnitudeB = MathF.Sqrt(magnitudeB);
// Cosine similarity: dot product / (magnitude_a * magnitude_b)
return dotProduct / (magnitudeA * magnitudeB);
}
La similitude des cosine varie de -1 à 1:
1.0= signification identique0.0= non lié-1.0= sens opposé ( rare dans la pratique
Exemple ::
var dockerEmbed = new float[] { 0.5f, 0.3f, -0.2f, 0.8f }; // "Docker container"
var containerEmbed = new float[] { 0.45f, 0.35f, -0.18f, 0.75f }; // "containerization"
var cakeEmbed = new float[] { -0.7f, 0.1f, 0.9f, -0.3f }; // "chocolate cake"
Console.WriteLine(CosineSimilarity(dockerEmbed, containerEmbed)); // ~0.95 (very similar!)
Console.WriteLine(CosineSimilarity(dockerEmbed, cakeEmbed)); // ~0.15 (unrelated)
Pourquoi les emplacements sont parfaits pour notre cas d'utilisation
Rappelez-vous que nous construisons un assistant de rédaction lorsque je commence à écrire.
"Dans ce post , IM SK2je montrerai comment utiliser le cadre d’entités avec..."
Le système devrait trouver des articles antérieurs sur:
- Cadre d'entité
- Méthodes d'accès à la base de données
- Configurations de l'ORM
- Autres sujets connexes ASP.NET
Non seulement les mots clés correspondent! Il devrait comprendre que "EF Migrations de base" est liée même s’il n’y a pas d’interprétation de ce qui précède.
Embeddings vs Recherche traditionnelle
graph TB
subgraph "Traditional Keyword Search"
A1[Query: 'Docker setup'] --> B1[Find: 'Docker' OR 'setup']
B1 --> C1[❌ Misses: 'containerization guide']
B1 --> D1[❌ Misses: 'running containers']
B1 --> E1[✅ Finds: 'Docker setup tutorial']
end
subgraph "Embedding-Based Semantic Search"
A2[Query: 'Docker setup'] --> B2[Generate embedding]
B2 --> C2[Find similar embeddings]
C2 --> D2[✅ Finds: 'containerization guide']
C2 --> E2[✅ Finds: 'running containers']
C2 --> F2[✅ Finds: 'Docker setup tutorial']
end
class B2,C2 semantic
classDef semantic stroke:#333,stroke-width:2px
Choisir un modèle d'emplacement
Il y a de nombreux modèles embeddings.
- Fonctionne localement (privéeté + vitesseM SK2
- Travaille avec ONNX Runtime (so que nous puissions utiliser notre GPU)
- Bonne qualité (compréhension sémantique exacte
- Taille droite (384-768 les dimensions sont bien équilibrées
Options populaires
| Modèle | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| all-MiniLMM SK1L 6-v2 | 384 ≥ | ≤80MB MSC4 Bon MSC5 Très rapide SMC6 ±MSC7 | |||||||||
| all-mpnetM SK1base-vMSC3 | 768 | ||||||||||
| bge-smallM SK1en -v1.5 | 384 | ||||||||||
| bge-baseMSC1en-vM SK3 | 768 | 2 | 3 | MB | 4 | Meilleure | 5 | Rapide | 6 | 7 | |
| Texte de l’OpenAI-embedding-adaM SK2 | 1536 |
Ma recommandation: bge-baseMSC1en-vM SK3
- État-deM SK1du-modèle open source
- 768 dimensions ( bon soldeM SK2
- Il fonctionne très bien avec ONNX Runtime
- Libre et fonctionne localement
Conversion au format ONNX
La plupart des modèles sont en format PyTorch. Nous avons besoin d’onNX pour C#.
Installer la bibliothèque Optimum (Python pour la conversion):
pip install optimum[exporters]
Convertre le modèle BGE:
optimum-cli export onnx --model BAAI/bge-base-en-v1.5 --task feature-extraction bge-base-en-onnx/
Cela crée:
bge-base-en-onnx/
model.onnx # The neural network
tokenizer.json # Text → tokens converter
tokenizer_config.json
special_tokens_map.json
config.json
Télécharger si vous ne voulez pas convertir ': De nombreux modèles sont avant -convertis et disponibles sur Hugging Face avec "onnx" sous le nom de .
Utilisation des embeddings dans C#
Let's bâtir un générateur d'embedding pratique à l'aide de Runtime ONNX
Établissement du projet
mkdir EmbeddingTest
cd EmbeddingTest
dotnet new console
dotnet add package Microsoft.ML.OnnxRuntime.Gpu --version 1.16.3
dotnet add package Microsoft.ML.Tokenizers --version 0.1.0-preview.23511.1
Pourquoi ces paquets?
OnnxRuntime.Gpu- Faire fonctionner le modèle sur GPUMicrosoft.ML.Tokenizers- Convertir le texte en IDs de token format d'entrée modèleM SK2
Tokenisation Premièrement
Avant d’incorporer ,, nous devons tokeniser (convertir le texte en chiffres):
using Microsoft.ML.Tokenizers;
using System;
using System.Linq;
public class SimpleTokenizer
{
private readonly Tokenizer _tokenizer;
public SimpleTokenizer(string tokenizerPath)
{
// Load the tokenizer.json file
_tokenizer = Tokenizer.CreateTokenizer(tokenizerPath);
}
public (long[] InputIds, long[] AttentionMask) Tokenize(string text, int maxLength = 512)
{
// Tokenize the text
var encoding = _tokenizer.Encode(text);
// Get token IDs
var ids = encoding.Ids.Select(i => (long)i).ToArray();
// Pad or truncate to maxLength
var inputIds = new long[maxLength];
var attentionMask = new long[maxLength];
int length = Math.Min(ids.Length, maxLength);
// Copy actual tokens
Array.Copy(ids, inputIds, length);
// Set attention mask (1 = real token, 0 = padding)
for (int i = 0; i < length; i++)
{
attentionMask[i] = 1;
}
return (inputIds, attentionMask);
}
}
Qu’est-ce qui se passe ici?
Encodage - "Welcome to the world
[101, 8667, 2088, 102]- Chaque numéro est un ID de token du vocabulaire du modèle'.
- Cotisations spéciales: M SK1 ≥= [TÉU, TPAC, TPMD, TM, TMÉU, TC: [TÉU, TPAC et TPMD
Établissement de padding - Les modèles s’attendent à une entrée de longueur fixe
- Si le texte est court: pad avec zéro
- Si le texte est long: atténuer
Maske d'attention - Dit au modèle quels tokens sont réels contre le padding
1= traiter ce token0= ignorer (itM SK2s padding)
graph LR
A["Text: 'Docker setup'"] --> B[Tokenizer]
B --> C[Token IDs:<br/>101, 12849, 12229, 102]
C --> D[Pad to 512]
D --> E[Input IDs:<br/>101, 12849, 12229, 102, 0, 0,...]
D --> F[Attention Mask:<br/>1, 1, 1, 1, 0, 0,...]
E --> G[Feed to Model]
F --> G
class B,G process
classDef process stroke:#333,stroke-width:2px
Classe des générateurs intégrés
Maintenant, le pipeline d’implantation complet:
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using System;
using System.Collections.Generic;
using System.Linq;
public class EmbeddingGenerator : IDisposable
{
private readonly InferenceSession _session;
private readonly SimpleTokenizer _tokenizer;
private readonly int _embeddingDimension;
public EmbeddingGenerator(string modelPath, string tokenizerPath, bool useGpu = true)
{
// Setup session options
var options = new SessionOptions();
if (useGpu)
{
options.AppendExecutionProvider_CUDA(0);
}
// Load model
_session = new InferenceSession(modelPath, options);
// Load tokenizer
_tokenizer = new SimpleTokenizer(tokenizerPath);
// Get embedding dimension from model output shape
var outputMetadata = _session.OutputMetadata["last_hidden_state"];
_embeddingDimension = outputMetadata.Dimensions[2]; // Usually 768 for base models
}
public float[] GenerateEmbedding(string text)
{
// Step 1: Tokenize
var (inputIds, attentionMask) = _tokenizer.Tokenize(text);
// Step 2: Create input tensors
var inputIdsTensor = new DenseTensor<long>(inputIds, new[] { 1, inputIds.Length });
var attentionMaskTensor = new DenseTensor<long>(attentionMask, new[] { 1, attentionMask.Length });
var inputs = new List<NamedOnnxValue>
{
NamedOnnxValue.CreateFromTensor("input_ids", inputIdsTensor),
NamedOnnxValue.CreateFromTensor("attention_mask", attentionMaskTensor)
};
// Step 3: Run inference
using var results = _session.Run(inputs);
// Step 4: Extract embeddings from output
var outputTensor = results.First().AsTensor<float>();
// Output shape is [batch_size, sequence_length, embedding_dim]
// We want [batch_size, embedding_dim] by mean pooling
return MeanPooling(outputTensor, attentionMask);
}
private float[] MeanPooling(Tensor<float> outputTensor, long[] attentionMask)
{
int seqLength = outputTensor.Dimensions[1];
int embeddingDim = outputTensor.Dimensions[2];
var embedding = new float[embeddingDim];
int tokenCount = 0;
// Average across all non-padded tokens
for (int seq = 0; seq < seqLength; seq++)
{
if (attentionMask[seq] == 0) continue; // Skip padding
tokenCount++;
for (int dim = 0; dim < embeddingDim; dim++)
{
embedding[dim] += outputTensor[0, seq, dim];
}
}
// Divide by count to get mean
for (int dim = 0; dim < embeddingDim; dim++)
{
embedding[dim] /= tokenCount;
}
// Normalize to unit length (common practice)
return Normalize(embedding);
}
private float[] Normalize(float[] vector)
{
float magnitude = 0;
foreach (var val in vector)
{
magnitude += val * val;
}
magnitude = MathF.Sqrt(magnitude);
var normalized = new float[vector.Length];
for (int i = 0; i < vector.Length; i++)
{
normalized[i] = vector[i] / magnitude;
}
return normalized;
}
public void Dispose()
{
_session?.Dispose();
}
}
Répartition des codes:
- Chargement du modèle - Crée une session ONNX avec support GPU
- Tokenisation - Convert le texte à des ID de token
- Création de tensors - Données de forme pour l'entrée du modèle
- Inférence - Conduit le réseau neuronal
- Répartition moyenne - Moyenne des emplacements de tokens → Emplacement d'une seule phrase
- Normalisation - Fait la longueur du vecteur 1 simplifie le calcul de la similitude
Répartition moyenne Expliqué
graph TB
A[Model Output:<br/>Token Embeddings] --> B["Token 0 (CLS):<br/>(0.1, 0.5, -0.3, ...)"]
A --> C["Token 1 (Docker):<br/>(0.4, 0.2, -0.1, ...)"]
A --> D["Token 2 (setup):<br/>(0.3, 0.6, -0.2, ...)"]
A --> E["Token 3 (SEP):<br/>(0.2, 0.3, -0.4, ...)"]
B --> F[Average]
C --> F
D --> F
E --> F
F --> G["Sentence Embedding:<br/>(0.25, 0.4, -0.25, ...)"]
class A input
class F process
class G output
classDef input stroke:#333
classDef process stroke:#333,stroke-width:2px
classDef output stroke:#333,stroke-width:2px
Nous sommes en moyenne parce:
- Chaque token a son propre emplacement
- Nous avons besoin d'un emplacement pour toute la phrase.
- L'évaluation permet de saisir le sens global
Exemple d'utilisation
using System;
class Program
{
static void Main(string[] args)
{
using var embedder = new EmbeddingGenerator(
modelPath: "bge-base-en-onnx/model.onnx",
tokenizerPath: "bge-base-en-onnx/tokenizer.json",
useGpu: true
);
// Generate embeddings
var embedding1 = embedder.GenerateEmbedding("Docker containerization tutorial");
var embedding2 = embedder.GenerateEmbedding("Setting up containers with Docker");
var embedding3 = embedder.GenerateEmbedding("Baking a chocolate cake");
Console.WriteLine($"Embedding dimension: {embedding1.Length}");
Console.WriteLine($"First 5 values: {string.Join(", ", embedding1.Take(5).Select(f => f.ToString("F4")))}");
// Calculate similarities
float sim12 = CosineSimilarity(embedding1, embedding2);
float sim13 = CosineSimilarity(embedding1, embedding3);
Console.WriteLine($"\nSimilarity (Docker vs Containers): {sim12:F4}"); // ~0.85
Console.WriteLine($"Similarity (Docker vs Cake): {sim13:F4}"); // ~0.10
}
static float CosineSimilarity(float[] a, float[] b)
{
// Since vectors are normalized, dot product = cosine similarity
float dot = 0;
for (int i = 0; i < a.Length; i++)
{
dot += a[i] * b[i];
}
return dot;
}
}
Résultats:
Embedding dimension: 768
First 5 values: 0.0123, -0.0456, 0.0789, -0.0234, 0.0567
Similarity (Docker vs Containers): 0.8542
Similarity (Docker vs Cake): 0.1023
Très beau! Contenu sémantiquement similaire a une grande similaritéM SK1 Contenus non liés sont faibles.
Bases de données des vecteurs
Nous devons en stocker des millions et effectuer une recherche efficace.
Le problème
Let's disons que nous avons 1000 des articles de blogM SK2 chacun en morceaux 10 pieces
Recherche naïve:
float bestSimilarity = -1;
int bestIndex = -1;
for (int i = 0; i < 10000; i++)
{
float sim = CosineSimilarity(queryEmbedding, storedEmbeddings[i]);
if (sim > bestSimilarity)
{
bestSimilarity = sim;
bestIndex = i;
}
}
Problème: C'est OM SK1n) - nous vérifions EVERY embedding
Pour 10,000 embeddings × 768 dimensions chacuneM SK3
- ~30 millions d’opérations de point flottant
- ~50-100ms même sur un CPU rapide
Nous avons besoin de quelque chose plus rapide.
Résolution de la base de données des vecteurs
Les bases de données vectorielles utilisent des structures de données intelligentes (comme l'HNSW - Graphiques hiérarchiques navigables du monde petitM SK2 pour trouver les voisins les plus proches dans O(log nMSC4 tempsMNK5
graph TB
A[Query Embedding] --> B[Vector Database]
B --> C{HNSW Index}
C --> D[Layer 2:<br/>Coarse Search]
D --> E[Layer 1:<br/>Refined Search]
E --> F[Layer 0:<br/>Exact Search]
F --> G[Top K Results]
H[10,000 vectors] -.Indexed.-> C
class B db
class C index
class G results
classDef db stroke:#333,stroke-width:4px
classDef index stroke:#333,stroke-width:2px
classDef results stroke:#333,stroke-width:2px
Comparaison de vitesse:
- Recherche naïve: M SK1ms pour les vecteurs 10K
- DB de vecteur (HNSWM SK1 1-5ms pour les vecteurs ≥10K
- 10-50x plus rapidesM SK1
Et il s’élargit : millions de vecteurs encore seulement prend ~10-20ms.
Choix d'une base de données Vector
Pour notre projet C#, nous avons besoin de ,
- Bonne bibliothèque de clients C#
- Facile à déployer (Docker)
- Bon rendement
- Source libre /
| Base de données | ||||
|---|---|---|---|---|
| Qdrant | ✅ Excellent MSC2 Docker MSC3 Très rapide SMC4 Apache SMC5 ≥MSC6 | |||
| pgvector | ✅ ≥(par l'entremise de Npgsql) ≤ | Extension Postgres MSC5 Fast MSC6 Licence PostgreSQL SMC7 | ||
| Favoriser | ✅ Bon MSC2 Docker MSC3 Très rapide SMC4 BSDMSc5 ♫ | |||
| Milvus | ⚠️ Limitée | Docker/KM SK4s M | Très rapide P | Apache MPSK7 ≥ |
| Chrome | ❌ Python-première Python3 Docker | Fast |
Mon choix: Qdrant
- Excellent client C# (
Qdrant.Client) - Déploiement simple du Docker
- Élaboré spécialement pour la recherche par vecteur (non encadré sur)
- Une excellente documentation
- Développement actif
Solutions de rechange: pgvector
- Nous utilisons déjà PostgreSQL pour le blog.
- Peut tenir tout dans une seule base de données
- Une architecture légèrement moins performante mais plus simple
Pour cette série, je vais utiliser Qdrant parce qu’il est constructif et plus facile à comprendre les concepts, mais je montrerai le pgvector comme alternative.
Établissement du Qdrant
Déploiement du docker
docker run -p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrant
Ports:
6333- REST API6334- gRPC API faster, nousM SK3nous utiliserons ce
Entreposage: Les données persistent à ./qdrant_storage sur l'hôte
Établissement du client C#
dotnet add package Qdrant.Client --version 1.7.0
Création d'une collection
Une collection est comme une table - elle contient des vecteurs d'une dimension spécifiqueM SK1
using Qdrant.Client;
using Qdrant.Client.Grpc;
public class QdrantSetup
{
private readonly QdrantClient _client;
public QdrantSetup(string host = "localhost", int port = 6334)
{
_client = new QdrantClient(host, port);
}
public async Task CreateCollectionAsync(string collectionName, ulong vectorSize)
{
// Check if collection exists
var collections = await _client.ListCollectionsAsync();
if (collections.Any(c => c.Name == collectionName))
{
Console.WriteLine($"Collection '{collectionName}' already exists");
return;
}
// Create collection
await _client.CreateCollectionAsync(
collectionName: collectionName,
vectorsConfig: new VectorParams
{
Size = vectorSize, // 768 for bge-base
Distance = Distance.Cosine // Cosine similarity
}
);
Console.WriteLine($"Created collection '{collectionName}' with {vectorSize} dimensions");
}
}
Paramètres clés:
Size- doit correspondre à votre modèle d'implantation 768 pour bgeDistance- Comment mesurer la similitudeM SK1Distance.Cosine- Similarité cosine la plus communeM SK2Distance.Euclid- Distance euclidienneDistance.Dot- Produit ponctuel
Insertion des vecteurs
using Qdrant.Client.Grpc;
using System.Collections.Generic;
public class QdrantInserter
{
private readonly QdrantClient _client;
public QdrantInserter(QdrantClient client)
{
_client = client;
}
public async Task InsertBlogChunkAsync(
string collectionName,
ulong id,
float[] embedding,
string blogPostSlug,
string chunkText,
int chunkIndex)
{
var point = new PointStruct
{
Id = id,
Vectors = embedding,
Payload =
{
["blog_post_slug"] = blogPostSlug,
["chunk_text"] = chunkText,
["chunk_index"] = chunkIndex,
["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
}
};
await _client.UpsertAsync(collectionName, new[] { point });
}
public async Task InsertBatchAsync(
string collectionName,
List<(ulong id, float[] embedding, Dictionary<string, object> payload)> points)
{
var qdrantPoints = points.Select(p => new PointStruct
{
Id = p.id,
Vectors = p.embedding,
Payload = { p.payload }
}).ToList();
// Batch insert for efficiency
await _client.UpsertAsync(collectionName, qdrantPoints);
Console.WriteLine($"Inserted {points.Count} points");
}
}
Charge de travail expliquée:
- Comme les métadonnées attachées à chaque vecteur
- Peut stocker quelque chose: titre d'affichage , texte en morceauxM SK2 date, catégories
- Recherche et filtration!
Insertion de lots:
- beaucoup plus rapide qu'un
- Qdrant manipule efficacement les lots de 100-1000
Vektors de recherche
public class QdrantSearcher
{
private readonly QdrantClient _client;
public QdrantSearcher(QdrantClient client)
{
_client = client;
}
public async Task<List<SearchResult>> SearchAsync(
string collectionName,
float[] queryEmbedding,
int topK = 10)
{
var searchResult = await _client.SearchAsync(
collectionName: collectionName,
vector: queryEmbedding,
limit: (ulong)topK,
scoreThreshold: 0.7f // Only return if similarity > 0.7
);
return searchResult.Select(r => new SearchResult
{
Id = r.Id.Num,
Score = r.Score,
BlogPostSlug = r.Payload["blog_post_slug"].StringValue,
ChunkText = r.Payload["chunk_text"].StringValue,
ChunkIndex = (int)r.Payload["chunk_index"].IntegerValue
}).ToList();
}
public async Task<List<SearchResult>> SearchWithFilterAsync(
string collectionName,
float[] queryEmbedding,
string blogPostSlug, // Only search within this post
int topK = 5)
{
var filter = new Filter
{
Must =
{
new Condition
{
Field = new FieldCondition
{
Key = "blog_post_slug",
Match = new Match { Keyword = blogPostSlug }
}
}
}
};
var searchResult = await _client.SearchAsync(
collectionName: collectionName,
vector: queryEmbedding,
filter: filter,
limit: (ulong)topK
);
return searchResult.Select(r => new SearchResult
{
Id = r.Id.Num,
Score = r.Score,
BlogPostSlug = r.Payload["blog_post_slug"].StringValue,
ChunkText = r.Payload["chunk_text"].StringValue,
ChunkIndex = (int)r.Payload["chunk_index"].IntegerValue
}).ToList();
}
}
public class SearchResult
{
public ulong Id { get; set; }
public float Score { get; set; }
public string BlogPostSlug { get; set; }
public string ChunkText { get; set; }
public int ChunkIndex { get; set; }
}
Fonctions de recherche:
limit- Retour en haut K le plus similairescoreThreshold- Retour seulement si la similitude dépasse le seuilfilter- Filtrer par métadonnées e.gM SK3 rechercher uniquement des postes spécifiquesMSC4
Exemple complète: Pipeline de recherche
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
// Setup
var embedder = new EmbeddingGenerator(
"bge-base-en-onnx/model.onnx",
"bge-base-en-onnx/tokenizer.json",
useGpu: true
);
var client = new QdrantClient("localhost", 6334);
var searcher = new QdrantSearcher(client);
// User query
string query = "How do I set up Docker with ASP.NET Core?";
// Generate query embedding
Console.WriteLine($"Searching for: {query}");
var queryEmbedding = embedder.GenerateEmbedding(query);
// Search
var results = await searcher.SearchAsync(
collectionName: "blog_embeddings",
queryEmbedding: queryEmbedding,
topK: 5
);
// Display results
Console.WriteLine($"\nFound {results.Count} results:\n");
foreach (var result in results)
{
Console.WriteLine($"Score: {result.Score:F4}");
Console.WriteLine($"Post: {result.BlogPostSlug}");
Console.WriteLine($"Chunk: {result.ChunkText.Substring(0, Math.Min(100, result.ChunkText.Length))}...");
Console.WriteLine();
}
}
}
Résultats:
Searching for: How do I set up Docker with ASP.NET Core?
Found 5 results:
Score: 0.8923
Post: dockercomposedevdeps
Chunk: In this post, I'll show you how to set up a development environment using Docker Compose. This is p...
Score: 0.8654
Post: dockercompose
Chunk: Docker Compose is a tool for defining and running multi-container Docker applications. With Compose...
Score: 0.8102
Post: addingentityframeworkforblogpostspt1
Chunk: You can set it up either as a windows service or using Docker as I presented in a previous post on...
Score: 0.7891
Post: imagesharpwithdocker
Chunk: When running ASP.NET Core applications in Docker containers, you may encounter issues with ImageSha...
Score: 0.7654
Post: selfhostingseq
Chunk: I use Docker Compose to run all my services. Here's the relevant part of my docker-compose.yml file...
Perfect! Il a trouvé des articles pertinents sur Docker et ASP.NET CoreM SK2 même si la phrase exacte n’apparaît pasMSC3
Optimisation du rendement
Établissement d'un emplacement par lots
Donn't generate embeddings one -by-oneM SK3 Batch themMSC4
public class BatchEmbeddingGenerator
{
private readonly EmbeddingGenerator _embedder;
public BatchEmbeddingGenerator(EmbeddingGenerator embedder)
{
_embedder = embedder;
}
public List<float[]> GenerateBatch(List<string> texts, int batchSize = 32)
{
var embeddings = new List<float[]>();
for (int i = 0; i < texts.Count; i += batchSize)
{
var batch = texts.Skip(i).Take(batchSize).ToList();
foreach (var text in batch)
{
embeddings.Add(_embedder.GenerateEmbedding(text));
}
Console.WriteLine($"Processed {Math.Min(i + batchSize, texts.Count)} / {texts.Count}");
}
return embeddings;
}
}
Pourquoi battre?
- Utilisation du GPU: maintient le GPU occupé
- Efficacité de la mémoire: réutilise les tampons
- Traçage des progrès: Rétroaction de l'utilisateur
Cachement des embeddings
Don'ne régénérer les embeddings pour le contenu inchangé!
using System.Security.Cryptography;
using System.Text;
public class EmbeddingCache
{
private readonly Dictionary<string, float[]> _cache = new();
public float[] GetOrGenerate(string text, Func<string, float[]> generator)
{
string hash = ComputeHash(text);
if (_cache.TryGetValue(hash, out var cached))
{
return cached;
}
var embedding = generator(text);
_cache[hash] = embedding;
return embedding;
}
private string ComputeHash(string text)
{
using var sha256 = SHA256.Create();
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(text));
return Convert.ToBase64String(bytes);
}
}
pgvector Alternative
Si vous voulez garder tout dans PostgreSQL:
Installer l'extension pgvector
CREATE EXTENSION vector;
Créer une table
CREATE TABLE blog_embeddings (
id SERIAL PRIMARY KEY,
blog_post_slug VARCHAR(255),
chunk_text TEXT,
chunk_index INT,
embedding VECTOR(768) -- 768 dimensions
);
-- Create HNSW index for fast search
CREATE INDEX ON blog_embeddings USING hnsw (embedding vector_cosine_ops);
Insérer de C#
using Npgsql;
using Pgvector;
public async Task InsertEmbeddingAsync(
string slug,
string chunkText,
int chunkIndex,
float[] embedding)
{
await using var conn = new NpgsqlConnection(connectionString);
await conn.OpenAsync();
await using var cmd = new NpgsqlCommand(
"INSERT INTO blog_embeddings (blog_post_slug, chunk_text, chunk_index, embedding) VALUES ($1, $2, $3, $4)",
conn
)
{
Parameters =
{
new() { Value = slug },
new() { Value = chunkText },
new() { Value = chunkIndex },
new() { Value = new Vector(embedding) }
}
};
await cmd.ExecuteNonQueryAsync();
}
Recherche avec pgvector
public async Task<List<SearchResult>> SearchAsync(float[] queryEmbedding, int topK = 10)
{
await using var conn = new NpgsqlConnection(connectionString);
await conn.OpenAsync();
await using var cmd = new NpgsqlCommand(
@"SELECT blog_post_slug, chunk_text, chunk_index,
1 - (embedding <=> $1) as similarity
FROM blog_embeddings
ORDER BY embedding <=> $1
LIMIT $2",
conn
)
{
Parameters =
{
new() { Value = new Vector(queryEmbedding) },
new() { Value = topK }
}
};
var results = new List<SearchResult>();
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
results.Add(new SearchResult
{
BlogPostSlug = reader.GetString(0),
ChunkText = reader.GetString(1),
ChunkIndex = reader.GetInt32(2),
Score = reader.GetFloat(3)
});
}
return results;
}
<=> opérateur: Distance de cosine ( inférieure est plus semblableM SK2
1 - distance: Convertre à la note de similitude (plus élevé est meilleurM SK2
Résumé
Nous'nous avons couvert:
- ✅ Quels sont les emplacements et pourquoi ils permettent la recherche sémantique
- ✅ Comment choisir et convertir un modèle d'implantation en ONNX
- ✅ Gérer des embeddings dans C# avec ONNX Runtime
- ✅ Concepts de bases de données vectorielles (HNSWM SK2 recherche de similitude)
- ✅ Utilisation de Qdrant pour l'entreposage et la recherche des vecteurs
- ✅ Solutions de rechange: pgvector dans PostgreSQL
- ✅ Optimisation du rendement (batching, cachingCSK3
Nous avons maintenant la technologie de base pour trouver des contenus blog sémantiquement similaires!
Qu’est-ce que's Suivant?
À l'intérieur **Partie 4: Établissement du pipeline d’ingestion**Nous construisons le pipeline d’ingestion.
- Lecture et analyse des fichiers de marquage (nous faisons déjà cela pour le blog!)
- Stratégies de regroupement intelligents
- Gérer des embeddings pour tous les contenus
- Insertion en vrac Qdrant
- Traitement des mises à jour et des suppressions
- Surveillance des progrès
Nous ' traiterons l’ensemble du corpus de notre blog et disposerons d’une base de connaissances recherchable.
Navigation des séries
- Partie 1: Introduction & Architecture
- Partie 2: Établissement d'un GPU & CUDA en C#
- Partie 3: Comprendre les emplacements & Bases de données vectorielles (le présent articleM SK1
- Partie 4: Établissement du pipeline d’ingestion
- Partie 5: Le client Windows
- Partie 6: Intégration locale de LLM
- Partie 7: Production de contenu & Génie rapide
- Partie 8: Faits avancés & Déploiement de la production
Ressources
- Transformers de phrase
- Documentation de Qdrant
- pgvector GitHub
- Le temps de fonctionnement d'ONNX
- Microsoft.ML.Tokenizers
Nous vous ferons plaisir. Partie 4!