向量数据库深度解析——从原理到选型

0. 阅读导引 ✨

嘿~这是「AI 基础设施三部曲」的第二篇(第一篇是隔壁的 RAG 完全指南)!

如果你曾经有过这些疑问——

  • “向量数据库和 MySQL 有什么区别?为什么不能直接用传统数据库存向量?” 🤔
  • “HNSW、IVF、PQ…这几个大写字母到底是干嘛的?” 📖
  • “ChromaDB、Milvus、Qdrant、Weaviate、Pinecone…这么多到底选哪个?” 😵‍💫

——那这篇文章就是为你准备的!

读完你会获得什么?

  1. 从数学原理到工业实现的完整向量数据库认知
  2. 能独立做向量数据库的技术选型
  3. 理解 ANN 索引算法的核心思想(无需深厚的数学背景)
  4. 知道如何优化向量检索的 QPS 和召回率

本文约 1.8 万字,配合咖啡食用更佳 ☕


1. 先搞懂”向量”到底是个啥 📐

1.1 从一句话到一个数组

想象你要向一个从没见过水果的外星人描述”苹果”和”香蕉” 👽

你可能会建立一个特征表:

甜度 硬度 颜色偏红 颜色偏黄 形状圆 形状长
🍎 苹果 0.7 0.8 0.9 0.1 0.9 0.1
🍌 香蕉 0.8 0.3 0.1 0.9 0.2 0.9

把苹果的特征提取出来:[0.7, 0.8, 0.9, 0.1, 0.9, 0.1]

这个数组就是苹果的 向量(Vector)。6 个数字 = 6 维。

真实世界的 Embedding 模型不是手工设计特征,而是通过神经网络自动学习——但核心理念完全一样:把语义相似的文本映射到空间中相近的位置

1
2
3
4
5
6
7
8
9
10
高维空间中的距离示意图(画成 2D):

🍎(0.7, 0.8)
│ ← 距离很近(都是水果)
🍌(0.6, 0.7)

│ ← 距离很远!

🚗(0.1, -0.3) ---- 🏍️(-0.1, -0.2)
← 距离很近(都是交通工具)

1.2 向量的三个核心操作

在向量数据库的世界里,你只需要理解三种操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
① 存(Insert)
文档 → Embedding 模型 → 向量 → 存入数据库
"今天天气真好" → [0.23, -0.45, 0.89, ..., 0.12]

向量数据库(持久化存储)

② 查(Search)
用户问题 → Embedding 模型 → 查询向量
"外面天气怎么样?" → [0.21, -0.43, 0.91, ..., 0.15]

在库里找到最相似的 K 个向量 → 返回原始文本

③ 相似度计算
query_vec · doc_vec → 相似度分数(越大越像)
或 ||query_vec - doc_vec|| → 距离(越小越像)

1.3 三种相似度度量——怎么计算”像不像” 📏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np

a = np.array([0.7, 0.8, 0.9])
b = np.array([0.6, 0.7, 0.8])

# ① 欧氏距离(Euclidean)—— 直线距离
# 越小越相似,受向量长度影响大
euclidean = np.linalg.norm(a - b)
print(f"欧氏距离: {euclidean:.4f}") # ~0.173

# ② 余弦相似度(Cosine)—— 夹角
# 越大越相似(范围 -1 到 1),不受向量长度影响
cosine = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
print(f"余弦相似度: {cosine:.4f}") # ~0.997

# ③ 内积(Dot Product)—— 投影长度
# 越大越相似,某种程度上等价于余弦相似度
dot = np.dot(a, b)
print(f"内积: {dot:.4f}") # ~1.70

选哪个?

场景 推荐度量 原因
文本 Embedding(已 L2 归一化) 余弦 / 内积 归一化后两者等价
图片 Embedding(未归一化) 余弦 不受亮度/对比度影响
推荐系统 内积 与矩阵分解的评分预测一致
地理坐标/传感器数据 欧氏 物理距离有实际意义

⚠️ 关键提醒:如果你用的 Embedding 模型已经做了 L2 归一化(normalize_embeddings=True),余弦相似度和内积是完全等价的!大多数现代 Embedding 模型默认归一化。


2. 为什么不能直接用 MySQL?——向量检索的特殊性 🔍

2.1 传统数据库的”查找”,和向量检索的”查找”

1
2
3
4
5
6
7
-- 传统数据库:精确/范围查找(O(log N) 或 O(1) 用索引)
SELECT * FROM products WHERE price BETWEEN 100 AND 200;
-- BTREE 索引轻松搞定,百万数据毫秒级

-- 向量数据库:"找最像的 K 个"
-- 没有 SQL 能直接表达的查询语义
-- "找和这张图最像的 10 张图" → 需要和 100 万张图逐一比较?

2.2 暴力检索的成本

假设你有 100 万个 1024 维的 32-bit 向量:

