🏠

Qdrant Vector Retrieval Repository

概述

想象一下,你有一个巨大的图书馆,里面存放着数百万本书的"语义指纹"——每本书都被转换成了一串数字向量,相似的书籍在数学空间中彼此靠近。当用户问一个问题时,你需要在毫秒级时间内找到语义上最相关的那些书。qdrant_vector_retrieval_repository 模块就是这个图书馆的智能索引系统

这个模块是系统中多个向量检索后端之一(其他包括 ElasticsearchMilvusPostgreSQL),它使用 Qdrant 作为底层向量数据库。Qdrant 是一个专为向量相似度搜索设计的数据库,特别适合高维向量的快速检索和过滤。

为什么需要专门的向量检索后端? 朴素的做法是把所有向量存在关系型数据库里,然后计算每个向量与查询向量的距离——但这在数据量达到百万级时会变得不可接受地慢。Qdrant 使用 HNSW(Hierarchical Navigable Small World)等近似最近邻算法,能在亚秒级时间内从数亿向量中找到最相似的结果,同时支持基于元数据的过滤(比如"只搜索某个知识库内的内容")。

这个模块的核心职责是:

  1. 存储:将文档片段的向量表示及其元数据持久化到 Qdrant
  2. 检索:根据查询向量快速找到最相似的文档片段
  3. 管理:支持批量操作、条件删除、状态更新等运维需求

架构与数据流

graph TB subgraph "上层服务" ChatPipeline[Chat Pipeline Plugins] RetrieveEngine[RetrieveEngine Service] end subgraph "检索引擎层" RetrieveEngineRepo[RetrieveEngineRepository Interface] end subgraph "Qdrant 向量检索仓库 (本模块)" QdrantRepo[qdrantRepository] QdrantClient[Qdrant Go Client] VectorEmbedding[QdrantVectorEmbedding] VectorWithScore[QdrantVectorEmbeddingWithScore] end subgraph "Qdrant 数据库" QdrantServer[(Qdrant Server)] Collections[Collections by Dimension] end ChatPipeline --> RetrieveEngine RetrieveEngine --> RetrieveEngineRepo RetrieveEngineRepo --> QdrantRepo QdrantRepo --> QdrantClient QdrantClient --> QdrantServer QdrantRepo -. 缓存初始化状态 .- Collections VectorEmbedding -. 写入数据结构 .- QdrantRepo VectorWithScore -. 读取返回结构 .- QdrantRepo

架构角色解析

qdrantRepository 是整个模块的核心协调者,它扮演着网关 + 缓存层的角色:

  1. 网关职责:将系统内部的统一接口(RetrieveEngineRepository)转换为 Qdrant 特有的 API 调用
  2. 缓存层职责:通过 initializedCollections sync.Map 缓存已初始化的集合,避免每次操作都检查集合是否存在

数据流动路径(以检索操作为例):

用户查询 → Chat Pipeline → RetrieveEngineService → qdrantRepository.Retrieve()
    → Qdrant Client.Search() → Qdrant Server → 返回带分数的向量
    → 转换为 QdrantVectorEmbeddingWithScore → 转换为 types.RetrieveResult → 返回上层

关键设计洞察:注意 collectionBaseName 字段——这意味着系统会为不同维度的向量创建不同的 Qdrant 集合(例如 vectors_384vectors_768)。这种设计避免了在同一个集合中混合不同维度的向量(这会导致检索错误),同时允许系统支持多种嵌入模型。


核心组件深度解析

1. qdrantRepository

设计意图:这是模块的主要实现类,封装了与 Qdrant 数据库的所有交互。它不是简单的 CRUD 包装器,而是承担了连接管理、集合生命周期管理、批量操作优化等多重职责。

type qdrantRepository struct {
    client             *qdrant.Client
    collectionBaseName string
    // Cache for initialized collections (dimension -> true)
    initializedCollections sync.Map
}

字段解析

