犀牛鸟 2026 研究笔记 estelledc.github.io

案例 #1668:腾讯向量库复制时的 ID 映射

PR:#1668 | 2026-06-16 合入 | 1 文件 +82/-26 | 栈:Go · tencentvectordb/repository.go

结论先行

知识库 复制 / 迁移 时,需要把源 KB 的向量 CopyIndices 到目标 KB,并把每条向量的 document id 重写为 新 chunk_id。旧实现用 Limit: len(ids) 一次性 query 全量 ID,在腾讯向量库侧会 静默截断;加上 ID 字段优先级与 SourceID 映射不完整,导致大库复制后 部分 question 向量仍指向旧 ID,检索空命中或 duplicate key。

修复:分页 Query(每页 500)+ 严格 kb 过滤 + IndexInfo.ID / ChunkID 优先级 + sha256 稳定目标 ID

适合谁读:改 internal/infrastructure/vectordb/、做 KB 克隆/备份/竞赛数据迁移的贡献者。

Ch30 向量库适配器 · Ch42 增量更新 · 与 case-1774 同属「删/复制前关联数据不能 orphan」。


1. 问题现象(What)

操作路径

  1. 租户 A 有一个含大量 question 类型 chunk 的知识库(FAQ / 问答对 ingest)
  2. 执行 复制 knowledge base 到目标 KB(或跨环境迁移)
  3. 复制任务 表面成功,无报错
  4. 目标 KB 做 hybrid-search 或 question 检索:
    • 部分 query 永远无结果
    • 或偶发 ID 冲突 / 命中源 KB 的 ghost 向量

规模依赖

修复前 / 修复后(CopyIndices)

规模 修复前 修复后
≤500 ID 可能正常 正常
>500 ID 静默截断、ID 未 remap 分页 500 直到空页

类比

搬家时要把每个房间物品贴 新门牌号。旧流程一次性向仓库管理员要「全部物品清单」,但管理员 最多只给 500 条 且没说明截断;你贴完一半新门牌,剩下物品仍指着旧地址 — 客人(检索 query)按新地址找,找不到。


2. 定位步骤

rg "replicasNum|CopyIndices|copyIndicesQueryPageSize" \
  internal/application/repository/retriever/tencentvectordb/
go test ./internal/application/repository/retriever/tencentvectordb/ -v

3. 根因分析(Why)

数据流

flowchart LR
    A[CopyIndices 入口] --> B[构造 source chunk ID 列表]
    B --> C{旧逻辑: 单次 Query<br/>Limit=len ids}
    C --> D[腾讯 VectorDB<br/>实际返回 ≤ 上限]
    D --> E[部分 ID 未映射 ❌]
    A --> F{新逻辑: offset 分页<br/>pageSize=500}
    F --> G[循环直到空页]
    G --> H[逐条 rewrite<br/>KnowledgeBaseID + targetChunkID]
    H --> I[目标 KB 检索正常 ✅]

技术根因(PR 描述归纳)

问题 后果
Limit: len(ids) 一次拉全 向量库单次 query 有上限,大 batch 静默截断,后续 ID 未参与 copy
query 未严格按 sourceKnowledgeBaseID 过滤 可能混库,映射错乱
ID 字段优先级不清 question 数据在 IndexInfo.ID vs ChunkID 上行为不一致
目标 document id 不稳定 重复 copy 或冲突时难以幂等

关键文件

文件 改动
internal/infrastructure/vectordb/tencentvectordb/repository.go CopyIndicescopyIndicesQueryPageSizecopySourceQueryParams

常量:

const copyIndicesQueryPageSize = 500

核心循环模式:

for offset := int64(0); ; offset += copyIndicesQueryPageSize {
    query, err := r.client.Query(ctx, nil, copySourceQueryParams(..., offset))
    if len(query) == 0 {
        break
    }
    // 对每条: sourceChunkID → targetChunkID
    // embedding.ID = targetChunkID
    // KnowledgeBaseID = targetKB
}

PR 还提到:优先 IndexInfo.ID,兜底 ChunkID;目标侧用 sha256 生成稳定 document id,避免冲突。


4. 解决方案(How)

设计要点

  1. 分页直到空结果 — 不假设 len(ids) 可一次返回
  2. 每页带 offset + kb 过滤copySourceQueryParams(..., offset)
  3. 统一 ID 解析规则 — question 类型走 SourceID 映射链
  4. rewrite 后写入目标 KB — 每条向量的 KnowledgeBaseIDID 同步更新

为何不在应用层「复制完再全量 rebuild index」?

验证思路(PR 未附大测,读者可自建)

# 1. grep 入口
rg "CopyIndices|copyIndicesQueryPageSize" internal/infrastructure/vectordb/

# 2. 单测/集成测(若有)或 staging 复制 ≥600 question 的 KB
# 3. 目标 KB hybrid-search 抽样 query

5. Review 与合入


6. 可复用模式


7. 常见误区

错误认知:复制 KB 只复制 PostgreSQL 元数据就够了。
正确理解:向量在 独立 VectorDB;元数据与向量 双写一致 才是完整复制。

错误认知:测试库 50 条 question 通过 = 复制逻辑正确。
正确理解:截断 bug 是 规模阈值型 — 应用 copyIndicesQueryPageSize 做边界测试。

错误认知:检索失败一定是 embed 模型问题。
正确:先查 目标 KB 向量条数 与 chunk 数是否一致,再查 ID 映射。

错误认知:CopyIndices 与 replica 配置无关。
正确:case-1759 建库 replica 配错会导致 根本走不到 CopyIndices。


8. 思考点

  1. 若换 Milvus,CopyIndices 分页 API 不同,repository 接口应抽象什么?
  2. sha256 目标 ID 的 输入盐 应包含哪些字段才能跨租户不碰撞?
  3. case-1774 删除模型前查引用 — 复制 KB 前是否也应查 跨 KB 共享向量?(见 catalog #1611)
  4. 设计一个 复制后一致性校验 job:对比 chunk 表 count 与 VectorDB count。

章末自检


延伸阅读