1
2
3
4
存储:100万 × 1024 × 4 bytes = 约 4 GB(纯向量,不算元数据)
一次查询的计算量:100万 × 1024 次浮点运算 = 约 10 亿次 FLOPS
单次查询耗时(单线程):~100ms(仅计算时间,不算 I/O)
100 QPS 的服务器:需要 10 亿次/秒 的吞吐...

100 万数据就这样了,如果 1 亿呢?100 亿呢?

这就是向量数据库存在的根本理由——你不能每次都做全量扫描。

2.3 近似最近邻(ANN)——“差不多就行了”的艺术 🎨

关键洞察:在 Embedding 空间里,大多数向量都和你的查询不相关。

既然如此,为什么要把时间浪费在那些”无关”的向量上呢?

1
2
3
4
精确 KNN:                     近似 ANN:
比较 100万 个向量 只比较 ~3000 个向量
100% 的 Top-K 是正确答案 95-99% 的 Top-K 是正确答案
耗时 100ms 耗时 1-3ms

这就是 ANN 的哲学:用一点点精度换回几十倍的性能。


3. 向量索引算法——数据库的心脏 🫀

这是本文最”硬核”的一节,我会尽量用图解和类比来解释,零数学恐惧症友好 (๑•̀ㅂ•́)و✧

3.1 哈希类方法(LSH)——分桶大法 🪣

核心理念:设计一种”有语义感知”的哈希函数,让相似的向量有更大的概率被分到同一个桶里。

1
2
3
4
传统哈希:                    LSH 哈希:
"apple" → 桶 #42 向量 A [0.7, 0.8, ...] → 桶 #3
"applf" → 桶 #999 向量 B [0.6, 0.7, ...] → 桶 #3 ← 同一个桶!
向量 C [-0.5, 0.3, ...] → 桶 #7 ← 不同桶

构建时:用多个 LSH 函数(如 10 个),每个向量存在 10 个桶里
查询时:计算查询向量的 10 个桶号,只搜索这些桶里的向量

1
2
3
优点:构建快、理论保证好
缺点:精度偏低(通常 70-85%)、内存消耗大(多副本)
现状:已被新兴方法(HNSW/IVF)取代,主要用于理论研究和特定场景

3.2 聚类类方法(IVF)——先找”区”,再找”人” 🏘️

核心思想:把 100 万个向量分成 1000 个”小区”(聚类),查询时先定位到最近的几个小区,只在这些小区里搜。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# IVF 工作流程(简化实现)
from sklearn.cluster import KMeans
import numpy as np

class SimpleIVF:
def __init__(self, n_clusters=1000, n_probe=10):
self.n_clusters = n_clusters
self.n_probe = n_probe # 查询时要探索的聚类数
self.kmeans = KMeans(n_clusters=n_clusters)
self.vectors = None
self.cluster_assignments = None

def build(self, vectors):
"""构建索引"""
self.vectors = vectors
self.cluster_assignments = self.kmeans.fit_predict(vectors)
# 每个聚类存一个向量列表
self.clusters = {i: [] for i in range(self.n_clusters)}
for idx, cluster_id in enumerate(self.cluster_assignments):
self.clusters[cluster_id].append(idx)

def search(self, query_vec, k=10):
"""搜索 Top-K"""
# Step 1: 找到最近的 n_probe 个聚类中心
centers = self.kmeans.cluster_centers_
distances = np.linalg.norm(centers - query_vec, axis=1)
nearest_clusters = np.argsort(distances)[:self.n_probe]

# Step 2: 在这几个聚类中暴力检索
candidates = []
for cid in nearest_clusters:
for idx in self.clusters[cid]:
dist = np.linalg.norm(self.vectors[idx] - query_vec)
candidates.append((idx, dist))

# Step 3: 排序取 Top-K
candidates.sort(key=lambda x: x[1])
return candidates[:k]

# 使用示例
ivf = SimpleIVF(n_clusters=1000, n_probe=10)
ivf.build(my_vectors) # 100 万向量
results = ivf.search(query, k=10) # 只检索约 10,000 个向量(原来的 1/100)

IVF 关键参数

  • n_clusters(聚类数):通常设为 sqrt(N)4*sqrt(N)。100 万向量 → 1000-4000 个聚类
  • n_probe(探测聚类数):越大精度越高但越慢。常用 8-32

3.3 图类方法(HNSW)——当前工业界的王者 👑

HNSW(Hierarchical Navigable Small World)是目前最主流的向量索引算法。几乎所有现代向量数据库都把 HNSW 作为默认/推荐索引。

3.3.1 用”六度分隔”来理解

你可能听说过”六度分隔理论”——世界上任意两个人之间,最多通过 6 个中间人就能认识。

HNSW 做的就是这个:它构建一个朋友关系网(图),每个向量是图中的节点,相似的向量之间连一条边。

1
2
3
4
5
6
7
8
9
10
没有 HNSW 时:
从起点 → 遍历所有点 → 找到最近的点
(太慢了!)

有了 HNSW:
起点 "狗.jpg"
→ 朋友 "狼.jpg"(有点像)
→ 朋友的朋友 "狐狸.jpg"(更像了!)
→ 朋友的朋友的朋友 "郊狼.jpg"(找到了!)
(只跳了 3 步)

