向量数据库:让嵌入们去开派对的地方

技术选型会开到下午四点,所有人都昏昏欲睡。小王在屏幕上的架构图里写了“向量数据库选型”六个字,老李立刻来劲了。

“停停停,”老李用手指敲着桌面,“我们不是已经有 PostgreSQL 了吗?pgvector 插件一装,向量存进去,一样查。搞什么专门的向量数据库?不就是又一个 MongoDB 吗,炒作几年然后大家发现还是关系数据库靠谱。”

“老李你想啊——”

“我想什么想,”老李拧开保温杯,“我们以前没有这向量那向量的,MySQL 一张表跑天下,不也好好的?pgvector 我都调研过了,支持 IVFFlat 索引,查向量快得很。你这又要引入一个新的数据库,运维谁搞?备份谁做?出了事谁半夜起来修?”

小王不慌不忙,把笔记本接上投影仪,打开了一个 benchmark 脚本。

“老李,我问你一个简单的问题。B+ 树为什么快?”

老李哼了一声:“考我数据库原理?因为它是平衡多路查找树,叶子节点有序链表,范围查询直接扫——我背得比你熟。”

“那 B+ 树面对向量检索的时候,能干嘛?”

老李张了张嘴,保温杯停在半空。


当 B+ 树遇见 1536 维

“B+ 树之所以快,是因为它能按大小排序。小于某值的走左边,大于的走右边,一次砍掉一半数据。”小王在白板上画了一棵平衡树。

“但向量呢?向量是一串 1536 维的数字。两个向量之间,谁大谁小?[0.8, 0.1, 0.2][0.6, 0.5, 0.3] 大还是小?”

老李愣了两秒:“比不了。”

“对!一维数据可以排序,二维数据可以按距离排,三维也能建空间索引。但 1536 维呢?高维空间里所有点之间的距离都差不多,这个现象叫‘维度诅咒’。B+ 树在三维空间里就已经开始吃力了,到了 1536 维——它基本就是随机散步。”

小王把笔记本上的 benchmark 跑了起来:

python
import time
import numpy as np
from pgvector.psycopg2 import register_vector
import psycopg2

# 准备工作:生成 10 万条 1536 维向量,插入 pgvector
# (初始化代码省略,假设表已有 10 万行)

conn = psycopg2.connect("dbname=test user=postgres")
register_vector(conn)
query_vec = np.random.randn(1536).tolist()

# pgvector 精确搜索:遍历所有行算余弦
start = time.time()
cur = conn.cursor()
cur.execute("""
    SELECT id FROM embeddings 
    ORDER BY embedding <=> %s::vector 
    LIMIT 10
""", (query_vec,))
results = cur.fetchall()
print(f"pgvector 精确搜索 10万条: {time.time() - start:.3f} 秒")
# 输出大概 0.5~1.2 秒,数据越多越慢

“10 万条,一次精确搜索要半秒到一秒。如果是 100 万条,那就是五秒以上。而你想想,每次用户提问,RAG 都要做一次检索——五秒的延迟,用户早关页面了。”

老李眉头一皱:“那 pgvector 不是有索引吗?IVFFlat 什么的?”

“IVFFlat 就是用了近似最近邻算法,它就是在向‘专用向量数据库’靠拢。但它的索引构建、内存管理、并行检索,跟专门做这件事的数据库比起来,还是差一截。”


用社交网络理解 ANNS

“那专用向量数据库到底快在哪儿?”老李追问。

“因为他们很诚实,”小王笑了,“他们知道自己不可能在 1536 维空间里找精确的‘最近’,所以干脆不找精确的。他们找近似的——ANNS,近似最近邻搜索。”

“近似?那不就不准了?”

“老李你想啊,你找‘最相似的文档’,排第一和排第二有本质区别吗?语义检索本来就是模糊匹配,近似算法返回的前 10 条,跟精确算法返回的前 10 条,重合率通常超过 99%。但你换来的速度是百倍甚至千倍。”

小王打开白板,画了一堆散点和几条线:

图:ANNS 的思想——不遍历所有人,而是顺着图结构快速跳向目标

“最主流的 ANNS 算法叫 HNSW——分层可导航小世界图。听着吓人,其实就是给向量们建一个社交网络。”

老李的眉毛抬了起来:“社交网络?”

“对。你想象公司里所有人是一个社交网络。新人小王入职,想找到‘跟自己技能最相似的人’,他不用一个一个问全公司的人。他先问老李——老李认识的人多,是社交网络里的‘枢纽节点’。老李说‘你去问问老王吧,他跟你方向近’。老王又把你介绍给老赵。三四步之内,你就能找到公司里跟你最像的那个人。”