字段 类型 设计意图
client *qdrant.Client Qdrant Go 官方客户端实例,负责所有底层通信
collectionBaseName string 集合命名前缀,配合维度生成完整集合名(如 vectors_384
initializedCollections sync.Map 线程安全的缓存,记录哪些维度的集合已初始化,避免重复检查

为什么使用 sync.Map 这是一个关键的性能优化。在高并发场景下,多个 goroutine 可能同时尝试写入同一个维度的集合。sync.Map 提供了无锁读取和细粒度锁写入,比传统 map + mutex 更适合"读多写少"的缓存场景。

隐式契约

  • 该结构体实现了 internal.types.interfaces.retriever.RetrieveEngineRepository 接口(虽然方法实现未在当前代码片段中展示,但从接口定义可推断)
  • 集合命名遵循 {collectionBaseName}_{dimension} 模式
  • 所有方法应该是线程安全的(因为 clientinitializedCollections 都是并发安全的)

2. QdrantVectorEmbedding

设计意图:这是写入 Qdrant 的数据模型,它将系统内部的 IndexInfo 转换为 Qdrant 可存储的格式。注意它包含了丰富的元数据字段——这些不仅是"附加信息",更是检索时过滤和溯源的关键

type QdrantVectorEmbedding struct {
    Content         string    `json:"content"`
    SourceID        string    `json:"source_id"`
    SourceType      int       `json:"source_type"`
    ChunkID         string    `json:"chunk_id"`
    KnowledgeID     string    `json:"knowledge_id"`
    KnowledgeBaseID string    `json:"knowledge_base_id"`
    TagID           string    `json:"tag_id"`
    Embedding       []float32 `json:"embedding"`
    IsEnabled       bool      `json:"is_enabled"`
}

字段语义与用途

字段 用途 检索时的作用
Content 原始文本内容 返回给用户的结果展示
SourceID 源文档 ID 溯源:用户点击结果可跳转到原文档
SourceType 源类型(FAQ/手动/自动提取) 过滤:某些场景只搜索特定类型的内容
ChunkID 分块 ID 精确删除/更新:按分块 ID 删除或更新状态
KnowledgeID 知识 ID 过滤:限定搜索范围到特定知识
KnowledgeBaseID 知识库 ID 最常用的过滤条件:用户通常只搜索自己有权访问的知识库
TagID 标签 ID FAQ 优先级过滤:带标签的 FAQ 通常质量更高
Embedding 向量本身 相似度计算的核心
IsEnabled 启用状态 软删除:禁用而非物理删除,便于恢复

设计权衡

  • 冗余 vs 查询效率KnowledgeBaseIDKnowledgeID 等字段在 Qdrant 中会作为 payload(有效载荷)存储,支持过滤。这增加了存储开销,但避免了"先查向量再查元数据"的两次查询。
  • []float32 vs []float64:选择 float32 是因为 Qdrant 原生支持 32 位浮点数,且对于向量相似度搜索,32 位精度已足够,同时节省一半存储空间。

IndexInfo 的映射关系

IndexInfo (内部统一模型)          QdrantVectorEmbedding (Qdrant 专用模型)
├── ID              ──────────────→ (不直接存储,作为 Qdrant Point ID)
├── Content         ──────────────→ Content
├── SourceID        ──────────────→ SourceID
├── SourceType      ──────────────→ SourceType
├── ChunkID         ──────────────→ ChunkID
├── KnowledgeID     ──────────────→ KnowledgeID
├── KnowledgeBaseID ──────────────→ KnowledgeBaseID
├── KnowledgeType   ──────────────→ (用于选择集合,不存储在向量中)
├── TagID           ──────────────→ TagID
├── IsEnabled       ──────────────→ IsEnabled
└── IsRecommended   ──────────────→ (未在此结构体中,可能在其他后端使用)

3. QdrantVectorEmbeddingWithScore

设计意图:这是检索操作的返回模型,它在 QdrantVectorEmbedding 的基础上增加了相似度分数。使用嵌入(embedding)而非组合(composition)的设计模式,使得返回结果可以直接访问所有元数据字段。

type QdrantVectorEmbeddingWithScore struct {
    QdrantVectorEmbedding
    Score float64
}

为什么 Scorefloat64Embedding[]float32

  • Qdrant 返回的相似度分数通常是 64 位浮点数(内部计算精度更高)
  • 向量本身使用 32 位是因为存储和传输开销考虑
  • 这是一个精度与性能的平衡:向量不需要双精度,但分数的高精度有助于排序和阈值过滤

典型使用场景

// 检索后过滤低分结果
results, _ := repo.Retrieve(ctx, params)
for _, r := range results {
    if r.Score < 0.7 {  // 阈值过滤
        continue
    }
    // 处理高分结果...
}

依赖关系分析

上游依赖(谁调用本模块)

application_services_and_orchestration
└── retrieval_and_web_search_services
    └── retriever_engine_composition_and_registry
        ├── CompositeRetrieveEngine
        └── RetrieveEngineRegistry
            └── [qdrantRepository 实例]

调用链示例

  1. PluginSearch(搜索插件)发起检索请求
  2. CompositeRetrieveEngine 根据租户配置选择合适的检索引擎
  3. RetrieveEngineRegistry 解析出 qdrantRepository 实例
  4. 调用 qdrantRepository.Retrieve() 执行实际检索

契约期望

  • 上层期望 Retrieve() 方法在超时时间内返回(通常 < 500ms)
  • 上层期望错误是可分类的(网络错误 vs 查询错误 vs 权限错误)
  • 上层期望返回结果按分数降序排列

下游依赖(本模块调用谁)

qdrantRepository
└── github.com/qdrant/go-client/qdrant (Qdrant 官方 Go 客户端)
    └── gRPC 连接 → Qdrant Server

关键依赖特性

  • Qdrant Go 客户端是线程安全的,支持并发调用
  • 客户端内部维护连接池,无需手动管理连接
  • 客户端会自动处理重试和故障转移(如果配置了多节点)

数据契约

写入契约Save / BatchSave):