3.3.2 为什么是”分层的”(Hierarchical)?

1
2
3
4
5
6
7
8
9
10
11
第 2 层(最稀疏): ───── ● ───── ● ─────
/ \
第 1 层(中等): ─ ● ─ ─ ● ─ ─ ● ─ ● ─ ─
/ \ / \
第 0 层(全部节点):● ─ ● ─ ● ─ ● ─ ● ─ ●
(所有向量都在这一层)

搜索策略:
① 从第 2 层开始,做"大跨步"(少数节点,跳得快)
② 到第 1 层,做"中跨步"
③ 到第 0 层,做"小碎步"精细搜索

这就像你先看世界地图定位国家,再看国家地图定位城市,最后看城市地图精确找到街道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# HNSW 核心参数
HNSW_CONFIG = {
"M": 16, # 每个节点最多有多少个"朋友"(边数)
# 建议范围:4-64,越大精度越高但内存越大
"ef_construction": 200, # 构建时的搜索宽度
# 建议范围:100-500,越大构建越慢但图越好
"ef_search": 50, # 查询时的搜索宽度
# 建议范围:50-200,越大精度越高但越慢
}

# M 的影响:
# M=8 → 每个节点 8 个朋友 → 内存小,精度 ~90%
# M=16 → 每个节点 16 个朋友 → 内存中,精度 ~95%(最常用)
# M=64 → 每个节点 64 个朋友 → 内存大,精度 ~99%

# ef_construction 和 ef_search 的区别:
# ef_construction:构建时用的,调大 = 更好的图 = 更慢的构建
# ef_search:查询时用的,调大 = 更好的召回 = 更慢的查询

3.3.3 HNSW 的优缺点

1
2
3
4
5
6
7
8
9
10
✅ 优点:
· 查询速度极快(1-3ms @ 百万级)
· 召回率高(95-99%)
· 增量插入友好(不需要重建)
· 实现成熟,几乎每个向量数据库都支持

❌ 缺点:
· 内存占用大(需要存图结构和全量向量)
· 构建时间较长(特别是 ef_construction 设得大时)
· 删除操作相对复杂

3.4 量化类方法(PQ)——“压缩饼干”技术 🍪

当数据量达到亿级时,即使是 HNSW 也可能需要几百 GB 内存。这时候你需要 **Product Quantization(PQ)**来压缩向量。

3.4.1 PQ 的核心思想

1
2
3
4
5
6
7
8
9
10
原始向量(1024 维 × 4 字节 = 4KB/条):
[0.23, -0.45, 0.89, 0.12, -0.67, ..., 0.34]

PQ 压缩后:
① 把 1024 维切成 64 段,每段 16 维
② 对每段做 K-Means 聚类(比如 256 个中心)
③ 每段只需要存"聚类中心编号"(0-255,只需 1 字节)
④ 64 段 × 1 字节 = 64 字节/条!

4KB → 64 字节,压缩了 64 倍!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# PQ 压缩示例
from sklearn.cluster import KMeans
import numpy as np

class SimplePQ:
def __init__(self, n_subvectors=64, n_centroids=256):
self.n_subvectors = n_subvectors # 分成几段
self.n_centroids = n_centroids # 每段几个聚类中心
self.codebooks = [] # 64 个"词典"

def train(self, vectors):
"""训练 64 个 K-Means 模型"""
dim = vectors.shape[1]
sub_dim = dim // self.n_subvectors

for i in range(self.n_subvectors):
start = i * sub_dim
end = (i + 1) * sub_dim
sub_vectors = vectors[:, start:end]

kmeans = KMeans(n_clusters=self.n_centroids)
kmeans.fit(sub_vectors)
self.codebooks.append(kmeans)

def compress(self, vector):
"""把一个 1024 维向量压缩成 64 个编码"""
dim = len(vector)
sub_dim = dim // self.n_subvectors
codes = []

for i in range(self.n_subvectors):
start = i * sub_dim
end = (i + 1) * sub_dim
sub = vector[start:end].reshape(1, -1)
code = self.codebooks[i].predict(sub)[0]
codes.append(code)

return np.array(codes, dtype=np.uint8) # 64 字节

3.4.2 PQ 的实际使用

PQ 通常与 IVF 组合使用(IVF+PQ),这是处理亿级以上数据的标准方案:

  • IVF 负责”粗筛”(缩小搜索范围)
  • PQ 负责让所有向量都能”装进内存”
1
2
3
场景:10 亿个 1024 维向量
原始:10亿 × 4KB = 4 TB(装不进内存)
IVF+PQ 后:10亿 × 64 字节 = 64 GB(可以装进内存!)

3.5 算法全景对比 🗺️

