一种检索器,三种后端:将NumPy搜索功能移植到Chroma、FAISS和Pinecone
将手工实现的NumPy余弦检索器移植到Chroma、FAISS和Pinecone中,了解索引、元数据过滤及持久化功能的作用,以及会破坏相关性的错误。
来源: Vector Databases, Hands-On — From Your numpy Array to Pinecone
最简单的向量存储就是由嵌入向量构成的 NumPy 矩阵,再加上一行运算:chunk_vectors @ q_vec,该运算可计算每个向量块与查询向量的相似度,随后通过排序选出最佳结果。这其实就是一种真正的向量数据库,只不过是极为简化的形式。本指南将采用这种检索方式,并将其应用到 Chroma、FAISS 和 Pinecone 三种实际后端中,这样你就能清楚地了解每种后端带来了哪些功能,各种知名产品(Chroma、FAISS、Qdrant、Weaviate、Milvus、pgvector、Pinecone)分别适用于何种场景,以及哪些错误会悄悄破坏结果的关联性。
本地示例无需 API 密钥,也无需支付任何费用。Pinecone 提供免费的服务器less入门级服务,需要注册并获取密钥,但在撰写本文时无需信用卡,因此也可以免费试用托管版本。该代码假设存在一个名为 vdb.py 的脚本,需逐块运行。
何时仅靠 NumPy 数组不够用
手动实现的检索器会在每次查询时与所有存储的数据块计算余弦相似度。当数据块数量仅为几百个时,操作几乎即时完成;但一旦达到数百万个,速度就会变慢,整个矩阵必须存储在内存中,且进程退出后所有嵌入向量都会丢失。向量数据库解决了这三个问题:
- 大规模下的高速处理能力。它并非对每个向量进行精确的 O(n) 比较,而是构建诸如 HNSW 这样的近似最近邻索引,从而以大约 O(log n) 的时间找到近乎最近的向量。为此需要略微牺牲一些召回率,但能获得极大的速度提升。
- 持久性。向量被存储在磁盘或托管服务器上,因此能够在系统重启后依然存在。
- 元数据与过滤功能。每个向量可以携带诸如
{"source": "doc12", "date": "2026-01"}这样的属性,查询也可以针对这些属性进行限制,例如仅查找来源为doc12的最近向量。
这两种查询方式的差异只需两句话就能概括:
your numpy array: query ──► compare to ALL n vectors ──► sort ──► top-k (exact, slow at scale)
a vector database: query ──► walk HNSW graph ──► ~top-k (approximate, fast, on disk, filterable)
这就是其核心价值所在。后续内容均为实现细节。
你会不断遇到的词汇
- 嵌入向量:用于编码含义的数字列表;此处示例使用的是
all-MiniLM-L6-v2。 - 距离度量:用于衡量相似度的指标。对于文本处理,通常选择 余弦值(即向量之间的夹角);其他可选指标包括 点积和 欧几里得距离(L2距离)。应使用与你的嵌入模型训练时所用的度量方式一致的那个。
- 索引:用于提升搜索速度的结构。大多数数据库的默认索引类型是 HNSW,即可导航图;而 IVF和 PQ则通过不同方式在内存占用与搜索速度之间取得平衡。
- 插入或更新:根据ID插入向量,如果该ID已存在则对其进行更新。
Chroma:最自然的初次升级选择
只需一次安装,Chroma即可在您的Python进程中运行,无需服务器也不需要Docker。这使其成为从NumPy数组升级的最直接途径。可将其与嵌入库一同安装:
pip install chromadb sentence-transformers
下面的脚本使用了一个小型语料库,描述了一款名为 Nimbus 的虚构笔记产品,每篇文档都带有主题标签。它创建了一个内存中的客户端,通过 hnsw:space 打开一个基于余弦距离配置的集合,明确地对文档进行嵌入处理,并为它们添加编号和元数据。接着它嵌入一个问题,查询距离最近的兩篇文档,并输出每篇文档及其对应的距离。请注意 chromadb.Client() 的注释:只需将它替换为 PersistentClient(path=...),即可将数据保存在磁盘上。
# vdb.py
import chromadb
from sentence_transformers import SentenceTransformer
embedder = SentenceTransformer("all-MiniLM-L6-v2")
# Same Nimbus corpus from the RAG tutorial
DOCS = [
"Nimbus Pro costs 8 dollars per month billed annually, with 50 GB storage.",
"Nimbus offers a 30-day refund policy on all paid plans.",
"Nimbus Lite (free) includes 1 GB of storage and up to 50 notes.",
"Nimbus stores notes encrypted at rest with AES-256; E2E encryption is Pro-only.",
"Live chat support is Pro-only, Mon-Fri 9-6 UTC; free users get 48h email support.",
]
metadatas = [{"topic": t} for t in ["pricing","refund","pricing","security","support"]]
client = chromadb.Client() # in-memory; use PersistentClient(path=...) to save to disk
collection = client.create_collection(
"nimbus", metadata={"hnsw:space": "cosine"} # choose the distance metric
)
# Embed and add. (↔ your chunk_vectors = embedder.encode(...))
collection.add(
ids=[f"doc{i}" for i in range(len(DOCS))],
embeddings=embedder.encode(DOCS).tolist(),
documents=DOCS,
metadatas=metadatas,
)
# Query. (↔ your retrieve(): embed query, find nearest)
q = "How much does the paid plan cost?"
res = collection.query(query_embeddings=embedder.encode([q]).tolist(), n_results=2)
for doc, dist in zip(res["documents"][0], res["distances"][0]):
print(f"[{dist:.3f}] {doc}")
将其与手工实现的版本对应起来,从概念上来看并无新意。collection.add取代了文档的编码及矩阵存储过程;collection.query则替代了矩阵乘法、argsort函数以及取前k个元素的操作。Chroma的贡献在于基于相同理念实现了HNSW索引、持久化存储以及元数据支持。
如果传入documents=而不指定embeddings=,Chroma还可以利用内置模型为你完成文本嵌入。如上所述,显式进行嵌入操作可以让相关机制更加清晰,同时让你自行选择模型,这一点很重要,因为添加数据和查询数据时必须使用相同的模型。
基于元数据的过滤
过滤是普通数组所不具备的功能。下一个查询会搜索“数据安全”,但通过where参数将候选项限制在topic为security的向量中:
res = collection.query(
query_embeddings=embedder.encode(["data security"]).tolist(),
n_results=3,
where={"topic": "security"}, # only search vectors tagged security
)
print(res["documents"][0])
在实际应用中,这一功能支撑了诸如“该客户的类似工单”或“今年更新的文档”之类的查询。过滤是与向量搜索一起应用的,而非作为后续处理步骤,因此仍然会返回最多n_results个符合条件的结果。作为练习,将过滤条件改为where={"topic": "pricing"},确认退款相关文档和安全类文档不会被检索出来。
FAISS:一个快速的索引库,而非数据库
来自 Meta 的 FAISS 根本不是数据库,而是一个高度优化的最近邻库,它没有元数据,也没有自己的存储层,旨在嵌入到您的应用程序中。许多其他系统也在内部使用它。请安装 CPU 版本:
pip install faiss-cpu
该代码使用 normalize_embeddings=True 对文档进行编码,并将它们转换为 FAISS 所要求的 float32 类型。接着它创建了一个内积索引 IndexFlatIP;由于向量已被归一化为单位长度,内积值就等于余弦相似度。在添加向量之后,它以相同方式对查询进行编码,然后调用 search 函数获取排名靠前的两个结果,最后将返回的位置映射回原始文档。
import faiss, numpy as np
vectors = embedder.encode(DOCS, normalize_embeddings=True).astype("float32")
index = faiss.IndexFlatIP(vectors.shape[1]) # IP = inner product = cosine on normalized vectors
index.add(vectors) # ↔ exactly your numpy array, but optimized in C++
q = embedder.encode(["refund window?"], normalize_embeddings=True).astype("float32")
scores, idx = index.search(q, k=2) # ↔ your argsort top-k
for s, i in zip(scores[0], idx[0]):
print(f"[{s:.3f}] {DOCS[i]}")
这几乎就是 NumPy 中的检索器:IndexFlatIP 实际上是一种暴力式的余弦搜索,只是用优化过的 C++ 实现的。当用类似 IndexHNSWFlat 或 IndexIVFFlat 这样的近似索引替换它时,就能获得优势——这些索引保持相同的接口,但搜索速度可达亚线性水平。如果你希望在单个进程内实现最高速度,并且愿意自行处理持久化与元数据(例如通过维护位置到文档记录的独立映射),那就选择 FAISS。
Pinecone:完全托管且无服务器
到目前为止的所有操作都在你的自有进程中进行。Pinecone 则是一种可通过网络访问的托管型向量数据库:无需运行任何服务器,且能自动扩展。它适合那些不想自行管理基础设施的团队。
请注意,Pinecone SDK 已发生重大变化。旧的 pinecone.init(api_key=, environment=) 写法已不再存在;现在需要创建一个 Pinecone 客户端对象,且该库的名称为 pinecone,而非 pinecone-client。如果下面有任何操作失败,请查阅当前的 SDK 文档。
该代码块以安装命令开头,随后是Python代码(复制时请将两者分开)。它首先创建一个客户端,仅在不存在名为nimbus的无服务器索引时才创建该索引,设置dimension=384以匹配MiniLM,同时指定metric="cosine",然后连接到该索引。接着它插入或更新(id, vector, metadata)格式的元组,将原始文本存储在metadata中,因为Pinecone只会返回你存储的内容。最后,它通过类似Mongo的$eq过滤器根据topic字段筛选出排名前两位的结果,并输出各自的得分与文本。在实际代码中,应从环境变量或密钥存储库中读取该键值,而非直接写在源代码中。
pip install pinecone
from pinecone import Pinecone, ServerlessSpec
pc = Pinecone(api_key="YOUR_API_KEY") # get a free key at app.pinecone.io
# Create a serverless index (only once). dimension must match your embedder: MiniLM = 384.
if not pc.has_index("nimbus"):
pc.create_index(
name="nimbus",
dimension=384,
metric="cosine",
spec=ServerlessSpec(cloud="aws", region="us-east-1"),
)
index = pc.Index("nimbus")
# Upsert: (id, vector, metadata) tuples
index.upsert(vectors=[
(f"doc{i}", embedder.encode(d).tolist(), {"text": d, "topic": m["topic"]})
for i, (d, m) in enumerate(zip(DOCS, metadatas))
])
# Query with metadata filtering
res = index.query(
vector=embedder.encode("how much is pro?").tolist(),
top_k=2,
include_metadata=True,
filter={"topic": {"$eq": "pricing"}}, # Mongo-style filter operators
)
for match in res["matches"]:
print(f"[{match['score']:.3f}] {match['metadata']['text']}")
其结构与Chroma完全一致:先创建索引,再插入或更新ID、向量及元数据,最后通过可选过滤器查询前k个结果。检索步骤与之前相同,只是现在在API背后的他人托管的自动扩展基础设施上运行。Pinecone还支持命名空间,可用于对单个索引进行分区,以及集成推理功能,该功能会自动为upsert_records和search_records处理文本嵌入工作,从而无需用户自行实现嵌入功能。
更重要的启示是,从Chroma转向Pinecone仅改变了连接和调用存储的方式,而并未改变其背后的RAG逻辑。在结构良好的应用中,向量存储只是一个可替换的组件,这也是为何像LangChain的.as_retriever()这样的框架方法无论后端如何都保持相同接口的原因。
根据需求选择后端
没有一种向量数据库是绝对最好的;只有适合特定需求组合的才是最佳选择。大致如下:
- NumPy数组(手动实现):在进程内运行;适用于向量数量在1万以内的场景,适合学习或需要完全控制的情况。
- Chroma:嵌入式数据库,可在进程内运行或作为本地服务器使用;适合原型开发及中小型应用,是最简单的入门选择。
- FAISS:索引库,在进程内运行;当你自行处理存储和元数据时能实现最高速度。
- pgvector:现有数据库中的Postgres扩展;如果你已经在使用Postgres并且希望将向量数据与关系型数据放在一起管理,这是理想选择。
- Qdrant:数据库,可通过Docker自行托管或使用其云服务;具备强大的过滤功能、Rust语言带来的高性能,为开源产品,支持混合搜索。
人们反复讨论的决策是使用托管服务还是自行部署。像 Pinecone 或其他产品的云版这类托管服务虽然能免除运营工作,但会产生持续的费用,并将数据存储在供应商的服务器上。而自行部署 Chroma、Qdrant、Weaviate、Milvus 或 pgvector 则意味着由自己来运行这些系统,同时能够掌控成本和数据存储地点。一个常见的经验法则是:当托管服务的月费达到数百美元时,自行部署才开始具有经济优势;在此之前,为运营这些系统所投入的工程时间成本通常会高于其节省的费用。请将此视为一种经验参考,而非绝对规则,因为团队的运营能力与费用金额同样重要。
对于学习及大多数辅助项目,Chroma或pgvector就已足够。如果团队不想自行管理基础设施,那么Pinecone或Qdrant Cloud是合适选择;而当需要处理数十亿个向量时,Milvus则是最佳方案。如果您还在考虑是否真的需要专用的向量存储,可阅读为何生产环境中的RAG正在摆脱专用向量数据库的束缚。
数据库运行后可能出现的错误
- 维度不匹配是最常见的错误。索引的
dimension值必须与嵌入模型的输出尺寸一致:MiniLM为384,许多OpenAI嵌入模型则为1536。将不同模型的向量混存在同一个索引中必然会导致故障。
ef_search或IVF的nprobe,它们可以在提高召回率的同时增加延迟。如需深入了解这些参数,请参阅为生产环境RAG优化HNSW索引。PersistentClient或基于服务器的数据库来解决此问题。一个不错的练习方法是取一个现有的基于 NumPy 的 RAG 脚本,仅将其检索函数替换为前面展示的 Chroma 集合。生成部分完全无需更改,这表明检索层确实是一个独立且可替换的模块。
故障排除快速参考
维度不匹配: 你的向量尺寸与索引期望的不一致。请使用正确的dimension重新创建索引,或使用构建该索引时所用的嵌入模型。- Chroma 的检索结果看似随机: 可能是评估指标混用了不同类型,或者添加数据和查询数据时使用了不同的嵌入模型。两者必须保持一致。
has_index或create_index函数出现错误:请确认您使用的是当前版本的SDK(from pinecone import Pinecone),而非已废弃的pinecone.init方式。search函数返回-1作为ID:说明索引中存储的向量数量少于k的值。请添加更多数据或降低k的值。核心要点
- 向量数据库是一种经过优化、具备持久性且可过滤功能的检索系统。添加向量包括编码与存储操作,而查询则采用余弦相似度Top-k算法。
- 近似最近邻索引是唯一真正具有创新性的部分:通过接受近似结果,即便数据量不断增加,搜索速度依然保持快速。