系列文章目录
概述
Chroma是一款流行的开源嵌入(embedding)数据库。Chroma通过将知识、事实和技能等可插拔(pluggable)到 LLM(大语言模型)中,从而使得构建 LLM 应用变得很容易。这里需要说明的是:
(1)什么是嵌入(embed和embedding)?可以理解成将数据嵌入到向量里,想象有一个布满格子的模具,这些格子被排列成一行,现在将一个橡皮泥材质的小猫摁入(embed)到模具中,小猫就变成了模具的形状,这个塑形的过程就是嵌入,小猫代表一条数据,每个格子代表小猫的一个特征,一排格子就是一个特征向量,一个特征向量就被称为一个embedding,嵌入的本质就是将数据表示为特征向量,即数据向量化,这里的数据包括但不限于:单词,句子,段落,文章,公式,图片,语音,视频等任何可在计算机中表示的事物。
(2)什么是可插拔(pluggable)?经过Chroma处理或存储的数据可以做为上下文发送给 LLM,并被LLM识别,这些数据一旦存储后就可以被重复使用。
Chrome与App,LLM通常会被搭配使用,它们之间的配合流程如下图,该图摘自Chrome官网。首先App会将查询内容(一般是用户的提出的问题)发送给Chroma,Chroma查找与查询内容最相似的文档内容,App将查找的文档内容与查询内容一起作为LLM的上下文窗口(context window)发送给LLM,LLM回答问题并返回给App。
实际开发中,大多会与LangChain结合使用,LangChain对Chroma进行了集成封装,具体结合方式可以查看这个demo:https://github.com/hwchase17/chroma-langchain。
Chroma是基于CPU的,默认使用all-MiniLM-L6-v2嵌入模型,可以将嵌入模型更换为支持GPU的模型来支持GPU,能在Chroma中使用的支持GPU的模型包括:hkunlp/instructor-base,hkunlp/instructor-large和hkunlp/instructor-xl。
Chroma提供了以下工具:
- 存储嵌入和它们的元数据
- 嵌入文档和查询嵌入
- 搜索嵌入
Chroma的优势在于: - 简单,提高了开发者的生产率,API较少,调用容易,上手快,且足够灵活,能满足各种场景
- 在搜索基础上提供了分析和处理功能,比如:where筛选,计数等
- 具有快速的数据存储和查询能力,可以在短时间内处理大规模的向量数据
- 被集成到了LangChain中,可以快速构建基于Chroma的AI应用
参考资料
官网主页:https://www.trychroma.com/
官网文档:https://docs.trychroma.com/
github主页:https://github.com/chroma-core/
github项目:https://github.com/chroma-core/chroma/
版本发布
Chroma是一个比较新的项目,通过github的可以看到,在2022-10-16创建了第一个版本0.0.1,2022-10-23发布了第一个预发布版本0.0.2,2023-07-18发布了第一个正式发布版本0.4.0,此后每次发布基本上都是正式发布,鲜有预发布。Chrome的迭代速度很快,平均下来几乎每周都会有发布,有时会密集发布。需要注意的是,预发布的版本通常未经过充分测试和验证,可能存在不稳定性或不完全符合预期的行为。
架构
Chroma采用C/S(Client/Server)架构模式,包括三部分:Python客户端SDK,JavaScript/TypeScript客户端SDK和服务端应用,服务端应用是基于Python的。开发者可以通过使用这三种编程语言去调用Chroma的服务端查询或存储数据。
Chroma提供了两种运行模式:in-memory和client/server,即内存模式和C/S模式。两者的区别在于是否通过url和端口号访问服务端,C/S模式可以请求其它机器上部署的Chroma服务端的服务,而in-memory则只能调用本地的Chroma服务端的服务。对于Python的C/S模式尚处于alpha开发阶段。
对于in-memory,也分成两种模式:ephemeral和persistent,即短暂模式和持久化模式。短暂模式下程序一旦结束数据就被销毁,持久化模式下数据会被存储到本地硬盘文件中供以后重复使用,再次启动程序会自动加载数据,关闭程序会自动保存数据。
不同的编程语言支持的模式不同,具体如下表格,该表格摘自官网。可以看出,Python同时支持两种模式,JavaScript仅支持客户端,而其它语言需要借助中间件才能使用客户端。
in-memory | client | |
---|---|---|
Python | ✅ | ✅(by Chroma) |
Javascript | ➖ | ✅ (by Chroma) |
Ruby | ➖ | ✅ from @mariochavez |
Java | ➖ | ✅ from @t_azarov |
Go | ➖ | ✅ from @t_azarov |
C# | ➖ | ✅ from @microsoft |
Rust | ➖ | ✅ from @Anush008 |
Elixir | ➖ | ✅ from @3zcurdia |
Dart | ➖ | ✅ from @davidmigloz |
Other? | ❓ | ❓ |
开发和运行环境
以下安装和示例程序都是针对Python编程,在win10的conda虚拟环境中运行过,chromadb被安装在conda虚拟环境中。
包安装
如果网速不快,可以使用清华源。
pip install chromadb -i https://pypi.tuna.tsinghua.edu.cn/simple
编码流程及示例
这里只演示in-memory的ephemeral模式,该模式仅支持Python,对于其它模式请关注后续文章。in-memory模式的编码流程包含四步:
1.获取客户端 > 2.创建或获取被查询集合 > 3.往集合中添加被查询数据 > 4.查询集合中的数据
在ephemeral模式下数据并没有持久化到硬盘中,程序运行结束数据就会销毁。如果要持久化,则需要在Client()中设置setting参数,或使用PersistentClient(path)创建客户端(Chroma的版本要大于0.4.0),将数据保存到指定的文件中。持久化后,collection可以被重复使用,之后再次启动客户端,会自动加载collection的数据,若添加数据,关闭客户端会自动保存。持久化会放到后续文章介绍。
步骤1:获取客户端
import chromadb
# in-memory ephemeral model
client = chromadb.Client()
如果Chroma版本大于0.4.0,可以这样调用。
import chromadb
# in-memory ephemeral model
client = chromadb.EphemeralClient()
步骤2:创建或获取被查询集合
集合是一个用来存放嵌入,文档和元数据的容器。允许同时创建多个集合,集合名称是必须的,用来区分多个不同的集合,且名称长度限制在3-63个字符之间,还有一些其他限制,后续文章会详细介绍。存贮在集合中的数据会提供给用户进行查询。嵌入即特征向量,文档即文本内容,元数据是用来描述文档的属性,包括:名称,来源,类型,作者等自定义属性,具体可以参考第三步的添加数据。
collection = client.create_collection(name="my_collection")
对于已存在的集合,可以直接通过名称获取,如果集合不存在会报错。
collection = client.get_collection(name="my_collection")
也可以通过以下方式调用,如果给定名称的集合已经存在则直接获取,否则创建。
collection = client.get_or_create_collection(name="my_collection")
步骤3:往集合中添加被查询数据
添加被查询数据后,就可以通过查询方法找出与用户输入信息最相近的数据。add方法的参数有:
- ids:多个文档的唯一标识列表,必传参数。
- embeddings:多个文档的嵌入列表,嵌入也是数组,选传参数,但是与documents参数至少有一个要传。
- documents:多个文档的文本内容字符串列表,选传参数,但是与embeddings参数至少有一个要传。
- metadatas:多个文档的元数据字典列表,元数据的数据可以是任何自定义文档属性,包括:名称,来源,类型,作者等。
这四个参数都是列表,且长度要一致,有几个文档就要有几个元素。
如果传入的是文档的内容文本,Chroma会自动处理分词(tokenization),嵌入(embedding)和索引(indexing),后续的查询方法需要使用query_texts参数。
collection.add(
ids=["id1", "id2", "id3", "id4", "id5"],
documents=["浙江的省会是杭州", "河北的省会是石家庄", "山东的省会是济南", "杭州是个美丽的城市", "杭州位于浙江省"],
metadatas=[{"source": "myDoc"}, {"source": "myDoc"}, {"source": "myDoc"}, {"source": "myDoc"}, {"source": "myDoc"}],
)
如果已经有生成好的嵌入,可以通过embeddings参数直接传入,后续的查询方法需要使用query_embeddings参数。
collection.add(
ids=["id1", "id2", "id3", "id4", "id5"],
# 注意如果使用了embedings参数,且嵌入维度不是384(当前是3),则第四步中的查询需要使用query_embeddings传参查询
# 其余情况,可以使用query_texts传参查询
embeddings=[[1.2, 2.3, 4.5], [3.3, 5.1, 2.7], [6.1, 8.2, 9.2], [1.8, 0.2, 2.1], [5.5, 6.6, 9.9]],
documents=["浙江的省会是杭州", "河北的省会是石家庄", "山东的省会是济南", "杭州是个美丽的城市", "杭州位于浙江省"],
metadatas=[{"source": "myDoc"}, {"source": "myDoc"}, {"source": "myDoc"}, {"source": "myDoc"}, {"source": "myDoc"}],
)
步骤4:查询集合中的数据
查询集合中与用户给定的文本或嵌入(特征向量)最相似的n个文档,n由用户指定,这n条文档会按照相似度从高到低排列被返回。这里用到的查询策略不是常规的模式匹配,而是采用相似度(K-近邻算法),相似度愈高,返回数据中的距离(distance)值越小,如果值为0,表明内容一模一样。query方法的参数有:
- query_texts:查询文本列表
- query_embeddings:查询嵌入列表
- n_results:与每个查询文本或嵌入最相近的文档数
results = collection.query(
query_texts=["西湖在浙江杭州"],
n_results=5,
)
# 运行结果如下:
{'ids': [['id5', 'id4', 'id1', 'id3', 'id2']], 'distances': [[0.43458253145217896, 0.48921626806259155, 0.5748544931411743, 0.9643734097480774, 1.0526437759399414]], 'metadatas': [[{'source': 'myDoc'}, {'source': 'myDoc'}, {'source': 'myDoc'}, {'source': 'myDoc'}, {'source': 'myDoc'}]], 'embeddings': None, 'documents': [['杭州位于浙江省', '杭州是个美丽的城市', '浙江的省会是杭州', '山东的省会是济南', '河北的省会是石家庄']]}