算法 内存占用 查询速度 召回率 增量插入 百万级 亿级
暴力 KNN 最低 🐌 极慢 🎯 100%
LSH 🐇 快 ⭐⭐ 一般
IVF 低-中 🐇 快 ⭐⭐⭐ 好 ❌ 需重建
HNSW 🚀 最快 ⭐⭐⭐⭐ 极好 ❌ 内存
IVF+PQ 极低 🐇 快 ⭐⭐⭐ 好
DiskANN 低(SSD) 🚀 快 ⭐⭐⭐⭐ 极好

3.6 DiskANN——2025 年最值得关注的新方向 💾

2023 年微软开源的 DiskANN 解决了一个核心矛盾:精度高的(HNSW)吃内存,省内存的(IVF+PQ)精度低

DiskANN 的核心思路:把图索引放在 SSD(固态硬盘) 上,查询时只把需要的节点加载到内存。

1
2
3
4
5
6
7
8
传统 HNSW:100% 在内存 → 内存瓶颈
DiskANN: 图结构在 SSD → 查询时按需读取 → 突破内存限制

性能实测(微软论文):
10 亿个 100 维向量
HNSW:需要 1.2 TB 内存(太贵了)
DiskANN:只需 64 GB RAM + 1TB SSD
查询延迟:< 3ms,召回率 > 95%

2025 年在哪些数据库里可用?

  • LanceDB:内置 DiskANN 支持(LANCEDB_BUILDER=diskann)
  • Qdrant:2025.04 开始支持 mmap(内存映射)模式,类似思路
  • Milvus:MMap 模式支持

4. 主流向量数据库深度对比 🔬

好,理论准备足了。现在来逐一解剖每一个主流向量数据库。我会从架构、性能、适用场景、坑点四个维度来说。

4.1 Milvus——企业级航母 🚢

1
2
3
4
架构:云原生、存算分离
语言:Go(核心)+ C++(索引)
许可证:Apache 2.0
GitHub Stars:~32k

架构

索引支持:HNSW, IVF_FLAT, IVF_PQ, IVF_SQ8, DISKANN, GPU_IVF_FLAT, GPU_IVF_PQ

适合场景

  • ✅ 数据量 1000 万~100 亿
  • ✅ 需要分布式、高可用
  • ✅ 公司有专门的运维团队
  • ✅ 混合查询(标量过滤 + 向量检索)

不适合

  • ❌ 个人项目/原型验证(太重了)
  • ❌ 只有几千条数据的小场景(杀鸡用牛刀)
  • ❌ 需要在客户端(浏览器/手机)运行

一句话总结:向量数据库界的 Kubernetes——功能强大,但运维复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Milvus 使用示例
from pymilvus import MilvusClient, DataType

client = MilvusClient("http://localhost:19530")

# 创建集合(需要定义 Schema)
schema = client.create_schema(
auto_id=False,
enable_dynamic_field=True
)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("text", DataType.VARCHAR, max_length=65535)
schema.add_field("vector", DataType.FLOAT_VECTOR, dim=1024)

client.create_collection(
collection_name="my_docs",
schema=schema,
index_params={
"field_name": "vector",
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {"M": 16, "efConstruction": 200}
}
)

# 插入
client.insert("my_docs", [
{"id": 1, "text": "人工智能正在改变世界...", "vector": embedding_1},
{"id": 2, "text": "机器学习是 AI 的子领域...", "vector": embedding_2},
])

# 搜索
results = client.search(
collection_name="my_docs",
data=[query_embedding],
limit=10,
output_fields=["text"] # 返回文本字段
)

4.2 Qdrant——Rust 写的性能怪兽 🦀

1
2
3
4
架构:单机为主(也支持分布式)
语言:Rust ⚡
许可证:Apache 2.0
GitHub Stars:~22k

核心优势

  • 🚀 Rust 实现——极致性能,内存安全,CPU 效率高
  • 🎯 过滤能力业界第一——自定义 payload 索引 + 布尔表达式
  • 📦 单体二进制——一个可执行文件,下载即用
  • 🔒 内置访问控制——JWT 认证开箱即用

Qdrant 最特别的——Payload Filtering(载荷过滤)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, Range

client = QdrantClient("localhost", port=6333)

# 精确到令人发指的过滤搜索
results = client.search(
collection_name="products",
query_vector=query_emb,
limit=10,
query_filter=Filter(
must=[
FieldCondition(
key="price",
range=Range(gte=100, lte=500) # 价格 100-500
),
FieldCondition(
key="category",
match={"value": "electronics"} # 分类:电子产品
),
],
must_not=[
FieldCondition(
key="brand",
match={"value": "山寨牌"} # 排除山寨品牌
)
]
)
)

量化功能(2025 年新特性)

  • Scalar Quantization:float32 → uint8,4x 压缩
  • Binary Quantization:float32 → 1 bit,32x 压缩
  • MMap:大索引不全部加载到内存

适合场景

  • ✅ 中等数据量(10 万~5000 万)
  • ✅ 需要复杂的元数据过滤
  • ✅ 对性能有极致要求
  • ✅ 需要快速部署(Docker 一键)

不适合

  • ❌ 十亿级以上(还是 Milvus 更合适)
  • ❌ 需要在浏览器端运行

