Skip to content

向量数据库

RAG 系统里的检索质量,有一半取决于你怎么存数据、用什么索引方式检索。这是向量数据库要解决的问题。

这页接着 Embedding 与向量检索 往下讲,关注工程选型和实际使用中的判断点,不重复向量检索的基础原理。

为什么不直接用普通数据库存向量

从技术上讲,你可以把向量存在 PostgreSQL 里,配合 pgvector 插件也能做近似查询。对于小规模项目,这完全够用。

向量数据库(专用的)解决的是规模问题:

  • 当你有几百万甚至几亿条向量时,普通数据库的近似最近邻(ANN)查询速度会急剧下降
  • 向量数据库对 ANN 算法做了深度优化,在大规模下能在几十毫秒内返回结果
  • 支持多种过滤条件(metadata filter)和批量操作的效率也更高

所以第一个判断是:你的规模到了需要专用向量数据库的程度了吗?

1-2 万条文档,先用 pgvector 或者 Chroma(本地)就够了。几百万条,才需要认真考虑选型。

主流选型对比

工具适用场景部署方式特点
Chroma本地开发、原型本地进程 / Docker最易上手,无需注册,适合快速验证
pgvector已有 PostgreSQL 的项目现有 PG 加扩展不引入新系统,运维成本低
Qdrant生产环境自建Docker / 云服务高性能,Rust 实现,过滤功能强
Weaviate需要混合检索(向量 + 关键词)Docker / 云服务内置 BM25 支持,schema 更灵活
Pinecone不想自运维纯托管服务全托管,开箱即用,按量付费
Milvus超大规模,需要水平扩展分布式部署生产级,但运维成本高

选型时主要关注三个问题:

  1. 你需要自建还是托管?(成本 vs 控制权)
  2. 数据规模在什么量级?
  3. 需不需要向量 + 关键词的混合检索?

索引算法:HNSW vs IVF

这两种是向量数据库最常用的 ANN 索引,了解它们的差异,能帮你理解为什么不同场景下性能表现不一样。

HNSW(Hierarchical Navigable Small World)

多层图结构,查询时从顶层快速锁定候选区域,逐层细化。

  • 查询速度快,召回率高
  • 构建索引慢,占用内存多
  • 适合读多写少的场景(建完库之后主要是查询)

HNSW 为什么快,关键在“图是怎么建的”。你可以把每个向量想成图里的一个点,点和点之间连接的是“语义上比较近的邻居”。查询时不再全量扫描所有点,而是从某个入口点开始,沿着更接近 query 的邻居一路走过去。

HNSW 里的 H 是 hierarchical,表示它不是只有一张图,而是多层图:

  • 上层图节点少、边更长,负责快速跳到大概区域
  • 下层图节点多、边更密,负责在局部范围内精细搜索
  • 插入新向量时,会为它选择若干近邻并建立边
  • 查询时从最高层入口开始,逐层下降,最终在底层找候选集合

这有点像城市导航。先走高速路接近目标城区,再走主干道,最后进小路。高速路上的路口少,所以移动快;小路更密,适合最后找具体位置。

HNSW 常见参数也和这个过程有关:

参数影响
M每个节点最多连接多少邻居,越大召回越好,内存越高
efConstruction建索引时搜索多少候选邻居,越大索引质量越好,构建越慢
efSearch查询时保留多少候选,越大召回越好,延迟越高

如果你只记一句:HNSW 是用更多内存和更慢构建,换更快查询和更高召回。

IVF(Inverted File Index)

把向量空间划分为若干个簇,查询时只在最近的几个簇里找。

  • 构建快,内存占用少
  • 查询时 recall 率取决于扫描多少个簇(nprobe 参数)
  • 适合数据量大、内存有限的场景

大多数向量数据库默认用 HNSW,对于大多数 RAG 项目直接用默认值就好,不需要手动调。

LSH:用哈希近似相似度

LSH(Locality Sensitive Hashing,局部敏感哈希)的思路是:相似的向量经过哈希后,更大概率落到同一个桶里。查询时先找同桶或相邻桶的候选,再在候选里算精确距离。

普通哈希追求“均匀打散”,LSH 追求“相似对象尽量撞到一起”。这点正好相反。

一个简单直觉是随机超平面哈希:

  1. 随机生成几条超平面
  2. 看向量落在每条超平面的哪一侧
  3. 每一侧记作 0 或 1
  4. 多个 0/1 组成一个哈希签名

