做搜索和推荐这几年,一个特别直观的感受是:传统的关键词匹配越来越不够用了。用户搜“苹果怎么保存”,你拿“苹果”两个字去数据库里做精确匹配,回来的可能是水果价格、产地,甚至是手机参数,唯独没有“怎么保存”这个核心意图。真正要解决这种语义层面的匹配问题,行业里的通用做法是把文本、图片、音视频统一映射成高维向量,再交给向量数据库做近似检索。Chroma就是这类工具里对新手最友好的一个,它轻量、本地就能跑、API 设计简洁,特别适合用来快速验证想法或者搭一个中小规模的语义搜索/知识库原型。这篇文章就围绕 Chroma 从零开始,讲清楚装环境、理解概念、跑通落地全流程,顺带把我在本地安装和实际使用中踩过的坑一并说出来。
1. 先搞清楚:向量数据库到底解决了什么问题
1.1 从“搜得到”到“搜得准”:语义检索的痛点
传统关系型数据库擅长的是精确匹配和结构化查询。比如“工资大于 10000 的员工”,这种条件用 SQL 一查就出来了。但一旦遇到“找一些和这段描述意思相近的文章”这种需求,传统数据库就彻底抓瞎了。你可以用 LIKE 做模糊匹配,但“苹果怎么保存”和“苹果存储技巧”“新鲜苹果的保鲜方法”在字面上几乎没有共同点,LIKE 根本匹配不上。
打破这个困局的思路是换一种表示方式:先把文本交给 embedding 模型,让它把整句话压缩成一个几百维的浮点数数组,也就是向量。语义相近的句子,向量在高维空间里的距离也近。比如“今天天气怎么样”和“明天会下雨吗”虽然是两个不同的句子,但它们在向量空间中的位置非常接近。向量数据库干的事情就是把这些向量存下来,并提供“给定一个向量,找出距离最近的 K 个向量”的能力,也就是近似最近邻搜索。
所以向量数据库的核心能力不是存储,而是检索。它内部用 HNSW、IVF 这类索引结构来加速最近邻搜索,本质上是用空间换时间,在亿级向量中也能做到毫秒级返回。理解了这一点,你就知道为什么不能拿普通数据库来硬扛向量检索,也不要指望自己写个 for 循环算余弦相似度能支撑起业务。
1.2 Chroma 与主流方案对比:为什么新手选它
市面上向量数据库不止 Chroma 一个,Milvus、Qdrant、pgvector、FAISS 都经常被拿出来对比。我自己的体验是:不同工具定位差异很大,选型一定要先想清楚场景,而不是追热度。
先看 Milvus。它功能强大,支持分布式部署,能撑起十亿级别的向量规模,但代价是架构复杂,依赖 etcd、MinIO、Pulsar 等一系列组件,本地搭建一套完整的 Milvus 集群对新手来说门槛相当高。Qdrant 用 Rust 写的,性能非常出色,也支持独立部署,但需要单独起一个服务,还要管理配置文件。pgvector 是 PostgreSQL 的扩展,如果你已经有 PG 环境,它是最省事的增量方案,但它的检索性能和高级功能相对有限。FAISS 严格来说不是一个数据库,它是一个向量检索库,没有数据管理、持久化、过滤这类功能,适合离线场景。
我整理了一个对比表,方便你直观感受差异:
| 方案 | 部署方式 | 适合规模 | 上手难度 | 适用场景 |
|---|---|---|---|---|
| Chroma | 嵌入式 / 独立服务 | 百万级以下 | 低 | 学习、原型验证、中小型应用 |
| Milvus | 分布式集群 | 十亿级 | 高 | 大规模生产环境 |
| Qdrant | 独立服务(Rust) | 千万级 | 中 | 生产级语义检索 |
| pgvector | PostgreSQL 插件 | 千万级 | 中 | 已有 PG 环境的增量方案 |
| FAISS | Python 库 | 百万级 | 中 | 离线检索、研究实验 |
Chroma 对我来说最大的价值就是“轻”。它不需要你提前部署一个服务,直接在 Python 进程里跑,数据落盘到本地目录。这种嵌入式设计的优势非常明显:安装一个 pip 包就能用,没有网络依赖,没有服务编排,特别适合新手入门和对延迟敏感的原型项目。等你确认了业务方向、数据量真的涨上来了,再切换到其他重量级方案也不迟。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 本地安装 Chroma:5 分钟跑通第一个例子
2.1 安装前的准备
Chroma 的安装在很多场景下就是一个 pip 命令的事,但安装前我建议先确认两件事:Python 版本和虚拟环境。
Chroma 官方支持 Python 3.9 及以上版本。我实测在 Python 3.10 和 3.11 下都跑得很稳,3.8 及以下就别试了,依赖会编译报错。如果你机器上有多个 Python 版本,务必先确认当前 shell 里用的是哪个:
bash复制python --version
然后是虚拟环境。我知道有些人图省事直接装到全局环境,我一开始也这么干过,后来不同项目依赖打架打得怀疑人生。强烈建议用 venv 或 conda 隔离一个干净的环境:
bash复制python -m venv chroma-env
source chroma-env/bin/activate # Windows 下是 chroma-env\Scripts\activate
虚拟环境的好处不只是隔离依赖,调试的时候也容易排查问题,出了问题直接删掉重建,成本极低。
2.2 最小可运行示例
安装 Chroma 本身很简单:
bash复制pip install chromadb
这个包会带上一堆依赖,包括 onnxruntime、numpy、pydantic 等。如果你的网络环境一般,安装过程可能会比较慢,甚至卡在 onnxruntime 这个比较大的包上,这时候换国内 pip 镜像能快很多:
bash复制pip install chromadb -i https://pypi.tuna.tsinghua.edu.cn/simple
装完之后,可以先跑一个最短的验证脚本,确认环境没问题:
python复制import chromadb
client = chromadb.Client()
collection = client.create_collection("demo")
collection.add(
documents=["今天天气挺好的", "明天可能会下雨", "苹果的保鲜方法是放冰箱"],
ids=["1", "2", "3"]
)
results = collection.query(
query_texts=["天气预报"],
n_results=2
)
print(results["documents"])
这段代码干了几件事:创建一个内存模式的客户端、建一个集合、塞进去三条文本、跑一次语义检索。如果控制台能输出两条最接近“天气预报”的结果,说明整个链路已经通了。注意我这里面还用了 chromadb.Client() 而不是 PersistentClient,它们之间的区别后面会专门讲。
2.3 安装过程中的常见坑
第一个坑是 onnxruntime 安装超时。Chroma 默认带一个基于 ONNX 的 embedding 模型,这个模型的推理依赖 onnxruntime,而 onnxruntime 的安装包比较大,网络不好很容易失败。解决方案是在 pip 命令里加上超时时间,或者直接换镜像源。
第二个坑是版本兼容。Chroma 迭代速度很快,API 有过多次调整。比如老版本的 client.create_collection 和后来的 client.get_or_create_collection 行为有差异,而 chromadb.Client() 在不同版本里默认的持久化行为也不一样。我个人的建议是装完看一眼版本号:
bash复制pip show chromadb | grep Version
如果你在网上找教程,一定要确认对方用的版本和你一致。版本差两三个小版本,API 写法可能就完全变了。
第三个坑比较隐蔽:如果你在 Jupyter Notebook 里跑,第一次 import chromadb 可能会比较慢,因为要加载 onnxruntime 和模型文件。这不是卡死了,给它几秒钟时间就好。我一度以为是环境坏了,重启了内核好几次,后来才发现它只是加载慢。
3. 核心概念与 API 全解
3.1 Client 与 Collection:理解 Chroma 的组织方式
Chroma 的两个最核心概念是 Client 和 Collection。你可以把 Client 理解成数据库实例,Collection 理解成数据库里的表。但和普通表不太一样的是,Collection 存储的是文档、向量和元数据的组合体。
Client 有两种形态。第一种是 chromadb.Client(),纯内存模式,数据只存在当前进程里,进程一结束数据就没了,适合测试和调试。第二种是 chromadb.PersistentClient(path="./my_chroma_data"),数据会持久化到本地磁盘,进程重启后数据还在。这个选择非常关键,很多人跑完 demo 后发现数据“丢了”,其实就是用了内存模式。
我强烈建议哪怕只是学习,也直接用 PersistentClient 指定一个目录,养成好习惯。后面数据量大了想迁移,直接拷目录就行,很方便:
python复制import chromadb
client = chromadb.PersistentClient(path="./my_chroma_data")
Collection 是整个检索的最小单元。创建集合时可以指定名称、距离函数和 embedding 函数。我一般建议用 get_or_create_collection 而不是 create_collection,前者是存在就获取、不存在就创建,幂等性好,重复执行脚本不会报错:
python复制collection = client.get_or_create_collection(
name="my_docs",
metadata={"hnsw:space": "cosine"}
)
这里的 metadata={"hnsw:space": "cosine"} 指定了距离函数。Chroma 支持三种距离度量:
| 距离函数 | 名称 | 说明 |
|---|---|---|
| l2 | 欧氏距离 | 默认值,基于向量坐标的空间距离 |
| cosine | 余弦相似度 | 更关注方向而非长度,文本场景常用 |
| ip | 内积 | 适用于归一化后的向量 |
文本检索场景我一般选 cosine。要注意的是,Chroma 里 cosine 给出的是 1 - 余弦相似度,所以数值越小代表越相似,和直觉里“相似度越大越好”是反的,看结果时别搞混。
3.2 增删改查:管理你的向量数据
Chroma 的写入接口非常直观,核心方法是 add。最简形式只需要传两个参数:documents 和 ids:
python复制collection.add(
documents=["这是第一篇文档", "这是第二篇文档"],
ids=["doc_1", "doc_2"]
)
你没传 embeddings 参数,Chroma 会调用默认的 embedding 模型自动把文本转成向量。这个设计对新手很友好,当然代价是每次写入都要跑一次模型,批量写入大数据时速度会慢一些。
如果你自己提前算好了向量,也可以直接传 embeddings 参数,跳过内部的 embedding 过程:
python复制import numpy as np
embedding = np.random.rand(384).tolist()
collection.add(
documents=["自定义向量对应的文本"],
ids=["doc_custom"],
embeddings=[embedding]
)
除了文档和向量,Chroma 还支持 metadatas 参数,也就是元数据。元数据的存在很重要,它是过滤检索范围的关键,比如给每篇文档打上分类、来源、时间标签:
python复制collection.add(
documents=["苹果的保鲜方法是放冰箱"],
ids=["doc_3"],
metadatas={"category": "life", "source": "wiki"}
)
删除和更新也很简单:
python复制# 删除
collection.delete(ids=["doc_1"])
# 更新,如果 id 不存在,update 不生效
collection.update(ids=["doc_2"], documents=["更新后的内容"])
# upsert,存在则更新,不存在则插入
collection.upsert(ids=["doc_2", "doc_4"], documents=["更新后的内容", "新文档"])
这里有个小细节:update 和 upsert 的行为不一样,前者对不存在的 id 是静默忽略,后者会执行插入。如果你不确定 id 是否已存在,直接用 upsert 更保险。
3.3 相似度检索:参数与过滤条件
查询是向量数据库的核心操作。Chroma 的 query 方法最基础的调用方式是传 query_texts 和 n_results:
python复制results = collection.query(
query_texts=["冰箱里苹果怎么放"],
n_results=5
)
返回的 results 是一个字典,包含 ids、distances、metadatas、documents 等字段。结构对应你传入的查询语句,如果你查询了 1 条,那么每个字段都是套了一层 list 的结构,比如 results["documents"][0] 才是第一条查询的结果列表。
除了文本查询,还可以直接传向量查询:
python复制query_vector = [0.1, 0.2, ...]
results = collection.query(
query_embeddings=[query_vector],
n_results=5
)
这是更底层的方式,适合你已经把问题转换成向量的场景。
where 参数用于元数据过滤,这和 SQL 里的 WHERE 类似。比如只检索来源为 wiki 的文档:
python复制results = collection.query(
query_texts=["苹果保鲜"],
n_results=3,
where={"source": "wiki"}
)
对比检索效果时,distances 字段直接反映了语义距离。数值越小意味着越相似,你可以把它当作一个置信度来用,比如只返回距离小于 0.5 的结果,避免低质量匹配。
4. 落地实战:做一个本地语义搜索 Demo
4.1 设计你的 embedding 流程
了解基础 API 之后,我建议你动手做一个真正能用的语义搜索 Demo,而不是停留在增删改查层面。第一步是设计 embedding 流程。
Chroma 自带的默认 embedding 模型是基于 ONNX 的 all-MiniLM-L6-v2,它对英文支持不错,但对中文的支持只能说勉强够用。我做中文场景的项目,一般会换成中文效果更好的模型。有两个思路:一是用 embedding_functions 直接指定 HuggingFace 模型,二是自己用其他库算好向量再传给 Chroma。
先看第一种思路。Chroma 提供了一个 HuggingFace Embedding Function 的封装:
python复制from chromadb.utils import embedding_functions
sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="moka-ai/m3e-base"
)
moka-ai/m3e-base 是一个中文文本 embedding 模型,效果比默认模型好不少。这种方式的好处是集成简单,但缺点是多了一层模型加载,首次调用会下载模型文件,需要网络。如果你网络受限,可以考虑把模型下载好之后离线加载。
第二种思路更灵活,完全绕开 Chroma 内置的 embedding,用你自己选的任何模型生成向量。这个思路我第一次跑通的时候还是很有成就感的:
python复制from sentence_transformers import SentenceTransformer
model = SentenceTransformer("moka-ai/m3e-base")
texts = ["苹果如何保鲜", "冰箱温度怎么设置"]
embeddings = model.encode(texts).tolist()
collection.add(
documents=texts,
embeddings=embeddings,
ids=["0", "1"]
)
这样做的好处是 embedding 过程完全可控,模型可以做缓存,批处理效率更高。缺点是你需要手动管理文本和向量的对应关系,同时保证 query 时也要用同一个模型生成查询向量,否则牛头不对马嘴。
关于 embedding 维度,有一点需要提醒:同一个 Collection 里所有向量的维度必须一致。如果你中途换了 embedding 模型,导致新旧模型输出维度不同,会出现插入失败或者检索异常的情况。参数上,m3e-base 输出 768 维,而默认的 MiniLM-L6-v2 输出 384 维,混用会直接出错。
4.2 元数据过滤:让检索更精准
生产环境里的数据通常是带分类的,比如一个本地文档库里混着技术博客、产品文档和日常笔记。如果每次检索都全局扫描,不仅慢,而且结果容易跑偏。这时候元数据过滤的价值就体现出来了。
我在实际项目里的做法是:写入时给每条数据打上足够丰富的元数据,包括来源、分类、时间戳等字段。这样在查询时就能像 SQL 一样缩小检索范围,速度和准确率都会提升。
一个典型场景:只检索某个时间段内的文档。
python复制collection.add(
documents=["2024 年度技术总结"],
ids=["annual_2024"],
metadatas={"category": "blog", "year": 2024}
)
results = collection.query(
query_texts=["年度总结"],
n_results=5,
where={"year": 2024}
)
where 参数还支持逻辑组合。比如 $and 和 $or 运算符:
python复制# 检索 category 为 blog 且 year 大于 2022 的文档
where={"$and": [
{"category": {"$eq": "blog"}},
{"year": {"$gt": 2022}}
]}
这个能力虽然不如 SQL 那么灵活,但对日常过滤场景已经够用了。建议在还没有数据的时候,先想好元数据 schema,后面再补会很麻烦。
4.3 与 LangChain 集成构建问答雏形
很多人接触 Chroma 是为了做 RAG,也就是检索增强生成。最简单的方式是配合 LangChain 使用,LangChain 提供了对 Chroma 的官方集成,封装程度很高,几行代码就能把文档库和 LLM 串起来。
一个很简化的流程是:把文档切片后写入 Chroma,查询时从 Chroma 里检索相关片段,再把片段和问题一起拼进 prompt 给大模型。
python复制from langchain.vectorstores import Chroma
from langchain.embeddings import HuggingFaceEmbeddings
embedding = HuggingFaceEmbeddings(model_name="moka-ai/m3e-base")
vectordb = Chroma(
persist_directory="./my_chroma",
embedding_function=embedding
)
然后在查询时,直接从 vectorstore 里做相似度检索:
python复制docs = vectordb.similarity_search("苹果怎么保存", k=3)
for doc in docs:
print(doc.page_content)
LangChain 的封装把 Client 和 Collection 的细节隐藏了,对新手来说上手更快。但我个人的建议是:如果你准备把 Chroma 用在自己的项目里,还是要把原生 API 搞懂,封装层能省事但也会掩盖问题,一旦出 bug 你会很难定位。
文档切片的技巧也需要提一下。切得太短,片段语义不完整;切得太长,向量表示不够精确,检索效果差。我经验上,中文文本按 200 到 500 字切一个 chunk 比较合适,重叠 50 字左右,避免截断句子导致语义断裂。
5. 常见问题排查与避坑指南
5.1 数据“丢了”?理解持久化逻辑
新手最容易遇到的一个问题:明明 add 了数据,第二天重启程序再查,结果什么都不剩。这个问题的根源基本都出在 Client 类型上。如果你用的是 chromadb.Client(),那就是纯内存模式,进程退出数据自然就没了。解决办法是改用 PersistentClient(path=...),所有数据会落到你指定的目录。
另一个容易忽略的点是:即使用了 PersistentClient,如果你创建集合时没有指定同一个 embedding 函数,重启后可能遇到维度不一致的报错。因为 Collection 在首次创建时就确定了 embedding 函数的类型,如果再次加载时传了不同的 embedding 函数,Chroma 会认为你存的数据格式不同。排查这个问题的方法是把 path 目录下的文件清理掉,重建 Collection,同时保证每次加载时传入相同的 embedding 配置。
5.2 中文检索效果差?换 Embedding 模型
默认的 all-MiniLM-L6-v2 对中文的语义理解能力确实比较薄弱。我在测试中发现,用默认模型做中文语义检索,经常会返回一些莫名其妙的结果,比如搜“苹果保鲜”返回的是“天气预报”。原因不是 Chroma 本身的问题,而是 embedding 模型对中文理解不够充分。
解决办法就是换模型。推荐几个我用过效果还不错的:
moka-ai/m3e-base:中文效果均衡,通用性强,768 维BAAI/bge-large-zh-v1.5:检索效果很好,但对显存有一定要求shibing624/text2vec-base-chinese:轻量,适合本地部署
换模型之后,记得要清空原有集合重建,因为不同模型生成的向量 空间不同,混用的话检索结果完全不可信。
5.3 安装失败与环境兼容性
如果你是 Windows 用户,安装 chromadb 时偶尔会遇到 Microsoft C++ Build Tools 相关的报错。这通常是一些 Python 包需要本地编译导致的。解决方案是先安装 Visual C++ 构建工具,或者直接用预编译的 wheel 包安装。
另一个兼容性问题是 Python 版本过新。比如你用的是 Python 3.13,某些依赖可能还没有适配。我的建议是尽量用 Python 3.10 或 3.11,这两个版本是目前兼容性最稳定的。
还有一个小经验:如果你在生产环境使用,建议把 chromadb 的版本固定下来,不要用 pip install chromadb 直接安装最新版,而是用 pip install chromadb==对应版本号。因为 Chroma 的 API 更新很频繁,今天写的代码过两个月可能就不好使了。固定版本可以让你的环境可控,避免不必要的惊吓。
关于数据规模,我再多说一句。Chroma 在百万级以下的中小场景里表现不错,但如果你数据量超过这个级别,或者并发请求很高,就要认真考虑迁移到 Qdrant 或 Milvus 了。判断的信号很直观:查询延迟开始显著上升,或者持久化文件越来越大导致启动变慢。到时候你可以考虑横向扩展方案。
我个人在实战中的一个体会是:学习向量数据库,不要一开始就去啃索引结构和分布式原理,先用 Chroma 把“写入 - 检索 - 过滤”这条链路跑通,建立直观感受,再去研究底层机制。另外一个小技巧:调试阶段用内存模式加快迭代,功能稳定后再切换到持久化模式,这样能节省不少时间。最后,如果你第一次跑出来的检索结果不符合预期,先别急着怀疑工具,多半是 embedding 模型没选对,或者元数据过滤条件写错了。这个项目后续可以在 embedding 调优和检索策略上继续深挖,把每一步的细节做扎实,比泛泛了解一堆概念有用得多。
