scored_index_reference_model 模块技术深度解析
1. 模块概述
scored_index_reference_model 模块(即 internal.types.retriever 包中的 IndexWithScore 相关类型)是整个检索系统的核心数据结构,它负责将来自不同检索引擎的结果统一表示为带评分的索引引用。
问题背景
在现代知识库系统中,通常会使用多种检索方式来获取相关内容:
- 关键词检索
- 向量相似度检索
- Web 搜索
- 混合检索
每种检索方式可能使用不同的后端引擎(PostgreSQL、Elasticsearch、Milvus、Qdrant 等),并且产生不同格式的结果。如果没有一个统一的结果表示方式,上层应用需要处理多种不同的数据格式,这会导致代码复杂度高、维护困难。
设计目标
IndexWithScore 的设计目标是:
- 统一结果表示:无论使用哪种检索引擎和方式,结果都用同一种数据结构表示
- 保持溯源信息:结果必须包含足够的元数据,以便追踪到原始内容
- 支持排序和筛选:包含评分、匹配类型等信息,便于后续处理
- 多租户支持:明确标识租户、知识库等层级信息
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 | 是否启用 | 允许在检索结果中过滤掉禁用的内容 |
设计亮点
完整的层级引用链:
- 从
ChunkID→KnowledgeID→KnowledgeBaseID,形成了完整的内容溯源链 - 每个层级都可以独立访问,支持不同粒度的操作
评分的标准化:
- 虽然不同检索引擎的评分范围可能不同,但统一通过
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字段允许不同检索引擎传递特定参数- 这是一种 "契约式设计",核心参数标准化,特殊参数通过扩展机制传递
多维度过滤:
- 支持按知识库、知识、标签等多个维度过滤
- 同时支持排除特定内容(
ExcludeKnowledgeIDs、ExcludeChunkIDs)
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 │
│ 引擎 │ │搜索 │ │ 引擎 │
└─────────┘ └─────────┘ └─────────┘
(多种检索引擎实现)
典型数据流
- 请求进入:上层应用构造
RetrieveParams - 路由分发:根据
RetrieverType和RetrieverEngineType路由到相应的引擎 - 引擎执行:各检索引擎执行查询,产生原始结果
- 结果转换:各引擎将原始结果转换为
[]*IndexWithScore - 结果封装:包装成
RetrieveResult,包含引擎信息和可能的错误 - 结果合并/排序:如果是混合检索,多个
RetrieveResult会被合并和重新排序 - 返回应用:最终结果返回给上层应用
4. 设计决策与权衡
决策 1:使用扁平结构而非嵌套结构
选择:IndexWithScore 使用了扁平的字段结构,而不是将相关字段组织成嵌套结构。
原因:
- 序列化友好:扁平结构在 JSON 序列化和数据库映射时更简单
- 访问便捷:不需要通过多层嵌套访问字段
- 兼容性:更容易与不同的数据存储系统兼容
权衡:
- 牺牲了一些结构上的清晰度
- 字段较多时,构造对象会比较繁琐
决策 2:包含完整的引用链
选择:同时包含 ChunkID、KnowledgeID、KnowledgeBaseID,即使理论上可以通过 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为空但Error为nil:表示检索成功但没有匹配结果Error不为nil:表示检索过程中出现错误,可能有部分结果,也可能没有
评分的相对性
Score 字段的值是相对的,只在同一检索结果集中有意义。不要:
- 比较不同检索请求的评分
- 假设评分有绝对的语义(如 "0.8 以上就是好结果")
数据一致性
虽然 IndexWithScore 包含完整的引用链,但这些数据是检索时的快照。如果原始数据在检索后被修改,IndexWithScore 中的信息不会自动更新。
IsEnabled 字段
这个字段表示检索时内容是否启用,但应该由上层应用决定如何处理禁用的内容。有些场景可能希望显示禁用内容(如管理员预览),有些场景可能希望过滤掉。
7. 总结
scored_index_reference_model 模块虽然代码量不大,但它是整个检索系统的"语言学"基础。通过定义统一的结果表示,它使得多种检索引擎可以协同工作,同时为上层应用提供了简洁一致的接口。
这个模块的设计体现了几个重要的原则:
- 接口与实现分离:定义统一的接口,允许多种实现
- 实用性优先:为了性能和便利性,接受一定程度的数据冗余
- 扩展性设计:通过扩展点(如
AdditionalParams)保持灵活性 - 完整性考虑:包含足够的元数据,使得结果可以独立使用
对于新加入团队的开发者,理解这个模块是理解整个检索系统的第一步。