4.3 LanceDB——新生代的全能选手 🏹

1
2
3
4
5
架构:嵌入式(无服务器)
语言:Rust 核心 + Python/JS/Java SDK
许可证:Apache 2.0
GitHub Stars:~5k
存储格式:Lance(列式存储,Apache Arrow 生态)

LanceDB 是我个人最看好的年轻向量数据库。它的架构哲学和其他所有向量数据库都不一样:

1
2
3
4
5
6
7
传统向量数据库思维:
开一个服务器 → 连接 → 操作
(PostgreSQL / Redis 模式)

LanceDB 的思路:
import lancedb → 直接读写文件
(SQLite / DuckDB 模式)

这就是所谓的”无服务器(Serverless)向量数据库”——不需要启动任何服务,一个 Python 包就够了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import lancedb
import numpy as np

# 零配置,直接开始!
db = lancedb.connect("./my_lancedb") # 一个目录 = 一个数据库

# 创建表
table = db.create_table(
"documents",
data=[{
"vector": np.random.randn(1024).tolist(),
"text": "LanceDB 太方便了",
"id": 1
}]
)

# 创建索引(支持 IVF_PQ, IVF_HNSW_PQ, DiskANN)
table.create_index(
metric="cosine",
num_partitions=256,
num_sub_vectors=64 # PQ 压缩参数
)

# 搜索
results = table.search(query_vec).limit(10).to_pandas()

LanceDB 的独特功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1. 🗜️ Lance 列式存储格式
→ 比 Parquet 更快的随机访问
→ 原生支持的向量和 BLOB 类型
→ 零拷贝读取(Arrow FFI)

2. 📊 完整的 SQL 支持(通过 DataFusion)
SELECT text, vector FROM documents
WHERE category = 'tech'
ORDER BY l2_distance(vector, query_vec)
LIMIT 10

3. 🔄 多模态向量支持
同一行可以有多个向量列:
- text_embedding (1024维)
- image_embedding (512维)
- audio_embedding (768维)

4. 🌊 流式摄入
→ 支持增量写入
→ 支持数据版本管理(类似 Git 的 time travel)

适合场景

  • ✅ 原型开发和个人项目(最方便)
  • ✅ 客户端应用(Python/JS SDK 零依赖)
  • ✅ 多模态数据(文本+图片+音频向量存在一起)
  • ✅ CI/CD 中的向量数据测试
  • ✅ 需要版本管理的数据集

不适合(目前):

  • ❌ 超高并发(嵌入式架构的天然限制)
  • ❌ 分布式(LanceDB Cloud 还在发展中)
  • ❌ 企业级的权限和安全管控

4.4 ChromaDB——AI 开发者的”初恋” 💚

1
2
3
4
架构:嵌入式(Python 优先)
语言:Python(核心是 hnswlib + DuckDB)
许可证:Apache 2.0
GitHub Stars:~18k

ChromaDB 是很多 AI 开发者的第一个向量数据库——因为它和 LangChain / LlamaIndex 的集成做得最好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import chromadb

# 最简洁的 API(之一)
client = chromadb.PersistentClient(path="./chroma_data")
collection = client.get_or_create_collection("my_docs")

# 插入(自动生成 ID)
collection.add(
documents=["这是一段文本", "另一段文本"],
embeddings=[[0.1, 0.2, ...], [0.3, 0.4, ...]],
metadatas=[{"source": "a.pdf"}, {"source": "b.pdf"}],
ids=["doc1", "doc2"]
)

# 查询(甚至可以直接用文本,内置了默认的 Embedding 模型!)
results = collection.query(
query_texts=["什么是向量数据库?"],
n_results=10
)
# ChromaDB 有一个内置的 all-MiniLM-L6-v2 Embedding(不推荐生产用)

ChromaDB 的定位

  • 🎯 简单到极致——三行代码就能跑起来
  • 🔌 生态最好——LangChain、LlamaIndex、Haystack 的默认选择
  • 快速原型——从想法到验证只需 5 分钟

但也有一些现实的短板

  • 🐌 性能上限低(Python 实现的瓶颈)
  • 📉 百万级以上数据量会明显变慢
  • 🔧 生产部署不够成熟

一句话总结:最好的入门选择,但大部分人最终会迁移到 Qdrant 或 Milvus。

4.5 pgvector——“我就要用 PostgreSQL!”党 🐘

1
2
3
架构:PostgreSQL 扩展
语言:C
许可证:PostgreSQL License
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 就这么简单:PostgreSQL + pgvector 扩展
CREATE EXTENSION vector;

CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(1024) -- 在 PostgreSQL 里直接存向量!
);

-- 插入
INSERT INTO documents (content, embedding)
VALUES ('文档内容', '[0.1, 0.2, 0.3, ...]');

-- 创建索引
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);

-- 查询(SQL!)
SELECT content, 1 - (embedding <=> '[0.1, 0.2, ...]') AS similarity
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'
LIMIT 10;

