🏠

scored_index_reference_model 模块技术深度解析

1. 模块概述

scored_index_reference_model 模块(即 internal.types.retriever 包中的 IndexWithScore 相关类型)是整个检索系统的核心数据结构,它负责将来自不同检索引擎的结果统一表示为带评分的索引引用。

问题背景

在现代知识库系统中,通常会使用多种检索方式来获取相关内容:

  • 关键词检索
  • 向量相似度检索
  • Web 搜索
  • 混合检索

每种检索方式可能使用不同的后端引擎(PostgreSQL、Elasticsearch、Milvus、Qdrant 等),并且产生不同格式的结果。如果没有一个统一的结果表示方式,上层应用需要处理多种不同的数据格式,这会导致代码复杂度高、维护困难。

设计目标

IndexWithScore 的设计目标是:

  1. 统一结果表示:无论使用哪种检索引擎和方式,结果都用同一种数据结构表示
  2. 保持溯源信息:结果必须包含足够的元数据,以便追踪到原始内容
  3. 支持排序和筛选:包含评分、匹配类型等信息,便于后续处理
  4. 多租户支持:明确标识租户、知识库等层级信息

2. 核心组件深度解析

IndexWithScore 结构体

IndexWithScore 是这个模块的核心数据结构,它表示一个带评分的索引引用。

type IndexWithScore struct {
    // ID
    ID string
    // Content
    Content string
    // Source ID
    SourceID string
    // Source type
    SourceType SourceType
    // Chunk ID
    ChunkID string
    // Knowledge ID
    KnowledgeID string
    // Knowledge base ID
    KnowledgeBaseID string
    // Tag ID
    TagID string
    // Score
    Score float64
    // Match type
    MatchType MatchType
    // IsEnabled
    IsEnabled bool
}

字段详解

字段 类型 描述 设计意图
ID string 唯一标识符 标识这个特定的检索结果条目
Content string 内容片段 实际返回给用户的内容,通常是文本片段
SourceID string 源 ID 原始内容的来源标识
SourceType SourceType 源类型 表示内容来源类型(如文档、FAQ、网页等)
ChunkID string 分块 ID 指向具体的 Chunk,用于精确定位
KnowledgeID string 知识 ID 指向所属的 Knowledge
KnowledgeBaseID string 知识库 ID 指向所属的 KnowledgeBase
TagID string 标签 ID 用于分类和筛选,特别是在 FAQ 场景中
Score float64 评分 表示匹配程度,用于排序
MatchType MatchType 匹配类型 表示是关键词匹配、向量匹配还是其他类型
IsEnabled bool 是否启用 允许在检索结果中过滤掉禁用的内容

设计亮点

完整的层级引用链

  • ChunkIDKnowledgeIDKnowledgeBaseID,形成了完整的内容溯源链
  • 每个层级都可以独立访问,支持不同粒度的操作

评分的标准化

  • 虽然不同检索引擎的评分范围可能不同,但统一通过 Score 字段表示
  • 配合 GetScore() 方法,实现了可比较接口的设计意图

RetrieveParams 结构体

RetrieveParams 定义了检索请求的参数。

type RetrieveParams struct {
    // Query text
    Query string
    // Query embedding (used for vector retrieval)
    Embedding []float32
    // Knowledge base IDs
    KnowledgeBaseIDs []string
    // Knowledge IDs
    KnowledgeIDs []string
    // Tag IDs for filtering (used for FAQ priority filtering)
    TagIDs []string
    // Excluded knowledge IDs
    ExcludeKnowledgeIDs []string
    // Excluded chunk IDs
    ExcludeChunkIDs []string
    // Number of results to return
    TopK int
    // Similarity threshold
    Threshold float64
    // Knowledge type (e.g., "faq", "manual") - determines which index to use
    KnowledgeType string
    // Additional parameters, different retrievers may require different parameters
    AdditionalParams map[string]interface{}
    // Retriever type
    RetrieverType RetrieverType
}

