技术选型会开到下午四点,所有人都昏昏欲睡。小王在屏幕上的架构图里写了“向量数据库选型”六个字,老李立刻来劲了。
“停停停,”老李用手指敲着桌面,“我们不是已经有 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 跑了起来:
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 | 中小到大型 | 零运维,按量付费,数据要上云 |
| pgvector | PostgreSQL 扩展 | 随 PG 部署 | 小型 | 已有 PG 时零成本引入,规模大后吃力 |
“老李你看,选什么取决于咱们的规模、运维能力和合规要求。”
老李盯着表格看了许久:“咱们公司数据不能上公有云,Pinecone 先排除。Milvus 看起来功能强,但咱们团队就五个人,K8s 运维谁搞?”
“所以我建议从 Chroma 开始。它可以直接嵌入 Python 跑,也可以单独部署。当前十万级别的文档,绰绰有余。等咱们数据量上百万了,再迁到 Qdrant 或 Milvus。”
“你这不就是先跑起来再说?”老李狐疑地看着小王。
“这叫务实选型!Chroma 的 API 就几行代码,我给你演示一下——”
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 图,它到底怎么确定谁是‘社交达人’?有没有公式?你再给我讲讲,我看看跟跳表是不是一个思路。”
小王看着老李重新坐下的身影,笑着从包里掏出了一本算法导论。窗外的天已经暗了,选型会变成了算法课,保温杯里的枸杞又续了一轮。

