第 4 章 Schema 设计:把知识库建对 Schema 是知识库的"结构合同"——一旦建好,所有写入和检索都要围绕它展开。RAG 系统的很多问题,根子在 Schema 设计错了:该建索引的没建、不该存的存了一堆、维度选错导致后期推倒重来。这一章给你一套可直接套用的 RAG Schema 范式。 4.1 Schema 的三个组成部分 回顾第 2 章, 由三部分构成。这一章我们把每一部分在 RAG 场景下"该怎么填"讲透: 组成 | 作用 | RAG 里的典型内容 | Collection 标识符 | 如 、 | 标量字段列表 | 原文 text、出处 source、时间、分类标签…… | 向量字段列表 | 稠密向量(语义)、稀疏向量(关键词) 💡 Zvec 的 Schema
Schema 是知识库的"结构合同"——一旦建好,所有写入和检索都要围绕它展开。RAG 系统的很多问题,根子在 Schema 设计错了:该建索引的没建、不该存的存了一堆、维度选错导致后期推倒重来。这一章给你一套可直接套用的 RAG Schema 范式。
回顾第 2 章,CollectionSchema 由三部分构成。这一章我们把每一部分在 RAG 场景下"该怎么填"讲透:
| 组成 | 作用 | RAG 里的典型内容 |
|---|---|---|
name |
Collection 标识符 | 如 product_manual_kb、support_faq_kb |
fields |
标量字段列表 | 原文 text、出处 source、时间、分类标签…… |
vectors |
向量字段列表 | 稠密向量(语义)、稀疏向量(关键词) |
💡 Zvec 的 Schema 是动态的——你可以随时加列、改索引,不用重建 Collection(详见 4.5 节 Schema 演进)。但这不代表可以乱设计:核心向量字段的维度一旦定下,改起来代价极大(见第 3 章)。所以 Schema 设计的核心,是"先把向量维度和要过滤的字段想清楚"。
这是 Schema 设计里性价比最高的决策点。原则很朴素:只给"经常用来过滤/检索"的字段建索引,其余只存。
要不要给这个标量字段建倒排索引? │ ├─ 会出现在 query 的 filter 里吗? ──► 会 ──► ✅ 建倒排索引 │ (如 source / publish_year / category) │ ├─ 会被全文检索吗? ──► 会 ──► ✅ 建全文索引 │ (如正文 text) │ └─ 只用来展示/喂给 LLM? ──► 是 ──► ❌ 只存,不建索引 (如纯展示用的 url、摘要)
⚠️ 索引不是越多越好。每个索引都有代价:占额外存储、每次写入都要维护(写放大)。给一个从不参与过滤的字段建索引,纯粹是浪费。第 2 章那张"三类索引分工表"在这里要反复对照。
倒排索引还有两个可选项,专为特定查询加速:
| 选项 | 作用 | 何时开启 |
|---|---|---|
enable_range_optimization=True |
加速范围查询(price > 100、year >= 2024) |
数值字段会被范围过滤时 |
enable_extended_wildcard=True |
支持中缀/后缀通配(name LIKE '%abc') |
有复杂字符串模式匹配需求时 |
向量字段的 Schema 由四个属性决定,每一个都值得深思:
| 属性 | 决策要点 | RAG 建议 |
|---|---|---|
name |
见名知意 | dense(稠密语义)、sparse(稀疏关键词)、image_vec(多模态) |
data_type |
精度 vs 存储 | 起步用 VECTOR_FP32;要省内存可 VECTOR_FP16(见第 5 章量化) |
dimension |
必须等于模型输出维度,定下难改 | 768/1024/1536,由 Embedding 模型决定 |
index_param |
索引类型 + 度量 | 文本 RAG 默认 HnswIndexParam(metric_type=COSINE)(第 5 章展开) |
⚠️ 稠密向量维度必填,稀疏向量不填维度。稀疏向量(
SPARSE_VECTOR_FP32)天然没有固定长度,只需存非零项,所以VectorSchema里不要给稀疏向量写dimension。这是个容易写错的细节。
下面这套 Schema 覆盖了生产级 RAG 的大多数需求:语义检索(稠密)+ 关键词检索(稀疏+全文)+ 元数据过滤。可以直接作为你项目的起点:
import zvec schema = zvec.CollectionSchema( name="rag_kb", fields=[ # —— 原文 & 出处 —— # 正文:要喂给 LLM,并存一份供全文检索(BM25 关键词召回) zvec.FieldSchema( name="text", data_type=zvec.DataType.STRING, index_param=zvec.FtsIndexParam(tokenizer_name="jieba"), # 中文用 Jieba ), # 出处:常用于"只在某文档里找",建倒排 zvec.FieldSchema( name="source", data_type=zvec.DataType.STRING, index_param=zvec.InvertIndexParam(), ), # —— 过滤字段 —— # 时间:范围过滤("只查近一年"),建倒排 + 范围优化 zvec.FieldSchema( name="publish_year", data_type=zvec.DataType.INT32, index_param=zvec.InvertIndexParam(enable_range_optimization=True), ), # 多标签:数组成员查询(CONTAIN_ANY / CONTAIN_ALL),建倒排 zvec.FieldSchema( name="category", data_type=zvec.DataType.ARRAY_STRING, index_param=zvec.InvertIndexParam(), ), # —— 只存不索引 —— # 摘要:只用于展示,从不参与过滤,省索引开销 zvec.FieldSchema(name="summary", data_type=zvec.DataType.STRING), ], vectors=[ # 语义向量:768 维稠密,HNSW + COSINE(文本 RAG 默认配置) zvec.VectorSchema( name="dense", data_type=zvec.DataType.VECTOR_FP32, dimension=768, index_param=zvec.HnswIndexParam(metric_type=zvec.MetricType.COSINE), ), # 关键词向量:稀疏向量,IP 度量(加权词项匹配) # 若用 bge-m3 等模型,可同时拿到 dense 和 sparse,一次 Embedding 双倍召回 zvec.VectorSchema( name="sparse", data_type=zvec.DataType.SPARSE_VECTOR_FP32, index_param=zvec.HnswIndexParam(metric_type=zvec.MetricType.IP), ), ], ) kb = zvec.create_and_open(path="./rag_kb", schema=schema)
💡 为什么同时要 FTS 和稀疏向量? 两者都做"关键词召回",但有分工:FTS 直接对原文建索引,零额外 Embedding 成本,适合简单关键词;稀疏向量由模型(如 bge-m3 稀疏头、SPLADE)生成,能学到词项权重甚至语义扩展,召回更智能。轻量场景用 FTS 就够;追求召回上限时用稀疏向量(或两者配合)。第 6、7 章会演示它们的检索写法。
创建 Collection 时还能传 option,控制运行时行为。两个关键开关:
| 选项 | 作用 | 何时用 |
|---|---|---|
read_only=True |
只读模式,禁止任何写入 | 多进程共享同一个 Collection 时必用(第 8 章) |
enable_mmap=True(默认) |
内存映射 I/O,用少量内存换更快访问 | 数据量大于内存时尤其有用 |
collection = zvec.create_and_open( path="./rag_kb", schema=schema, option=zvec.CollectionOption(read_only=False, enable_mmap=True), )
这是 Zvec 区别于很多向量库的杀手锏——Collection 建好后,还能在线改 Schema,不用停机、不用重新导入数据、不用重新建索引。它通过"数据定义语言 DDL"实现,分两类:
| DDL 类别 | 管什么 | 支持的操作 |
|---|---|---|
| Column DDL | 存什么数据(字段) | 加列 add_column、删列 drop_column、改列 alter_column |
| Index DDL | 怎么搜索(索引) | 建索引 create_index、删索引 drop_index |
💡 RAG 的实际价值:知识库上线后,产品同学突然说"加个字段标记是否内部文档,好做权限过滤"。在不能改 Schema 的库里,你得重建整个库;在 Zvec 里,一行
add_column就搞定了。
# Column DDL:给已有 Collection 加一个数值字段,并用表达式给历史数据填默认值 collection.add_column( field_schema=zvec.FieldSchema(name="importance", data_type=zvec.DataType.INT32), expression="3", # 历史数据默认重要性 = 3 ) # Index DDL:后来发现某标量字段常用来过滤,补建倒排索引(不用重新导入数据!) collection.create_index( field_name="source", index_param=zvec.InvertIndexParam(), ) # Index DDL:把某个向量字段的 HNSW 换成 Flat(调试/小数据场景) collection.create_index( field_name="dense", index_param=zvec.FlatIndexParam(metric_type=zvec.MetricType.COSINE), )
⚠️ 演进有边界:
- 向量字段目前不支持在线添加/删除(这是已规划但尚未支持的特性)——所以向量字段要在建库时一次想清楚。
- 向量字段的索引必须始终存在(不允许
drop_index删向量索引),但可以用create_index替换索引类型。add_column目前主要支持数值型标量字段,历史数据通过expression填默认值。
设计完、写入后,随时用这几个属性查看 Collection 的真实状态,这是排查问题的第一步:
| 属性 | 看什么 | 排查场景 |
|---|---|---|
collection.schema |
当前结构(字段/向量/索引) | 确认 Schema 是否按预期演进 |
collection.stats |
运行时指标(Doc 数量、索引构建进度) | 判断是否该 optimize(第 8 章) |
collection.option |
运行时配置(只读/mmap) | 确认多进程共享设置 |
collection.path |
磁盘位置 | 确认数据落在哪、可否迁移 |
print(collection.schema) # 看结构 print(collection.stats) # 看 Doc 数和索引进度 —— optimize 节奏就靠它
add_column 加一个 importance 字段,检索时加 filter="importance >= 3" 验证新字段立刻可用。source 字段不建索引的库,写入数据后用 filter="source = 'x.md'" 感受慢;再 create_index 补建,对比过滤速度。name / fields(标量)/ vectors(向量);核心是决定每个字段建什么索引。HNSW+COSINE、稀疏 IP。索引类型怎么选、参数怎么调,是下一章的主题。详见第 5 章。