zvec 深度实战:把向量数据库塞进进程里——从 ANN 算法、HNSW 图索引到混合检索与十亿级向量的完整指南(2026)
当 RAG、语义搜索、多模态检索成为每一个 AI 应用的标配,"向量数据库"早已不是大厂专属的基础设施。但绝大多数团队一上来就部署 Milvus、Qdrant、Weaviate 这类**客户端-服务器(C/S)**架构的集群,等于为了查 10 万条知识库文档,先养了一个分布式系统。
阿里巴巴开源的 zvec 走了一条相反的路:它把向量数据库做成了进程内(in-process / embedded)库——没有服务、没有端口、没有运维,import 进来就能用,毫秒级检索数十亿向量。这背后是一整套关于 ANN 算法、图索引、量化压缩与混合检索的工程取舍。
本文不是一篇"zvec 安装教程",而是一份从数学直觉到可运行代码、从算法原理到生产调优的完整拆解。读完后你不仅能用 zvec,更能回答一个更本质的问题:为什么向量检索必须靠"近似"而非"精确"?
一、背景:为什么我们需要"向量"这种奇怪的数据库
1.1 传统数据库的盲区
关系型数据库(PostgreSQL、MySQL)和文档数据库(MongoDB)解决的核心问题是:结构化数据的精确匹配与聚合。你写 WHERE user_id = 42 AND status = 'paid',数据库走 B+ 树索引,O(log n) 精准命中。
但 2026 年的 AI 应用面对的是另一类问题:
- "找出和这句话意思最接近的 10 条客服记录"
- "给这张图片找视觉上相似的商品"
- "根据用户刚才的行为,推荐品味相近的内容"
这些问题的共同特征是:查询条件不是等值或范围,而是"相似"。而"相似"在高维空间里,B+ 树完全帮不上忙——你没法对"语义"建一个有序索引。
1.2 Embeddings:把万物塞进一个向量
破局的关键是把非结构化数据(文本、图片、音频)用深度学习模型编码成一个高维浮点向量(embedding)。直觉上:
- 语义相近的句子,编码后的向量在空间中距离更近;
- "苹果(水果)"和"苹果(公司)"会有不同的向量方向;
- 一张猫的图片和一张狗的图片,向量距离小于它和一辆汽车的向量距离。
这个编码由 embedding 模型完成(OpenAI text-embedding 系列、BGE、E5、Jina 等),输出维度通常是 384 / 768 / 1024 / 1536 / 3072。于是问题被转化为:
给定一个查询向量 q,在一个包含 N 个向量的集合里,找出与 q 距离最近的 K 个向量。
这就是 k-NN(k-Nearest Neighbors) 问题。向量数据库,本质就是为这个问题而生的专用存储与检索引擎。
1.3 "向量数据库" vs "带向量字段的数据库"
很多人会问:PostgreSQL 的 pgvector、ClickHouse 的向量类型,不也能存向量做相似度吗?区别在哪?
| 维度 | pgvector / ClickHouse 向量类型 | 专用向量数据库(含 zvec) |
|---|---|---|
| 定位 | 通用数据库顺手提一嘴向量 | 为 ANN 检索深度优化 |
| 索引 | IVFFlat / HNSW(功能有限) | HNSW / IVF-PQ / 量化全家桶 |
| 十亿级规模 | 勉强,内存压力巨大 | 原生支持量化压缩、磁盘索引 |
| 混合检索 | 需自己拼 SQL | 向量+全文+标量过滤原生融合 |
| 部署形态 | 独立服务 | 可进程内嵌入 |
这里要划重点:zvec 的独特之处在于它是 in-process 的。pgvector 再强,你也得先跑一个 PostgreSQL 实例。而 zvec 的核心引擎是 C++ 写的库,编译进你的进程,连 socket 都不用建立。
1.4 客户端-服务器 vs 进程内:一张架构图说清取舍
【C/S 架构:Milvus / Qdrant / 云服务】
App ──网络 RPC──> Vector Server ──> 磁盘/内存索引
↑ 序列化开销、网络延迟、需独立运维与扩缩容
【In-process 架构:zvec(及 SQLite 的哲学)】
App
└─ zvec 引擎(同进程、同内存空间)
└─ 直接遍历/检索本地索引,零序列化、零网络
进程内架构的优势是延迟极低、零运维、易嵌入(CLI 工具、桌面应用、边缘设备、Serverless 函数都合适);代价是多进程协作不如 C/S 灵活(zvec 的设计是:多进程可同时读同一个 Collection,写入为单进程独占)。这恰好契合"单写多读、本地优先"的大量真实场景。
二、核心概念:把"相似度"翻译成数学
2.1 三种距离度量,别用错
向量之间的"远近"由距离/相似度函数定义。zvec 及主流向量库通常支持三种:
(1) 余弦相似度(Cosine)—— 最常用
衡量两个向量方向的接近程度,与长度无关:
$$\cos(\theta)=\frac{A\cdot B}{|A|,|B|}=\frac{\sum_i A_iB_i}{\sqrt{\sum_i A_i^2}\cdot\sqrt{\sum_i B_i^2}}$$
取值范围 [-1, 1],越接近 1 越相似。绝大多数文本 embedding 默认输出已归一化(模长=1),此时余弦相似度等价于点积(内积),计算更快。
(2) 内积(Inner Product / Dot Product)
$$A\cdot B=\sum_i A_iB_i$$
当向量已 L2 归一化时,内积 = 余弦相似度。未归一化时,内积会受向量长度影响,适合"长度本身也携带信息"的场景(如推荐系统中用户活跃度)。
(3) 欧氏距离(L2 / Euclidean)
$$|A-B|_2=\sqrt{\sum_i(A_i-B_i)^2}$$
适合图像特征、数值型向量等对"绝对位置"敏感的场景。
工程经验:文本语义检索用余弦(或归一化后的内积);图像/特征检索常用 L2。度量方式选错,检索效果会断崖式下跌,这是新手最常见的坑。
2.2 维度灾难:为什么"精确"检索会崩溃
一个朴素的想法是:把 N 个向量全存下来,每次查询拿 q 和所有向量算一遍距离,取最小的 K 个(即 Flat / 暴力检索)。这样做召回率 100%(结果绝对正确),但复杂度是 O(N·d)——N 是向量数,d 是维度。
当 N = 1 亿、d = 1536 时,单次查询要做约 1.5 万亿次浮点乘加。即便用 SIMD 优化到每秒千亿次运算,一次查询也要十几毫秒且吃满 CPU,并发直接崩。
更糟的是"维度灾难":在高维空间里,任意两个向量的距离趋于"彼此都差不多远",最近邻和最远邻的区分度急剧下降,纯暴力检索的性价比越来越差。
于是业界转向 ANN(Approximate Nearest Neighbor,近似最近邻):牺牲一点点召回率,换取几个数量级的加速。zvec、Milvus、Faiss 的核心,全是在做"如何更聪明地近似"。
2.3 ANN 索引家族速览
| 索引 | 思想 | 召回 | 内存 | 适用规模 |
|---|---|---|---|---|
| Flat | 暴力全扫 | 100% | 高(原始向量) | 小数据、要绝对正确 |
| IVF (倒排文件) | 聚类分桶,只搜最近的桶 | 中高 | 中 | 大规模、可容忍略低召回 |
| HNSW (分层可导航小世界图) | 多层图,像"高速路+乡道"导航 | 高 | 高(要存图) | 中大规模、要低延迟 |
| PQ / RabitQ (乘积量化) | 把向量"有损压缩"成短编码 | 中 | 极低 | 十亿级、内存紧张 |
| IVF-PQ / HNSW+PQ | 组合拳 | 高 | 低 | 工业级十亿规模 |
zvec 同时支持 HNSW / IVF / Flat 索引与 RabitQ 等量化技术,还能在单次查询里把稠密向量 + 稀疏向量 + 标量过滤 + 全文检索融在一起——这就是它主打的"混合检索(Hybrid Search)"。
三、架构分析:zvec 是怎么把十亿向量塞进一个进程的
3.1 分层架构总览
zvec 的架构可以拆成三层:
┌─────────────────────────────────────────────┐
│ 应用进程(Python / Go / Rust / Node 你的代码) │
├─────────────────────────────────────────────┤
│ 语言绑定层(SDK):类型安全 API、序列化封装 │
├─────────────────────────────────────────────┤
│ zvec 核心引擎(C++): │
│ · 索引管理(HNSW/IVF/Flat) │
│ · 量化(RabitQ / PQ) │
│ · FTS 全文检索 │
│ · 混合检索调度(MultiQuery) │
│ · WAL 预写日志 / 持久化 │
│ · 并发控制(多进程读、单进程写) │
└─────────────────────────────────────────────┘
关键点:核心引擎是 C++,但通过官方 Go / Rust SDK、Python / Node 绑定暴露能力。这意味着你在 Python 里 import zvec 调的,底层是同一份高性能原生代码——既没有 C/S 的网络栈,也没有"Python 慢"的瓶颈。
3.2 索引:HNSW 为何成为工业界默认答案
HNSW(Hierarchical Navigable Small World,分层可导航小世界图)由 Yury Malkov 等人在 2016 年提出,如今是 Milvus、Qdrant、Weaviate、zvec 的共同内核选择。它凭什么?
思想比喻:像城市路网。
- 顶层(高层) 是稀疏的"高速公路"——只有极少数节点,连接跨度很大,负责快速把搜索从"全城任意点"拉到"目标区域附近";
- 底层(第 0 层) 是密集的"乡间小路"——几乎所有节点都在,负责在目标区域精细查找。
搜索时从顶层进入,沿边贪心往"离 q 更近"的邻居走,到局部最优后下沉到下一层,直到第 0 层输出候选。整个过程只需访问几百个节点,而不是上亿个。
后面第三章的代码实战,我们会亲手实现一个简化版 HNSW,让你看清这套"导航"到底怎么转。
3.3 量化:RabitQ 如何把内存砍掉一个数量级
HNSW 快,但图要存大量原始向量和边,内存开销大。当向量规模冲向十亿级、维度 1536 时,原始向量本身就要上 TB 内存。
zvec 支持 RabitQ(一种基于旋转与二进制量化的压缩技术,可视为 PQ 家族的现代演进):把每个浮点向量压缩成一串极短的比特编码。检索时先用压缩编码做粗筛,再对少数候选解压精算。结果是:
- 内存占用下降一个数量级;
- 十亿级向量可以放进单机内存或结合磁盘索引;
- 召回率损失控制在可接受范围。
直觉类比:PQ/RabitQ 就像"先看缩略图找目标,再点开大图确认"。牺牲的是"缩略图可能漏掉极相似的几个",换来的却是内存和带宽的巨幅节省。
3.4 混合检索(Hybrid Search):单次查询融合四种信号
纯向量检索有个先天缺陷:语义对不上关键词时容易翻车。比如用户搜"2024 年 Q3 退款政策",向量检索可能返回一堆"退款""政策"相关但年份不对的内容。
zvec 的杀手锏是 MultiQuery 混合检索:在一次查询里同时表达:
- 稠密向量(dense)—— 语义相似度;
- 稀疏向量(sparse,如 BM25 / SPLADE)—— 关键词精确命中;
- 标量过滤(scalar filter)——
year = 2024 AND quarter = 'Q3'; - 全文检索 FTS(原生关键词检索,无需外接 ES)。
引擎内部用 RRF(Reciprocal Rank Fusion,倒数排名融合) 或加权分数把多路结果合并,兼顾"语义对了"和"关键词/条件也对齐"。这对 RAG 场景几乎是从"能用"到"好用"的质变。
3.5 持久化与并发:WAL 预写日志
in-process 不代表"内存一关数据就没"。zvec 用 WAL(Write-Ahead Log,预写日志) 保证持久性:
- 写入先追加到日志,再更新内存索引;
- 进程崩溃 / 断电后,重启可重放日志恢复;
- 多进程可同时读取同一个 Collection(适合多副本只读服务);
- 写入为单进程独占(避免并发写冲突,符合"单写多读"的本地优先范式)。
再加上官方 Zvec Studio 可视化工具、RISC-V 架构支持,zvec 把"嵌入式向量数据库"的工程完备度拉到了生产级。
四、代码实战:从零手写 ANN,再接上 zvec
光讲原理不过瘾。这一章我们分三步走:(A) 朴素 Flat 暴力检索 → (B) 手写简化 HNSW → (C) 用 zvec SDK 做生产级混合检索。
4.1 朴素实现:余弦相似度 + Flat 检索(Python)
先建立直觉。下面这段代码就是"精确 k-NN"的全部真相:
import numpy as np
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
"""余弦相似度。a、b 已 L2 归一化时,结果等价于点积。"""
return float(np.dot(a, b)) # 归一化后无需再除模长
def flat_search(query: np.ndarray, vectors: np.ndarray, k: int):
"""
暴力全扫:对 query 与每个向量算余弦相似度,取 Top-K。
vectors: shape (N, d) query: shape (d,)
复杂度 O(N*d),召回率 100%,但慢。
"""
# 一次性矩阵乘法,比 for 循环快得多(底层走 BLAS)
sims = vectors @ query # shape (N,)
top_idx = np.argpartition(-sims, k)[:k] # 只排前 k 个,O(N)
top_idx = top_idx[np.argsort(-sims[top_idx])]
return [(int(i), float(sims[i])) for i in top_idx]
if __name__ == "__main__":
rng = np.random.default_rng(42)
N, d = 100_000, 768
vectors = rng.standard_normal((N, d))
vectors /= np.linalg.norm(vectors, axis=1, keepdims=True) # 归一化
query = rng.standard_normal(d); query /= np.linalg.norm(query)
import time
t0 = time.perf_counter()
res = flat_search(query, vectors, k=5)
print(f"N={N}, 暴力检索耗时 {1000*(time.perf_counter()-t0):.1f} ms")
print("Top-5:", res)
跑这段代码你会发现:10 万条 768 维向量,单次查询也要好几毫秒且吃满一个 CPU 核。放大到 1 亿条,单查询几十毫秒还纯 CPU 占用——这正是 ANN 存在的理由。
4.2 手写简化版 HNSW:看透"图导航"
下面这个实现刻意简化(单层近似、固定参数、纯 Python),目的是让你"看得见"HNSW 的核心机制:用图代替全扫,用贪心游走代替遍历。
import heapq, math, random
import numpy as np
class SimpleHNSW:
"""
简化版 HNSW(教学用,非生产级):
- 单层图(真实 HNSW 是多层,这里用一层示意导航思想)
- 每个节点维护最多 M 个邻居
- 插入时连边;查询时从随机入口贪心游走
"""
def __init__(self, dim, M=16, ef=32, rng=None):
self.dim = dim; self.M = M; self.ef = ef
self.rng = rng or random.Random(0)
self.vectors = [] # 原始向量列表
self.adj = [] # 邻接表:adj[i] = [邻居id...]
def _dist(self, a, b) -> float:
# 用内积近似余弦(假设向量已归一化)
return -float(np.dot(a, b)) # 取负,方便用"最小堆"当最大堆
def insert(self, vec: np.ndarray):
vid = len(self.vectors)
self.vectors.append(vec)
self.adj.append([])
if vid == 0:
return # 第一个节点没有邻居
# 从已有节点里贪心找最近的 ef 个,作为候选邻居
candidates = []
for other in range(vid):
d = self._dist(vec, self.vectors[other])
candidates.append((d, other))
candidates.sort() # 按距离升序
neighbors = [o for _, o in candidates[:self.M]]
# 双向连边
for nb in neighbors:
self.adj[vid].append(nb)
if len(self.adj[nb]) < self.M:
self.adj[nb].append(vid)
def search(self, query: np.ndarray, k=5):
# 贪心游走:从一个入口出发,不断跳到更近的邻居
entry = self.rng.randrange(len(self.vectors))
visited = {entry}
# 动态候选集(最小堆,存负距离)
heap = [(self._dist(query, self.vectors[entry]), entry)]
heapq.heapify(heap)
while heap:
d, cur = heapq.heappop(heap)
improved = False
for nb in self.adj[cur]:
if nb in visited:
continue
visited.add(nb)
nd = self._dist(query, self.vectors[nb])
if len(heap) < self.ef or nd < heap[0][0]:
heapq.heappush(heap, (nd, nb))
improved = True
if not improved:
break
# 从候选堆里取 Top-K(注意距离是负的,要转回相似度)
top = sorted(heap, key=lambda x: x[0])[:k]
return [(oid, -dist) for dist, oid in top]
if __name__ == "__main__":
rng = np.random.default_rng(7)
N, d = 20_000, 128
data = rng.standard_normal((N, d))
data /= np.linalg.norm(data, axis=1, keepdims=True)
idx = SimpleHNSW(dim=d, M=16, ef=32, rng=random.Random(7))
for v in data:
idx.insert(v)
q = data[123] # 用自己的一个向量当查询,理想召回应该是它自己
res = idx.search(q, k=5)
print("查询向量 id=123,Top-5 召回:", res)
print("是否召回自身(id=123):", any(oid == 123 for oid, _ in res))
这段代码揭示了 HNSW 的精髓:查询不遍历所有点,而是沿图的边"跳着走",每一步都逼近目标。真实 HNSW 的"多层"结构只是让"第一步跳跃"跨度更大、更快收敛。生产级实现(如 zvec 底层)还会做:
- 层级分配:每个节点按
level = floor(-ln(uniform) * mL)决定所处层数(mL = 1/ln(M)); - 双向择优连边 + 邻居去重,控制图的度数避免退化;
- efConstruction / efSearch 参数控制"候选集大小",直接权衡召回与速度。
4.3 zvec 实战:进程内、零服务器、混合检索
下面用示意性 API展示 zvec 的 in-process 用法(方法名随 SDK 版本演进,核心能力不变)。注意最大特点:没有 connect() 到任何服务,直接开一个本地 Collection 就能读写。
# 示意:zvec Python SDK 的 in-process 用法
import zvec
# 1) 在进程内创建一个 Collection,纯本地、无需服务器
# metric="cosine",索引选 HNSW,维度 768
collection = zvec.Collection.create(
path="./kb_zvec", # 本地目录,WAL 持久化到这里
dim=768,
metric="cosine",
index_type="hnsw",
hnsw_m=32, # 每层最大邻居数,越大召回越高、内存越涨
ef_construction=256, # 建索引时的候选集大小
)
# 2) 把 embedding 插进去(假设 embed() 是你的 embedding 模型封装)
docs = load_knowledge_base()
for doc in docs:
vec = embed(doc.text) # np.ndarray, shape (768,)
collection.insert(
vector=vec,
# 标量字段,用于后续过滤;同时可挂 FTS 全文索引
fields={"text": doc.text, "year": doc.year, "quarter": doc.quarter},
)
# 3) 混合检索:向量语义 + 标量过滤 + 全文检索,一次查询搞定
query_vec = embed("2024 年 Q3 的退款政策是什么?")
results = collection.search(
vector=query_vec,
top_k=5,
# 标量过滤:只在指定范围里找
filter="year = 2024 AND quarter = 'Q3'",
# 全文检索:关键词必须命中,融合进排序
full_text="退款 政策",
)
for r in results:
print(f"score={r.score:.3f} year={r.fields['year']} {r.fields['text'][:60]}")
# 4) 进程退出前(或按需)持久化
collection.flush()
几个工程要点:
hnsw_m和ef_construction越大,召回越稳但内存/建库时间越涨;生产环境要按数据量做 ablation;filter+full_text同时给出时,zvec 在引擎内部做融合排序,比"先向量召回再应用层过滤"更准、更快(后者容易因过滤把 Top-K 抽空);path指向本地目录,WAL 保障崩溃可恢复——这就是"嵌入式"的运维红利。
4.4 不止 Python:Go / Rust 同样进程内
zvec 的官方 Go / Rust SDK 让它在服务端、CLI、边缘场景都能零部署嵌入:
// 示意:zvec Go SDK(进程内)
col, err := zvec.Open(zvec.Config{
Path: "./kb_zvec",
Dim: 768,
Metric: zvec.Cosine,
Index: zvec.HNSW{M: 32, EfConstruction: 256},
})
if err != nil { panic(err) }
_ = col.Insert(vector, zvec.Fields{"text": text, "year": 2024})
hits, _ := col.Search(query, zvec.SearchOpt{
TopK: 5,
Filter: "year = 2024",
})
// 示意:zvec Rust SDK(进程内,零 GC,适合高性能服务)
let col = zvec::Collection::open(zvec::Config {
path: "./kb_zvec".into(),
dim: 768,
metric: zvec::Metric::Cosine,
index: zvec::Index::Hnsw { m: 32, ef_construction: 256 },
});
col.insert(&vector, &[("text", text), ("year", "2024")]).unwrap();
let hits = col.search(&query, zvec::SearchOpt { top_k: 5, filter: "year = 2024" });
注意 Rust 版本天然无 GC、内存可控、可编译进单二进制——这正是把向量检索塞进 Serverless 函数、IoT 网关、CLI 工具的理想形态。
五、性能优化:把 zvec 榨到极致
5.1 量化是第一杠杆
当向量规模上亿,原始向量占的内存往往比图本身还大。优先策略:
- 小数据(< 百万):HNSW + 原始向量,追求极限低延迟;
- 中大数据:HNSW + RabitQ/PQ 量化存储,内存降一个数量级,召回只掉一点;
- 十亿级:量化 + 磁盘索引(zvec 支持从内存到磁盘的多种索引类型)。
经验法则:先量化压内存,再调 HNSW 参数保召回。
5.2 索引参数调优(HNSW)
| 参数 | 调大影响 | 调小影响 | 建议 |
|---|---|---|---|
M(每层邻居数) | 召回↑、内存↑、建库慢 | 召回↓、更快更省 | 16–64,常用 32 |
ef_construction | 建库质量↑、更慢 | 图质量↓ | 100–400 |
ef_search | 查询召回↑、更慢 | 查询快但易漏 | 64–256,按 SLA 取 |
实践:用**固定的评测集(比如 1000 条带标注 query)**画一条"召回率 vs 延迟"曲线,选拐点。别拍脑袋。
5.3 距离计算的硬件加速:SIMD 与批处理
- zvec 底层 C++ 用 SIMD(AVX2 / AVX-512 / NEON) 并行算点积/距离,比标量循环快 4–16 倍;
- 批量查询(batch search) 远比逐条查询高效——一次传 100 个 query,矩阵化计算吃满向量化单元;
- embedding 维度能降则降:384 维 BGE-small 往往够用,没必要无脑上 3072 维,维度减半等于距离计算量减半。
5.4 混合检索的排序融合细节
RRF(倒数排名融合)公式:
$$score(d)=\sum_{r \in 检索路} \frac{1}{k + \text{rank}_r(d)}$$
其中 k 通常取 60。它不关心各路分数的量纲差异,只看"排名",所以把向量语义排名和关键词 BM25 排名融在一起非常稳。zvec 的 MultiQuery 内部就用类似机制,你只需分别给出各路条件。
5.5 一个真实的取舍清单
| 场景 | 推荐配置 | 理由 |
|---|---|---|
| 个人笔记 / 本地 RAG(< 50万) | HNSW, M=32 | 简单、快、零运维 |
| 客服知识库(百万~千万) | HNSW + RabitQ | 内存可控、低延迟 |
| 十亿级商品图搜 | IVF-PQ / 量化 + 磁盘 | 内存是硬约束 |
| 边缘 / Serverless | in-process zvec | 不能起服务 |
| 多团队协作、高并发写 | C/S 架构(Milvus 类) | 需要多写、分布式 |
5.6 倒排文件 IVF:另一种"分而治之"
HNSW 靠"图"导航,而 IVF(Inverted File,倒排文件) 靠"聚类分桶"。思路非常朴素:
- 建库时用 K-means 把所有向量聚成
nlist个簇,得到每个簇的中心(centroid); - 每个向量被分配到离它最近的簇,记录进该簇的"倒排列表";
- 查询时,先算 q 离哪些簇中心最近(只算
nlist次),挑最近的nprobe个簇,只在这几个簇内部做精确搜索。
复杂度从 O(N·d) 降到 O(nprobe/N·nlist · d),当 nlist 很大时加速比惊人。代价是:如果 q 恰好落在簇边界,最近邻可能在没被选中的簇里 → 召回略降。
import numpy as np
class SimpleIVF:
"""简化版 IVF:聚类分桶 + 桶内暴力,演示"分而治之"思想。"""
def __init__(self, nlist=64, nprobe=4, rng=None):
self.nlist = nlist; self.nprobe = nprobe
self.rng = rng or np.random.default_rng(0)
self.centroids = None
self.buckets = {} # cluster_id -> list of (vec_id, vector)
def build(self, vectors: np.ndarray):
N = len(vectors)
init = self.rng.choice(N, size=min(self.nlist, N), replace=False)
self.centroids = vectors[init]
for cid in range(len(self.centroids)):
self.buckets[cid] = []
for vid, v in enumerate(vectors):
d = self.centroids @ v
cid = int(np.argmax(d))
self.buckets[cid].append((vid, v))
def search(self, query: np.ndarray, k=5):
cd = self.centroids @ query
top_clusters = np.argpartition(-cd, self.nprobe)[:self.nprobe]
cand = []
for cid in top_clusters:
for vid, v in self.buckets[int(cid)]:
cand.append((float(v @ query), vid))
cand.sort(reverse=True)
return cand[:k]
data = np.random.default_rng(1).standard_normal((50_000, 128))
data /= np.linalg.norm(data, axis=1, keepdims=True)
ivf = SimpleIVF(nlist=128, nprobe=8); ivf.build(data)
print(ivf.search(data[7], k=5))
nlist越大、桶越细,召回越高但建库与选簇越贵;nprobe越大越准也越慢。IVF-PQ(先分桶再量化)是十亿级规模的经典组合拳,zvec 的 IVF 索引正是这条路线上的生产实现。
5.7 乘积量化 PQ:把向量"压成身份证号"
原始向量是 d 个 float32(如 768×4 = 3072 字节/条)。PQ(Product Quantization,乘积量化) 的魔法:
- 把
d维向量切成M段(如 768 维切成 8 段,每段 96 维); - 对每段独立 K-means 聚类成
k*个"码本"(如 256 个),每段用一个 1 字节的码字 表示最近的码本中心; - 于是每条向量被压成 M 个字节(如 8 字节),而非 3072 字节——压缩比近 384 倍。
检索时用"查表 + 距离近似"快速算查询与各压缩向量的距离,先粗筛再对 Top 候选用原始向量精算。
import numpy as np
class SimplePQ:
"""极简 PQ:演示"分段量化=用短码字代替原始向量"的思想。"""
def __init__(self, M=8, ksub=256, rng=None):
self.M = M; self.ksub = ksub
self.rng = rng or np.random.default_rng(0)
self.codebooks = []
self.codes = None
def fit(self, vectors: np.ndarray):
N, d = vectors.shape
assert d % self.M == 0
sub = d // self.M
self.codes = np.zeros((N, self.M), dtype=np.uint8)
for m in range(self.M):
seg = vectors[:, m*sub:(m+1)*sub]
idx = self.rng.choice(N, size=min(self.ksub, N), replace=False)
cb = seg[idx]
assign = np.argmax(seg @ cb.T, axis=1)
self.codebooks.append(cb)
self.codes[:, m] = assign.astype(np.uint8)
return self
def memory_bytes(self, N, d):
raw = N * d * 4
compressed = N * self.M
return raw, compressed
N, d = 1_000_000, 768
pq = SimplePQ(M=8); raw, comp = pq.memory_bytes(N, d)
print(f"原始内存 ≈ {raw/1e9:.1f} GB,PQ(8字节/条) ≈ {comp/1e9:.2f} GB")
RabitQ 是 PQ 思路的现代演进(引入旋转与二进制化),在 zvec 里把压缩率与召回进一步推高。结论很直接:当向量规模逼近十亿,量化不是可选项,是必经之路。
5.8 把 zvec 接进 RAG:分块、召回、重排的工程闭环
向量检索很少孤立存在,它通常是 RAG(检索增强生成) 的第一棒。一个能上生产的闭环长这样:
# 示意:用 zvec 搭一个最小可用 RAG 检索器
import zvec
col = zvec.Collection.create(path="./rag_zvec", dim=768,
metric="cosine", index_type="hnsw")
def index_documents(docs):
for doc in docs:
# 关键工程点 1:先切块(chunk),别整篇塞一个向量
for chunk in split_into_chunks(doc.text, max_tokens=512, overlap=64):
col.insert(embed(chunk),
fields={"text": chunk, "doc_id": doc.id,
"offset": chunk.offset})
def rag_retrieve(question: str, top_k=5):
# 关键工程点 2:多召回,留给重排
q_vec = embed(question)
hits = col.search(q_vec, top_k=top_k * 3, filter="")
# 关键工程点 3:用 cross-encoder 重排,纠正向量召回偏差
reranked = cross_encoder_rerank(question, [h.fields["text"] for h in hits])
return reranked[:top_k]
三个老手才知道的坑:
- 切块比模型选择更影响效果:chunk 太大稀释语义、太小丢失上下文,512 token + 64 重叠是常见甜点;
- 向量召回要"多召回、后重排":直接取 Top-5 容易漏,先取 Top-15 再让 cross-encoder 重排,质量显著上升;
- 元数据过滤前置:权限、时间范围用标量过滤在引擎内完成,别召回完再在应用层丢掉,否则 Top-K 会被抽空。
六、总结与展望:向量检索正在"下沉"到进程里
回看这篇文章的主线,其实是一条基础设施下沉的清晰路径:
- 数学层:相似度度量 + k-NN 问题,是永恒的底座;
- 算法层:ANN(HNSW / IVF / PQ)用"可控的近似"换"数量级的加速",是工程智慧的结晶;
- 系统层:从 C/S 集群到 in-process 嵌入式(zvec 的路线),向量检索正在像 SQLite 之于关系型数据库一样,下沉为可嵌入、零运维的本地原语;
- 应用层:混合检索(向量 + 全文 + 标量)让 RAG 从"语义沾边"走向"条件精确对齐"。
给你的判断框架:
- 数据量不大、追求零运维、跑在本地/边缘/函数里 → zvec 这类 in-process 向量库是正解;
- 需要多写多读、分布式扩缩容、团队协作 → 仍选 C/S 架构的专用向量数据库;
- 已经在用 Postgres 且规模小 → pgvector 够用,别过度设计。
2026 年的趋势越来越清楚:AI 应用的基础设施正在从"重服务"走向"轻库"。zvec 把"十亿级向量检索"装进一个 import,背后是 C++ 引擎、HNSW 图、RabitQ 量化、WAL 持久化与混合检索调度共同撑起来的工程厚度。理解它,不只是多会用一个工具,更是理解了"近似计算 + 嵌入式架构"这一整套正在重塑 AI 基础设施的设计哲学。
如果你只想记住一句话:当别人忙着为一次语义搜索部署一个分布式集群时,真正的高手在进程里 import 了一个 zvec。
附:核心概念速查表
| 术语 | 一句话 | 关键参数 |
|---|---|---|
| Embedding | 把文本/图片编码成高维向量 | 维度 d |
| Cosine | 比方向,最常用 | 需归一化 |
| k-NN | 找最近的 K 个 | 精确但慢 |
| ANN | 近似近邻,换速度 | 召回率<100% |
| HNSW | 多层图导航,工业默认 | M, ef |
| IVF | 聚类分桶,只搜近桶 | nlist, nprobe |
| PQ/RabitQ | 向量量化压缩 | 压缩比 |
| Hybrid | 向量+全文+标量融合 | RRF |
| WAL | 预写日志保持久化 | —— |