方向相近的向量,在这些随机超平面上的 0/1 结果更容易相同。它们就会被分到相同或相近的桶。

LSH 的优点是实现相对简单,适合高维近似搜索的入门理解;缺点是召回和参数很敏感,现代向量数据库的文本 RAG 场景里,HNSW 更常见。面试里问 LSH,通常是考你是否理解 ANN 不只有图索引,也可以用哈希做候选缩小。

HNSW、IVF、LSH 怎么选

别把索引算法当成孤立知识点,选型要回到数据量、内存和更新频率:

算法更适合主要代价
HNSW中大规模、读多写少、追求高召回内存占用高,构建慢
IVF超大规模、内存压力明显、可接受调参需要训练聚类,召回依赖 nprobe
LSH需要快速缩小候选、对召回要求没那么极致参数敏感,文本语义检索中不如 HNSW 常用

在应用开发里,你更多是在调数据库参数,而不是自己实现索引。但知道这些原理,可以帮你判断“为什么加大 efSearch 后召回上去了,但 P95 延迟也变差了”。

实际工程链路

一次典型的 RAG 建库和检索流程:

python
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Qdrant
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams

# 初始化客户端
client = QdrantClient(url="http://localhost:6333")

# 创建集合(指定向量维度和距离函数)
client.create_collection(
    collection_name="docs",
    vectors_config=VectorParams(size=1536, distance=Distance.COSINE)
)

# 初始化 LangChain 向量库
embeddings = OpenAIEmbeddings()
vectorstore = Qdrant(
    client=client,
    collection_name="docs",
    embeddings=embeddings
)

# 存入文档
texts = ["文档内容一", "文档内容二"]
metadatas = [{"source": "doc1.pdf", "page": 1}, {"source": "doc1.pdf", "page": 2}]
vectorstore.add_texts(texts, metadatas=metadatas)

# 检索(带 metadata 过滤)
from qdrant_client.models import Filter, FieldCondition, MatchValue

results = vectorstore.similarity_search_with_score(
    query="用户的问题",
    k=5,
    filter={"source": "doc1.pdf"}  # 只在特定文档里检索
)

混合检索:BM25 + 向量

纯向量检索擅长语义相似,但对精确词不一定敏感。比如用户问 ERR_AUTH_401、订单号、函数名、法规条款编号,关键词检索可能比向量更可靠。

混合检索会同时跑两路:

  • dense retrieval:用 embedding 找语义相近文档
  • sparse retrieval:用 BM25 或关键词匹配找词面相关文档

最后把两路结果合并排序。生产系统里可以用 Weaviate、Elasticsearch + 向量插件、Qdrant sparse vector,或者自己做结果融合。下面是一个最小 Python demo,用标准库模拟“词面分数 + 向量分数”的融合:

python
import math
from collections import Counter

docs = [
    {"id": "a", "text": "ERR_AUTH_401 表示用户未登录或 token 过期"},
    {"id": "b", "text": "登录接口会签发 access token 和 refresh token"},
    {"id": "c", "text": "RAG 系统会先检索文档,再把上下文交给模型"},
]

# 为了让示例能直接运行,这里用手写向量代替 embedding API。
vectors = {
    "a": [0.9, 0.1, 0.0],
    "b": [0.7, 0.3, 0.0],
    "c": [0.0, 0.1, 0.9],
}
query_vector = [0.8, 0.2, 0.0]
query = "ERR_AUTH_401 token 过期怎么办"


def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    norm_a = math.sqrt(sum(x * x for x in a))
    norm_b = math.sqrt(sum(y * y for y in b))
    return dot / (norm_a * norm_b)


def keyword_score(query_text, doc_text):
    query_terms = Counter(query_text.lower().split())
    doc_terms = Counter(doc_text.lower().split())
    return sum(min(doc_terms[t], c) for t, c in query_terms.items())


ranked = []
for doc in docs:
    dense = cosine(query_vector, vectors[doc["id"]])
    sparse = keyword_score(query, doc["text"])
    score = 0.7 * dense + 0.3 * sparse
    ranked.append((score, dense, sparse, doc["id"], doc["text"]))

for item in sorted(ranked, reverse=True):
    print(item)

真实 BM25 会考虑词频、逆文档频率和文档长度,不会像这里这么粗糙。这个示例想表达的是融合方式:两路召回可以先各自打分,再归一化、加权或用 RRF(Reciprocal Rank Fusion)合并。

