Bancos de dados vetoriais
Vimos na postagem embeddings que os embeddings são uma forma de representar palavras em um espaço vetorial. Nesta postagem, veremos como podemos armazenar esses embeddings em bancos de dados vetoriais e como podemos fazer consultas a eles.
Este caderno foi traduzido automaticamente para torná-lo acessível a mais pessoas, por favor me avise se você vir algum erro de digitação..
Quando temos uma consulta, podemos criar a incorporação da consulta, pesquisar no banco de dados de vetores as incorporações que mais se aproximam da consulta e retornar os documentos que correspondem a essas incorporações ou uma explicação desses documentos.
Ou seja, vamos gerar um banco de dados de informações, vamos criar embeddings dessas informações e vamos armazená-las em um banco de dados vetorial. Então, quando um usuário fizer uma consulta, converteremos a consulta em embeddings, pesquisaremos no banco de dados os embeddings com a maior similaridade e retornaremos os documentos que correspondem a esses embeddings.
Além dos documentos, outras informações podem ser armazenadas no banco de dados, que chamaremos de metadados. Por exemplo, se estivermos trabalhando com um conjunto de itens de notícias, podemos armazenar o título, a data, o autor etc. do item de notícias.
Croma
Nesta postagem, vamos analisar o crhoma, pois ele é o banco de dados de vetores mais usado, como pode ser visto neste relatório langchain state of ai 2023.
Vectorstores mais usados](https://blog.langchain.dev/content/images/size/w1000/2023/12/Top-vectorstores--1-.png)
Instalação
Portanto, para instalar o chroma com o conda, você precisa fazer o seguinte
conda install conda-forge::chromadb
```
Ou se você quiser instalar com o pip
````bash
pip install chromadb
```
Uso rápido
Para uma aplicação rápida, primeiro importamos o chroma
import chromadb
Em seguida, criamos um cliente chroma.
import chromadbchroma_client = chromadb.Client()
Criamos uma coleção. Uma coleção é o local onde as incorporações, as incorporações e os metadados serão armazenados.
import chromadbchroma_client = chromadb.Client()collection = chroma_client.create_collection(name="my_collection")
Como você pode ver, é exibida uma mensagem indicando que nenhuma função de embeddings foi introduzida e, portanto, o padrão será all-MiniLM-L6-v2
, que é semelhante ao modelo paraphrase-MiniLM-L6-v2
que usamos na postagem embeddings.
Falaremos mais sobre isso mais tarde, mas podemos escolher como gerar os embeddings.
Agora, adicionamos documentos, IDs e metadados à coleção.
import chromadbchroma_client = chromadb.Client()collection = chroma_client.create_collection(name="my_collection")collection.add(documents=["This is a python docs", "This is JavaScript docs"],metadatas=[{"source": "Python source"}, {"source": "JavaScript source"}],ids=["id1", "id2"])
Agora podemos fazer uma consulta
import chromadbchroma_client = chromadb.Client()collection = chroma_client.create_collection(name="my_collection")collection.add(documents=["This is a python docs", "This is JavaScript docs"],metadatas=[{"source": "Python source"}, {"source": "JavaScript source"}],ids=["id1", "id2"])results = collection.query(query_texts=["This is a query of Python"],n_results=2)
import chromadbchroma_client = chromadb.Client()collection = chroma_client.create_collection(name="my_collection")collection.add(documents=["This is a python docs", "This is JavaScript docs"],metadatas=[{"source": "Python source"}, {"source": "JavaScript source"}],ids=["id1", "id2"])results = collection.query(query_texts=["This is a query of Python"],n_results=2)results
{'ids': [['id1', 'id2']],'distances': [[0.6205940246582031, 1.4631636142730713]],'metadatas': [[{'source': 'Python source'}, {'source': 'JavaScript source'}]],'embeddings': None,'documents': [['This is a python docs', 'This is JavaScript docs']],'uris': None,'data': None}
Como podemos ver, a distância para id1 é menor do que a distância para id2, portanto, parece que o documento 1 é mais adequado para responder à consulta.
Bancos de dados persistentes
O banco de dados que criamos anteriormente é temporário e, assim que fecharmos o notebook, ele desaparecerá. Portanto, para criar um banco de dados persistente, é necessário passar para o chroma o caminho onde ele será salvo.
Primeiro, vamos criar a pasta onde salvaremos o banco de dados
from pathlib import Pathchroma_path = Path("chromadb")chroma_path.mkdir(exist_ok=True)
Agora, criamos um cliente na pasta que criamos.
from pathlib import Pathchroma_path = Path("chromadb")chroma_path.mkdir(exist_ok=True)chroma_client_persistent = chromadb.PersistentClient(path = str(chroma_path))
Coleções
Criar coleções
Ao criar uma coleção, um nome deve ser especificado. O nome deve ter as seguintes considerações:
- O comprimento do nome deve ter entre 3 e 63 caracteres.
- O nome deve começar e terminar com uma letra minúscula ou dígito e pode conter pontos finais, hífens e sublinhados no meio.
- O nome não deve conter dois dois pontos consecutivos.
- O nome não deve ser um endereço IP válido.
Também podemos atribuir a ele uma função de incorporação. Se não for atribuída uma função, o padrão será a função all-MiniLM-L6-v2
.
from pathlib import Pathchroma_path = Path("chromadb")chroma_path.mkdir(exist_ok=True)chroma_client_persistent = chromadb.PersistentClient(path = str(chroma_path))collection = chroma_client.create_collection(name="my_other_collection")
Como você pode ver, uma segunda coleção foi criada para o mesmo cliente chroma_client
, portanto, para um único cliente, podemos ter várias coleções.
Recuperar coleções
Se quisermos recuperar uma coleção de um cliente, podemos fazer isso com o método get_collection
.
from pathlib import Pathchroma_path = Path("chromadb")chroma_path.mkdir(exist_ok=True)chroma_client_persistent = chromadb.PersistentClient(path = str(chroma_path))collection = chroma_client.create_collection(name="my_other_collection")collection = chroma_client.get_collection(name = "my_collection")
Recuperar ou criar coleções
Podemos obter coleções e, se elas não existirem, criá-las com o método get_or_create_collection
.
from pathlib import Pathchroma_path = Path("chromadb")chroma_path.mkdir(exist_ok=True)chroma_client_persistent = chromadb.PersistentClient(path = str(chroma_path))collection = chroma_client.create_collection(name="my_other_collection")collection = chroma_client.get_collection(name = "my_collection")collection = chroma_client.get_or_create_collection(name = "my_tird_collection")
Excluir coleções
Podemos excluir uma coleção com o método delete_collection
.
from pathlib import Pathchroma_path = Path("chromadb")chroma_path.mkdir(exist_ok=True)chroma_client_persistent = chromadb.PersistentClient(path = str(chroma_path))collection = chroma_client.create_collection(name="my_other_collection")collection = chroma_client.get_collection(name = "my_collection")collection = chroma_client.get_or_create_collection(name = "my_tird_collection")chroma_client.delete_collection(name="my_tird_collection")
Obter itens de coleções
Podemos obter os primeiros 10 itens da coleção com o método peek
.
from pathlib import Pathchroma_path = Path("chromadb")chroma_path.mkdir(exist_ok=True)chroma_client_persistent = chromadb.PersistentClient(path = str(chroma_path))collection = chroma_client.create_collection(name="my_other_collection")collection = chroma_client.get_collection(name = "my_collection")collection = chroma_client.get_or_create_collection(name = "my_tird_collection")chroma_client.delete_collection(name="my_tird_collection")collection = chroma_client.get_collection(name = "my_collection")collection.peek()
{'ids': ['id1', 'id2'],'embeddings': [[-0.06924048811197281,0.061624377965927124,-0.090973399579525,0.013923337683081627,0.006247623357921839,-0.1078396588563919,-0.012472339905798435,0.03485661745071411,-0.06300634145736694,-0.00880391988903284,0.06879935413599014,0.0564003586769104,0.07040536403656006,-0.020754728466272354,-0.04048658534884453,-0.006666888482868671,-0.0953674241900444,0.049781784415245056,0.021780474111437798,-0.06344643980264664,0.06119797006249428,0.0834411084651947,-0.034758951514959335,0.0029120452236384153,...-0.013378280214965343]],'metadatas': [{'source': 'Python source'}, {'source': 'JavaScript source'}],'documents': ['This is a python docs', 'This is JavaScript docs'],'uris': None,'data': None}
Nesse caso, apenas dois foram obtidos, porque nossa coleção tem apenas dois documentos.
Se você quiser obter outro número de itens, poderá especificá-lo com o argumento limit
.
collection.peek(limit=1)
{'ids': ['id1'],'embeddings': [[-0.06924048811197281,0.061624377965927124,-0.090973399579525,0.013923337683081627,0.006247623357921839,-0.1078396588563919,-0.012472339905798435,0.03485661745071411,-0.06300634145736694,-0.00880391988903284,0.06879935413599014,0.0564003586769104,0.07040536403656006,-0.020754728466272354,-0.04048658534884453,-0.006666888482868671,-0.0953674241900444,0.049781784415245056,0.021780474111437798,-0.06344643980264664,0.06119797006249428,0.0834411084651947,-0.034758951514959335,0.0029120452236384153,...0.012315398082137108]],'metadatas': [{'source': 'Python source'}],'documents': ['This is a python docs'],'uris': None,'data': None}
Obter o número total de itens nas coleções
Podemos obter o número total de itens na coleção com o método count
.
collection.count()
2
Alterar a função de similaridade
Anteriormente, quando fizemos uma consulta, obtivemos a similaridade dos embeddings com nossa consulta, porque, por padrão, em uma coleção, a função de distância é usada, mas podemos especificar qual função de similaridade queremos usar. As possibilidades são
- L2 ao quadrado (
l2
) - Produto interno (
ip
) - Similaridade de cosseno (
cosine
)
Na postagem [Measurement of similarity between embeddings] (http://maximofn.com/embeddings-similarity/), vimos a similaridade L2 e cosseno, caso queira saber mais sobre elas.
Portanto, podemos criar coleções com outra função de similaridade com o argumento metadata={"hnsw:space": <function>}
.
collection = chroma_client.create_collection(name="colection_cosine", metadata={"hnsw:space": "cosine"})
Adicionar dados à coleção
Adicionar documentos
Vamos examinar os dados que temos na coleção novamente com o método peek
.
collection = chroma_client.create_collection(name="colection_cosine", metadata={"hnsw:space": "cosine"})collection.peek()
{'ids': [],'embeddings': [],'metadatas': [],'documents': [],'uris': None,'data': None}
Como podemos ver, ela está vazia, porque a última coleção que criamos foi a função de similaridade cosine
, mas não adicionamos dados a ela. Vamos ver como é isso, obtendo o nome da coleção
collection.name
'colection_cosine'
Assim, trazemos de volta a primeira coleção que criamos e para a qual inserimos dados.
collection = chroma_client.get_collection(name = "my_collection")
Agora podemos adicionar dados à coleção com o método add
.
collection = chroma_client.get_collection(name = "my_collection")collection.add(documents=["This is a Mojo docs", "This is Rust docs"],metadatas=[{"source": "Mojo source"}, {"source": "Rust source"}],ids=["id3", "id4"])
Como você pode ver, os IDs são consecutivos e não têm o mesmo valor de antes, pois os
IDs precisam ser exclusivos.
Se tentarmos adicionar dados repetindo ID
s, ele indicará que já existem dados com esses ID
s.
collection = chroma_client.get_collection(name = "my_collection")collection.add(documents=["This is a Mojo docs", "This is Rust docs"],metadatas=[{"source": "Mojo source"}, {"source": "Rust source"}],ids=["id3", "id4"])collection.add(documents=["This is a Pytorch docs", "This is TensorRT docs"],metadatas=[{"source": "Pytorch source"}, {"source": "TensorRT source"}],ids=["id3", "id4"])
Add of existing embedding ID: id3Add of existing embedding ID: id4Insert of existing embedding ID: id3Insert of existing embedding ID: id4
Não foi possível adicionar os documentos do Pytorch e do TensorRT.
Vamos dar uma olhada nos dados de coleta
collection.peek()
{'ids': ['id1', 'id2', 'id3', 'id4'],'embeddings': [[-0.06924048811197281,0.061624377965927124,-0.090973399579525,0.013923337683081627,0.006247623357921839,-0.1078396588563919,-0.012472339905798435,0.03485661745071411,-0.06300634145736694,-0.00880391988903284,0.06879935413599014,0.0564003586769104,0.07040536403656006,-0.020754728466272354,-0.04048658534884453,-0.006666888482868671,-0.0953674241900444,0.049781784415245056,0.021780474111437798,-0.06344643980264664,0.06119797006249428,0.0834411084651947,-0.034758951514959335,0.0029120452236384153,...{'source': 'JavaScript source'},{'source': 'Mojo source'},{'source': 'Rust source'}],'documents': ['This is a python docs','This is JavaScript docs','This is a Mojo docs','This is Rust docs'],'uris': None,'data': None}
Como pode ser visto, os conteúdos originais de ID3
e ID4
foram mantidos.
Adicionar incorporações
Podemos adicionar embeddings diretamente sem adicionar documentos. No entanto, isso não faz muito sentido, pois se adicionarmos apenas embeddings, quando quisermos fazer uma consulta, não haverá documentos para recuperar.
Obtemos algumas incorporações para criar outras com as mesmas dimensões.
embedding1 = collection.peek(1)['embeddings']len(embedding1), len(embedding1[0])
(1, 384)
Criamos novas incorporações com todos eles para sabermos quais foram criados.
new_embedding = [1] * len(embedding1[0])new_embedding = [new_embedding]len(new_embedding), len(new_embedding[0])
(1, 384)
Agora adicionamos os novos embeddings
collection.add(embeddings=new_embedding,metadatas=[{"source": "Only embeddings"}],ids=["id5"])
Vamos dar uma olhada nos dados de coleta
collection.add(embeddings=new_embedding,metadatas=[{"source": "Only embeddings"}],ids=["id5"])collection.peek()['embeddings'][-1]
[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,...,1.0,1.0,1.0,1.0,1.0]
O último elemento da condição tem as incorporações que adicionamos.
Observação: Se tentarmos adicionar bordados com um tamanho diferente dos que já estão na coleção, ocorrerá um erro.
new_embedding_differetn_size = [1] * (len(embedding1[0])-1)new_embedding_differetn_size = [new_embedding_differetn_size]len(new_embedding_differetn_size), len(new_embedding_differetn_size[0])
(1, 383)
Como você pode ver, a dimensão de incorporação é 383, em vez de 384.
collection.add(embeddings=new_embedding_differetn_size,metadatas=[{"source": "New embeddings different size"}],ids=["id6"])
---------------------------------------------------------------------------InvalidDimensionException Traceback (most recent call last)Cell In[28], line 1----> 1 collection.add(2 embeddings=new_embedding_differetn_size,3 metadatas=[{"source": "New embeddings different size"}],4 ids=["id6"]5 )File ~/miniforge3/envs/crhomadb/lib/python3.11/site-packages/chromadb/api/models/Collection.py:168, in Collection.add(self, ids, embeddings, metadatas, documents, images, uris)163 raise ValueError(164 "You must set a data loader on the collection if loading from URIs."165 )166 embeddings = self._embed(self._data_loader(uris))--> 168 self._client._add(ids, self.id, embeddings, metadatas, documents, uris)File ~/miniforge3/envs/crhomadb/lib/python3.11/site-packages/chromadb/telemetry/opentelemetry/__init__.py:127, in trace_method.<locals>.decorator.<locals>.wrapper(*args, **kwargs)125 global tracer, granularity126 if trace_granularity < granularity:--> 127 return f(*args, **kwargs)128 if not tracer:129 return f(*args, **kwargs)File ~/miniforge3/envs/crhomadb/lib/python3.11/site-packages/chromadb/api/segment.py:375, in SegmentAPI._add(self, ids, collection_id, embeddings, metadatas, documents, uris)365 records_to_submit = []366 for r in _records(367 t.Operation.ADD,368 ids=ids,(...)373 uris=uris,374 ):--> 375 self._validate_embedding_record(coll, r)376 records_to_submit.append(r)377 self._producer.submit_embeddings(coll["topic"], records_to_submit)File ~/miniforge3/envs/crhomadb/lib/python3.11/site-packages/chromadb/telemetry/opentelemetry/__init__.py:127, in trace_method.<locals>.decorator.<locals>.wrapper(*args, **kwargs)125 global tracer, granularity126 if trace_granularity < granularity:--> 127 return f(*args, **kwargs)128 if not tracer:129 return f(*args, **kwargs)File ~/miniforge3/envs/crhomadb/lib/python3.11/site-packages/chromadb/api/segment.py:799, in SegmentAPI._validate_embedding_record(self, collection, record)797 add_attributes_to_current_span({"collection_id": str(collection["id"])})798 if record["embedding"]:--> 799 self._validate_dimension(collection, len(record["embedding"]), update=True)File ~/miniforge3/envs/crhomadb/lib/python3.11/site-packages/chromadb/telemetry/opentelemetry/__init__.py:127, in trace_method.<locals>.decorator.<locals>.wrapper(*args, **kwargs)125 global tracer, granularity126 if trace_granularity < granularity:--> 127 return f(*args, **kwargs)128 if not tracer:129 return f(*args, **kwargs)File ~/miniforge3/envs/crhomadb/lib/python3.11/site-packages/chromadb/api/segment.py:814, in SegmentAPI._validate_dimension(self, collection, dim, update)812 self._collection_cache[id]["dimension"] = dim813 elif collection["dimension"] != dim:--> 814 raise InvalidDimensionException(815 f"Embedding dimension {dim} does not match collection dimensionality {collection['dimension']}"816 )817 else:818 returnInvalidDimensionException: Embedding dimension 383 does not match collection dimensionality 384
Adicionar documentos e embeddings
O Chroma também permite adicionar documentos e embeddings ao mesmo tempo. Portanto, se isso for feito, ele não criará os embeddings do documento.
collection.add(documents=["This is a Pytorch docs"],embeddings=new_embedding,metadatas=[{"source": "Pytorch source"}],ids=["id6"])
Se observarmos os embeddings do último elemento da coleção, veremos que eles são os que adicionamos.
collection.add(documents=["This is a Pytorch docs"],embeddings=new_embedding,metadatas=[{"source": "Pytorch source"}],ids=["id6"])collection.peek()['embeddings'][-1]
[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,...,1.0,1.0,1.0,1.0,1.0]
Consultas
Consultas por documentos
Para fazer uma consulta, usamos o método query
. Com o parâmetro n_results
, podemos especificar quantos resultados queremos obter.
collection.query(query_texts=["python"],n_results=1,)
{'ids': [['id1']],'distances': [[0.5389559268951416]],'metadatas': [[{'source': 'Python source'}]],'embeddings': None,'documents': [['This is a python docs']],'uris': None,'data': None}
Se, em vez de n_results = 1
, definirmos um valor maior, ele retornará mais resultados.
collection.query(
query_texts=["python"],
n_results=10,
)
Podemos filtrar por um valor de metadados com o argumento where
.
collection.query(
query_texts=["python"],
n_results=10,
where={"source": "Python source"}
)
Podemos ver que apenas um resultado já foi retornado
Também podemos filtrar pelo conteúdo do documento com o argumento where_document
.
collection.query(
query_texts=["python"],
n_results=10,
where_document={"$contains": "python"}
)
Mais tarde, falaremos mais sobre as possibilidades que temos aqui
Quando fazemos uma consulta, podemos dizer quais dados queremos que sejam retornados, por exemplo, apenas os embeddings, apenas os metadados ou vários dados, especificando-os em uma lista usando o argumento include
.
collection.query(
query_texts=["python"],
n_results=10,
include=["documents", "distances"]
)
Vemos que metadatas
agora é None
.
Várias consultas ao mesmo tempo
Podemos consultar a coleção várias vezes ao mesmo tempo, passando uma lista para o parâmetro query_texts
.
collection.query(query_texts=["python"],n_results=10,)collection.query(query_texts=["python"],n_results=10,where={"source": "Python source"})collection.query(query_texts=["python"],n_results=10,where_document={"$contains": "python"})collection.query(query_texts=["python"],n_results=10,include=["documents", "distances"])collection.query(query_texts=["programming language", "high level", "multi propuse"],n_results=1,)
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6{'ids': [['id1'], ['id1'], ['id3']],'distances': [[1.152251958847046], [1.654376745223999], [1.6786067485809326]],'metadatas': [[{'source': 'Python source'}],[{'source': 'Python source'}],[{'source': 'Mojo source'}]],'embeddings': None,'documents': [['This is a python docs'],['This is a python docs'],['This is a Mojo docs']],'uris': None,'data': None}
Para cada consulta, ele retornou um resultado
Isso é muito útil quando o banco de dados está hospedado em um servidor e somos cobrados por cada consulta que fazemos. Portanto, em vez de fazer uma consulta para cada pergunta que temos, fazemos uma consulta com todas as perguntas que temos.
Consultas para incorporações
Quando consultamos os documentos, o que o chroma faz é calcular a incorporação dos textos de consulta
e procurar os documentos que mais se assemelham a essa incorporação. Mas se já tivermos a incorporação, podemos fazer a consulta diretamente com a incorporação.
Primeiro, vamos obter a incorporação de uma consulta com a mesma função de incorporação das coleções.
query_texts = ["python language"]query_embeddings = collection._embedding_function(query_texts)query_embeddings
[[-0.04816831275820732,0.014662696048617363,-0.031021444126963615,0.008308809250593185,-0.07176128774881363,-0.10355626791715622,0.06690476089715958,0.04229631647467613,-0.03681119903922081,-0.04993892088532448,0.03186540678143501,0.015252595767378807,0.0642094686627388,0.018130118027329445,0.016300885006785393,-0.028082313016057014,-0.03994889184832573,0.023195551708340645,0.004547565709799528,-0.11764183640480042,0.019792592152953148,0.0496944822371006,-0.013253907673060894,0.03610404208302498,0.030529780313372612,-0.01815914921462536,-0.009753326885402203,0.03412770479917526,0.03020440600812435,...0.02079579420387745,-0.00972712505608797,0.13462257385253906,0.15277136862277985,-0.028574923053383827]]
Agora podemos fazer a consulta com a incorporação
collection.query(query_embeddings=query_embeddings,n_results=1,)
{'ids': [['id1']],'distances': [[0.6297433376312256]],'metadatas': [[{'source': 'Python source'}]],'embeddings': None,'documents': [['This is a python docs']],'uris': None,'data': None}
Como antes, podemos obter mais resultados aumentando o valor do parâmetro n_results
e podemos filtrar com os parâmetros where
e where_document
. Também podemos fazer várias consultas ao mesmo tempo e especificar quais dados queremos retornar com o parâmetro include
.
Observação: se tentarmos fazer uma consulta com uma incorporação de uma dimensão diferente das que já estão na coleção, receberemos um erro.
Recuperar documentos por ID
.
Se soubermos o ID
de um documento, poderemos recuperar o documento com o método get
.
collection.get(ids=["id1"],)
{'ids': ['id1'],'embeddings': None,'metadatas': [{'source': 'Python source'}],'documents': ['This is a python docs'],'uris': None,'data': None}
Também é possível recuperar vários documentos de uma só vez.
collection.get(ids=["id1", "id2", "id3"],)
{'ids': ['id1', 'id2', 'id3'],'embeddings': None,'metadatas': [{'source': 'Python source'},{'source': 'JavaScript source'},{'source': 'Mojo source'}],'documents': ['This is a python docs','This is JavaScript docs','This is a Mojo docs'],'uris': None,'data': None}
Como antes, podemos filtrar com os argumentos where
e where_document
. Também podemos fazer várias consultas ao mesmo tempo e especificar quais dados queremos retornar com o parâmetro include
.
Filtragem
Como vimos, você pode filtrar por metadados com o parâmetro where
e pelo conteúdo do documento com o parâmetro where_document
.
Filtragem por metadados
Como os metadados entrarão em mim como um dicionário
collection.add(
documents=["This is a python docs", "This is JavaScript docs"],
metadatas=[{"source": "Python source"}, {"source": "JavaScript source"}],
ids=["id1", "id2"]
)
```
A primeira coisa que temos de fazer é indicar a chave dos metadados pelos quais queremos filtrar. Em seguida, temos de colocar um operador e o valor
````python
{
"metadata_field": {
<Operador>: <Valor>.
}
}
```
Os valores possíveis do oerador são
$eq - igual a (string, int, float) $ne - não é igual a (string, int, float) $gt - maior que (int, float) $gte - maior ou igual a (int, float) $lt - menor que (int, float) $lte - menor que ou igual a (int, float)
Vamos dar uma olhada em uma consulta
collection.query(query_texts=["python"],n_results=1,where={"source":{"$eq": "Python source"}})
{'ids': [['id1']],'distances': [[0.5389559268951416]],'metadatas': [[{'source': 'Python source'}]],'embeddings': None,'documents': [['This is a python docs']],'uris': None,'data': None}
Se não colocarmos o operador, o padrão será $eq
, ou seja, isto
{
"metadata_field": {
<"$eq">: <Value>.
}
}
```
É o mesmo que isto
````python
{
"metadata_field": <Value>.
}
```
Observação**: O Chroma só pesquisará dados que tenham os metadados
source
; por exemplo, se você pesquisarwhere={"version": {"$ne": 1}}
, ele só retornará dados que tenham uma chaveversion
em seus metadados e que não sejam 1.
Filtragem por conteúdo do documento
Ao filtrar pelo conteúdo do documento, temos duas chaves possíveis: $contains
e $not_contains
.
Por exemplo, procuramos dados na coleção em que a palavra "python" aparece em seu documento.
collection.query(
query_texts=["python"],
n_results=10,
where_document={"$contains": "python"}
)
E todos os dados na coleção em que a palavra python
não aparece em seu documento
collection.query(
query_texts=["python"],
n_results=10,
where_document={"$not_contains": "python"}
)
Também podemos usar os operadores lógicos $and
e $or
para fazer consultas mais complexas.
{
"$e": [
{
<Operador>: <Valor>.
},
{
<Operador>: <Valor>.
}
}
```
````python
{
"$or": [
{
<Operador>: <Valor>.
},
{
<Operador>: <Valor>.
}
}
```
Por exemplo, pesquisamos todos os documentos que contêm as palavras python
e docs
.
collection.query(
query_texts=["python"],
n_results=10,
where_document=
{
"$and": [
{"$contains": "python"},
{"$contains": "docs"},
],
},
)
Atualizar dados
Qualquer item de dados pode ser atualizado com o método update.
collection.query(query_texts=["python"],n_results=10,where_document={"$contains": "python"})collection.query(query_texts=["python"],n_results=10,where_document={"$not_contains": "python"})collection.query(query_texts=["python"],n_results=10,where_document={"$and": [{"$contains": "python"},{"$contains": "docs"},],},)collection.update(ids=["id1"],documents=["This is a updated Python docs"])
Vamos ver se ela foi atualizada
collection.query(
query_texts=["python"],
n_results=10,
where_document={"$contains": "Python"}
)
Observação: se você tentar atualizar um
ID
que não existe, ocorrerá um erro.
Observação: se você tentar atualizar uma incorporação com outra incorporação de tamanho diferente, ocorrerá um erro.
Atualizar ou adicionar dados
Com o método upsert
, podemos atualizar um dado se ele já existir ou adicioná-lo se ele não existir.
collection.query(query_texts=["python"],n_results=10,where_document={"$contains": "python"})collection.query(query_texts=["python"],n_results=10,where_document={"$not_contains": "python"})collection.query(query_texts=["python"],n_results=10,where_document={"$and": [{"$contains": "python"},{"$contains": "docs"},],},)collection.update(ids=["id1"],documents=["This is a updated Python docs"])collection.query(query_texts=["python"],n_results=10,where_document={"$contains": "Python"})collection.upsert(ids=["id6"],documents=["This is a Pytorch docs"],metadatas=[{"source": "Pytorch source"}],)
Vamos ver se ele foi adicionado à coleção
collection.query(query_texts=["python"],n_results=10,where_document={"$contains": "python"})collection.query(query_texts=["python"],n_results=10,where_document={"$not_contains": "python"})collection.query(query_texts=["python"],n_results=10,where_document={"$and": [{"$contains": "python"},{"$contains": "docs"},],},)collection.update(ids=["id1"],documents=["This is a updated Python docs"])collection.query(query_texts=["python"],n_results=10,where_document={"$contains": "Python"})collection.upsert(ids=["id6"],documents=["This is a Pytorch docs"],metadatas=[{"source": "Pytorch source"}],)collection.peek()
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6{'ids': ['id1', 'id2', 'id3', 'id4', 'id5', 'id6'],'embeddings': [[-0.08374718576669693,0.01027572900056839,-0.04819200187921524,0.01758415624499321,0.013158757239580154,-0.11435151100158691,-0.024248722940683365,-0.01319972239434719,-0.09626100957393646,-0.010561048053205013,0.09369225800037384,0.06017905846238136,0.031283188611269,0.014855983667075634,-0.0015984248602762818,0.023238031193614006,-0.04709107056260109,-0.007838696241378784,0.012870412319898605,-0.028354981914162636,-0.007653804495930672,0.09018168598413467,0.060235824435949326,0.0005205210763961077,...0.014388148672878742]],'metadatas': [{'source': 'Python source'},{'source': 'JavaScript source'},{'source': 'Mojo source'},{'source': 'Rust source'},{'source': 'Only embeddings'},{'source': 'Pytorch source'}],'documents': ['This is a updated Python docs','This is JavaScript docs','This is a Mojo docs','This is Rust docs',None,'This is a Pytorch docs'],'uris': None,'data': None}
Vemos que sim
Excluir dados
Podemos excluir dados de uma coleção com o método delete
.
Removeremos os dados com ID
id5
, que é o que adicionamos com sua incorporação de todos os dados
collection.delete(ids=["id5"])
Vamos ver se ele foi removido
collection.delete(ids=["id5"])collection.peek()
{'ids': ['id1', 'id2', 'id3', 'id4', 'id6'],'embeddings': [[-0.08374718576669693,0.01027572900056839,-0.04819200187921524,0.01758415624499321,0.013158757239580154,-0.11435151100158691,-0.024248722940683365,-0.01319972239434719,-0.09626100957393646,-0.010561048053205013,0.09369225800037384,0.06017905846238136,0.031283188611269,0.014855983667075634,-0.0015984248602762818,0.023238031193614006,-0.04709107056260109,-0.007838696241378784,0.012870412319898605,-0.028354981914162636,-0.007653804495930672,0.09018168598413467,0.060235824435949326,0.0005205210763961077,...0.07033486664295197,0.014388148672878742]],'metadatas': [{'source': 'Python source'},{'source': 'JavaScript source'},{'source': 'Mojo source'},{'source': 'Rust source'},{'source': 'Pytorch source'}],'documents': ['This is a updated Python docs','This is JavaScript docs','This is a Mojo docs','This is Rust docs','This is a Pytorch docs'],'uris': None,'data': None}
Vemos que não é mais
Embeddings
Conforme mencionado acima, podemos usar diferentes funções de embeddings e, se nenhuma for especificada, ele usará all-MiniLM-L6-v2
. Na página chroma embeddings documentation, podemos ver as diferentes funções de embeddings que podemos usar. Como isso pode mudar e algumas delas são pagas e exigem uma chave de API, explicaremos apenas como usar as do HuggingFace.
Primeiro, definimos a função de incorporação
import chromadb.utils.embedding_functions as embedding_functionshuggingface_ef = embedding_functions.HuggingFaceEmbeddingFunction(api_key="YOUR_API_KEY",model_name="sentence-transformers/all-mpnet-base-v2")
No meu caso, utilizo o sentence-transformers/all-mpnet-base-v2
, que é o mais baixado dos sentence-transformers
no momento em que escrevo esta postagem.
Para adicionar agora a função de incorporação à coleção, precisamos adicionar o argumento metadata={"embedding": <function>}
.
import chromadb.utils.embedding_functions as embedding_functionshuggingface_ef = embedding_functions.HuggingFaceEmbeddingFunction(api_key="YOUR_API_KEY",model_name="sentence-transformers/all-mpnet-base-v2")collection = chroma_client.create_collection(name="colection_huggingface",embedding_function=huggingface_ef)
Podemos verificar se adicionamos a nova função de incorporação calculando as incorporações de uma palavra.
import chromadb.utils.embedding_functions as embedding_functionshuggingface_ef = embedding_functions.HuggingFaceEmbeddingFunction(api_key="YOUR_API_KEY",model_name="sentence-transformers/all-mpnet-base-v2")collection = chroma_client.create_collection(name="colection_huggingface",embedding_function=huggingface_ef)embedding = collection._embedding_function(["python"])len(embedding), len(embedding[0])
(1, 768)
O comprimento da incorporação é de 768
Se agora calcularmos a incorporação com a função de incorporação da coleção anterior
collection = chroma_client.get_collection(name = "my_collection")
collection = chroma_client.get_collection(name = "my_collection")embedding = collection._embedding_function(["python"])len(embedding), len(embedding[0])
(1, 384)
Vemos que agora o comprimento da incorporação é 384, ou seja, usamos uma nova função de incorporação antes.
Multimodalidade
Podemos adicionar embeddings de imagem, pois o chroma tem o OpenCLIP incorporado. O OpenCLIP é uma implementação de código aberto do CLIP (Contrastive Language-Image Pre-Training), que é uma rede neural OpenAI capaz de fornecer uma descrição de uma imagem.
Para usar o OpenCLIP, temos que instalá-lo com o pip
pip install open-clip-torch
```
Depois de instalado, podemos usá-lo para criar embeddings da seguinte imagem
Eu o tenho em meu caminho local ../images/chromadb_dalle3.webp
.
from chromadb.utils.embedding_functions import OpenCLIPEmbeddingFunctionembedding_function = OpenCLIPEmbeddingFunction()image = "../images/chromadb_dalle3.webp"embedding = embedding_function(image)len(embedding), len(embedding[0])
(30, 512)
Como podemos ver, ele cria uma incorporação de tamanho 30x512.
O Chroma também vem com um carregador de imagens.
from chromadb.utils.data_loaders import ImageLoaderdata_loader = ImageLoader()data = data_loader._load_image(image)type(data), data.shape
(numpy.ndarray, (1024, 1024, 3))
Portanto, podemos criar uma coleção multimodal com essa função de incorporação e o carregador de imagens.
collection = chroma_client.create_collection(name="multimodal_collection",embedding_function=embedding_function,data_loader=data_loader)
E podemos adicionar os embeddings das imagens a ele.
collection = chroma_client.create_collection(name="multimodal_collection",embedding_function=embedding_function,data_loader=data_loader)collection.add(ids=['id1'],images=[image])
Vamos ver o que ele salvou
collection = chroma_client.create_collection(name="multimodal_collection",embedding_function=embedding_function,data_loader=data_loader)collection.add(ids=['id1'],images=[image])collection.peek()
{'ids': ['id1'],'embeddings': [[-0.014372998848557472,0.0063015008345246315,-0.03794914484024048,-0.028725482523441315,-0.014304812066257,-0.04323698952794075,0.008670451119542122,-0.016066772863268852,-0.02365742437541485,0.07881983369588852,0.022775636985898018,0.004407387692481279,0.058205753564834595,-0.02389293536543846,-0.027586588636040688,0.05778728798031807,-0.2631031572818756,0.044124454259872437,0.010588622651994228,-0.035578884184360504,-0.041719693690538406,-0.0033654430881142616,-0.04731074720621109,-0.0019943572115153074,...0.04397008568048477,0.04396628588438034]],'metadatas': [None],'documents': [None],'uris': None,'data': None}
O Chroma não armazena as imagens, apenas os embeddings, portanto, para não perder a relação entre os embeddings e as imagens, podemos salvar o caminho para as imagens nos metadados. Vamos usar o método update
para adicionar o caminho à imagem
collection.update(ids=['id1'],images=[image],metadatas=[{"source": image}])
Se voltarmos e olharmos para o que a coleção tem reservado
collection.update(ids=['id1'],images=[image],metadatas=[{"source": image}])collection.peek()
{'ids': ['id1'],'embeddings': [[-0.014372998848557472,0.0063015008345246315,-0.03794914484024048,-0.028725482523441315,-0.014304812066257,-0.04323698952794075,0.008670451119542122,-0.016066772863268852,-0.02365742437541485,0.07881983369588852,0.022775636985898018,0.004407387692481279,0.058205753564834595,-0.02389293536543846,-0.027586588636040688,0.05778728798031807,-0.2631031572818756,0.044124454259872437,0.010588622651994228,-0.035578884184360504,-0.041719693690538406,-0.0033654430881142616,-0.04731074720621109,-0.0019943572115153074,...0.04397008568048477,0.04396628588438034]],'metadatas': [{'source': '../images/chromadb_dalle3.webp'}],'documents': [None],'uris': None,'data': None}
Como a coleção é multimodal, podemos adicionar documentos a ela como antes.
collection.add(ids=['id2', 'id3'],documents=["This is a python docs", "This is JavaScript docs"],metadatas=[{"source": "Python source"}, {"source": "JavaScript source"}])collection.peek()
{'ids': ['id1', 'id2', 'id3'],'embeddings': [[-0.014372998848557472,0.0063015008345246315,-0.03794914484024048,-0.028725482523441315,-0.014304812066257,-0.04323698952794075,0.008670451119542122,-0.016066772863268852,-0.02365742437541485,0.07881983369588852,0.022775636985898018,0.004407387692481279,0.058205753564834595,-0.02389293536543846,-0.027586588636040688,0.05778728798031807,-0.2631031572818756,0.044124454259872437,0.010588622651994228,-0.035578884184360504,-0.041719693690538406,-0.0033654430881142616,-0.04731074720621109,-0.0019943572115153074,...-0.061795610934495926,-0.02433035336434841]],'metadatas': [{'source': '../images/chromadb_dalle3.webp'},{'source': 'Python source'},{'source': 'JavaScript source'}],'documents': [None, 'This is a python docs', 'This is JavaScript docs'],'uris': None,'data': None}
Por fim, podemos fazer consultas com texto
collection.query(
query_texts=["persona trabajando en una mesa"],
)
Com o texto, não obtivemos a imagem como o primeiro resultado, se eu seguir a documentação do python
Mas também podemos criá-las com imagens. Neste caso, vou fazer isso com esta imagem
query_image = "https://pub-fb664c455eca46a2ba762a065ac900f7.r2.dev/chromadb_elegant.webp"
collection.query(
query_images=[query_image],
)
Agora, ele apresenta como primeiro resultado a imagem que salvamos