最适合:已经用 PostgreSQL 的团队,想加向量检索但不想引入新的基础设施。

局限性

  • HNSW 索引构建时锁表(PG 的固有限制)
  • 性能不如专用向量数据库(但 pgvector 在 0.6+ 大幅改进)
  • 没有分布式/分片支持(PG 本身的分片方案复杂)

4.6 Weaviate——“全自动”的向量数据库 🤖

1
2
3
4
架构:云原生
语言:Go
许可证:BSD-3
GitHub Stars:~12k

Weaviate 的最大特色是它的”自动向量化”——你不需要自己写 Embedding 逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import weaviate

client = weaviate.Client("http://localhost:8080")

# 什么都不用管,直接存入文本
client.data_object.create(
data_object={"content": "这是一篇关于 AI 的文章"},
class_name="Document",
# Weaviate 自动调 Embedding 服务把文本转成向量!
)

# GraphQL 查询(Weaviate 用 GraphQL 而不是 REST)
response = client.query.get(
"Document", ["content"]
).with_near_text({
"concepts": ["人工智能"] # 自动把查询文本转成向量
}).with_limit(10).do()

特色功能

  • 自动向量化:内置集成 OpenAI、Cohere、HuggingFace 等 Embedding 服务
  • GraphQL API:比 REST 更灵活的查询方式
  • Hybrid Search(混合检索):一行命令即可开启向量 + BM25
  • 多租户:生产级的多用户隔离

不足

  • 上手比 ChromaDB 复杂
  • Java/Python 二重生态,有些混乱
  • 资源消耗较高(Java 基因)

4.7 选型速查表 🗺️

你的需求 推荐 理由
个人学习/原型 ChromaDB / LanceDB 最简洁,零配置
中小项目上线 Qdrant 性能好,部署简单
大厂/亿级数据 Milvus 唯一能打的分布式方案
已有 PostgreSQL pgvector 不需要新服务
需要自动 Embedding Weaviate 内置集成各种模型
客户端/JS 应用 LanceDB WASM/JS SDK
多模态数据 LanceDB / Weaviate 原生支持多向量列
混合检索优先 Qdrant / Weaviate 过滤能力最强
极致性价比 LanceDB / Qdrant 单机就能跑得很好

4.8 一张表看清所有差异 📊

特性 Milvus Qdrant LanceDB ChromaDB pgvector Weaviate
语言 Go+C++ Rust Rust Python C Go
架构 分布式 单机+分布式 嵌入式 嵌入式 PG 扩展 分布式
最小部署 Docker Compose 单容器 pip install pip install CREATE EXTENSION Docker
索引 10+ 种 HNSW IVF_PQ/DiskANN HNSW HNSW/IVF HNSW
过滤 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
混合检索 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
磁盘索引 ✅ DiskANN ✅ MMap ✅ DiskANN
量化 PQ/SQ SQ/BQ PQ PQ/BQ/SQ
10万 QPS ❌(嵌入) ❌(嵌入) ⚠️
亿级数据 ⚠️ ⚠️
GPU 加速
多模态 ⚠️

5. 性能调优实战手册 ⚡

5.1 索引参数调优指南

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
场景 1:追求极致召回率(>99%)
─────────────────────────────────
HNSW: M=64, ef_construction=500, ef_search=200
IVF: nlist=4096, nprobe=128
代价:内存增加约 3 倍,查询慢约 2 倍

场景 2:追求极致 QPS(>10000)
─────────────────────────────────
HNSW: M=8, ef_construction=100, ef_search=20
IVF: nlist=256, nprobe=4
代价:召回率可能降到 85-90%

场景 3:平衡之选(推荐默认)✨
─────────────────────────────────
HNSW: M=16, ef_construction=200, ef_search=50
IVF: nlist=1024, nprobe=16
召回率 ~95%,查询 ~3ms,内存适中

5.2 向量化优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1. 批量生成 Embedding(而非逐条)
# ❌ 坏做法——每次 encode 一条
for doc in documents:
embedding = model.encode(doc) # GPU 利用率极低
db.insert(embedding)

# ✅ 好做法——批量 encode
batch_size = 256
for i in range(0, len(documents), batch_size):
batch = documents[i:i+batch_size]
embeddings = model.encode(batch, show_progress_bar=False)
db.insert_batch(embeddings, batch)

# 2. 向量归一化(让内积 = 余弦相似度)
# 大多数 Embedding 模型输出未归一化,需要:
import torch.nn.functional as F
normalized_emb = F.normalize(torch.tensor(emb), p=2, dim=-1).numpy()

# 3. 使用 FP16 半精度(精度损失可忽略,内存减半)
model = SentenceTransformer("BAAI/bge-m3", device="cuda")
model.half() # float32 → float16

5.3 常见性能瓶颈及解决方案

