This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
用 ONNX 和 Qdrant 友好语义搜索
Tuesday, 25 November 2025
一. 导言导言 导言 导言 导言
📖+RAG系列的一部分 本部分为“4a -核心执行”
- RAG 起源和基本要点 “-”什么嵌入是“,”,为什么它们重要?
- RAG 建筑和内部 @ -_ Chunking,}代号@,}矢量数据库
- 内容:3: 建立完整的RAG系统
- “4a :ONNX”“& Qdrant 履行” MPK0 @ this article\ _ _ MS K2} CPU% -}友好的语义搜索基金会
- 操作中的语义搜索 -typeahead,混合搜索@,和UI组件
- @ 5:_ 混合搜索@ MS K1\ 自动 @ MASK2 @ Indexing 生产一体化模式
- 6: 图表集 - 用于 proposal_- 水平理解的知识图表
1-3 @ 解释 为什么 语义搜索工作@. @% 此文章显示 如何 建立基金会 CPU=-}便利执行 使用 ONNX 运行时间和 Qdrant @ . 第一部分#4b 包括搜索 UI 和混合搜索执行@,}及 部分#5 覆盖自动生产@-_indexing @.
《挑战》 多数语义搜索解决方案都需要昂贵的 GPU 基础设施或高成本的管理服务@. @如果您是独立开发商,
解决方案#: 功能齐全的语义搜索系统, 它完全运行在 CPU@ MSKK0}使用 Free Open @ MS K1 source makings . {这是这个博客上正在运行的确切设置 {-} 零额外成本超过现有主机 < MSC4 > =%
核心概念
这些概念在《公约》中得到了深入阐述。 RAG系列@, @% 但在此 @ '_%s 您需要知道什么来实施此执行@:
内嵌嵌入@ :文本为数字
嵌入式是捕捉到该天体的矢量 (数数数中的矢量\ ) 意思 类似的含义产生相似的矢量 -\ \ ,\ MOSK2\ + 魔法= MSC3\ {
graph TD
A["Text: 'The cat sat on the mat'"] --> B[Embedding Model]
B --> C["Vector: [0.25, -0.18, 0.91, ... 384 more numbers]"]
D["Text: 'A feline rested on the carpet'"] --> B
B --> E["Vector: [0.27, -0.16, 0.89, ... similar numbers!]"]
C -.Similar vectors = similar meaning.-> E
style A stroke:#10b981,stroke-width:2px
style D stroke:#10b981,stroke-width:2px
style B stroke:#6366f1,stroke-width:3px
style C stroke:#f59e0b,stroke-width:2px
style E stroke:#f59e0b,stroke-width:2px
关键洞察力: 具有类似含义的文字将会有相似矢量 {(} embeddings).这样我们才能找到 \ "}\ 相关{MS K3 内容 {-}\ we}'re 字面上测量意义之间的距离=!}
理解余心相似性
余心相似性 测量两个矢量之间的角@-,如果它们指向类似的方向 @, 它们#'re 语义相似:
flowchart LR
subgraph "Vector Space (simplified to 2D)"
direction TB
A["'Docker tutorial'"] -.-> B((0.85))
C["'Container deployment'"] -.-> B
D["'Cooking recipes'"] -.-> E((0.12))
A -.-> E
end
B --> F["High Similarity<br/>Related content!"]
E --> G["Low Similarity<br/>Different topics"]
style A stroke:#10b981,stroke-width:2px
style C stroke:#10b981,stroke-width:2px
style D stroke:#f59e0b,stroke-width:2px
style B stroke:#22c55e,stroke-width:3px
style E stroke:#ef4444,stroke-width:3px
style F stroke:#22c55e,stroke-width:2px
style G stroke:#ef4444,stroke-width:2px
公式=: similarity = (A · B) / (||A|| × ||B||) @-,但自从我们L2- 调整我们的向量#, 它简化到仅仅点产物@!
什么是ONNX ??
开放神经网络交流 是一个用于机器学习模型的开放标准格式,使其能够在不同平台上高效运行。 “.” 把它想象成是AI 模式的通用翻译器“.” NONX 运行时间 是执行这些模型的 微软'+Qs high-}性能推断引擎=.}
为什么我们需要ONNX?
- CPU 上的运行@( @no GPU 需要 !)}\ MPK2看到 ONNX 运行时 CPU 执行供应商 docs
- 比在 Python 运行模型快得多
- 较小的记忆足迹
- 通过 & . @NET Microsoft .ML.OnnxRuntime Nuget 软件包
- 支助支助 图形优化 快速推断
flowchart LR
subgraph "ONNX Inference Pipeline"
A[Raw Text] --> B[Tokenizer]
B --> C["Tokens: [CLS] the cat sat [SEP]"]
C --> D[Token IDs: 101 1996 4937 2068 102]
D --> E[ONNX Runtime]
E --> F[384-dim Vector]
F --> G[L2 Normalize]
G --> H[Final Embedding]
end
style A stroke:#10b981,stroke-width:2px
style B stroke:#f59e0b,stroke-width:2px
style C stroke:#f59e0b,stroke-width:2px
style D stroke:#f59e0b,stroke-width:2px
style E stroke:#6366f1,stroke-width:3px
style F stroke:#8b5cf6,stroke-width:2px
style G stroke:#8b5cf6,stroke-width:2px
style H stroke:#ef4444,stroke-width:2px
什么是 Qdrant?
解冻 是 OpenQ-源矢量数据库 @ - 基本上是一个存储和搜索这些嵌入矢量的优化数据库@ MS K2 要深潜到 Qdrant'}{概念\ , 配置@,和 Cç#}整合 , 见 带有 Qdrant 的自定义“-” 矢量数据库# 和你在一起的时候# 能够 PostgreSQL, Qdrant 中的存储矢量是为此而建造的,并且提供\ :
- 使用 Lightning+-#fast 类似搜索 HNSW 算法
- 元数据过滤 - 有效载荷字段的过滤结果
- 向成百万个矢量的 已分配部署
- 低资源使用量 - 轻巧的硬件可舒适地运行
- 以 Docker 为主的自定义@- @ access Qdrant Docker 快速启动
- g/RPC和REST APPs 融合的融入
- @.NET 本地支持 Qdrant.Cclient Nuget 软件包
flowchart TB
subgraph "Qdrant Vector Storage"
direction TB
A[Collection: blog_posts] --> B[Point 1]
A --> C[Point 2]
A --> D[Point N...]
B --> B1["Vector: [0.12, -0.08, ...]"]
B --> B2["Payload: {slug, title, language}"]
C --> C1["Vector: [0.25, 0.14, ...]"]
C --> C2["Payload: {slug, title, language}"]
end
subgraph "Vector Search"
E[Query Vector] --> F[HNSW Index]
F --> G[Cosine Similarity]
G --> H[Top K Results]
end
style A stroke:#ef4444,stroke-width:3px
style B stroke:#8b5cf6,stroke-width:2px
style C stroke:#8b5cf6,stroke-width:2px
style D stroke:#8b5cf6,stroke-width:2px
style B1 stroke:#f59e0b,stroke-width:2px
style B2 stroke:#10b981,stroke-width:2px
style C1 stroke:#f59e0b,stroke-width:2px
style C2 stroke:#10b981,stroke-width:2px
style E stroke:#6366f1,stroke-width:2px
style F stroke:#ec4899,stroke-width:3px
style G stroke:#ec4899,stroke-width:2px
style H stroke:#10b981,stroke-width:2px
建筑结构概览
我们的语义搜索系统如何搭配 : *
flowchart TB
subgraph "Content Ingestion"
A[Blog Post Markdown] --> B[Extract Plain Text]
B --> C[ONNX Embedding Service]
C --> D[Generate 384-dim Vector]
D --> E[Qdrant Vector Store]
end
subgraph "Search Flow"
F[User Query] --> G[ONNX Embedding Service]
G --> H[Generate Query Vector]
H --> I[Qdrant Search]
E -.Vector Similarity.-> I
I --> J[Ranked Results]
end
subgraph "Related Posts"
K[Current Blog Post] --> L[Get Post Vector from Qdrant]
L --> M[Find Similar Vectors]
E -.->M
M --> N[Top 5 Related Posts]
end
style A stroke:#10b981,stroke-width:2px
style B stroke:#10b981,stroke-width:2px
style C stroke:#6366f1,stroke-width:3px
style D stroke:#f59e0b,stroke-width:2px
style E stroke:#ef4444,stroke-width:3px
style F stroke:#10b981,stroke-width:2px
style G stroke:#6366f1,stroke-width:3px
style H stroke:#f59e0b,stroke-width:2px
style I stroke:#ef4444,stroke-width:2px
style J stroke:#8b5cf6,stroke-width:2px
style K stroke:#10b981,stroke-width:2px
style L stroke:#ef4444,stroke-width:2px
style M stroke:#ef4444,stroke-width:2px
style N stroke:#8b5cf6,stroke-width:2px
以纯英语的流 :
- 索引编制: 写博客 post @,时, 我们将其转换为矢量并存储在 Qdrant
- 搜索@ : @ {当有人搜索 MS K1} 我们将其查询转换为矢量, 在 Qdrant 中找到相似的矢量
- 相关员额@:_ 任何博客的possible post@,}我们能找到类似矢量的其他文章
项目结构
我们创造了一个干净的“,”模块结构 “:”
Mostlylucid.SemanticSearch/
├── Config/
│ └── SemanticSearchConfig.cs # Configuration settings
├── Models/
│ ├── BlogPostDocument.cs # Document model for indexing
│ └── SearchResult.cs # Search result model
├── Services/
│ ├── IEmbeddingService.cs # Embedding interface
│ ├── OnnxEmbeddingService.cs # ONNX-based embeddings
│ ├── IVectorStoreService.cs # Vector store interface
│ ├── QdrantVectorStoreService.cs # Qdrant implementation
│ ├── ISemanticSearchService.cs # High-level search interface
│ └── SemanticSearchService.cs # Orchestration service
├── Extensions/
│ └── ServiceCollectionExtensions.cs # DI registration
├── download-models.sh # Model download script
└── README.md
执行 执行情况
设置工程
创建新类库@: @
dotnet new classlib -n Mostlylucid.SemanticSearch -f net9.0
dotnet sln add Mostlylucid.SemanticSearch
添加必需的 Nuget 软件包@: @% 1
cd Mostlylucid.SemanticSearch
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Microsoft.ML.OnnxRuntime --version 1.21.1
dotnet add package Qdrant.Client --version 1.14.0
dotnet add reference ../Mostlylucid.Shared/Mostlylucid.Shared.csproj
步骤 @ 2:_ 配置
让' @%s 设置我们的配置类@ . @ we'}使用 IConfigSection Mostlylucid@ : NAME OF TRANSLATORS 中所使用的值
using Mostlylucid.Shared.Config;
namespace Mostlylucid.SemanticSearch.Config;
/// <summary>
/// Configuration for semantic search functionality
/// </summary>
public class SemanticSearchConfig : IConfigSection
{
public static string Section => "SemanticSearch";
/// <summary>
/// Enable or disable semantic search
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Qdrant server URL (e.g., http://localhost:6333)
/// </summary>
public string QdrantUrl { get; set; } = "http://localhost:6333";
/// <summary>
/// Optional read-only API key for Qdrant (used for search operations)
/// </summary>
public string? ReadApiKey { get; set; }
/// <summary>
/// Optional read-write API key for Qdrant (used for indexing operations)
/// </summary>
public string? WriteApiKey { get; set; }
/// <summary>
/// Collection name in Qdrant for blog posts
/// </summary>
public string CollectionName { get; set; } = "blog_posts";
/// <summary>
/// Path to the ONNX embedding model file
/// </summary>
public string EmbeddingModelPath { get; set; } = "models/all-MiniLM-L6-v2.onnx";
/// <summary>
/// Path to the tokenizer vocabulary file
/// </summary>
public string VocabPath { get; set; } = "models/vocab.txt";
/// <summary>
/// Embedding vector size (384 for all-MiniLM-L6-v2)
/// </summary>
public int VectorSize { get; set; } = 384;
/// <summary>
/// Number of related posts to return
/// </summary>
public int RelatedPostsCount { get; set; } = 5;
/// <summary>
/// Minimum similarity score (0-1) for related posts
/// </summary>
public float MinimumSimilarityScore { get; set; } = 0.5f;
/// <summary>
/// Number of search results to return
/// </summary>
public int SearchResultsCount { get; set; } = 10;
}
为何分隔 API 键“?” 安全@! 您的读密钥可以用于公共场合 @ - afacing search endpoints@ MS K2}而您写入的密钥只用于管理操作 @ MPK4}
添加此添加到您的 appsettings.json:
{
"SemanticSearch": {
"Enabled": false,
"QdrantUrl": "http://localhost:6333",
"ReadApiKey": "",
"WriteApiKey": "",
"CollectionName": "blog_posts",
"EmbeddingModelPath": "models/all-MiniLM-L6-v2.onnx",
"VocabPath": "models/vocab.txt",
"VectorSize": 384,
"RelatedPostsCount": 5,
"MinimumSimilarityScore": 0.5,
"SearchResultsCount": 10
}
}
ONNX嵌入服务
在此出现魔法发生的地方 @.We're 使用全部@-MiniLM}-L6-vMS K5model_,。 该模型是专门为语义相似性任务设计的,并高效运行于 CPU+.
为何这个模型?
- 大小小@(~90MB)
- 每个嵌入中的 CPU @ (~50-100 @ ms
- 优质嵌入@(384 @ 维度 @ )#
- 培训了超过10亿对的刑期
这里的'%s 完整执行@: @%
using Microsoft.Extensions.Logging;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using Mostlylucid.SemanticSearch.Config;
using System.Text.RegularExpressions;
namespace Mostlylucid.SemanticSearch.Services;
public class OnnxEmbeddingService : IEmbeddingService, IDisposable
{
private readonly ILogger<OnnxEmbeddingService> _logger;
private readonly SemanticSearchConfig _config;
private readonly InferenceSession? _session;
private readonly Dictionary<string, int> _vocabulary;
private readonly SemaphoreSlim _semaphore = new(1, 1);
private bool _disposed;
private const int MaxSequenceLength = 256;
private const string PadToken = "[PAD]";
private const string UnkToken = "[UNK]";
private const string ClsToken = "[CLS]";
private const string SepToken = "[SEP]";
public OnnxEmbeddingService(
ILogger<OnnxEmbeddingService> logger,
SemanticSearchConfig config)
{
_logger = logger;
_config = config;
_vocabulary = new Dictionary<string, int>();
if (!_config.Enabled)
{
_logger.LogInformation("Semantic search is disabled");
return;
}
try
{
// Check if model file exists
if (!File.Exists(_config.EmbeddingModelPath))
{
_logger.LogWarning("Embedding model not found at {Path}. Semantic search will be disabled.",
_config.EmbeddingModelPath);
return;
}
// Load vocabulary if it exists
if (File.Exists(_config.VocabPath))
{
LoadVocabulary(_config.VocabPath);
}
// Create ONNX session with CPU execution provider
var sessionOptions = new SessionOptions
{
ExecutionMode = ExecutionMode.ORT_SEQUENTIAL,
GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL
};
_session = new InferenceSession(_config.EmbeddingModelPath, sessionOptions);
_logger.LogInformation("ONNX embedding model loaded successfully from {Path}",
_config.EmbeddingModelPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize ONNX embedding service");
}
}
private void LoadVocabulary(string vocabPath)
{
var lines = File.ReadAllLines(vocabPath);
for (int i = 0; i < lines.Length; i++)
{
var token = lines[i].Trim();
if (!string.IsNullOrEmpty(token))
{
_vocabulary[token] = i;
}
}
_logger.LogInformation("Loaded vocabulary with {Count} tokens", _vocabulary.Count);
}
public async Task<float[]> GenerateEmbeddingAsync(string text, CancellationToken cancellationToken = default)
{
if (_session == null || !_config.Enabled)
{
return new float[_config.VectorSize];
}
if (string.IsNullOrWhiteSpace(text))
{
return new float[_config.VectorSize];
}
// Use semaphore to prevent concurrent ONNX inference (not thread-safe)
await _semaphore.WaitAsync(cancellationToken);
try
{
return await Task.Run(() => GenerateEmbedding(text), cancellationToken);
}
finally
{
_semaphore.Release();
}
}
private float[] GenerateEmbedding(string text)
{
try
{
// Tokenize the input text
var tokens = Tokenize(text);
// Create input tensors for ONNX model
var inputIds = CreateInputTensor(tokens, "input_ids");
var attentionMask = CreateAttentionMaskTensor(tokens.Length);
var tokenTypeIds = CreateTokenTypeIdsTensor(tokens.Length);
// Run inference
var inputs = new List<NamedOnnxValue>
{
NamedOnnxValue.CreateFromTensor("input_ids", inputIds),
NamedOnnxValue.CreateFromTensor("attention_mask", attentionMask),
NamedOnnxValue.CreateFromTensor("token_type_ids", tokenTypeIds)
};
using var results = _session!.Run(inputs);
// Extract the output tensor (sentence embedding)
var output = results.First().AsTensor<float>();
var embedding = output.ToArray();
// Normalize the vector (L2 normalization)
return NormalizeVector(embedding);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating embedding for text: {Text}",
text[..Math.Min(100, text.Length)]);
return new float[_config.VectorSize];
}
}
private List<int> Tokenize(string text)
{
// Simple whitespace + punctuation tokenization
var tokens = new List<int>();
// Add [CLS] token at the start
if (_vocabulary.TryGetValue(ClsToken, out var clsId))
tokens.Add(clsId);
// Tokenize the text
var words = Regex.Split(text.ToLowerInvariant(), @"(\W+)")
.Where(w => !string.IsNullOrWhiteSpace(w))
.Take(MaxSequenceLength - 2); // Leave room for [CLS] and [SEP]
foreach (var word in words)
{
if (_vocabulary.Count > 0)
{
if (_vocabulary.TryGetValue(word, out var tokenId))
tokens.Add(tokenId);
else if (_vocabulary.TryGetValue(UnkToken, out var unkId))
tokens.Add(unkId);
}
else
{
// Fallback: use hash code as token ID
tokens.Add(Math.Abs(word.GetHashCode()) % 30000);
}
}
// Add [SEP] token at the end
if (_vocabulary.TryGetValue(SepToken, out var sepId))
tokens.Add(sepId);
return tokens;
}
private Tensor<long> CreateInputTensor(List<int> tokens, string name)
{
var length = Math.Min(tokens.Count, MaxSequenceLength);
var tensorData = new long[1, MaxSequenceLength];
for (int i = 0; i < length; i++)
{
tensorData[0, i] = tokens[i];
}
// Pad the rest
var padId = _vocabulary.TryGetValue(PadToken, out var id) ? id : 0;
for (int i = length; i < MaxSequenceLength; i++)
{
tensorData[0, i] = padId;
}
return new DenseTensor<long>(tensorData, new[] { 1, MaxSequenceLength });
}
private Tensor<long> CreateAttentionMaskTensor(int actualLength)
{
var length = Math.Min(actualLength, MaxSequenceLength);
var tensorData = new long[1, MaxSequenceLength];
for (int i = 0; i < length; i++)
{
tensorData[0, i] = 1; // Attend to actual tokens
}
return new DenseTensor<long>(tensorData, new[] { 1, MaxSequenceLength });
}
private Tensor<long> CreateTokenTypeIdsTensor(int actualLength)
{
var tensorData = new long[1, MaxSequenceLength];
// All zeros for single sentence
return new DenseTensor<long>(tensorData, new[] { 1, MaxSequenceLength });
}
private float[] NormalizeVector(float[] vector)
{
// L2 normalization
var sumOfSquares = vector.Sum(v => v * v);
var magnitude = MathF.Sqrt(sumOfSquares);
if (magnitude > 0)
{
for (int i = 0; i < vector.Length; i++)
{
vector[i] /= magnitude;
}
}
return vector;
}
public void Dispose()
{
if (_disposed) return;
_session?.Dispose();
_semaphore?.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}
初级 Devs: 的要点
- 声当化:We' 正在将文字破解成小块 @ MS K2tokens@)该模型能够理解
- 代数: 这些是MONX模型使用的多维数组
- 注意掩码: 告诉了输入中哪些部分是实际内容的模型 vs.padding
- L2 正常化: 使所有向量都具有相同的 MS K1 长 ", 这样我们就可以公平地比较它们
- 塞马磷@ :_ 确保线条安全 % 1@ ONNX is MS K2} t线索@ -# 默认安全 * _
Qdrant 矢量存储
现在让我们执行矢量存储和搜索@: @%
using Microsoft.Extensions.Logging;
using Mostlylucid.SemanticSearch.Config;
using Mostlylucid.SemanticSearch.Models;
using Qdrant.Client;
using Qdrant.Client.Grpc;
namespace Mostlylucid.SemanticSearch.Services;
public class QdrantVectorStoreService : IVectorStoreService
{
private readonly ILogger<QdrantVectorStoreService> _logger;
private readonly SemanticSearchConfig _config;
private readonly QdrantClient? _client;
private bool _collectionInitialized;
public QdrantVectorStoreService(
ILogger<QdrantVectorStoreService> logger,
SemanticSearchConfig config)
{
_logger = logger;
_config = config;
if (!_config.Enabled)
{
_logger.LogInformation("Semantic search is disabled");
return;
}
try
{
var uri = new Uri(_config.QdrantUrl);
var host = uri.Host;
var port = uri.Port > 0 ? uri.Port : 6334; // Default gRPC port
_client = new QdrantClient(host, port, https: uri.Scheme == "https");
_logger.LogInformation("Connected to Qdrant at {Host}:{Port}", host, port);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to connect to Qdrant at {Url}", _config.QdrantUrl);
}
}
public async Task InitializeCollectionAsync(CancellationToken cancellationToken = default)
{
if (_client == null || !_config.Enabled || _collectionInitialized)
return;
try
{
var collections = await _client.ListCollectionsAsync(cancellationToken);
var collectionExists = collections.Any(c => c.Name == _config.CollectionName);
if (!collectionExists)
{
_logger.LogInformation("Creating collection {CollectionName}", _config.CollectionName);
await _client.CreateCollectionAsync(
collectionName: _config.CollectionName,
vectorsConfig: new VectorParams
{
Size = (ulong)_config.VectorSize,
Distance = Distance.Cosine // Cosine similarity for semantic search
},
cancellationToken: cancellationToken
);
_logger.LogInformation("Collection {CollectionName} created successfully", _config.CollectionName);
}
_collectionInitialized = true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize collection {CollectionName}", _config.CollectionName);
throw;
}
}
public async Task<List<SearchResult>> FindRelatedPostsAsync(
string slug,
string language,
int limit = 5,
CancellationToken cancellationToken = default)
{
if (_client == null || !_config.Enabled)
return new List<SearchResult>();
try
{
// Find the document by slug and language
var scrollResults = await _client.ScrollAsync(
collectionName: _config.CollectionName,
filter: new Filter
{
Must =
{
new Condition
{
Field = new FieldCondition
{
Key = "slug",
Match = new Match { Keyword = slug }
}
},
new Condition
{
Field = new FieldCondition
{
Key = "language",
Match = new Match { Keyword = language }
}
}
}
},
limit: 1,
cancellationToken: cancellationToken
);
var point = scrollResults.FirstOrDefault();
if (point == null)
{
_logger.LogWarning("Post {Slug} ({Language}) not found in vector store", slug, language);
return new List<SearchResult>();
}
// Use the document's vector to find similar posts
var searchResults = await _client.SearchAsync(
collectionName: _config.CollectionName,
vector: point.Vectors.Vector.Data.ToArray(),
limit: (ulong)(limit + 1), // +1 because the first result will be the post itself
scoreThreshold: _config.MinimumSimilarityScore,
cancellationToken: cancellationToken
);
// Filter out the original post and return top N similar posts
return searchResults
.Where(r => r.Payload["slug"].StringValue != slug || r.Payload["language"].StringValue != language)
.Take(limit)
.Select(result => new SearchResult
{
Slug = result.Payload["slug"].StringValue,
Title = result.Payload["title"].StringValue,
Language = result.Payload["language"].StringValue,
Categories = result.Payload.TryGetValue("categories", out var cats)
? cats.ListValue.Values.Select(v => v.StringValue).ToList()
: new List<string>(),
Score = result.Score,
PublishedDate = DateTime.Parse(result.Payload["published_date"].StringValue)
})
.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to find related posts for {Slug} ({Language})", slug, language);
return new List<SearchResult>();
}
}
// ... Additional methods for IndexDocument, Search, Delete, etc.
}
这里发生什么了?
- 余弦距离:We're使用相近的余弦@,,这非常适合比较正常向量
- 元数据存储: Qdrant 让我们在矢量的同时存储额外数据@ MS K1 payload)
- 过滤在比较矢量之前,我们可以通过元数据过滤结果
- 最低分数: 只有返回结果超过某种相似得分
指挥服务处
高端服务将一切连结在一起
using Microsoft.Extensions.Logging;
using Mostlylucid.SemanticSearch.Config;
using Mostlylucid.SemanticSearch.Models;
using System.Security.Cryptography;
using System.Text;
namespace Mostlylucid.SemanticSearch.Services;
public class SemanticSearchService : ISemanticSearchService
{
private readonly ILogger<SemanticSearchService> _logger;
private readonly SemanticSearchConfig _config;
private readonly IEmbeddingService _embeddingService;
private readonly IVectorStoreService _vectorStoreService;
public SemanticSearchService(
ILogger<SemanticSearchService> logger,
SemanticSearchConfig config,
IEmbeddingService embeddingService,
IVectorStoreService vectorStoreService)
{
_logger = logger;
_config = config;
_embeddingService = embeddingService;
_vectorStoreService = vectorStoreService;
}
public async Task IndexPostAsync(BlogPostDocument document, CancellationToken cancellationToken = default)
{
if (!_config.Enabled)
return;
try
{
// Prepare text for embedding: combine title and content
// We give more weight to the title by including it twice
var textToEmbed = $"{document.Title}. {document.Title}. {document.Content}";
// Truncate to reasonable length (embedding models have token limits)
const int maxLength = 2000;
if (textToEmbed.Length > maxLength)
{
textToEmbed = textToEmbed[..maxLength];
}
// Generate embedding
var embedding = await _embeddingService.GenerateEmbeddingAsync(textToEmbed, cancellationToken);
// Compute content hash if not provided
if (string.IsNullOrEmpty(document.ContentHash))
{
document.ContentHash = ComputeContentHash(document.Content);
}
// Store in vector database
await _vectorStoreService.IndexDocumentAsync(document, embedding, cancellationToken);
_logger.LogInformation("Indexed post {Slug} ({Language})", document.Slug, document.Language);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to index post {Slug} ({Language})", document.Slug, document.Language);
}
}
public async Task<List<SearchResult>> SearchAsync(
string query,
int limit = 10,
CancellationToken cancellationToken = default)
{
if (!_config.Enabled || string.IsNullOrWhiteSpace(query))
return new List<SearchResult>();
try
{
// Generate embedding for the search query
var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(query, cancellationToken);
// Search in vector store
var results = await _vectorStoreService.SearchAsync(
queryEmbedding,
Math.Min(limit, _config.SearchResultsCount),
_config.MinimumSimilarityScore,
cancellationToken);
_logger.LogDebug("Search for '{Query}' returned {Count} results", query, results.Count);
return results;
}
catch (Exception ex)
{
_logger.LogError(ex, "Search failed for query '{Query}'", query);
return new List<SearchResult>();
}
}
public async Task<List<SearchResult>> GetRelatedPostsAsync(
string slug,
string language,
int limit = 5,
CancellationToken cancellationToken = default)
{
if (!_config.Enabled)
return new List<SearchResult>();
try
{
var results = await _vectorStoreService.FindRelatedPostsAsync(
slug,
language,
Math.Min(limit, _config.RelatedPostsCount),
cancellationToken);
_logger.LogDebug("Found {Count} related posts for {Slug} ({Language})",
results.Count, slug, language);
return results;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get related posts for {Slug} ({Language})", slug, language);
return new List<SearchResult>();
}
}
private string ComputeContentHash(string content)
{
using var sha256 = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(content);
var hashBytes = sha256.ComputeHash(bytes);
return Convert.ToBase64String(hashBytes);
}
}
\ 6:\ 依赖注射设置
在 DI 容器中登记所有内容@: @%
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Mostlylucid.SemanticSearch.Config;
using Mostlylucid.SemanticSearch.Services;
using Mostlylucid.Shared.Config;
namespace Mostlylucid.SemanticSearch.Extensions;
public static class ServiceCollectionExtensions
{
public static void AddSemanticSearch(
this IServiceCollection services,
IConfiguration configuration)
{
// Bind configuration using POCO pattern
services.ConfigurePOCO<SemanticSearchConfig>(
configuration.GetSection(SemanticSearchConfig.Section));
// Register services as singletons for efficiency
services.AddSingleton<IEmbeddingService, OnnxEmbeddingService>();
services.AddSingleton<IVectorStoreService, QdrantVectorStoreService>();
services.AddSingleton<ISemanticSearchService, SemanticSearchService>();
}
}
在你的 Program.cs:
using Mostlylucid.SemanticSearch.Extensions;
using Mostlylucid.SemanticSearch.Services;
// Add services
services.AddSemanticSearch(config);
// Initialize after building the app
using (var scope = app.Services.CreateScope())
{
var semanticSearch = scope.ServiceProvider.GetRequiredService<ISemanticSearchService>();
await semanticSearch.InitializeAsync();
}
建立基础设施
Qdrant 的嵌入器合成
为语义搜索服务创建单独的 docker @ -}composte 文件@ MS K1
version: '3.8'
services:
qdrant:
image: qdrant/qdrant:latest
container_name: mostlylucid-qdrant
restart: unless-stopped
ports:
- "6333:6333" # HTTP API
- "6334:6334" # gRPC API
volumes:
- qdrant_storage:/qdrant/storage
environment:
- QDRANT__SERVICE__HTTP_PORT=6333
- QDRANT__SERVICE__GRPC_PORT=6334
networks:
- mostlylucid_network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:6333/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
qdrant_storage:
driver: local
networks:
mostlylucid_network:
name: mostlylucidweb_app_network
external: true
从:开始
docker-compose -f semantic-search-docker-compose.yml up -d
下载嵌入模型
我们正在使用 {\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2款 来自 Huggging Face 的模型@' @%s 句子变换器 该模型专门就语义相似性任务进行了培训,并制作了“384-”-维嵌入.}
自动下载@( @ 推荐@ )}
如果该服务确实存在 @ ' @ title@ MS K1} , 它会自动从 Hugging Face 下载模型的首运行模式 。
// In OnnxEmbeddingService.cs
private const string ModelUrl = "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx";
private const string VocabUrl = "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/vocab.txt";
public async Task EnsureInitializedAsync(CancellationToken cancellationToken = default)
{
if (_initialized || !_config.Enabled) return;
// Download model if not exists
if (!File.Exists(_config.EmbeddingModelPath))
{
_logger.LogInformation("Downloading ONNX embedding model to {Path}...", _config.EmbeddingModelPath);
await DownloadFileAsync(ModelUrl, _config.EmbeddingModelPath, cancellationToken);
}
// Download vocab if not exists
if (!File.Exists(_config.VocabPath))
{
_logger.LogInformation("Downloading vocabulary file to {Path}...", _config.VocabPath);
await DownloadFileAsync(VocabUrl, _config.VocabPath, cancellationToken);
}
// Initialize ONNX session...
}
这在使用 Docker {-} 部署时特别有用, 您可以绘制模型目录的音量@ :} @
volumes:
- ./mlmodels:/app/mlmodels # Model persists across container restarts
手动下载
option, 您可以手动下载 手动@ :
chmod +x Mostlylucid.SemanticSearch/download-models.sh
./Mostlylucid.SemanticSearch/download-models.sh
或直接来自 Huggging Face@: @
mkdir -p mlmodels
curl -L https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx -o mlmodels/all-MiniLM-L6-v2.onnx
curl -L https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/vocab.txt -o mlmodels/vocab.txt
此下载@ : @
all-MiniLM-L6-v2.onnx(~90MB) NSK2 ONNX- 出口嵌入模型vocab.txt(~230KB)- WordPiece 表示器词典
业绩考量
内嵌生成
- CPU 性能: ~50-100 mms 每嵌入现代CPU
- 优化: 我们用血压来防止同时发生ONNX的推理
- 条纹用于批量索引的: , 分批处理 process points
矢量搜索
- 搜索速度:{<10}%s 收藏至 @100K矢量
- 内存使用: MS K1KB 每个矢量( 元数据)
- 缩缩:Qdrant可以用微小的硬件处理数百万个矢量。
缓结战略
我们使用 ASP.NET 核心输出缓存\ :
[OutputCache(Duration = 7200, VaryByRouteValueNames = new[] {"slug", "language"})]
@2@ hours ,}大大降低负载=.
我们所建造的
您目前有一个完整的 ,工作语义搜索基础@:
- ✅ ONNX 嵌入器 - CPU- 友好} MS K2 自动 - 下載自 Hugging face
- ✅ Qdrant 矢量存储 - 具有元数据过滤功能的快速相似搜索
- ✅ 相关员额 @- 找到相似的语义内容
- ✅ APIP 搜索 -自然语言查询
- ✅ 内容索引编制 @-_ 存储博客文章作为矢量
这是在这个博客上运行的精确设置 零GPU+,+0 零额外费用
下次:语义搜索动作Name
内 操作中的语义搜索我们负责:
- 类型标题搜索 “-”搜索如何与阿尔卑斯山合作?
- 混合搜索 - 结合语义语义 + PostgreSQL full}- text 和对等合唱
- APIP 搜索 - 带有过滤器的完整 API 文件
- 相关员额 -DisaiUI组件,带有HTMX懒惰装载
- 高级过滤器 - 语言和日期范围过滤
继续 第一部分#4b 搜索 UI 和混合搜索执行@. @%
然后 @ 5:_ 混合搜索@ MS K1\ 自动 @ MASK2 @ Indexing 生产一体化模式*.
资源资源资源 资源和资源资源资源
ONNX 文件
- ONNX 官方网站 -ML模型的开放标准
- NONX 运行时间 - 微软' 高端- 性能发酵引擎
- ONNX 运行时间 @ .+NET API -C#API docs
- ONNX 运行时性能 -优化指南
Qdrant 文档
- 带有 Qdrant 的自定义“-” 矢量数据库 深潜到 Qdrant 概念和 C# 客户端
- 解冻官方文件 -主要文件中心
- 快速启动 Qdrant 正在启动
- Qdrant 矢量索引 -+HNSW 算法
- Qdrant @ .@NET 客户端 -=YTET -伊甸园字幕组=- 翻译:
嵌入模型
- {\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2款 - 我们使用的模型
- 句子变换器 -语义嵌入库
完整代码
所有代码都可用于 @: @% 多数为平质网,
Mostlylucid.SemanticSearch/-核心语义搜索库