参数设计哲学

灵活性与扩展性

  • AdditionalParams 字段允许不同检索引擎传递特定参数
  • 这是一种 "契约式设计",核心参数标准化,特殊参数通过扩展机制传递

多维度过滤

  • 支持按知识库、知识、标签等多个维度过滤
  • 同时支持排除特定内容(ExcludeKnowledgeIDsExcludeChunkIDs

RetrieveResult 结构体

RetrieveResult 封装了一次检索的完整结果。

type RetrieveResult struct {
    Results             []*IndexWithScore   // Retrieval results
    RetrieverEngineType RetrieverEngineType // Retrieval source type
    RetrieverType       RetrieverType       // Retrieval type
    Error               error               // Retrieval error
}

设计意图

结果与元数据分离

  • 实际结果在 Results
  • 关于这次检索的元数据(使用了什么引擎、什么检索方式)单独存储

错误处理

  • 包含 Error 字段,允许返回部分结果 + 错误信息
  • 这种设计在混合检索场景中很有用,即使一个引擎失败,其他引擎的结果仍可使用

GetScore 方法

func (i *IndexWithScore) GetScore() float64 {
    return i.Score
}

这个简单的方法揭示了一个重要的设计模式 - 接口抽象。虽然我们在代码中没有看到显式的接口定义,但这个方法的存在表明系统中可能存在这样一个接口,用于统一处理所有可比较评分的对象。

3. 架构与数据流

在系统中的位置

IndexWithScore 处于检索系统的核心位置,连接着底层检索引擎和上层应用:

┌─────────────────────────────────────────────────────────────┐
│                      上层应用层                               │
│  (聊天管道、问答服务、搜索服务等)                              │
└────────────────────┬────────────────────────────────────────┘
                     │
                     │ 使用
                     │
┌────────────────────▼────────────────────────────────────────┐
│              RetrieveResult / IndexWithScore                │
│                   (统一结果表示)                              │
└────────────────────┬────────────────────────────────────────┘
                     │
         ┌───────────┼───────────┐
         │           │           │
         ▼           ▼           ▼
┌─────────┐  ┌─────────┐  ┌─────────┐
│ Postgres│  │Elastic  │  │  Milvus │
│ 引擎    │  │搜索     │  │  引擎   │
└─────────┘  └─────────┘  └─────────┘
    (多种检索引擎实现)

典型数据流

  1. 请求进入:上层应用构造 RetrieveParams
  2. 路由分发:根据 RetrieverTypeRetrieverEngineType 路由到相应的引擎
  3. 引擎执行:各检索引擎执行查询,产生原始结果
  4. 结果转换:各引擎将原始结果转换为 []*IndexWithScore
  5. 结果封装:包装成 RetrieveResult,包含引擎信息和可能的错误
  6. 结果合并/排序:如果是混合检索,多个 RetrieveResult 会被合并和重新排序
  7. 返回应用:最终结果返回给上层应用

4. 设计决策与权衡

决策 1:使用扁平结构而非嵌套结构

选择IndexWithScore 使用了扁平的字段结构,而不是将相关字段组织成嵌套结构。

原因

  • 序列化友好:扁平结构在 JSON 序列化和数据库映射时更简单
  • 访问便捷:不需要通过多层嵌套访问字段
  • 兼容性:更容易与不同的数据存储系统兼容

权衡

  • 牺牲了一些结构上的清晰度
  • 字段较多时,构造对象会比较繁琐

决策 2:包含完整的引用链

选择:同时包含 ChunkIDKnowledgeIDKnowledgeBaseID,即使理论上可以通过 ChunkID 推导出其他 ID。

原因

  • 性能优化:避免了额外的数据库查询来获取层级信息
  • 可用性:即使在没有数据库连接的场景下,也能使用这些信息
  • 独立性:检索结果可以独立存在,不依赖于外部系统

权衡

  • 数据冗余
  • 更新时需要确保一致性(虽然在检索结果场景中很少更新)

决策 3:使用 AdditionalParams 作为扩展点

选择:通过 map[string]interface{} 类型的 AdditionalParams 来处理不同引擎的特殊需求。

原因

  • 灵活性:不需要修改核心结构就能支持新的参数
  • 渐进式采用:新引擎可以使用特殊参数,而不影响现有代码
  • 简化接口:核心接口保持稳定,特殊需求通过扩展处理

权衡

  • 类型安全性降低
  • 参数可发现性变差(需要查看文档或实现代码)
  • 序列化/反序列化复杂度增加

决策 4:评分字段的标准化处理

选择:所有引擎都将评分归一化到 Score 字段,并提供 GetScore() 方法。

原因

  • 统一排序:不同引擎的结果可以统一排序
  • 简化比较逻辑:上层代码不需要知道评分的具体来源
  • 接口抽象:为未来的扩展(如自定义评分函数)提供钩子

权衡

  • 不同引擎的评分语义可能不同,统一表示可能丢失信息
  • 归一化过程可能引入误差

5. 使用指南与最佳实践

构造 IndexWithScore

当实现新的检索引擎时,正确构造 IndexWithScore 对象非常重要:

result := &types.IndexWithScore{
    ID:               generateUniqueID(),
    Content:          chunk.Content,
    SourceID:         chunk.KnowledgeID, // 或其他合适的源标识
    ChunkID:          chunk.ID,
    KnowledgeID:      chunk.KnowledgeID,
    KnowledgeBaseID:  chunk.KnowledgeBaseID,
    TagID:            chunk.TagID,
    Score:            normalizeScore(rawScore), // 重要:归一化评分
    IsEnabled:        chunk.IsEnabled,
}

评分归一化

虽然系统没有强制规定评分范围,但通常建议:

  • 向量相似度:0 到 1 之间,越接近 1 越相关
  • 关键词匹配:可以使用 TF-IDF 或 BM25 分数,通常归一化到 0-1 范围
  • 混合检索:确保不同检索方式的评分具有可比性

处理 RetrieveResult

当使用检索结果时,应该总是检查 Error 字段:

result := engine.Retrieve(ctx, params)
if result.Error != nil {
    // 处理错误,但可能仍有部分结果可用
    log.Warn("Retrieval encountered error", "error", result.Error)
}

// 处理结果
for _, item := range result.Results {
    // 使用 item
}

6. 边缘情况与注意事项

空结果与错误的区别

  • Results 为空但 Errornil:表示检索成功但没有匹配结果
  • Error 不为 nil:表示检索过程中出现错误,可能有部分结果,也可能没有

评分的相对性

Score 字段的值是相对的,只在同一检索结果集中有意义。不要:

  • 比较不同检索请求的评分
  • 假设评分有绝对的语义(如 "0.8 以上就是好结果")

数据一致性

虽然 IndexWithScore 包含完整的引用链,但这些数据是检索时的快照。如果原始数据在检索后被修改,IndexWithScore 中的信息不会自动更新。

IsEnabled 字段

这个字段表示检索时内容是否启用,但应该由上层应用决定如何处理禁用的内容。有些场景可能希望显示禁用内容(如管理员预览),有些场景可能希望过滤掉。

7. 总结

scored_index_reference_model 模块虽然代码量不大,但它是整个检索系统的"语言学"基础。通过定义统一的结果表示,它使得多种检索引擎可以协同工作,同时为上层应用提供了简洁一致的接口。

这个模块的设计体现了几个重要的原则:

  1. 接口与实现分离:定义统一的接口,允许多种实现
  2. 实用性优先:为了性能和便利性,接受一定程度的数据冗余
  3. 扩展性设计:通过扩展点(如 AdditionalParams)保持灵活性
  4. 完整性考虑:包含足够的元数据,使得结果可以独立使用

对于新加入团队的开发者,理解这个模块是理解整个检索系统的第一步。

On this page