# 实施者RAG:混合搜索和自动索引

<datetime class="hidden">2025-11-22T12:00</datetime>

<!-- category -- ASP.NET, Semantic Search, ONNX, Qdrant, Machine Learning, Vector Search, RAG, AI-Article -->
# 一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

**部分RAG系列:** 这是第五部分 - 生产一体化模式:

- [第1部分:RAG起源和基本要点](/blog/rag-primer) - 什么是嵌入,为什么它们重要
- [第2部分:RAG建筑和内部](/blog/rag-architecture) - 启动、象征化、矢量数据库
- [第3部分:在实务中协助通知书](/blog/rag-practical-applications) - 建立完整的ARAG系统
- [第4a部分:ONNX和Qdrant执行](/blog/semantic-search-with-onnx-and-qdrant) - CPU友好语义搜索基金会
- [第4b部分:语义搜索行动](/blog/semantic-search-in-action) - 头型、混合搜索和UI组件
- **第5部分:混合搜索和自动插入** (本条) -- -- 生产一体化模式
- [第6部分:图表](/blog/graphrag-knowledge-graphs-for-rag) - 用于了解人身知识的知识图

内 [第4部分a](/blog/semantic-search-with-onnx-and-qdrant)我们建立了基础:ONNX嵌入和Qdrant存储。 [第4部分b](/blog/semantic-search-in-action),我们覆盖了搜索界面和混合搜索实施。 **自动自动索引化** (零触摸内容更新)通过文件系统监视器。

[TOC]

# 混合搜索:两个世界中最好的

语义搜索是强有力的,但传统的全文搜索在确切的词语和技术术语方面仍然优异。 **解决办法?** 两者皆用。

**为什么是混血?** 不同办法的优点不同:

- **PostgresSQL 全文本** ([覆盖于此](/blog/textsearchingpt1):精确匹配、技术术语、布尔操作员
- **语义矢量搜索**含义、上下文、同义词、概念相关内容

## 相互排名融合(RRF)

我们用 **相互排名融合** 组合多个搜索来源的结果:

```mermaid
flowchart TB
    A[User Query: 'docker containers'] --> B[PostgreSQL Full-Text Search]
    A --> C[Semantic Vector Search]

    B --> D["Results:<br/>1. 'Docker Basics' (rank 1)<br/>2. 'Containerizing Apps' (rank 2)<br/>3. 'Docker Compose' (rank 3)"]
    C --> E["Results:<br/>1. 'Containerizing Apps' (rank 1)<br/>2. 'Kubernetes Guide' (rank 2)<br/>3. 'Docker Basics' (rank 3)"]

    D --> F[RRF Algorithm]
    E --> F

    F --> G["Combined Results:<br/>1. 'Containerizing Apps'<br/>   (1/61 + 1/62 = 0.0328)<br/>2. 'Docker Basics'<br/>   (1/61 + 1/63 = 0.0322)<br/>3. 'Docker Compose'<br/>   (1/63 = 0.0159)"]

    style A stroke:#10b981,stroke-width:2px
    style B stroke:#3b82f6,stroke-width:2px
    style C stroke:#6366f1,stroke-width:2px
    style D stroke:#3b82f6,stroke-width:2px
    style E stroke:#6366f1,stroke-width:2px
    style F stroke:#ec4899,stroke-width:4px
    style G stroke:#8b5cf6,stroke-width:2px
```

**RRF公式:** `score = Σ(1 / (k + rank))`

- `k` = 60(防止早期排行排行排行排行至上)
- `rank` = 搜索方法结果中的位置
- 成果和成果 **两者兼** 每个来源加起来都得分

**RRF为何工作:**

- **调解**:同样的结果是两个来源的得分都更高
- **公平公正**:没有一种搜索方法不公平地占主导地位
- **简单化**:不需要复杂的调试

## 执行 执行情况 执行

```csharp
public class HybridSearchService : IHybridSearchService
{
    private readonly ISemanticSearchService _semanticSearchService;
    private const int RrfConstant = 60;

    public async Task<List<SearchResult>> SearchAsync(
        string query,
        string language = "en",
        int limit = 10,
        CancellationToken cancellationToken = default)
    {
        // Execute both searches in parallel
        var semanticResults = await _semanticSearchService.SearchAsync(
            query, limit * 2, cancellationToken);

        // Filter by language and apply RRF
        var filteredResults = semanticResults
            .Where(r => r.Language == language)
            .ToList();

        return ApplyReciprocalRankFusion(filteredResults)
            .Take(limit)
            .ToList();
    }

    private List<SearchResult> ApplyReciprocalRankFusion(List<SearchResult> results)
    {
        var rrfScores = new Dictionary<string, RrfScore>();

        for (int i = 0; i < results.Count; i++)
        {
            var result = results[i];
            var key = $"{result.Slug}_{result.Language}";

            if (!rrfScores.ContainsKey(key))
                rrfScores[key] = new RrfScore { Result = result };

            // RRF formula: 1 / (k + rank)
            rrfScores[key].Score += 1.0 / (RrfConstant + i + 1);
        }

        return rrfScores.Values
            .OrderByDescending(x => x.Score)
            .Select(x => x.Result)
            .ToList();
    }
}
```

> **注:** 这只显示语义搜索。 制作时, 平行执行 PostgreSQL 全文搜索, 并将这些结果纳入 RRF 计算 。

## 融入

如果您已经实施了 PostgreSQL 全文搜索( PostgreSQL) 。[所覆盖的](/blog/textsearchingpt1)添加语义搜索是直截了当的:

```csharp
// Program.cs
services.AddSemanticSearch(configuration);
services.AddSingleton<IHybridSearchService, HybridSearchService>();
```

```csharp
[HttpGet("search/hybrid")]
public async Task<IActionResult> HybridSearch(string query, string language = "en")
{
    var results = await _hybridSearchService.SearchAsync(query, language);
    return PartialView("_SearchResults", results);
}
```

# 使用文件系统监视器自动索引

最强大的特征是: **自动自动索引化**保存博客文章, 即刻可以搜索 - 没有手动干预。

## 如何运作

```mermaid
flowchart TB
    A[Save Markdown File] --> B[FileSystemWatcher Detects Change]
    B --> C{File in Main Directory?}
    C -->|Yes| D[Save to Database]
    C -->|No| E[Save to Database Only]
    D --> F[Create BlogPostDocument]
    F --> G[Generate Embedding via ONNX]
    G --> H[Store in Qdrant]
    H --> I[Post Searchable Immediately]

    style A stroke:#10b981,stroke-width:2px
    style B stroke:#f59e0b,stroke-width:2px
    style C stroke:#ec4899,stroke-width:3px
    style D stroke:#3b82f6,stroke-width:2px
    style E stroke:#6b7280,stroke-width:2px
    style F stroke:#8b5cf6,stroke-width:2px
    style G stroke:#6366f1,stroke-width:3px
    style H stroke:#ef4444,stroke-width:2px
    style I stroke:#10b981,stroke-width:2px
```

**关键设计决定:** 仅在 **主标记按下目录**,而不是子目录(`translated/`, `drafts/`, `comments/`这样可以保持搜索索引的干净。

## 文件监视器集成

博客已经有一个 `MarkdownDirectoryWatcherService`我们将其扩展至触发语义索引:

```csharp
// In MarkdownDirectoryWatcherService.cs
private async Task OnChangedAsync(WaitForChangedResult e)
{
    if (e.Name == null) return;

    await retryPolicy.ExecuteAsync(async () =>
    {
        var savedModel = await blogService.SavePost(slug, language, markdown);

        // Index ONLY if file is in main directory (no path separators in name)
        if (!e.Name.Contains(Path.DirectorySeparatorChar) &&
            !e.Name.Contains(Path.AltDirectorySeparatorChar))
        {
            await IndexPostForSemanticSearchAsync(scope, savedModel, language);
        }
    });
}

private async Task IndexPostForSemanticSearchAsync(
    IServiceScope scope,
    BlogPostDto post,
    string language)
{
    var semanticSearchService = scope.ServiceProvider.GetService<ISemanticSearchService>();
    if (semanticSearchService == null) return; // Not configured

    var document = new BlogPostDocument
    {
        Id = $"{post.Slug}_{language}",
        Slug = post.Slug,
        Title = post.Title,
        Content = post.PlainTextContent,
        Language = language,
        Categories = post.Categories?.ToList() ?? new List<string>(),
        PublishedDate = post.PublishedDate
    };

    await semanticSearchService.IndexPostAsync(document);
    _logger.LogInformation("Indexed {Slug} ({Language}) in semantic search", post.Slug, language);
}
```

## 处理删除

删除时, 从语义索引中删除 :

```csharp
private async Task OnDeletedAsync(WaitForChangedResult e)
{
    await blogService.Delete(slug, language);

    // Delete from semantic search ONLY if file was in main directory
    if (!e.Name.Contains(Path.DirectorySeparatorChar) &&
        !e.Name.Contains(Path.AltDirectorySeparatorChar))
    {
        var semanticSearchService = scope.ServiceProvider.GetService<ISemanticSearchService>();
        await semanticSearchService?.DeletePostAsync(slug, language);
    }
}
```

# 初始索引编制背景服务

启动时,尚未在Qdrant设置的现有员额有一个背景服务指数:

```mermaid
flowchart TB
    A[Application Starts] --> B[Wait 10 seconds]
    B --> C[Initialize Semantic Search]
    C --> D{Model Exists?}
    D -->|No| E[Download from Hugging Face]
    D -->|Yes| F[Load ONNX Model]
    E --> F
    F --> G[Scan Main Markdown Directory]
    G --> H{For Each .md File}
    H --> I[Compute Content Hash]
    I --> J{Hash Changed?}
    J -->|Yes| K[Generate Embedding]
    J -->|No| L[Skip - Already Indexed]
    K --> M[Store in Qdrant]
    M --> H
    L --> H
    H -->|Done| N[Indexing Complete]

    style A stroke:#10b981,stroke-width:2px
    style C stroke:#6366f1,stroke-width:2px
    style E stroke:#f59e0b,stroke-width:2px
    style F stroke:#6366f1,stroke-width:3px
    style G stroke:#8b5cf6,stroke-width:2px
    style J stroke:#ec4899,stroke-width:3px
    style K stroke:#6366f1,stroke-width:2px
    style M stroke:#ef4444,stroke-width:2px
    style N stroke:#10b981,stroke-width:2px
```

```csharp
public class SemanticIndexingBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Wait for app to be ready
        await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);

        // Initialize (downloads model if needed)
        await _semanticSearchService.InitializeAsync(stoppingToken);

        // Get all posts from main directory only
        var markdownFiles = Directory.GetFiles(
            _markdownConfig.MarkdownPath,
            "*.md",
            SearchOption.TopDirectoryOnly);  // NOT subdirectories

        foreach (var file in markdownFiles)
        {
            var needsIndexing = await _semanticSearchService.NeedsReindexingAsync(
                slug, language, contentHash, stoppingToken);

            if (needsIndexing)
                await _semanticSearchService.IndexPostAsync(document, stoppingToken);
        }
    }
}
```

**这确保:**

1. **Lazy 模型装货** - 首次使用下载,不阻碍启动
2. **递增指数化** - 只有(通过内容散列)重新索引的新的/修改的日志
3. **仅主目录** - 草稿和翻译文件不会污染索引

# 我们所建造的

横跨第4a、4b和5部分,我们现在有:

- ✅ **CPU 方便的语义搜索** - 不需要 GPU
- ✅ **相关职位的发现** - 具有历史相似内容的内容
- ✅ **自然语言搜索** - 不仅查找关键词,还查明关键词
- ✅ **混合查找** - 最好的语义语法+全文
- ✅ **自动索引化** - 零触碰内容更新
- ✅ **自自托管** - 您的数据保留在服务器上

**未来改进:**

- **类别软件搜索** - 特定类别的促进结果
- **多语种嵌入器** - 具体语言嵌入模式
- **开放搜索船一体化** - 在混合混合体中添加 OpenSearch( OpenSearch)[看我的《开放搜索》](/blog/textsearchingpt3))

# 结论 结论 结论 结论 结论

这完成了实际实施RAG式语义搜索。 [第4部分a](/blog/semantic-search-with-onnx-and-qdrant) (基金会)和(基金会) [第4部分b](/blog/semantic-search-in-action) (搜索 UI), 您需要的一切, 都可以在您的. NET 应用程序中添加智能搜索 - 完全在 CPU 上运行, 以零额外费用 。

## 继续学习

- **[RAG 第一部分:起源和基本](/blog/rag-primer)** - 嵌入背后的理论
- **[RAG 第二部分:建筑和内部](/blog/rag-architecture)** - 深入潜入RAG系统
- **[RAG 第三部分:实际应用](/blog/rag-practical-applications)** - 与LLLM整合完成RAG
- **[第4a部分:ONNX和Qdrant执行](/blog/semantic-search-with-onnx-and-qdrant)** - 基础:嵌入和病媒储存
- **[第4b部分:语义搜索行动](/blog/semantic-search-in-action)** - 头型、混合搜索和UI
- **[带有 PostgreSQL 的全文本搜索](/blog/textsearchingpt1)** - 混合搜索的全文

## 资源资源资源 资源资源资源 资源资源 资源资源

### Qdrant 和矢量数据库

- [带有 Qdrant 的自住矢量数据库](/blog/self-hosted-vector-databases-qdrant) - 深入潜入 Qdrant 概念、 HNSW 索引、过滤和 C# 客户端
- [Qdrant 混合搜索](https://qdrant.tech/documentation/concepts/hybrid-queries/) - Qdrant的本地混合支持

### 混合搜索

- [相互排名合并文件](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf) RRF算法

### 文件系统监视

- [文件系统监视器类](https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher) - .网 域 文件
- [背景服务类](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice) - ASP.NET核心托管服务

### 完整代码

所有代码可在以下网址查阅: [com/scottgal/ mostlylylucidweb 缩略图/ com/ scottgal/ mostlylylucidweb 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图/ 缩略图](https://github.com/scottgal/mostlylucidweb)

- `Mostlylucid.SemanticSearch/` - 核心语义搜索库
- `Mostlylucid/Blog/WatcherService/` - 带有语义索引的文件监视器