Bases de datos vectoriales
Hemos visto en el post de embeddings que los embeddings son una forma de representar palabras en un espacio vectorial. En este post vamos a ver cómo podemos almacenar esos embeddings en bases de datos vectoriales y cómo podemos hacer consultas sobre ellas.
Cuando tenemos una consulta, podemos crear el embedding de la consulta, buscar en la base de datos vectorial los embeddings que más se parezcan a la consulta y devolver los documentos que correspondan a esos embeddings o una explicaión sobre esos documentos.
Es decir, vamos a generar una base de datos de información, vamos a crear embeddings de esa información y la vamos a guardar en una base de datos vectorial. Luego cuando un usuario haga una consulta, convertiremos la consulta a embeddings, buscaremos en la base de datos los embeddings con mayor similitud y devolveremos los documentos que correspondan a esos embeddings.
Además de los documentos, en la base de datos se puede guardar información adicional que llamaremos metadata. Por ejemplo, si estamos trabajando con un conjunto de noticias, podemos guardar el título, la fecha, el autor, etc. de la noticia.
Chroma
En este post vamos a ver crhoma, ya que es la base de datos vectorial más usada, como se puede ver en este reporte del langchain state of ai 2023.
Instalación
De modo que para instalar chroma con conda hay que hacer
conda install conda-forge::chromadb
O si se quiere instalar con pip
pip install chromadb
Uso rápido
Para una aplicación rápida, primero importamos chroma
import chromadb
A continuación creamos un cliente de chroma
import chromadbchroma_client = chromadb.Client()
Creamos una colección. Una colección es el lugar donde se guardarán los embeddings, los embeddings y la metadata.
import chromadbchroma_client = chromadb.Client()collection = chroma_client.create_collection(name="my_collection")
Como vemos sale un mensaje indicando que no se ha introducido una función de embeddings y por lo tanto usará por defecto all-MiniLM-L6-v2
, que es similar al modelo paraphrase-MiniLM-L6-v2
que usamos en el post de embeddings.
Más adelante veremos esto, pero podemos elegir cómo vamos a generar los embeddings.
Ahora añadimos documentos, ids y metadata a la colección
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"])
Ahora podemos hacer una 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 vemos la distancia al id1 es menor a la distancia al id2, por lo que parece que el documento 1 es más apropiado para responder la consulta
Bases de datos persistentes
La base de datos que hemos creado antes es temporal, en cuanto cerremos el notebook desaparecerá. Por lo que para crear una base de datos persistente hay que pasarle a chroma el path donde guardarla
Primero vamos a crear la carpeta donde guardar la base de datos
from pathlib import Pathchroma_path = Path("chromadb")chroma_path.mkdir(exist_ok=True)
Ahora creamos un cliente en la carpeta que hemos creado
from pathlib import Pathchroma_path = Path("chromadb")chroma_path.mkdir(exist_ok=True)chroma_client_persistent = chromadb.PersistentClient(path = str(chroma_path))
Colecciones
Crear colecciones
A la hora de crear una colección hay que especificar un nombre. El nombre tiene que tener las siguientes consideraciones:
- La longitud del nombre debe tener entre 3 y 63 caracteres.
- El nombre debe comenzar y terminar con una letra minúscula o un dígito y puede contener puntos, guiones y guiones bajos en el medio.
- El nombre no debe contener dos puntos consecutivos.
- El nombre no debe ser una dirección IP válida.
También podemos darle una función de embedding. En caso de no darle una usará por defecto la función 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 se puede ver, se ha creado una segunda colección para el mismo cliente chroma_client
, por lo que para un solo cliente podemos tener varias colecciones.
Recuperar colecciones
Si queremos recuperar una colección de un cliente lo podemos hacer con el 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 o crear colecciones
Podemos obtener colecciones y que en caso de que no existan las cree con el 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")
Borrar colecciones
Podemos borrar una colección con el 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")
Obtener items de las colecciones
Podemos obtener los 10 primeros items de la colección con el 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}
En este caso solo se han obtenido dos, porque nuestra colección solo tiene dos documentos
Si se quiere obtener otra cantidad de items se puede especificar con el 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}
Obtener el número total de items de las colecciones
Podemos obtener el número total de items de la colección con el método count
collection.count()
2
Cambiar la función de similitud
Antes, cuando hicimos una consulta obtuvimos la similitud de los embeddings con nuestra consulta, ya que por defecto en una colección se usa la función de distancia, pero podemos especificar qué función de similitud queremos usar. Las posiilidades son
- Squared L2 (
l2
) - Inner product (
ip
) - Cosine similarity (
cosine
)
En el post Medida de similitud entre embeddings vimos L2 y cosine similarity, por si quieres profundizar en ellas.
Por lo que podemos crear colecciones con otra función de similitud con el argumento metadata={"hnsw:space": <funtion>}
collection = chroma_client.create_collection(name="colection_cosine", metadata={"hnsw:space": "cosine"})
Añadir datos a la colección
Añadir documentos
Vamos a volver a ver los datos que tenemos en la colección con el 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 vemos está vacío, eso es porque la última colección que hemos creado ha sido la de la función de similitud cosine
, pero no le hemos añadido datos. Veamos como es así obteniendo el nombre de la colección
collection.name
'colection_cosine'
Por lo que nos volvemos a traer la primera colección que hemos creado, que sí le hemos introducido datos
collection = chroma_client.get_collection(name = "my_collection")
Ahora ya podemos añadir datos a la collección con el 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 se puede ver los ID
s son consecutivos y no tienen el mismo valor que ya tenían antes, ya que los ID
s tienen que ser únicos.
Si intentamos añadir datos repitiendo ID
s, nos idiciará que ya existían datos con esas 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
No hemos podido añadir los documentos de Pytorch y TensorRT
Veamos los datos de la colección
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 vemos se han mantenido los contenidos originales de ID3
e ID4
Añadir embeddings
Podemos añadir embeddings directamente sin añadir documentos. Aunque esto no tiene mucho sentido, ya que si solo añadimos los embeddings, cuando queramos hacer una consulta no habrá documentos que recuperar
Obtenemos unos embeddings para poder crear otros con las mismas dimensiones
embedding1 = collection.peek(1)['embeddings']len(embedding1), len(embedding1[0])
(1, 384)
Creamos unos embeddings nuevos con todo unos para saber que son los que hemos creado
new_embedding = [1] * len(embedding1[0])new_embedding = [new_embedding]len(new_embedding), len(new_embedding[0])
(1, 384)
Ahora añadimos los nuevos embeddings
collection.add(embeddings=new_embedding,metadatas=[{"source": "Only embeddings"}],ids=["id5"])
Vamos a ver los datos de la colección
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]
El último elemento de la condición tiene los embeddings que hemos añadido
Nota: Si intentamos añadir embbedings con un tamaño diferente a los que ya hay en la colección, nos dará un error
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 se puede ver la dimensión del embedding es 383, en 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
Añadir documentos y embeddings
Chroma nos permite también añadir documentos y embeddings a la vez. De modo que si se hace esto, no creará los embeddings del documento
collection.add(documents=["This is a Pytorch docs"],embeddings=new_embedding,metadatas=[{"source": "Pytorch source"}],ids=["id6"])
Si miramos los embeddings del último elemento de la colección, veremos que son los que hemos añadido
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 hacer una consulta usamos el método query
. Con el parámetro n_results
podemos especificar cuántos resultados queremos obtener
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}
Si en vez de n_results = 1
ponemos un valor mayor, nos devolverá más resultados
collection.query(
query_texts=["python"],
n_results=10,
)
Podemos filtrar por un valor de metadato con el argumento where
collection.query(
query_texts=["python"],
n_results=10,
where={"source": "Python source"}
)
Vemos que ya solo nos devuelve 1 resultado
También podemos filtrar por el contenido del documento con el argumento where_document
collection.query(
query_texts=["python"],
n_results=10,
where_document={"$contains": "python"}
)
Más adelante veremos las posibilidades que tenemos aquí
Cuando hacemos una consulta podemos decir qué datos queremos que nos devuelva, por ejemplo solo los embeddings, solo la metadata, o varios datos especificándoselo en una lista mediante el argumento include
collection.query(
query_texts=["python"],
n_results=10,
include=["documents", "distances"]
)
Vemos que ahora metadatas
es None
Varias consultas a la vez
Podemos hacerle a lacolección varias consultas a la vez, para ello, le pasamos una lista al 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 me ha devuelto un resultado
Esto es muy útil cuando la base de datos está alojada en un servidor y nos cobran por cada consulta que hacemos. Por lo que en vez de hacer una consulta por cada duda que tengamos, hacemos una consulta con todas
Consultas por embeddings
Cuando hacemos una consulta por documentos, lo que hace chroma es calcular el embedding del query_texts
y buscar los documentos que más se parezcan a ese embedding. Pero si ya tenemos el embedding, podemos hacer la consulta directamente con el embedding
Vamos primero a obtener el embedding de una consulta con la misma función de embedding de las coleccion
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]]
Ahora podemos hacer la consulta con el embedding
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}
Al igual que antes podemos obtener más resultados aumentando el valor del parámetro n_results
, y podemos filtrar con los parámetros where
y where_document
. También podemos hacer varias consultas de una vez, y podemos especificar qué datos queremos que nos devuelva con el parámetro include
Nota: Si intentamos hacer una consulta con un embedding de diferente dimensión a los que ya hay en la colección, nos dará un error
Recuperar documentos por ID
Si sabemos la ID
de un documento, podemos recuperar el documento con el 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}
También se pueden recuperar varios documentos de una sola 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}
Al igual que antes podemos filtrar con los argumentos where
y where_document
. También podemos hacer varias consultas de una vez, y podemos especificar qué datos queremos que nos devuelva con el parámetro include
Filtrado
Como habíamos visto se pueden realizar filtrados por metadata con el parámetro where
, y por el contenido del documento con el parámetro where_document
Filtrado por metadata
Como los metadatos me introducian como un diccionario
collection.add(
documents=["This is a python docs", "This is JavaScript docs"],
metadatas=[{"source": "Python source"}, {"source": "JavaScript source"}],
ids=["id1", "id2"]
)
Lo primero que tenemos que hacer es indicar la llave de la metadata por la que queremos filtrar. A continuación tenemos que poner un operador y el valor
{
"metadata_field": {
<Operator>: <Value>
}
}
Los posibles valores del oerador son
- $eq - equal to (string, int, float)
- $ne - not equal to (string, int, float)
- $gt - greater than (int, float)
- $gte - greater than or equal to (int, float)
- $lt - less than (int, float)
- $lte - less than or equal to (int, float)
Vamos a ver ahora una 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}
Si no ponemos operador, por defecto será $eq
, es decir, esto
{
"metadata_field": {
<"$eq">: <Value>
}
}
Es lo mismo que esto
{
"metadata_field": <Value>
}
Nota: Chroma solo buscará en los datos que tengan el metadato
source
, por ejemplo si se hace la búsquedawhere={"version": {"$ne": 1}}
solo devolverá los datos que en su metadata haya una keyversion
y que no sea 1
Filtrado por el contenido del documento
A la hora de filtrar por el contenido del documento tenemos dos posibles llaves $contains
and $not_contains
Por ejemplo, buscamos los datos de la colección en los que aparece la palabra python
en su documento
collection.query(
query_texts=["python"],
n_results=10,
where_document={"$contains": "python"}
)
Y todos los datos de la colección en los que no aparece la palabra python
en su documento
collection.query(
query_texts=["python"],
n_results=10,
where_document={"$not_contains": "python"}
)
Adeás podemos usar los operadores lógicos $and
y $or
para hacer consultas más complejas
{
"$and": [
{
<Operator>: <Value>
},
{
<Operator>: <Value>
}
]
}
{
"$or": [
{
<Operator>: <Value>
},
{
<Operator>: <Value>
}
]
}
Por ejemplo buscamos todos los documentos en los que aparecen las palabras python
y docs
collection.query(
query_texts=["python"],
n_results=10,
where_document=
{
"$and": [
{"$contains": "python"},
{"$contains": "docs"},
],
},
)
Actualizar datos
Cualquier item de un dato se puede actualizar con el 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 a ver si se a actualizado
collection.query(
query_texts=["python"],
n_results=10,
where_document={"$contains": "Python"}
)
Nota: Si intentamos actualizar un
ID
que no existe, nos dará un error
Nota: Si intentamos actualizar un embeddings con otro de distinta dimension, nos dará un error
Actualizar o añadir datos
Con el método upsert
podemos actualizar un dato si ya existe, o añadirlo si no existe
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"}],)
Veamos si se a añadido a la colección
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 sí
Eliminar datos
Podemos eliminar datos de una colección con el método delete
Vamos a eliminar el dáto con ID
id5
que es el que añadimos con su embedding todo a unos
collection.delete(ids=["id5"])
Vamos a ver si se ha eliminado
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 ya no está
Embeddings
Como hemos dicho podemos usar distintas funciones de embeddings y si no se le especifica ninguna usará all-MiniLM-L6-v2
. En la página de la documentación de embeddings de chroma podemos ver las distintas funciones de embeddings que podemos usar. Como esto es algo que puede ir cambiando, y además algunas son de pago y requieren api key, vamos a explicar solo cómo usar las de HuggingFace
Primero establecemos la función de embedding
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")
En mi caso uso sentence-transformers/all-mpnet-base-v2
que es la más descargada de sentence-transformers
en el momento de escribir este post
Para ahora añadir la función de embedding a la colección, tenemos que añadir el 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 comprobar que hemos añadido la nueva función de embedding lo podemos hacer calculando los embeddings de una palabra
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)
La longitud del embedding es de 768
Si ahora calculamos el embedding con la función de embedding de la colección 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 ahora la longitud del ambedding es 384, es decir, sí habíamos usado antes una nueva función de embedding
Multimodalidad
Podemos añadir embeddings de imágenes ya que chroma tiene incorporado OpenCLIP. OpenCLIP es una implementación open source de CLIP (Contrastive Language-Image Pre-Training), que es una red neuronal de OpenAI la cual es capaz de dar una descripción de una imagen
Para poder usar OpenCLIP, tenemos que instalarlo con pip
pip install open-clip-torch
Una vez instalado podemos usarlo para crear embeddings de la siguiente foto
Que la tengo en mi path 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 vemos crea un embeddings de tamaño 30x512
Chroma también trae un cargador de imágenes
from chromadb.utils.data_loaders import ImageLoaderdata_loader = ImageLoader()data = data_loader._load_image(image)type(data), data.shape
(numpy.ndarray, (1024, 1024, 3))
Así que podemos crear una colección multimodal con esta función de embedding y el cargador de imágenes
collection = chroma_client.create_collection(name="multimodal_collection",embedding_function=embedding_function,data_loader=data_loader)
Y podemos añadirle los embeddings de las imágenes
collection = chroma_client.create_collection(name="multimodal_collection",embedding_function=embedding_function,data_loader=data_loader)collection.add(ids=['id1'],images=[image])
Vamos a ver qué ha guardado
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}
Chroma no almacena las imágenes, solo los embeddings, por lo que para no perder la relación entre los embeddings y las imágenes, podemos guardar la ruta de las imágenes en la metadata. Vamos a usar el método update
para añadir la ruta de la imagen
collection.update(ids=['id1'],images=[image],metadatas=[{"source": image}])
Si volvemos a ver qué tiene guardado la colección
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 la colección es multimodal, podemos añadirle documentos igual que 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 último, podemos hacer consulatas con texto
collection.query(
query_texts=["persona trabajando en una mesa"],
)
Con texto no nos ha dado la imagen como primer resultado, siuno la documentación de python
Pero también podemos hacerlas con imágenes, en este caso la voy a hacer con esta imagen
query_image = "https://pub-fb664c455eca46a2ba762a065ac900f7.r2.dev/chromadb_elegant.webp"
collection.query(
query_images=[query_image],
)
Ahora sí da como primer resultado la imgen que habíamos guardado