向量数据库在最近几年成了检索类应用的基础组件,尤其是 RAG(检索增强生成)类项目出现之后,几乎每个做 AI 应用的团队都会评估一次“要不要引入向量数据库”。很多开发者的第一反应是:传统数据库已经支持模糊查询、全文索引,为什么还要再引入一个专门的存储系统?答案在于:向量数据库解决的不是“关键词匹配”,而是“语义相似度检索”。本文围绕向量数据库的入门路径展开,从核心概念、相似度计算、主流工具选型,到一个可运行的最小案例,最后补充常见的故障场景和工程落地点。读完以后,你可以独立完成一个“文档语义搜索”的本地实验,并对生产环境会遇到的索引、持久化、过滤和选型问题建立基本判断。
1. 先理解为什么需要向量数据库
1.1 传统数据库很难做语义搜索
传统关系型数据库使用 SQL 查询,核心匹配方式是等值、范围、LIKE 和全文索引。LIKE 只能找“字面上相同”的内容,全文索引虽然引入了分词和倒排,但本质上仍然是关键词匹配。用户搜“怎么换门锁”,数据库无法判断“门锁打不开”“锁芯卡住了”“智能锁没反应”这些句子是否和用户问题相关,除非这些句子恰好包含同一个词。
这种场景在电商搜索、客服问答、文档知识库、推荐系统中非常常见。传统的解决方式有两种:一种是人工维护同义词表,另一种是训练文本分类模型。前者维护成本高,后者只能覆盖有限的分类维度,都无法做到对任意一段文本进行实时相关性判断。
1.2 向量和 Embedding 是什么
向量可以简单理解为一串浮点数,比如[0.12, -0.34, 0.56, ...]。如果一段文本、一张图片或一条用户行为记录被模型转换成向量,那么在理想情况下,语义相近的内容会映射到向量空间中距离相近的位置。
“门锁坏了”和“锁打不开了”可能分别被转换成不同的向量,但在向量空间里的距离非常接近。这就是 Embedding 模型的作用:把高维的语义信息压缩成一个固定维度的数值向量。常用的模型包括all-MiniLM-L6-v2、bge-large-zh、OpenAI 的text-embedding-3-small等。
向量数据库最基础的职责就是:存储这些向量,并在给定一个查询向量时,快速返回距离最近的 K 条记录。
1.3 向量数据库解决的核心问题
传统数据库也可以存向量字段,比如用JSON类型保存一个数组,但问题在于查询时无法高效地做相似度排序。从一个有千万行的表里扫描所有向量并逐一计算距离,在性能上是不可接受的。
向量数据库的核心能力有四点:
- 高效索引:使用 HNSW、IVF 等近似最近邻(ANN)索引,把检索范围压缩到一个很小的候选集合。
- 方便存储:每个向量可以附带元数据,查询时可以按标签过滤。
- 增量写入和更新:业务系统可以持续写入新的向量数据,而不需要重建整个库。
- 与 AI 链路集成:很多向量数据库原生支持 embedding 模型,写入文本时自动完成向量化。
一句话概括:传统数据库回答“哪个字段等于什么”,向量数据库回答“哪条记录和输入内容最相似”。
2. 核心概念:从 Collection 到相似度度量
2.1 最常用的几个抽象名词
不同向量数据库的术语略有差异,但核心概念基本一致。以 Chroma 为例:
| 名词 | 类比传统数据库 | 说明 |
|---|---|---|
| Collection | 表 | 一组向量的集合,通常按业务场景划分,例如news_docs、user_profiles |
| Vector | 记录的一列 | 一段文本或对象经过 Embedding 模型转换出的浮点数数组 |
| Metadata | 普通字段 | 附加在向量上的键值对,例如分类、作者、时间 |
| ID | 主键 | 每条向量记录的唯一标识 |
| Query | 查询 | 给一个查询向量,返回最相似的 K 条记录 |
对一个 Collection 的写入过程通常是这样:先确定一条记录的 ID,再写入 metadata,最后写入向量。如果使用默认 embedding function,可以直接传文本,由向量数据库内部完成向量化。
2.2 相似度度量:内积、余弦距离和欧氏距离
向量数据库判定“相似”靠的是距离函数。最常用的有三种:
- 内积(Inner Product,IP):适合浅层语义匹配,特别是当向量已经归一化时。内积值越大表示越相似。
- 余弦距离(Cosine):计算两个向量的夹角余弦值,适合文本语义相似度。它只关心方向,不关心长度,因此对不同长度的文本比较友好。
- 欧氏距离(L2):计算两个向量的直线距离,值越小越相似。适合向量长度本身有业务含义的场景。
实际选哪种,要看你使用的 embedding 模型。有很多模型在训练时已经统一做了归一化,这时内积和余弦距离在排序结果上基本等价。建议先参考模型文档,没有明确说明时默认使用余弦距离。
2.3 ANN 索引为什么重要
假设有 1000 万条向量,每条 384 维。如果每一次查询都做全量暴力计算,大约需要进行 384 亿次浮点运算,单机响应时间会非常难看。
近似最近邻索引的思路是:不保证返回全局最优结果,但保证在极短时间内返回“接近最优”的结果。常用索引包括:
| 索引类型 | 核心思想 | 优点 | 适用场景 |
|---|---|---|---|
| HNSW | 多层跳表结构 + 图遍历 | 查询快、召回高 | 大多数业务场景首选 |
| IVF | 聚类 + 倒排 | 内存占用可控 | 数据量大、对召回要求不极端 |
| PQ | 向量压缩 | 省内存 | 海量向量、内存有限 |
入门阶段不需要深入推导索引原理,但需要记住一个事实:相似度检索结果是近似结果。某些训练好的模型在不同索引下可能返回不同的 K 条数据,这是正常现象。
2.4 主流向量数据库有哪些
热词中提到的 Chroma、Milvus、pgvector、Qdrant 是当前最常被讨论的四个方向。它们不是同一个层级的产品,选型时要先想清楚使用场景:
| 产品 | 运行方式 | 适合阶段 | 主要特征 |
|---|---|---|---|
| Chroma | 嵌入式本地库 / 服务端 | 学习、原型、中小规模 | 安装简单,Python API 友好,支持默认 embedding |
| Qdrant | 服务端 | 中小规模生产 | Rust 实现,性能好,过滤功能强大 |
| Milvus | 分布式服务端 | 大规模生产 | 适合大数据量、高并发,依赖 etcd、对象存储等组件 |
| pgvector | PostgreSQL 扩展 | 已有 PostgreSQL 场景 | 不需要额外数据库,SQL 语法可复用 |
这四个方向在工程上都有大量实践。初学者不建议一开始就扑向分布式方案,先通过嵌入式数据库跑通流程,理解向量检索的基本链路,再去评估生产级组件会轻松很多。
3. 环境准备:用 Chroma 跑通第一步
3.1 为什么入门阶段选 Chroma
Chroma 是一个面向 AI 应用设计的嵌入式向量数据库,安装和启动成本很低。它提供了 Python SDK,甚至在本地不启动独立服务也能运行,非常适合用来学习向量检索流程。
它默认包含一个 embedding 函数,可以直接把文本传入add方法,由数据库内部完成向量化。这样你第一步可以跳过选择模型的复杂环节,先看到“文本进、相似结果出”的完整链路,之后再替换成自己的 embedding 模型。
要注意的是,Chroma 内部默认模型在不同版本中有变化。常见的是基于 MiniLM 的 ONNX 模型,首次运行时会从网络下载模型文件。如果公司网络受限,可能会卡在第一次写入。
3.2 环境要求和安装命令
建议环境如下:
| 项目 | 建议 |
|---|---|
| 操作系统 | macOS / Linux / Windows 均可 |
| Python | 3.9 及以上,具体以官方要求为准 |
| pip | 最新版本 |
| 推荐工具 | Jupyter Notebook 或 VS Code |
安装命令:
pip install chromadb安装完成后,可以先执行一个简短检查:
import chromadb print(chromadb.__version__)如果能正常输出版本号,说明安装成功。这里不需要额外安装数据库服务,Chroma 的客户端在本地运行时会自动创建数据目录。
3.3 第一个 Collection
打开一个新文件,写入以下代码:
import chromadb client = chromadb.PersistentClient(path="./chroma_demo") collection = client.get_or_create_collection( name="news_articles", metadata={"hnsw:space": "cosine"} ) print(collection.count())解释一下关键部分:
PersistentClient(path="./chroma_demo"):指定数据持久化目录,后续重启进程数据仍然存在。如果不传路径,Chroma 默认使用内存模式,进程退出后数据丢失。get_or_create_collection:如果集合不存在则创建,存在则直接获取,避免重复创建报错。metadata={"hnsw:space": "cosine"}:指定向量距离计算方式为余弦距离。这个参数定义在 HNSW 索引的命名空间下,实际由底层索引读取。
运行后输出0,表示这是一个空集合。
4. 最小可运行案例:文档语义搜索
4.1 需求拆解
在正式写代码前,先明确这个最小案例要完成什么任务:
- 准备几条简单的客服问题文本。
- 写入向量数据库,附带分类元数据。
- 输入一条新问题,返回最相似的已有问题。
- 通过元数据过滤,只搜索某个分类下的相似记录。
这个需求是“文档语义搜索”的最简版本,覆盖了写入、查询、过滤三个核心动作。
4.2 演示数据说明
假设有一个客服知识库,里面存了几条历史工单标题:
docs = [ "手机无法开机", "门锁卡住打不开", "电脑蓝屏报错", "门禁卡刷不了", "路由器无法上网", "空调制冷效果差" ]这些文本可以直接交给 Chroma 默认 embedding 函数处理。实际项目中,这些文本可能是一篇文档、一段 FAQ 或一条工单记录。
4.3 写入数据
继续使用之前的collection变量,执行写入:
ids = [str(i) for i in range(len(docs))] metadatas = [ {"category": "hardware"}, {"category": "smart_lock"}, {"category": "pc"}, {"category": "access_control"}, {"category": "network"}, {"category": "appliance"} ] collection.add( ids=ids, documents=docs, metadatas=metadatas ) print(collection.count())这里最关键的是documents参数:当你不显式传入embeddings时,Chroma 会自动调用内置 embedding 函数,先将每段文本变成向量,再写入集合。
元数据和主键不是必选项,但强烈建议写。因为真实项目里每个向量都要关联业务主键,否则搜到了结果却不知道对应哪条业务记录,查询就没有落地价值。
4.4 查询相似文本
写入完成后,执行一次最基础的相似度查询:
results = collection.query( query_texts=["指纹锁识别失败"], n_results=3 ) for idx, doc in enumerate(results["documents"][0]): print(idx, doc)查询文本是“指纹锁识别失败”,它和文档中的“门锁卡住打不开”“门禁卡刷不了”在语义上比较接近,但和“路由器无法上网”可能相关性较低。n_results=3表示返回最相似的 3 条。
可以看到,这里不需要分词,不需要写 SQL,也不需要人工维护关键词。查询过程完全由向量数据库根据语义距离完成。
4.5 带元数据过滤的查询
如果只想在“门锁”相关分类里搜索,可以把元数据过滤条件加进去:
filtered = collection.query( query_texts=["指纹锁识别失败"], n_results=3, where={"category": "smart_lock"} ) print(filtered["documents"])where参数是元数据过滤条件,它的表现比通常想象的更强大一点:它是一个字典结构,可以支持简单的等于判断,某些版本还支持$and、$or等组合条件。具体使用方式要参考对应版本的文档。
4.6 完整代码
把上面步骤合并成一个文件:
import chromadb client = chromadb.PersistentClient(path="./chroma_demo") collection = client.get_or_create_collection( name="news_articles", metadata={"hnsw:space": "cosine"} ) docs = [ "手机无法开机", "门锁卡住打不开", "电脑蓝屏报错", "门禁卡刷不了", "路由器无法上网", "空调制冷效果差" ] metadatas = [ {"category": "hardware"}, {"category": "smart_lock"}, {"category": "pc"}, {"category": "access_control"}, {"category": "network"}, {"category": "appliance"} ] collection.add( ids=[str(i) for i in range(len(docs))], documents=docs, metadatas=metadatas ) results = collection.query( query_texts=["指纹锁识别失败"], n_results=3 ) for idx, doc in enumerate(results["documents"][0]): print(idx, doc)预期输出并不是固定的,取决于内置 embedding 模型的语义判断。通常“门锁卡住打不开”会排在第一或第二位,“门禁卡刷不了”也有较大概率出现在结果中。如果第一轮结果不符合直觉,先不要怀疑程序写错了,可以打印results["distances"]查看相似度分数,判断差距是否明显。
5. 关键细节:向量维度、持久化与参数含义
5.1 显式 embedding 和默认 embedding 的区别
前面代码中,我们没有手动生成向量。这样做的缺点是:看不到真正的向量长什么样,也不清楚维度是多少。
可以查看集合里的向量维度:
collection.get(ids=["0"])返回结果中会有embeddings字段,里面是一个浮点数数组。Chroma 默认模型生成的向量通常是 384 维,这意味着每条文本最终被映射成了一个包含 384 个浮点数的数组。
显式生成向量的方式更贴近生产环境。生产项目中,文本向量通常由独立服务生成,再写入向量数据库,方便统一使用同一个模型:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("all-MiniLM-L6-v2") embeddings = model.encode(docs).tolist() collection.add( ids=[str(i) for i in range(len(docs))], embeddings=embeddings, metadatas=metadatas )查询时也要使用同一个模型生成查询向量:
query_embedding = model.encode(["指纹锁识别失败"]).tolist() results = collection.query( query_embeddings=query_embedding, n_results=3 )关键原则有两条:
- 写入时使用什么模型,查询时也必须使用同一个模型,否则向量空间不一致,相似度没有意义。
- 如果 collection 在创建时已经绑定了 embedding 函数,
add时既传documents又传embeddings,Chroma 会优先使用显式传入的向量。
5.2add与upsert的使用边界
Chroma 的add方法在遇到重复 ID 时会报错。如果要更新已有记录,应该使用upsert:
collection.upsert( ids=["0"], documents=["手机无法开机,尝试重启"], metadatas=[{"category": "hardware"}] )upsert会覆盖已有 ID 的向量和元数据。如果没有该 ID,则插入新记录。实际项目里,文档更新是很常见的动作,建议对更新操作统一走upsert,避免报错中断任务。
5.3 查询返回字段的取舍
前面使用collection.query时没有指定返回字段。默认情况下 Chroma 会返回documents、metadatas、distances等字段。如果你只需要业务主键,可以限制返回字段,减少网络和内存开销:
results = collection.query( query_texts=["指纹锁识别失败"], n_results=3, include=["documents", "distances"] )include参数支持documents、metadatas、distances、embeddings等。生产环境下建议按需选择,不要默认返回embeddings,因为向量对象通常很大,会拖慢响应速度。
5.4 为什么要配置持久化目录
Chroma 使用PersistentClient(path=...)时,数据会被存储在本地的 SQLite 和对应的索引文件中。这个设计对学习者非常友好:不需要启动 Docker,不需要配置网络,数据也能跨进程保留。
但要注意,这个能力只适合本地原型或中小规模场景。生产环境中如果把数据目录放在临时磁盘,服务重启后数据就会消失。需要将数据目录挂载到持久化存储,并做好备份机制。
5.5 初始参数设置常见错误
创建 collection 时最常见的错误是:
- 遗忘
metadata,导致默认距离度量不适合当前业务。 - 使用一个已经存在的 collection,但希望修改距离度量。此时不会生效,因为集合的索引已按旧参数创建,需要删除重建。
- 所有文本都使用同一个向量,导致查询结果全部等距。通常这是模型配置错误,不是数据库问题。
6. 从学习环境走向生产环境
6.1 本地 demo 与生产部署的差异
在学习阶段,一个 Chroma 本地库足以跑通流程。但进入生产环境后,有几道必答题:
| 关注点 | 学习环境 | 生产环境 |
|---|---|---|
| 数据持久化 | 本地目录 | 需要容量规划、备份和恢复 |
| 高可用 | 单进程 | 需要副本和故障切换 |
| 并发能力 | 单客户端 | 需要评估连接数和请求队列 |
| 向量模型 | 默认模型 | 需要独立模型服务,版本化管理 |
| 安全 | 本地文件 | 需要鉴权、网络隔离和数据加密 |
| 监控 | 无 | 需要指标、日志和告警 |
如果你的项目一开始就没有打算引入独立中间件,可以评估 pgvector:它只是 PostgreSQL 的一个扩展,不需要额外维护一套服务,直接利用现有数据库能力。适合数据规模不大、团队不需要新运维组件的场景。
6.2 数据更新和索引重建
向量数据库写入本身很快,但重复更新大量数据时,索引会出现老化,导致查询性能下降。生产环境通常采用两类策略:
- 增量更新:业务发生时实时写入或更新单条记录,适合时效性要求高的场景。
- 批量重建:周期性全量重建集合,适合文档集合相对稳定的场景。
如果需要频繁批量更新,建议先写入到一个临时集合,再切换集合名称,避免在查询链路上执行耗时操作。切换的原子性由业务代码控制,向量数据库本身不一定提供跨集合的原子切换能力。
6.3 生产环境的最小保障清单
在把向量数据库接入生产前,至少确认下面几项:
- 向量模型版本固定,代码里要显式记录模型的名称和版本。
- 每条向量能通过 ID 关联到业务主键,方便回源查详情。
- 对写入失败做补偿,避免部分文档写入失败导致检索不完整。
- 对查询结果做后置过滤,因为向量检索是近似检索,偶尔会返回语义上不太相关的结果。
- 监控查询耗时、写入耗时、集合大小、内存占用。
- 如果使用独立部署的向量数据库,先评估容器镜像版本和客户端 SDK 版本的兼容性。
7. 常见问题:现象、原因和排查路径
7.1 查询结果明显不符合语义
现象:输入“指纹锁识别失败”,返回结果却和门锁毫无关系。
可能原因有三个:
- embedding 模型不适合中文,支持中文能力较弱。
- 写入和查询使用了不同的 embedding 模型。
- 数据量太少,语义相似度分数普遍较低,排序结果不稳定。
排查方式:
先查看results["distances"],判断分数分布。如果所有距离都很大且差距很小,通常是模型对文本区分度不足。可以换用更擅长中文的模型,例如bge-small-zh,或者使用商用 embedding 服务。
7.2 维度不匹配报错
现象:执行查询时出现 embedding dimension mismatch 相关错误。
原因:查询时传入的向量维度和集合内向量维度不一致。
排查方式:
打印集合内已有的向量维度,再打印查询向量的维度:
item = collection.get(ids=["0"]) print(len(item["embeddings"][0])) query_vec = model.encode(["测试"]).tolist() print(len(query_vec))如果两者不一致,确认是否换了模型。注意:即使模型名称相同,不同版本也可能产生不同维度,因此生产环境必须固定模型版本。
7.3 数据重启后消失
现象:重新运行程序后collection.count()为 0。
原因:使用了默认的Client(),没有指定持久化路径。
排查方式:
核对创建客户端的方式,确认使用的是PersistentClient(path=...)。同时确认进程的工作目录是否一致,如果启动目录变了,相对路径会指向不同位置。
7.4 首次写入很慢,像卡住了
现象:执行collection.add时长时间没有返回。
原因:Chroma 内置 embedding 模型首次下载模型文件,网络较慢或网络受限。
解决办法:
- 提前下载模型文件,放到缓存目录。
- 改用显式 embedding 方式,先用独立模型生成向量,再写入 Chroma。
- 使用
Client()内存模式,不涉及持久化时能减少磁盘交互,但首次模型下载仍会发生。
7.5where条件不生效
现象:加了where后,返回结果中仍然包含其他分类的数据。
可能原因:
where字段名和写入时metadatas的键名不一致。- 当前版本不支持复杂的操作符组合。
- 查询条件写在了错误的参数位置。
排查时应先打印collection.get(where={"category": "smart_lock"}),确认过滤是否在“不带相似度排序”的查询下生效。如果这里也查不到数据,说明是元数据写入问题,而不是查询问题。
8. 最佳实践与学习路径
8.1 写向量数据库代码时的四个习惯
第一,所有add操作都带上明确的 ID。很多向量数据库允许自动生成 ID,但如果后续要删除和更新,自动生成的 ID 很难与业务记录对应。
第二,写入和查询的 embedding 模型统一封装成一个函数。不要在多处直接调用模型,因为一旦替换模型,全链路都要同步调整。
第三,不要把查询结果直接当作最终答案。向量检索只是召回阶段,后续应该把命中的文本取出来,再通过排序、重排或规则过滤完成精排。
第四,数据规模增长后第一件要观察的事是延迟。如果查询耗时从毫秒级变成秒级,优先检查索引类型、数据量和资源限制。
8.2 向量数据库选型速查
根据业务阶段做初步判断:
| 项目阶段 | 推荐选择 | 理由 |
|---|---|---|
| 学习原理、写 demo | Chroma | 安装简单、API 友好 |
| 已有 PostgreSQL,数据量不大 | pgvector | 避免引入新组件 |
| 需要稳定服务和丰富过滤 | Qdrant | 单机部署简单,功能完善 |
| 海量数据、高并发、分布式需求 | Milvus | 面向生产级规模设计 |
| 使用云厂商生态 | 各云厂商托管向量数据库 | 运维成本低,但要注意厂商绑定 |
选型没有绝对好坏。最好先在一个小数据集上做基准测试,关注召回质量、写入速度、查询延迟、内存占用和运维成本。
8.3 下一步建议
完成本文的 demo 后,可以从三个方向深入:
- 替换成真实业务数据,加入更完善的元数据过滤和业务主键关联。
- 对比不同 embedding 模型在同一组数据上的查询效果,建立“数据、模型、距离函数”三者配合的体感。
- 将向量检索接入一个简单的问答流程:先用向量库检索相关文档,再把文档片段和用户问题一起交给大语言模型生成答案,这就是一个最简 RAG 原型。
向量数据库本身是一个工具,真正的难点在于:如何评估模型效果、如何组织数据、如何处理检索质量不稳定的情况。这些能力来自反复实验和对指标的敏感度。建议先从小数据量开始,把一条链路彻底跑通,再逐步扩展到更大的规模。