Chroma 是 AI 原生开源矢量数据库。Chroma 通过为 LLM 提供知识、事实和技能,使构建 LLM 应用程序变得容易。同时也是实现大模型RAG技术方案的一种有效工具。
先简单看看,整体数据处理流程:
1.创建 client,简单看成是初始化数据库
2.创建和选择 collection,简单看成是创建数据表
3.在当前 collection 下进行增删改查,以及相似度检索。简单看成对当前数据表进行增删改查
一、安装
pip install chromadb
二、client 的三种创建方式
方式一:直接创建 client
import chromadb client = chromadb.Client()
数据都存储在内存,程序运行完,数据会丢失
方式二:创建 client 时,设置数据持久化路径
client = chromadb.PersistentClient(path="/path/to/save/to")
client 会自动进行数据存储,当路径不存在时,会新建一个文件;当路径存在时,直接加载当前文件。
方式三:使用 client/server 方式,推荐使用
首先开启 chroma 服务,命令如下:
chroma run --path db_path
db_path 是你的数据存储路径。然后,创建 client
import chromadb chroma_client = chromadb.HttpClient(host='localhost', port=8000)
client 相关的接口
检查心跳:
client.heartbeat() # 单位为 nanosecond
清空数据库:
client.reset()
列出所有的 collection 列表:
client.list_collections()
三、collection 的使用
创建好了 client,接下来就是新建 collection。
collection 的命令规则:
1、名字长度必须限制在 3~63 个字符之间
2、名字必须以小写字母或数字开头和结尾,中间可以包含 点、破折号、下划线,不能包含两个连续的点
3、名字不能是一个有效的ip地址
3.1 创建 collection 相关的接口
创建一个 collection:
collection = client.create_collection(name="my_collection", embedding_function=emb_fn)
切换到一个 collection:
collection = client.get_collection(name="my_collection", embedding_function=emb_fn)
快捷创建方式:collection 存在时,直接加载使用;不存在时,则创建
collection = client.get_or_create_collection(name="test")
删除 collection:
client.delete_collection(name="my_collection")
3.2 创建 collection 的接口的相关参数说明
以 create_collection 为例,其它接口类似。
collection = client.create_collection( name="collection_name", metadata={"hnsw:space": "cosine"}, embedding_function=emb_fn ) name:表示 collection 的名称 metadata:设置距离度量,{"hnsw:space": "cosine"} 表示使用的余弦距离,目前支持 "l2", "ip, 和 "cosine" 三种距离度量方式 l2:表示 squared L2 norm ip:表示 Inner product cosine:表示 Cosine similarity embedding_function:提取嵌入表示的函数,默认支持 sentence-transformer 接口和相关模型,也支持自定义该函数。该参数默认为None,为 None 时,后续添加文本数据时,需要自己手动计算文本 embedding。不为 None,已经设置好嵌入模型时,后续直接添加文本数据即可,chroma 内部会自动计算 embedding。 注意:chroma 里检索时,返回的都是距离度量,值越小越相似,不要和相似性度量搞混了哟。
3.3 collection 类相关的函数
返回 collection 下的前 10 条数据:
collection.peek()
返回 collection 包含的数据条目数:
collection.count()
修改 collection 名称:
collection.modify(name="new_name")
四、增删改查
创建好 collection 后,就可以在该 collection 下进行增删改查了。
4.1 添加文档
collection.add( documents=["Article by john", "Article by Jack", "Article by Jill"], embeddings=[[1,2,3],[4,5,6],[7,8,9]], metadatas=[{"author": "john"}, {"author": "jack"}, {"author": "jill"}], ids=["1", "2", "3"]) documents:文档列表 embeddings:文档的嵌入表示。如果创建 collection 时,已经设置了embedding_function,则可以忽略该参数,内部会自动计算 metadatas:文档的元数据 ids:文档id。每个文档都有独一无二的id,当添加数据时,如果文档id重复,则后续id重复的数据都会跳过,不会覆盖之前的记录。
4.2 删除文档
collection.delete( ids=["1"], where={"author": {"$eq": "jack"}}, # 表示 metadata 中 "author" 字段值等于 "jack" 的文档 where_document={"$contains": "john"}, # 表示文本内容中包含 "john" 的文档 ) ids:表示按照 文档id 删除 where:元数据过滤器,根据 metadata 中的字段进行过滤。后面详细介绍其语法规则 where_document:文本数据过滤器,根据文档内容进行过滤。后面详细介绍其语法规则 注意:ids,where,where_document 不能同时为空
4.3 更新文档
collection.update( documents=["Article by john", "Article by Jack", "Article by Jill"], embeddings=[[10,2,3],[40,5,6],[70,8,9]], metadatas=[{"author": "john"}, {"author": "jack"}, {"author": "jill"}], ids=["1", "2", "3"])
接口和添加文档接口一样,注意一下 ids 的值,如果某个 id 在数据库中不存在,不会影响已有 id 数据的更新。
4.4 同时更新和添加的接口
collection.upsert( documents=["Article by john", "Article by Jack", "Article by Jill"], embeddings=[[1,2,3],[2,5,6],[3,8,9]], metadatas=[{"author": "john"}, {"author": "jack"}, {"author": "jill"}], ids=["1", "2", "3"])
加强了的更新接口,如果 id 在数据库中存在,则进行更新;如果不存在,则进行添加。
4.5 查询接口
collection.get( ids=["1"], where={"author": {"$eq": "jack"}}, # 表示 metadata 中 "author" 字段值等于 "jack" 的文档 where_document={"$contains": "john"}, # 表示文本内容中包含 "john" 的文档 ) ids:表示按照 文档id 查询 where:元数据过滤器,根据 metadata 中的字段进行过滤。后面详细介绍其语法规则 where_document:文本数据过滤器,根据文档内容进行过滤。后面详细介绍其语法规则 注意:ids,where,where_document 可以同时为空,如果都为空,则查询整个 collection 的数据
4.6 相似文本检索接口
collection.query( query_embeddings=[[1,2,3]], # query_texts=["Article by john"], n_results=3, where={"author": {"$eq": "john"}}, # 表示 metadata 中 "author" 字段值等于 "jack" 的文档 where_document={"$contains": "john"}, # 表示文本内容中包含 "john" 的文档. ) query_embeddings:可以直接提供文本的嵌入,检索数据库中最相似的文本。一般不常用。 query_texts:待检索的文本 n_results:返回最相似的记录的条数 where:同上,注意过滤的顺序,chroma 里是先进行过滤,然后在过滤后的数据上进行相似度计算。 where_document:同上,注意过滤的顺序,chroma 里是先进行过滤,然后在过滤后的数据上进行相似度计算。 注意:query_embeddings 和 query_texts 不能同时使用,只能选取其中一个。
4.7 where 语法
where 过滤器是针对元数据 metadata 的,语法格式如下:
{ "metadata_field": { <Operator>: <Value> } } 其中支持的 Operator 算子包含: $eq - 等于 (string, int, float) $ne - 不等于 (string, int, float) $gt - 大于 (int, float) $gte - 大于等于 (int, float) $lt - 小于 (int, float) $lte - 小于等于 (int, float) $eq 算子有个特殊的简写方式 { "metadata_field": "search_string" } 等价于 { "metadata_field": { "$eq": "search_string" } } 如果 where 中的 key 在元数据metadata中不存在,程序运行不会报错,只是返回空数据 where 还支持包含操作符 $in 和 $nin,语法格式如下 { "metadata_field": { "$in": ["value1", "value2", "value3"] } } $in - metadata_field字段的值在预定义的列表里 (string, int, float, bool) $nin - metadata_field字段的值不在预定义的列表里 (string, int, float, bool)
4.8 where_document 语法
where_document 过滤器是针对文本内容的
chroma 中提供了两种操作符,$contains and $not_contains,格式如下: { "$contains": "search_string" } 表示检索的文本中包含 search_string 关键词 { "$not_contains": "search_string } 表示检索的文本中不包含 search_string 关键词
4.9 逻辑运算符
逻辑操作符包含 $and 和 $or,语法如下
{ "$and": [ { "metadata_field": { <Operator>: <Value> } }, { "metadata_field": { <Operator>: <Value> } } ] } $and:表示要满足列表中的所有过滤条件 $or:表示只需满足列表中的任一过滤条件
where 和 where_document 都支持逻辑运算符
五、demo
5.1 创建 collection
import chromadb # 一、创建客户端client = chromadb.PersistentClient(path="./vector_store") # 二、创建 collection client.delete_collection(name="test") collection = client.get_or_create_collection(name="test", metadata={"hnsw:space": "cosine"}) # 目前支持 cosine,ip,l2 三种距离,默认为 l2 距离
5.2 添加文档
collection.add( documents=["Article by John", "Article by Jack", "Article by Jill"], embeddings=[[1,2,3],[4,5,6],[7,8,9]], metadatas=[{"author": "john"}, {"author": "jack"}, {"author": "jill"}], ids=["1", "2", "3"]) collection.get()
打印结果:
{'ids': ['1', '2', '3'], 'embeddings': None, 'metadatas': [{'author': 'john'}, {'author': 'jack'}, {'author': 'jill'}], 'documents': ['Article by John', 'Article by Jack', 'Article by Jill'], 'uris': None, 'data': None}
5.3 查询文档
collection.get( # ids=["1"], # where={"author": {"$eq": "jack"}}, # 表示 metadata 中 "author" 字段值等于 "jack" 的文档 # where_document={"$contains": "john"}, # 表示文本内容中包含 "john" 的文档 )
5.4 检索相似性文档
collection.query( query_embeddings=[[1,2,3]], # query_texts=["Article by john"], n_results=3, where={"author": {"$in": ["john", "jack"]}}, # 表示 metadata 中 "author" 字段值在列表 ["john", "jack"] 里的文档 where_document={"$or": [{"$contains": "John"},{"$contains": "Jack"}]}, # 表示文本内容中包含 "John" 的文档或包含 "Jack"的文档 )
输出结果:
{'ids': [['1', '2']], 'distances': [[0.0, 0.025368153802923787]], 'metadatas': [[{'author': 'john'}, {'author': 'jack'}]], 'embeddings': None, 'documents': [['Article by John', 'Article by Jack']], 'uris': None, 'data': None}
可以自行修改 where 和 where_document 过滤器,查看过滤规则的作用方式
六、embedding
创建 collection 时,可以自定义 embeddding 函数,这里以 huggingface transformer 包为例,来加载个最近出来的 bge 模型。
import chromadb from chromadb import Documents, EmbeddingFunction, Embeddings from chromadb.utils.embedding_functions import ( SentenceTransformerEmbeddingFunction ) from transformers import AutoTokenizer, AutoModel import torch class MyEmbeddingFunction(EmbeddingFunction): def __init__(self, model_name, device="cpu") -> None: self.model_name = model_name self.tokenizer = AutoTokenizer.from_pretrained(model_name) self.model = AutoModel.from_pretrained(model_name) self.device = torch.device(device) self.model.to(self.device) @staticmethod def mean_pooling(model_output, attention_mask): token_embeddings = model_output[0] # First element of model_output contains all token embeddings input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(input_mask_expanded.sum(1), min=1e-9) def __call__(self, input: Documents) -> Embeddings: encoded_input = self.tokenizer(list(input), padding=True, truncation=True, return_tensors='pt').to(self.device) # print(encoded_input) with torch.no_grad(): model_output = self.model(**encoded_input) if "bge" in self.model_name.lower(): sentence_embeddings = model_output[0][:, 0] else: sentence_embeddings = self.mean_pooling(model_output, encoded_input['attention_mask']) sentence_embeddings = torch.nn.functional.normalize(sentence_embeddings, p=2, dim=1) return sentence_embeddings.cpu().numpy().tolist() # GanymedeNil_text2vec-large-chinese 模型 collection = client.create_collection( name=name, metadata={"hnsw:space": "cosine"}, embedding_function=MyEmbeddingFunction(model_name="GanymedeNil_text2vec-large-chinese", device="cpu") ) # bge-base-zh-v1.5 模型 collection = client.create_collection( name=name, metadata={"hnsw:space": "cosine"}, embedding_function=MyEmbeddingFunction(model_name="bge-base-zh-v1.5", device="cpu") )
使用 huggingface 上的 bge-base-zh-v1.5 或 GanymedeNil_text2vec-large-chinese 模型,自定义了一个基于huggingface transformer包的接口。实在不想折腾 sentence-transformer 包了,虽然 chroma 默认支持的是该包,但 sentence-transformer 不同版本的兼容性以及和 pytorch 版本的严格对齐要求,让人有点劝退。