// 输入
indexInfo *types.IndexInfo  // 必须包含有效的 KnowledgeBaseID 和 ChunkID
embedding []float32         // 维度必须与集合匹配

// 输出
error  // nil 表示成功,非 nil 可能是网络错误或维度不匹配

读取契约Retrieve):

// 输入
params types.RetrieveParams{
    Query: "...",              // 查询文本(某些实现会内部嵌入)
    Embedding: []float32{...}, // 查询向量(必需)
    KnowledgeBaseIDs: [...],   // 过滤条件(可选)
    TopK: 10,                  // 返回数量
    Threshold: 0.7,            // 分数阈值(可选)
}

// 输出
[]*types.RetrieveResult{
    {
        Results: []*IndexWithScore{...},  // 按分数降序
        Error: nil,
    },
}

设计决策与权衡

1. 为什么选择 Qdrant 而非其他向量数据库?

Qdrant 的优势(相对于 Milvus、Elasticsearch、PostgreSQL/pgvector):

维度 Qdrant Milvus Elasticsearch pgvector
部署复杂度 低(单二进制) 高(多组件) 低(PG 插件)
过滤性能 优秀(原生 payload 过滤) 良好 优秀 一般
HNSW 实现 高度优化 高度优化 一般 基础
云原生支持 优秀 优秀 优秀 依赖 PG
学习曲线 低(如果熟悉 PG)

本系统的选择逻辑:系统支持多后端并存(通过 RetrieverEngines 配置),租户可以根据自身需求选择。Qdrant 通常是默认推荐,因为它在过滤性能部署简单性之间取得了最佳平衡。

2. 集合按维度分离 vs 单一集合

当前设计collectionBaseName + dimension → 多个集合

替代方案:单一集合,向量字段带维度元数据