症状 可能原因 解决方案
查询慢(>100ms) 数据量超过内存 启用量化( PQ/SQ )或 DiskANN
插入慢 索引实时更新 批量插入后再建索引
内存爆炸 HNSW M 太大 降到 M=12,或换 IVF+PQ
召回率低(<80%) ef_search 太小 增大到 100+
过滤后结果少 过滤条件太紧 先向量搜索,再过滤 (pre-filter → post-filter)
首次查询慢 索引未加载到内存 预热(warmup)查询

5.4 成本优化建议 💰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
方案 A:全内存(HNSW,不做量化)
内存 = 向量数 × 维度 × 4 字节 × M 因子(~1.2)
1000万 × 1024 维 → 约 46 GB
→ 适合预算充足、追求极致性能的团队

方案 B:量化(IVF+PQ)
内存 = 向量数 × (压缩后每向量大小)
1000万 × 64 字节(PQ) → 约 640 MB
→ 适合大多数项目

方案 C:磁盘优先(DiskANN / MMap)
内存 = 图索引部分(~20%数据) + 热点缓存
1000万 × 1024 维 → 约 10 GB 内存 + 46 GB SSD
→ 适合预算敏感的大数据量场景

6. 新兴趋势与 2026 展望 🔮

6.1 Serverless 向量数据库

LanceDB 开创的”嵌入式”模式正在变成潮流。2025-2026 年我们看到:

  • 越来越多的向量数据库提供”embedded mode”
  • 向量数据库正在”SQLite 化”——从重型服务走向轻量库

6.2 多向量支持

传统模式:一个数据对象 → 一个向量
新模式:一个数据对象 → 多个向量(不同视角/模态)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 多向量表示例
item = {
"id": "product_12345",
"title": "超级好用的蓝牙耳机",
"text_embedding": [0.1, 0.2, ...], # 文本语义向量
"image_embedding": [-0.3, 0.5, ...], # 图片视觉向量
"audio_embedding": [0.8, -0.1, ...], # 音频向量(如果有)
"late_interaction_embedding": [ # ColBERT 风格的多向量
[0.1, 0.2, ...], # token 1
[0.3, 0.4, ...], # token 2
...
]
}
# 一条记录有多个向量,检索时可以同时考虑

ColBERT / ColPali 的 Token 级多向量

这是一个颠覆性的方向——不再把整段文本压缩成一个向量,而是每个 token 一个向量。检索时做细粒度的 token 级别匹配:

1
2
3
4
5
传统:文档 → [一个向量]
ColBERT:文档 → [[token1向量], [token2向量], [token3向量], ...]

优势:精确度大幅提升("MaxSim" 操作逐 token 计算最大相似度)
代价:存储增加 token_count 倍

6.3 向量数据库 + 全文检索的融合

2025 年的趋势是一个数据库同时搞定三种检索

Elasticsearch 8.x 已经支持向量检索、Qdrant 增强了全文索引、Weaviate 的 hybrid search 越来越成熟。“All-in-One 检索数据库”的时代正在到来。

6.4 GPU 向量检索

2025 年 NVIDIA 的 cuVS(CUDA Vector Search)库可以直接在 GPU 上做向量检索:

1
2
3
4
传统:Embedding 在 GPU → 拷回 CPU → 向量数据库 CPU 检索
cuVS:Embedding 在 GPU → 直接在 GPU 上检索 → 无需拷贝!

性能提升:10-50x 加速(特别是在高维向量场景)

Milvus 2.4+ 已支持 GPU 索引,Qdrant 也在实验 GPU 加速。

6.5 边缘向量数据库

2026 年值得关注的方向:在手机、浏览器、IoT 设备上跑的向量数据库。

  • LanceDB WASM:直接在浏览器里跑向量检索
  • ChromaDB light:Python 轻量版
  • SQLite-vec:SQLite 的向量扩展(纯 C 实现,可在任何平台编译)

想象一下:你的手机相册可以本地做”语义搜索”,不需要上传任何数据到云端。🔒


7. 动手实验:对比三个向量数据库 🧪

