ChromaDB

ChromaDB ChromaDB

Bases de datos vectorialeslink image 0

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.

vector database

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.

Chromalink image 1

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.

Most Used Vectorstores

Instalaciónlink image 2

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ápidolink image 3

Para una aplicación rápida, primero importamos chroma

	
import chromadb
Copy

A continuación creamos un cliente de chroma

	
import chromadb
chroma_client = chromadb.Client()
Copy

Creamos una colección. Una colección es el lugar donde se guardarán los embeddings, los embeddings y la metadata.

	
import chromadb
chroma_client = chromadb.Client()
collection = chroma_client.create_collection(name="my_collection")
Copy

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 chromadb
chroma_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"]
)
Copy

Ahora podemos hacer una consulta

	
import chromadb
chroma_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
)
Copy
	
import chromadb
chroma_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
Copy
	
{'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 persistenteslink image 4

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 Path
chroma_path = Path("chromadb")
chroma_path.mkdir(exist_ok=True)
Copy

Ahora creamos un cliente en la carpeta que hemos creado

	
from pathlib import Path
chroma_path = Path("chromadb")
chroma_path.mkdir(exist_ok=True)
chroma_client_persistent = chromadb.PersistentClient(path = str(chroma_path))
Copy

Coleccioneslink image 5

Crear coleccioneslink image 6

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 Path
chroma_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")
Copy

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 coleccioneslink image 7

Si queremos recuperar una colección de un cliente lo podemos hacer con el método get_collection

	
from pathlib import Path
chroma_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")
Copy

Recuperar o crear coleccioneslink image 8

Podemos obtener colecciones y que en caso de que no existan las cree con el método get_or_create_collection

	
from pathlib import Path
chroma_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")
Copy

Borrar coleccioneslink image 9

Podemos borrar una colección con el método delete_collection

	
from pathlib import Path
chroma_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")
Copy

Obtener items de las coleccioneslink image 10

Podemos obtener los 10 primeros items de la colección con el método peek

	
from pathlib import Path
chroma_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()
Copy
	
{'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)
Copy
	
{'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 coleccioneslink image 11

Podemos obtener el número total de items de la colección con el método count

	
collection.count()
Copy
	
2

Cambiar la función de similitudlink image 12

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"})
Copy

Añadir datos a la colecciónlink image 13

Añadir documentoslink image 14

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()
Copy
	
{'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
Copy
	
'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")
Copy

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"]
)
Copy

Como se puede ver los IDs son consecutivos y no tienen el mismo valor que ya tenían antes, ya que los IDs tienen que ser únicos.

Si intentamos añadir datos repitiendo IDs, nos idiciará que ya existían datos con esas IDs

	
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"]
)
Copy
	
Add of existing embedding ID: id3
Add of existing embedding ID: id4
Insert of existing embedding ID: id3
Insert 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()
Copy
	
{'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 embeddingslink image 15

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])
Copy
	
(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])
Copy
	
(1, 384)

Ahora añadimos los nuevos embeddings

	
collection.add(
embeddings=new_embedding,
metadatas=[{"source": "Only embeddings"}],
ids=["id5"]
)
Copy

Vamos a ver los datos de la colección

	
collection.add(
embeddings=new_embedding,
metadatas=[{"source": "Only embeddings"}],
ids=["id5"]
)
collection.peek()['embeddings'][-1]
Copy
	
[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])
Copy
	
(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"]
)
Copy
	
---------------------------------------------------------------------------
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, granularity
126 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, granularity
126 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, granularity
126 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"] = dim
813 elif collection["dimension"] != dim:
--> 814 raise InvalidDimensionException(
815 f"Embedding dimension {dim} does not match collection dimensionality {collection['dimension']}"
816 )
817 else:
818 return
InvalidDimensionException: Embedding dimension 383 does not match collection dimensionality 384

Añadir documentos y embeddingslink image 16

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"]
)
Copy

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]
Copy
	
[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]

Consultaslink image 17

Consultas por documentoslink image 18

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,
)
Copy
	
{'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,
      )
      
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
      
Out[100]:
{'ids': [['id1', 'id2', 'id4', 'id3', 'id5', 'id6']],
       'distances': [[0.5389559268951416,
         1.5743632316589355,
         1.578398585319519,
         1.59961998462677,
         384.56890869140625,
         384.56890869140625]],
       'metadatas': [[{'source': 'Python source'},
         {'source': 'JavaScript source'},
         {'source': 'Rust source'},
         {'source': 'Mojo source'},
         {'source': 'Only embeddings'},
         {'source': 'Pytorch source'}]],
       'embeddings': None,
       'documents': [['This is a python docs',
         'This is JavaScript docs',
         'This is Rust docs',
         'This is a Mojo docs',
         None,
         'This is a Pytorch docs']],
       'uris': None,
       'data': None}

