Learn
RAG/08-vector-store

索引与向量存储

上一章选了数据库,这章讲怎么把「向量 + 原文 + 元数据」真正落进去并高效查出来。存储设计直接决定检索质量与运维复杂度。

1. 一条记录里该存什么

向量库里的每条数据建议包含三部分:

{
  "id": "chunk_1024",
  "vector": [0.12, -0.33, ...],   # Embedding
  "text": "原始 chunk 文本",        # 检索命中后要回传给 LLM
  "metadata": {                    # 用于过滤 + 引用溯源
    "source": "员工手册.pdf",
    "page": 12,
    "category": "HR",
    "updated_at": "2024-08-01"
  }
}
ℹ️原文必须存回去

向量只用于「找」,真正喂给 LLM 的是 text 原文。很多新手只存向量,检索命中后却拿不到内容,无法组装 Prompt。

2. 写入(Upsert)

「Upsert」= 存在则更新、不存在则插入,便于增量更新文档。

# 以 Qdrant 风格为例
points = [
    {
        "id": i,
        "vector": vecs[i],
        "payload": {"text": texts[i], "source": sources[i], "page": pages[i]},
    }
    for i in range(len(vecs))
]
client.upsert(collection_name="kb", points=points)

3. 持久化

不同方案持久化方式不同:

方案持久化要点
FAISSindex.write_index() 落盘为文件,重启 read_index 读回
Qdrant挂载卷 + 快照(snapshot),或启用持久化模式
Milvus独立存储(对象存储/ETCD),配置副本
PGVector就是 Postgres 表,靠数据库备份保证持久
# FAISS 持久化
faiss.write_index(index, "kb.index")
index = faiss.read_index("kb.index")
⚠️别忘了增量更新

文档改了,要删旧 chunk 再插新 chunk,否则库里会有过期内容。用稳定 id(如 文件 hashes + 页码 + 段号)做 upsert key,能避免重复与泄漏。

4. 元数据过滤

只在某些范围内检索,既提升精度也降成本:

# Qdrant:只在 HR 分类且 2024 后的文档里搜
hits = client.search(
    collection_name="kb",
    query_vector=q_vec,
    query_filter=models.Filter(
        must=[
            models.FieldCondition(key="category", match=models.MatchValue(value="HR")),
            models.FieldCondition(key="updated_at", range=models.Range(gte="2024-01-01")),
        ]
    ),
    limit=5,
)

5. 索引类型与权衡

IndexFlatL2  精确检索,慢,适合小库验证
IndexIVF    倒排聚类,快,需训练,recall 略降
IndexHNSW   图索引,快且 recall 高,占内存,生产常用
💡先用精确索引调通,再上 ANN

开发期用 IndexFlat(精确)确认检索逻辑正确;上线前再换 HNSW/IVF 做性能压测,对比召回率是否可接受。

🎯练习

给你的向量库加上 category 和 updated_at 两个元数据字段,写一条「只检索技术类且半年内更新」的过滤查询,验证结果确实被过滤了。

小结

  • 每条记录 = vector + text + metadata,原文务必存回
  • Upsert 用稳定 id 实现增量更新,避免过期数据
  • 持久化按数据库类型落盘/快照/备份
  • 元数据过滤提升精度,索引类型在速度与召回间权衡
  • 下一章进入检索策略本身:相似度、混合检索、MMR →