混合检索适合这些问题:

  • 用户输入里有错误码、ID、专有名词、函数名
  • 文档里有大量表格、参数名、配置项
  • 向量检索能找到语义相近内容,但经常漏掉精确条款

如果你的 RAG 系统经常“意思找对了,但关键编号没对上”,优先试混合检索。

关于 Distance Metric

三种常用距离函数:

  • Cosine Similarity:最常用,适合文本语义检索,对向量长度不敏感
  • Dot Product:速度稍快,但向量必须归一化,否则结果会偏向长度大的向量
  • L2(欧氏距离):适合图像等需要感知距离大小的场景,文本检索用得少

如果你用 OpenAI 的 Embedding API,用 Cosine 就好,不用纠结。

几个容易忽视的工程细节

向量维度必须在建库时确定

不同 Embedding 模型的输出维度不同(OpenAI text-embedding-3-small 是 1536,有些开源模型是 768)。建库之后不能直接换模型,换了就得重新建库。

metadata 过滤是高频需求

实际项目里经常需要"只在用户有权限的文档里检索"或者"只检索某个时间段的内容"。这依赖向量数据库的 metadata filter 功能,选型时要确认支持。

数据更新的处理

文档更新时,对应的向量需要同步更新。如果用 ID 管理每个 chunk,更新时先 delete 旧向量再 insert 新的,比全量重建库更高效。

embedding 的成本

每次建库或者批量更新文档时都要调用 embedding API,如果文档量大,成本不可忽视。可以在本地缓存 embedding 结果,减少重复调用。

向量量化:为什么能省内存

向量数据库最大的成本之一是内存。假设你有 1000 万条向量,每条 1536 维,用 float32 存储,单条大约 6KB,只存向量就接近 60GB,还没算索引结构和 metadata。

量化的思路是:用更少的比特表示近似向量,牺牲一点精度,换内存和吞吐。

Scalar Quantization

Scalar Quantization 会把每个浮点数压缩成更小的数值类型。比如把 float32 压成 int8,单个维度从 4 字节变成 1 字节。内存大约能降到四分之一。

代价是距离计算不再完全精确,召回可能略降。对 RAG 来说,如果召回下降很小,但内存省了很多,通常值得考虑。

Product Quantization

PQ(Product Quantization)更进一步。它会把一个长向量切成多个子向量,每个子向量用一个聚类中心编号表示。原本要存一串浮点数,现在只需要存几个 code。

一个简化例子:

text
原向量:[0.12, 0.98, 0.33, 0.41, 0.77, 0.20, 0.18, 0.63]
切成 4 段:[[0.12, 0.98], [0.33, 0.41], [0.77, 0.20], [0.18, 0.63]]
每段找到最近的聚类中心,用编号表示:[7, 2, 9, 4]

查询时可以用预计算表快速估算距离,不必还原完整向量。PQ 常见于超大规模检索,尤其是你需要把索引压到有限内存里时。

量化不是免费的。它会引入近似误差,可能影响排序质量。上线前要用固定评测集比较量化前后的 recall@k、答案命中率和延迟。不要只看内存从 60GB 降到 20GB,就默认检索效果还能接受。


接下来是 RAG 进阶优化,讲在数据已经存进向量库之后,怎么提升检索质量。

常见面试考点

向量数据库的题目通常会从“为什么需要它”一路追到索引和工程细节:

  1. ANN 原理:近似最近邻不是全量精确搜索,而是用图、聚类或哈希缩小候选集合,在召回和延迟之间做权衡。
  2. HNSW / IVF / LSH 对比:HNSW 用多层近邻图,查询快但内存高;IVF 先聚类再扫部分簇,适合大规模;LSH 用局部敏感哈希让相似向量落到相近桶。
  3. Distance Metric:文本语义检索常用 Cosine,Dot Product 需要注意归一化,L2 在图像等距离敏感场景更常见。
  4. 混合检索:BM25 或关键词检索补足向量检索对错误码、ID、专有名词不敏感的问题,结果可用加权或 RRF 融合。
  5. 向量量化:Scalar Quantization 和 PQ 用精度换内存,适合大规模索引,但要评估 recall@k 和最终答案质量。
  6. 选型判断:小规模可以先用 pgvector 或 Chroma;需要高性能过滤、托管、混合检索或分布式扩展时,再考虑 Qdrant、Weaviate、Pinecone、Milvus 等方案。

面向开发者的 AI 实战路线——Vibe Coding 与 AI 应用开发