Podemos filtrar por un valor de metadato con el argumento where

collection.query(
          query_texts=["python"],
          n_results=10,
          where={"source": "Python source"}
      )
      
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
      
Out[100]:
{'ids': [['id1']],
       'distances': [[0.5389559268951416]],
       'metadatas': [[{'source': 'Python source'}]],
       'embeddings': None,
       'documents': [['This is a python docs']],
       'uris': None,
       'data': None}

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"}
      )
      
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
      
Out[100]:
{'ids': [['id1']],
       'distances': [[0.5389559268951416]],
       'metadatas': [[{'source': 'Python source'}]],
       'embeddings': None,
       'documents': [['This is a python docs']],
       'uris': None,
       'data': None}

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"]
      )
      
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
      
Out[100]:
{'ids': [['id1', 'id2', 'id4', 'id3', 'id5', 'id6']],
       'distances': [[0.5389559268951416,
         1.5743632316589355,
         1.578398585319519,
         1.59961998462677,
         384.56890869140625,
         384.56890869140625]],
       'metadatas': None,
       'embeddings': None,
       'documents': [['This is a python docs',
         'This is JavaScript docs',
         'This is Rust docs',
         'This is a Mojo docs',
         None,
         'This is a Pytorch docs']],
       'uris': None,
       'data': None}

Vemos que ahora metadatas es None

Varias consultas a la vezlink image 19

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,
)
Copy
	
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
Number 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 embeddingslink image 20

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
Copy
	
[[-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,
)
Copy
	
{'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 IDlink image 21

Si sabemos la ID de un documento, podemos recuperar el documento con el método get

	
collection.get(
ids=["id1"],
)
Copy
	
{'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"],
)
Copy
	
{'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

Filtradolink image 22

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 metadatalink image 23

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"
}
}
)
Copy
	
{'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úsqueda where={"version": {"$ne": 1}} solo devolverá los datos que en su metadata haya una key version y que no sea 1

Filtrado por el contenido del documentolink image 24

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"}
      )
      
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
      
Out[100]:
{'ids': [['id1']],
       'distances': [[0.5389559268951416]],
       'metadatas': [[{'source': 'Python source'}]],
       'embeddings': None,
       'documents': [['This is a python docs']],
       'uris': None,
       'data': None}

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"}
      )
      
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
      
Out[100]:
{'ids': [['id2', 'id4', 'id3', 'id6']],
       'distances': [[1.5743632316589355,
         1.578398585319519,
         1.59961998462677,
         384.56890869140625]],
       'metadatas': [[{'source': 'JavaScript source'},
         {'source': 'Rust source'},
         {'source': 'Mojo source'},
         {'source': 'Pytorch source'}]],
       'embeddings': None,
       'documents': [['This is JavaScript docs',
         'This is Rust docs',
         'This is a Mojo docs',
         'This is a Pytorch docs']],
       'uris': None,
       'data': None}

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"},
              ],
          },
      )
      
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
      
Out[100]:
{'ids': [['id1']],
       'distances': [[0.5389559268951416]],
       'metadatas': [[{'source': 'Python source'}]],
       'embeddings': None,
       'documents': [['This is a python docs']],
       'uris': None,
       'data': None}

Actualizar datoslink image 25

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"]
)
Copy

Vamos a ver si se a actualizado

collection.query(
          query_texts=["python"],
          n_results=10,
          where_document={"$contains": "Python"}
      )
      
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
      
Out[100]:
{'ids': [['id1']],
       'distances': [[0.8247963190078735]],
       'metadatas': [[{'source': 'Python source'}]],
       'embeddings': None,
       'documents': [['This is a updated Python docs']],
       'uris': None,
       'data': None}

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 datoslink image 26

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"}],
)
Copy

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()
Copy
	
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
Number of requested results 10 is greater than number of elements in index 6, updating n_results = 6
Number 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 datoslink image 27

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"]
)
Copy

Vamos a ver si se ha eliminado

	
collection.delete(
ids=["id5"]
)
collection.peek()
Copy
	
{'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á

Embeddingslink image 28

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_functions
huggingface_ef = embedding_functions.HuggingFaceEmbeddingFunction(
api_key="YOUR_API_KEY",
model_name="sentence-transformers/all-mpnet-base-v2"
)
Copy

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_functions
huggingface_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
)
Copy

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_functions
huggingface_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])
Copy
	