小王在白板上画了几层图:

“HNSW 就是把所有向量组织成这种社交网络。每个向量认识几个‘邻居’,其中有几个是跨区域的‘社交达人’。检索的时候,从社交达人入手,顺着邻居关系一步一步靠近目标,每一步都离目标更近。10 万条数据,精确搜索要遍历 10 万次,HNSW 只需要跳二三十步。”

老李若有所思:“所以它不保证找到最像的,但保证在可接受的时间里找到‘够像’的。”

“精准总结。而且你可以在建索引时调参数——是想要更高的召回率,还是更快的速度。这是一个权衡的艺术。”


谁在场上跳舞?主流向量数据库横评

“行了行了,理论我听懂了,”老李拿起记号笔,“你直接告诉我,现在市面上有哪些选择,咱们用什么?”

小王在大屏幕上投出了一张对比表格:

数据库定位部署方式适用规模优缺点
Chroma轻量级,开发友好嵌入式/服务器小到中型上手快,API 简洁,但不适合超大规模
Qdrant高性能,Rust 实现自托管/云中到大型过滤强,速度彪悍,中文社区偏小
Milvus企业级,分布式K8s 部署大型到超大型功能全面,可横向扩展,运维门槛高
Pinecone全托管云服务SaaS中小到大型零运维,按量付费,数据要上云
pgvectorPostgreSQL 扩展随 PG 部署小型已有 PG 时零成本引入,规模大后吃力

“老李你看,选什么取决于咱们的规模、运维能力和合规要求。”

老李盯着表格看了许久:“咱们公司数据不能上公有云,Pinecone 先排除。Milvus 看起来功能强,但咱们团队就五个人,K8s 运维谁搞?”

“所以我建议从 Chroma 开始。它可以直接嵌入 Python 跑,也可以单独部署。当前十万级别的文档,绰绰有余。等咱们数据量上百万了,再迁到 Qdrant 或 Milvus。”

“你这不就是先跑起来再说?”老李狐疑地看着小王。

“这叫务实选型!Chroma 的 API 就几行代码,我给你演示一下——”

python
import chromadb

# Chroma 三步上手:创建、入库、检索
client = chromadb.PersistentClient(path="./kb_store")
collection = client.get_or_create_collection(
    "company_docs",
    metadata={"description": "公司知识库向量存储"}
)

# 入库:文档向量化由 Chroma 内置 embedding 自动完成
collection.add(
    documents=["2025年会于11月8日在杭州举行", "数据库迁移POC下周启动"],
    ids=["doc_001", "doc_002"]
)

# 检索:返回最相关的 2 条
results = collection.query(query_texts=["去年年会在哪开的?"], n_results=2)
print(results["documents"][0])  
# 输出:['2025年会于11月8日在杭州举行', ...]

“就这几行?”老李看着代码,眼睛瞪得有点大。

“就这几行。embedding 生成、索引构建、持久化、查询——全封装好了。你甚至可以先用 Chroma 做 POC,验证效果后再决定要不要升级到更重的方案。”

老李靠在椅背上,保温杯在手里转了三圈。

“那你刚才说 pgvector 不够用,你是不是已经拿 pgvector 和 Chroma 做过对比了?”

小王嘿嘿一笑,把笔记本屏幕转过来。上面是一张折线图,横轴是数据量,纵轴是查询延迟。pgvector 的线在 10 万条之后像爬山一样上扬,Chroma 的线几乎贴着地平线。

“100 万条向量,pgvector 精确搜索 5.2 秒,开了 IVFFlat 索引后降到 0.8 秒。Chroma 用 HNSW 索引,同样的数据,0.03 秒。”

老李沉默了好一会儿。

“行吧,”他终于开口,“你先用 Chroma 搞个 POC。但我丑话说在前头——要是上了生产三天两头挂,你给我半夜起来修。”

“老李,这就是你认可了?”

“我没说认可。我是说……”老李站起来收拾笔记本,“下不为例啊。一个项目引入一个新数据库,下次再这样我让你把架构图画成清明上河图。”

走到门口,他又停住脚步,像是想起了什么。

“那什么,你刚才说的那个 HNSW 图,它到底怎么确定谁是‘社交达人’?有没有公式?你再给我讲讲,我看看跟跳表是不是一个思路。”

小王看着老李重新坐下的身影,笑着从包里掏出了一本算法导论。窗外的天已经暗了,选型会变成了算法课,保温杯里的枸杞又续了一轮。

分块的艺术:如何把文档切成 AI 爱吃的尺寸
向量嵌入:把文字变成数字的魔法