案例 #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)
操作路径
- 租户 A 有一个含大量 question 类型 chunk 的知识库(FAQ / 问答对 ingest)
- 执行 复制 knowledge base 到目标 KB(或跨环境迁移)
- 复制任务 表面成功,无报错
- 在 目标 KB 做 hybrid-search 或 question 检索:
- 部分 query 永远无结果
- 或偶发 ID 冲突 / 命中源 KB 的 ghost 向量
规模依赖
- 小库(<500 question 向量):可能 完全正常 — 问题被掩盖
- 大库(>500):必现或高概率 — 这是典型的「只在生产大数据量下暴露」bug
修复前 / 修复后(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 |
CopyIndices、copyIndicesQueryPageSize、copySourceQueryParams |
常量:
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)
设计要点
- 分页直到空结果 — 不假设
len(ids)可一次返回 - 每页带 offset + kb 过滤 —
copySourceQueryParams(..., offset) - 统一 ID 解析规则 — question 类型走 SourceID 映射链
- rewrite 后写入目标 KB — 每条向量的
KnowledgeBaseID与ID同步更新
为何不在应用层「复制完再全量 rebuild index」?
- rebuild 成本高(重新 embed),生产 KB 复制期望 向量级 CopyIndices
- 本 PR 属于 向量后端集成正确性 — 与 Milvus/pgvector 集成的「分页 cursor」同理
验证思路(PR 未附大测,读者可自建)
# 1. grep 入口
rg "CopyIndices|copyIndicesQueryPageSize" internal/infrastructure/vectordb/
# 2. 单测/集成测(若有)或 staging 复制 ≥600 question 的 KB
# 3. 目标 KB hybrid-search 抽样 query
5. Review 与合入
- 1 文件 +82/-26 — 典型「向量库冷路径」修复:不在 Search 热路径,但 数据迁移必踩
- 维护者 langcaiye 领域 — vectordb 改动需说明 分页语义 与 ID 优先级
- 无 swagger / 前端 — review 聚焦 repository 单文件
6. 可复用模式
- 向量库 PR 常藏在 Copy / Delete / Migrate,不在 hybrid-search handler
- 分页是默认模式:任何
Limit: len(all)对第三方向量库都要警惕 - chunk_id vs vector document id:复制时必须 显式 remap,不能假设后端自动跟随
7. 常见误区
错误认知:复制 KB 只复制 PostgreSQL 元数据就够了。
正确理解:向量在 独立 VectorDB;元数据与向量 双写一致 才是完整复制。
错误认知:测试库 50 条 question 通过 = 复制逻辑正确。
正确理解:截断 bug 是 规模阈值型 — 应用 copyIndicesQueryPageSize 做边界测试。
错误认知:检索失败一定是 embed 模型问题。
正确:先查 目标 KB 向量条数 与 chunk 数是否一致,再查 ID 映射。
错误认知:CopyIndices 与 replica 配置无关。
正确:case-1759 建库 replica 配错会导致 根本走不到 CopyIndices。
8. 思考点
- 若换 Milvus,CopyIndices 分页 API 不同,repository 接口应抽象什么?
- sha256 目标 ID 的 输入盐 应包含哪些字段才能跨租户不碰撞?
- case-1774 删除模型前查引用 — 复制 KB 前是否也应查 跨 KB 共享向量?(见 catalog #1611)
- 设计一个 复制后一致性校验 job:对比 chunk 表 count 与 VectorDB count。
章末自检
- 解释为何
Limit=len(ids)在大库会静默失败 - 说清 IndexInfo.ID 与 ChunkID 优先级场景
- 写出分页循环的退出条件