让我们用同一份数据对比 ChromaDB、Qdrant 和 LanceDB 的性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""
向量数据库 Benchmark
对比 ChromaDB / Qdrant / LanceDB 的插入和查询性能
"""
import time
import numpy as np
from typing import List, Tuple

# ─── 生成 10 万个 1024 维向量 ───
N = 100_000
DIM = 1024

np.random.seed(42)
train_vectors = np.random.randn(N, DIM).astype(np.float32)
# 归一化
train_vectors = train_vectors / np.linalg.norm(train_vectors, axis=1, keepdims=True)
query_vectors = np.random.randn(100, DIM).astype(np.float32)
query_vectors = query_vectors / np.linalg.norm(query_vectors, axis=1, keepdims=True)

# ─── 1. ChromaDB ───
import chromadb

chroma_client = chromadb.PersistentClient(path="./bench_chroma")
chroma_collection = chroma_client.get_or_create_collection(
"bench", metadata={"hnsw:space": "cosine"}
)

# 插入
t0 = time.time()
batch_size = 5000
for i in range(0, N, batch_size):
batch = train_vectors[i:i+batch_size]
chroma_collection.add(
embeddings=batch.tolist(),
ids=[f"vec_{j}" for j in range(i, min(i+batch_size, N))]
)
chroma_insert_time = time.time() - t0

# 查询
t0 = time.time()
for qv in query_vectors:
chroma_collection.query(query_embeddings=[qv.tolist()], n_results=10)
chroma_query_time = time.time() - t0

# ─── 2. LanceDB ───
import lancedb

lance_db = lancedb.connect("./bench_lancedb")
# 准备数据
data = [{"vector": train_vectors[i].tolist(), "id": i} for i in range(N)]
t0 = time.time()
lance_table = lance_db.create_table("bench", data=data, mode="overwrite")
lance_insert_time = time.time() - t0

# 创建索引
t0 = time.time()
lance_table.create_index(
metric="cosine",
num_partitions=256,
num_sub_vectors=64
)
lance_index_time = time.time() - t0

# 查询
t0 = time.time()
for qv in query_vectors:
lance_table.search(qv.tolist()).limit(10).to_list()
lance_query_time = time.time() - t0

# ─── 3. Qdrant ───
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct

qdrant_client = QdrantClient("localhost", port=6333)

# 删除旧集合
try:
qdrant_client.delete_collection("bench")
except:
pass

qdrant_client.create_collection(
"bench",
vectors_config=VectorParams(size=DIM, distance=Distance.COSINE)
)

# 插入
t0 = time.time()
points = [
PointStruct(
id=i,
vector=train_vectors[i].tolist()
)
for i in range(N)
]
qdrant_client.upsert("bench", points)
qdrant_insert_time = time.time() - t0

# 创建索引
t0 = time.time()
qdrant_client.create_payload_index(
"bench", field_name="id", field_schema="integer"
)
qdrant_index_time = time.time() - t0

# 查询
t0 = time.time()
for qv in query_vectors:
qdrant_client.search("bench", query_vector=qv.tolist(), limit=10)
qdrant_query_time = time.time() - t0

# ─── 结果汇总 ───
print(f"""
╔══════════════════════════════════════════╗
║ 向量数据库 Benchmark: {N:,} × {DIM}维 ║
╠══════════════════╦══════════╦═══════════╣
║ 数据库 ║ 插入耗时 ║ 查询耗时 ║
╠══════════════════╬══════════╬═══════════╣
║ ChromaDB ║ {chroma_insert_time:6.1f}s ║ {chroma_query_time:7.2f}s ║
║ LanceDB ║ {lance_insert_time:6.1f}s ║ {lance_query_time:7.2f}s ║
║ Qdrant ║ {qdrant_insert_time:6.1f}s ║ {qdrant_query_time:7.2f}s ║
╚══════════════════╩══════════╩═══════════╝

注:LanceDB 索引构建耗时: {lance_index_time:.1f}s
Qdrant 索引构建耗时: {qdrant_index_time:.1f}s
(ChromaDB 索引为自动构建,无单独计时)

预期典型结果(10万×1024维):
· 插入:Qdrant ~1s < LanceDB ~2s < ChromaDB ~5s
· 查询:Qdrant ~0.5s < LanceDB ~1s < ChromaDB ~2s
""")

8. 总结:向量数据库的”道”与”术” 🎯

道(原则)

  1. 没有”最好的”向量数据库,只有”最适合当前场景”的
  2. 先想清楚你的数据量和查询模式,再选数据库
  3. ANN 的本质是”用精度换速度”,99% 的场景不需要 100% 精确
  4. 向量数据库是基础设施,别在应用层做向量存储

术(实践)

  1. 学习阶段 → ChromaDB / LanceDB
  2. 项目上线 → Qdrant(单机性能王者)
  3. 规模化 → Milvus(分布式唯一选择)
  4. PG 生态 → pgvector(足够了)
  5. 多模态 → LanceDB(理念最先进)

我的推荐链路

1
2
3
4
5
6
7
新手 → ChromaDB(3 天上手)

要上线了 → Qdrant(迁移代价小)

数据量爆了 → Milvus(专业的分布式方案)

成本砍不下去了 → LanceDB(Serverless + 磁盘索引)

感谢你看到这里!向量数据库的世界还在快速演进,本文会持续更新。如果你在选型中遇到困惑,欢迎评论区交流~ (◕‿◕✿)

觉得有用就点个赞吧,如果能帮到更多人那就太好啦 (๑˃̵ᴗ˂̵)و

参考资料 📚

  1. Malkov & Yashunin - Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs (HNSW 原始论文) — 2016
  2. Jégou et al. - Product Quantization for Nearest Neighbor Search — 2011
  3. Microsoft - DiskANN: Fast Accurate Billion-point Nearest Neighbor Search on a Single Node — NeurIPS 2019
  4. Milvus 官方文档
  5. Qdrant 官方文档
  6. LanceDB 官方文档
  7. ChromaDB 官方文档
  8. pgvector GitHub
  9. NVIDIA cuVS — GPU 向量检索库
  10. MTEB Leaderboard — Embedding 模型排行榜