第 1 章 环境准备与首次跑通 读完这一章,你应该能在 5 分钟内跑通一个完整闭环:建库 → 写入 → 检索,并亲手拿到第一批 top-k 结果。先有手感,再去抠原理。 1.1 环境要求 项 | 要求 | 说明 操作系统 | Linux / macOS / Windows | 部分索引有平台限制:HNSW-RaBitQ 需 x8664 + AVX2;DiskANN 仅 Linux 且需 libaio Python | 3.8 及以上(建议 3.10+) | RAG 生态主流版本 包管理器 | pip | 磁盘 | 一个可写目录 | 每个 Collection 自包含于一个文件夹 内存 | 常规机器即可起步 | 内存决定能装多少向量,详见第 5、8 章 ⚠️ 别一开始就纠结索引平台限制。
读完这一章,你应该能在 5 分钟内跑通一个完整闭环:建库 → 写入 → 检索,并亲手拿到第一批 top-k 结果。先有手感,再去抠原理。
| 项 | 要求 | 说明 |
|---|---|---|
| 操作系统 | Linux / macOS / Windows | 部分索引有平台限制:HNSW-RaBitQ 需 x86_64 + AVX2;DiskANN 仅 Linux 且需 libaio |
| Python | 3.8 及以上(建议 3.10+) | RAG 生态主流版本 |
| 包管理器 | pip | pip install zvec |
| 磁盘 | 一个可写目录 | 每个 Collection 自包含于一个文件夹 |
| 内存 | 常规机器即可起步 | 内存决定能装多少向量,详见第 5、8 章 |
⚠️ 别一开始就纠结索引平台限制。本教程的起步示例用的是默认的 HNSW 索引,三大平台都能跑。DiskANN、HNSW-RaBitQ 这些"高级索引"是第 5 章才需要考虑的选型问题。
一个最小可运行的 RAG 检索 demo,只需要 Zvec 本身。Embedding 模型等到第 3、9 章再引入——本章我们先用"假向量"把链路打通,避免一上来就被模型下载、API Key 这些事绊住。
# 只装一个包就能起步 pip install zvec
| 依赖 | 何时需要 | 备注 |
|---|---|---|
zvec |
现在 | 核心,必装 |
Embedding 模型库(如 sentence-transformers、OpenAI SDK、FlagEmbedding) |
第 3、9 章 | 把文本变向量 |
| LLM SDK(OpenAI / 本地推理) | 第 9 章 | 生成最终答案 |
💡 为什么先用假向量? 因为 RAG 链路里"检索"这一段和"Embedding"是完全解耦的。先把检索跑通、看懂返回结构,再接模型,调试时你能一眼判断问题出在向量质量还是检索本身。
下面这段代码完整演示了 Zvec 的"建库—写入—优化—检索"闭环。不依赖任何外部模型,向量是手写的占位数组,目的是让你看到 API 长什么样、结果怎么返回。
import zvec # ── ① 定义 Schema:告诉 Zvec 这个知识库长什么样 ── # 一个标量字段(年份,建倒排索引用于过滤)+ 一个稠密向量字段(8 维,演示用) schema = zvec.CollectionSchema( name="quickstart_kb", fields=[ zvec.FieldSchema( name="publish_year", data_type=zvec.DataType.INT32, index_param=zvec.InvertIndexParam(enable_range_optimization=True), ), ], vectors=[ zvec.VectorSchema( name="embedding", data_type=zvec.DataType.VECTOR_FP32, dimension=8, # 演示维度;真实场景通常是 768/1024/1536 index_param=zvec.HnswIndexParam(metric_type=zvec.MetricType.COSINE), ), ], ) # ── ② 创建并打开 Collection(数据落到这个文件夹)── collection = zvec.create_and_open( path="./my_kb_data", # 这个文件夹会自包含整个知识库,可拷贝/迁移 schema=schema, ) # ── ③ 写入几条 Document(id + 向量 + 标量字段)── collection.insert([ zvec.Doc(id="d1", vectors={"embedding": [0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17]}, fields={"publish_year": 2019}), zvec.Doc(id="d2", vectors={"embedding": [0.20, 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27]}, fields={"publish_year": 2021}), zvec.Doc(id="d3", vectors={"embedding": [0.15, 0.16, 0.13, 0.12, 0.19, 0.20, 0.11, 0.10]}, fields={"publish_year": 2023}), ]) # ── ④ optimize:把暂存区的向量合并进正式索引,加速检索 ── # 新数据其实"立即可查",但数据量上来后必须优化(原理见第 8 章) collection.optimize() # ── ⑤ 用一个查询向量做相似度检索,取最相似的 top 3 ── result = collection.query( queries=zvec.Query( field_name="embedding", vector=[0.11, 0.12, 0.12, 0.13, 0.15, 0.16, 0.15, 0.16], # 与 d1/d3 比较接近 ), topk=3, ) for doc in result: print(doc.id, round(doc.score, 4), doc.fields)
运行后你会看到类似这样的输出(COSINE 距离下,分数越小越相似——这点很关键,第 3、6 章会详细解释):
d1 0.0008 {'publish_year': 2019} d3 0.0821 {'publish_year': 2023} d2 0.1167 {'publish_year': 2021}
💡 看懂这五行输出,你就理解了 Zvec 的基本交互模型:每个结果是一个
Doc对象,包含id(唯一标识)、score(相似度分数)、fields(标量字段值)。如果你想要回向量本身,加include_vector=True(默认关闭以省内存)。
把上面五步画成图,就是一次"建库到检索"的完整生命周期:
┌─────────────────────────────────────────────────────────────┐ │ create_and_open() │ │ └─► 在磁盘建文件夹,按 Schema 初始化空 Collection │ └──────────────────────────────┬──────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────────────────┐ │ insert([Doc, Doc, Doc]) │ │ └─► 类型校验 ──► 标量写主存 + 倒排索引;向量先进 Flat 暂存区 │ │ (此刻已可查询:暂存区用暴力检索兜底) │ └──────────────────────────────┬──────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────────────────┐ │ optimize() │ │ └─► 后台线程把 Flat 暂存区合并进 HNSW 图(不阻塞读写) │ └──────────────────────────────┬──────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────────────────┐ │ query(queries=Query(vector=...), topk=3) │ │ └─► HNSW 图导航找 top-3 ──► 返回 list[Doc](含 score/fields)│ └─────────────────────────────────────────────────────────────┘
这张图背后每一个环节的原理,会在第 2 章和第 5、8 章逐步展开。现在你只要记住:Zvec 的所有交互,就是在这台"流水线"上调用对应的方法。
topk=3 改成 topk=1,确认返回的是最相似的那一条;再改成 topk=10(超过库里的数量),观察它返回几条。query() 里加 filter="publish_year >= 2021",观察 2019 年的 d1 是否被过滤掉(这是 RAG 里"按时间/来源限定检索范围"的基础,详见第 6 章)。collection.optimize() 注释掉再检索,结果一样吗?想想为什么——答案在第 8 章(暂存区的暴力检索在小数据下看不出来差异)。pip install zvec,不需要任何服务或外部模型,先用假向量打通链路。Doc 对象列表,含 id / score / fields;COSINE 下分数越小越相似。optimize() 再合并进正式索引,这是第 8 章性能管理的伏笔。本章的五步闭环整理成了「五分钟建库检索闭环」示例,使用占位向量(把文本哈希成定长向量,无语义但能打通链路)。下面是完整脚本:
"""Zvec 五分钟建库检索闭环完整示例。 用占位向量打通 Schema → create_and_open → insert → optimize → query 五步闭环,并演示「向量 + 过滤」组合(第 6 章主力检索形态的雏形)。 依赖:pip install zvec """ import hashlib import numpy as np import zvec def fake_dense(text, dim=8): """把文本哈希成定长稠密向量(无语义,仅 demo 用,真实场景用 Embedding 模型)。""" h = hashlib.sha256(text.encode("utf-8")).digest() vec = np.zeros(dim, dtype=np.float32) for i in range(dim): vec[i] = ((h[i % len(h)] ^ (i * 17)) % 1000) / 1000.0 norm = np.linalg.norm(vec) or 1.0 return (vec / norm).tolist() def main(): # ── ① Schema:标量字段(年份,过滤用)+ 稠密向量字段 ── schema = zvec.CollectionSchema( name="quickstart_kb", fields=[ zvec.FieldSchema( name="publish_year", data_type=zvec.DataType.INT32, index_param=zvec.InvertIndexParam(enable_range_optimization=True), ), ], vectors=[ zvec.VectorSchema( name="embedding", data_type=zvec.DataType.VECTOR_FP32, dimension=8, index_param=zvec.HnswIndexParam(metric_type=zvec.MetricType.COSINE), ), ], ) # ── ② 创建并打开 Collection ── collection = zvec.create_and_open(path="./my_kb_data", schema=schema) # ── ③ 写入几条 Document ── docs = [("d1", "机器学习入门", 2019), ("d2", "深度学习实战", 2021), ("d3", "向量检索原理", 2023)] collection.insert([ zvec.Doc(id=did, vectors={"embedding": fake_dense(text)}, fields={"publish_year": year, "text": text}) for did, text, year in docs ]) # ── ④ optimize:合并暂存区到正式索引 ── collection.optimize() # ── ⑤ 单向量检索 ── result = collection.query( queries=zvec.Query(field_name="embedding", vector=fake_dense("向量检索 入门")), topk=3, ) print("=== 单向量检索 ===") for doc in result: print(f" {doc.id} score={doc.score:.4f} fields={doc.fields}") # ── ⑥ 向量 + 过滤:只看 2021 年及以后 ── result2 = collection.query( queries=zvec.Query(field_name="embedding", vector=fake_dense("向量检索 入门")), filter="publish_year >= 2021", topk=3, ) print("\n=== 向量 + 过滤(year >= 2021)===") for doc in result2: print(f" {doc.id} score={doc.score:.4f} fields={doc.fields}") if __name__ == "__main__": main()
💡 为什么先用占位向量? RAG 链路里「检索」和「Embedding」是解耦的。先用占位向量把检索跑通、看懂返回结构,再接模型,调试时你能一眼判断问题出在向量质量还是检索本身。后续章节的示例也大多用占位向量,第 7、9 章才接真实 bge-m3。
下一章我们把这些 API 背后的概念彻底讲透。详见第 2 章。