为什么选择多集合

  • 类型安全:Qdrant 不允许在同一集合中混合不同维度的向量,尝试这样做会导致运行时错误
  • 性能:HNSW 图是针对固定维度优化的,混合维度会破坏图结构
  • 运维清晰:可以独立管理不同嵌入模型的索引(例如单独删除 vectors_384 而不影响 vectors_768

代价

  • 集合数量 = 使用的嵌入模型数量 × 知识类型数量
  • 需要额外的逻辑来路由到正确的集合

3. IsEnabled 软删除 vs 物理删除

设计选择:使用 IsEnabled 字段标记启用状态,而非直接删除记录

原因

  • 可恢复性:误操作禁用可以立即恢复
  • 审计追踪:保留历史记录便于排查问题
  • 批量操作效率BatchUpdateChunkEnabledStatus 比批量删除再插入更高效

代价

  • 检索时需要额外过滤 IsEnabled = true
  • 存储空间不会自动释放(需要定期清理)

4. sync.Map 缓存初始化状态

设计选择:使用 sync.Map 缓存哪些维度的集合已初始化

为什么不每次操作都检查集合存在性

  • Qdrant 的 CollectionExists 调用需要网络往返(~1-5ms)
  • 在高并发场景下,这会成为瓶颈
  • 集合一旦创建就会持续存在(除非手动删除)

潜在风险

  • 如果集合在外部被删除,缓存会"过期",导致后续操作失败
  • 缓解策略:操作失败时清除缓存并重试(典型实现模式)

使用指南与示例

基本检索流程

// 1. 准备检索参数
params := types.RetrieveParams{
    Query: "如何重置密码?",
    Embedding: queryEmbedding,  // 由嵌入模型生成
    KnowledgeBaseIDs: []string{"kb_123"},
    TopK: 10,
    Threshold: 0.7,
    KnowledgeType: "faq",
}

// 2. 执行检索
results, err := qdrantRepo.Retrieve(ctx, params)
if err != nil {
    // 处理错误(网络错误、集合不存在等)
    return err
}

// 3. 处理结果
for _, result := range results {
    fmt.Printf("内容:%s\n", result.Content)
    fmt.Printf("分数:%f\n", result.Score)
    fmt.Printf("来源:%s\n", result.SourceID)
}

批量写入示例

// 准备一批向量
indexInfos := []*types.IndexInfo{
    {
        ID: "idx_1",
        Content: "密码重置步骤...",
        ChunkID: "chunk_1",
        KnowledgeBaseID: "kb_123",
        KnowledgeType: "faq",
        IsEnabled: true,
    },
    // ... 更多索引
}

// 批量保存
err := qdrantRepo.BatchSave(ctx, indexInfos, map[string]any{
    "dimension": 384,  // 指定集合维度
})

按条件删除

// 删除某个知识库的所有向量
err := qdrantRepo.DeleteByKnowledgeIDList(
    ctx,
    []string{"knowledge_456"},
    384,  // 维度
    "faq", // 知识类型
)

边界情况与注意事项

1. 维度不匹配错误

症状:写入或检索时返回类似 "vector dimension mismatch" 的错误

原因:尝试将 768 维向量写入 384 维集合,或反之

排查方法

// 检查嵌入模型配置与集合维度是否一致
// 通常维度由嵌入模型决定:
// - bge-small: 384
// - bge-base: 768
// - text-embedding-ada-002: 1536

解决方案:确保 BatchSave 时传入的 dimension 参数与嵌入模型输出一致。

2. 集合未初始化

症状:首次写入时失败,提示集合不存在

原因initializedCollections 缓存未命中,且自动创建逻辑失败

典型场景

  • Qdrant 服务不可达
  • 权限不足无法创建集合
  • 集合名包含非法字符

建议处理

// 在生产环境中,应在应用启动时预创建常用维度的集合
// 而不是依赖运行时自动创建
func InitializeCollections(ctx context.Context, dimensions []int) error {
    for _, dim := range dimensions {
        err := repo.ensureCollection(ctx, dim)
        if err != nil {
            return fmt.Errorf("failed to init collection for dim %d: %w", dim, err)
        }
    }
    return nil
}

3. 过滤条件与性能的权衡

陷阱:过度使用过滤条件会显著降低检索性能

原因:Qdrant 的 HNSW 索引针对"无过滤"场景优化,添加过滤条件后需要:

  1. 先找到候选向量
  2. 再应用过滤条件
  3. 如果过滤后结果不足,需要扩大搜索范围

最佳实践

  • 优先使用 KnowledgeBaseID 过滤(最常用的访问模式)
  • 避免同时使用多个高基数过滤条件(如同时过滤 100+ 个 TagID
  • 对于 TopK 较小的查询(< 10),过滤影响较小

4. 并发写入与一致性

注意BatchSave 不是原子的——部分写入可能成功,部分失败

场景:批量写入 1000 条,网络在第 500 条时中断

影响:前 500 条已写入,后 500 条丢失

建议

  • 实现幂等性:使用 ChunkID 作为去重键
  • 实现重试逻辑:失败后重新尝试未成功的批次
  • 对于关键数据,使用事务性保证(需要上层服务协调)

5. 分数阈值的选择

经验值

使用场景 推荐阈值 说明
FAQ 检索 0.75-0.85 FAQ 通常短小精悍,分数区分度高
长文档检索 0.6-0.7 长文本相似度普遍偏低
混合检索 0.65-0.75 平衡精确率和召回率

警告:阈值过高会导致"无结果",阈值过低会返回大量噪声。建议通过 A/B 测试或用户反馈调整。


与其他检索后端的对比

特性 Qdrant Elasticsearch Milvus PostgreSQL
向量检索性能 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
过滤性能 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
部署复杂度 ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐
运维成熟度 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
混合检索支持 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
推荐场景 默认选择 需要全文 + 向量混合检索 超大规模(亿级向量) 已有 PG 生态

选择建议

  • 新项目:优先选择 Qdrant(简单、性能好)
  • 已有 Elasticsearch:可复用 ES 的向量检索能力
  • 超大规模:考虑 Milvus 的分布式能力
  • PG 重度用户:pgvector 可减少技术栈复杂度

扩展点

添加自定义过滤条件

当前实现支持基于 KnowledgeBaseIDTagID 等字段的过滤。如需添加新的过滤维度:

  1. QdrantVectorEmbedding 中添加新字段
  2. 在 Qdrant 集合配置中将该字段声明为可过滤 payload
  3. Retrieve 方法中解析新的过滤参数并构建 Qdrant 过滤条件

支持新的嵌入维度

无需修改代码——initializedCollections 缓存和动态集合创建逻辑会自动处理新维度。只需确保:

  1. 在租户配置中注册新的嵌入模型
  2. 首次使用时会自动创建对应维度的集合

参考文档

On this page