(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")
Copy
	
collection = chroma_client.get_collection(name = "my_collection")
embedding = collection._embedding_function(["python"])
len(embedding), len(embedding[0])
Copy
	
(1, 384)

Vemos que ahora la longitud del ambedding es 384, es decir, sí habíamos usado antes una nueva función de embedding

Multimodalidadlink image 29

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

chroma db - python mixture

Que la tengo en mi path local ../images/chromadb_dalle3.webp

	
from chromadb.utils.embedding_functions import OpenCLIPEmbeddingFunction
embedding_function = OpenCLIPEmbeddingFunction()
image = "../images/chromadb_dalle3.webp"
embedding = embedding_function(image)
len(embedding), len(embedding[0])
Copy
	
(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 ImageLoader
data_loader = ImageLoader()
data = data_loader._load_image(image)
type(data), data.shape
Copy
	
(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
)
Copy

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]
)
Copy

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()
Copy
	
{'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}]
)
Copy

Si volvemos a ver qué tiene guardado la colección

	
collection.update(
ids=['id1'],
images=[image],
metadatas=[{"source": image}]
)
collection.peek()
Copy
	
{'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()
Copy
	
{'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"],
      )
      
WARNING:chromadb.segment.impl.vector.local_hnsw:Number of requested results 10 is greater than number of elements in index 3, updating n_results = 3
      
Out[59]:
{'ids': [['id2', 'id1', 'id3']],
       'distances': [[1.1276676654815674, 1.1777206659317017, 1.2047353982925415]],
       'metadatas': [[{'source': 'Python source'},
         {'source': '../images/chromadb_dalle3.webp'},
         {'source': 'JavaScript source'}]],
       'embeddings': None,
       'documents': [['This is a python docs', None, 'This is JavaScript docs']],
       'uris': None,
       'data': None}

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

chroma logo

query_image = "https://pub-fb664c455eca46a2ba762a065ac900f7.r2.dev/chromadb_elegant.webp"
      collection.query(
          query_images=[query_image],
      )
      
WARNING:chromadb.segment.impl.vector.local_hnsw:Number of requested results 10 is greater than number of elements in index 3, updating n_results = 3
      
Out[60]:
{'ids': [['id1', 'id2', 'id3']],
       'distances': [[0.6684874296188354, 0.9450105428695679, 1.0639115571975708]],
       'metadatas': [[{'source': '../images/chromadb_dalle3.webp'},
         {'source': 'Python source'},
         {'source': 'JavaScript source'}]],
       'embeddings': None,
       'documents': [[None, 'This is a python docs', 'This is JavaScript docs']],
       'uris': None,
       'data': None}

Ahora sí da como primer resultado la imgen que habíamos guardado

Seguir leyendo

DoLa – Decoding by Contrasting Layers Improves Factuality in Large Language Models

DoLa – Decoding by Contrasting Layers Improves Factuality in Large Language Models

¿Alguna vez has hablado con un LLM y te ha respondido algo que suena como si hubiera estado bebiendo café de máquina durante toda la noche? 😂 ¡Eso es lo que llamamos una alucinación en el mundo de los LLMs! Pero no te preocupes, porque no es que tu modelo de lenguaje esté loco (aunque a veces puede parecerlo 🤪). La verdad es que los LLMs pueden ser un poco... creativos cuando se trata de generar texto. Pero gracias a DoLa, un método que utiliza capas de contraste para mejorar la factibilidad de los LLMs, podemos evitar que nuestros modelos de lenguaje se conviertan en escritores de ciencia ficción 😂. En este post, te explicaré cómo funciona DoLa y te mostraré un ejemplo de código para que puedas entender mejor cómo hacer que tus LLMs sean más fiables y menos propensos a inventar historias. ¡Vamos a salvar a nuestros LLMs de la locura y hacer que sean más útiles! 🚀

Últimos posts -->

¿Has visto estos proyectos?

Subtify

Subtify Subtify

Generador de subtítulos para videos en el idioma que desees. Además a cada persona le pone su subtítulo de un color

Ver todos los proyectos -->

¿Quieres aplicar la IA en tu proyecto? Contactame!

¿Quieres mejorar con estos tips?

Últimos tips -->

Usa esto en local

Los espacios de Hugging Face nos permite ejecutar modelos con demos muy sencillas, pero ¿qué pasa si la demo se rompe? O si el usuario la elimina? Por ello he creado contenedores docker con algunos espacios interesantes, para poder usarlos de manera local, pase lo que pase. De hecho, es posible que si pinchas en alún botón de ver proyecto te lleve a un espacio que no funciona.

Ver todos los contenedores -->

¿Quieres aplicar la IA en tu proyecto? Contactame!

¿Quieres entrenar tu modelo con estos datasets?

short-jokes-dataset

Dataset de chistes en inglés

opus100

Dataset con traducciones de inglés a español

netflix_titles

Dataset con películas y series de Netflix

Ver más datasets -->