chore: 添加虚拟环境到仓库
- 添加 backend_service/venv 虚拟环境 - 包含所有Python依赖包 - 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
@@ -0,0 +1,863 @@
|
||||
from chromadb.api.types import * # noqa: F401, F403
|
||||
from chromadb.execution.expression import ( # noqa: F401, F403
|
||||
Search,
|
||||
Key,
|
||||
K,
|
||||
SearchWhere,
|
||||
And,
|
||||
Or,
|
||||
Eq,
|
||||
Ne,
|
||||
Gt,
|
||||
Gte,
|
||||
Lt,
|
||||
Lte,
|
||||
In,
|
||||
Nin,
|
||||
Regex,
|
||||
NotRegex,
|
||||
Contains,
|
||||
NotContains,
|
||||
Limit,
|
||||
Select,
|
||||
Rank,
|
||||
Abs,
|
||||
Div,
|
||||
Exp,
|
||||
Log,
|
||||
Max,
|
||||
Min,
|
||||
Mul,
|
||||
Knn,
|
||||
Rrf,
|
||||
Sub,
|
||||
Sum,
|
||||
Val,
|
||||
)
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Sequence, Optional, List, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
from overrides import override
|
||||
from chromadb.api.collection_configuration import (
|
||||
CreateCollectionConfiguration,
|
||||
UpdateCollectionConfiguration,
|
||||
)
|
||||
from chromadb.config import DEFAULT_DATABASE, DEFAULT_TENANT
|
||||
from chromadb.api.types import (
|
||||
CollectionMetadata,
|
||||
Documents,
|
||||
Embeddable,
|
||||
EmbeddingFunction,
|
||||
DataLoader,
|
||||
Embeddings,
|
||||
IDs,
|
||||
Include,
|
||||
IncludeMetadataDocumentsDistances,
|
||||
IncludeMetadataDocuments,
|
||||
Loadable,
|
||||
Metadatas,
|
||||
Schema,
|
||||
URIs,
|
||||
Where,
|
||||
QueryResult,
|
||||
GetResult,
|
||||
WhereDocument,
|
||||
SearchResult,
|
||||
DefaultEmbeddingFunction,
|
||||
)
|
||||
|
||||
from chromadb.auth import UserIdentity
|
||||
from chromadb.config import Component, Settings
|
||||
from chromadb.types import Database, Tenant, Collection as CollectionModel
|
||||
from chromadb.api.models.Collection import Collection
|
||||
from chromadb.api.models.AttachedFunction import AttachedFunction
|
||||
|
||||
# Re-export the async version
|
||||
from chromadb.api.async_api import ( # noqa: F401
|
||||
AsyncBaseAPI as AsyncBaseAPI,
|
||||
AsyncClientAPI as AsyncClientAPI,
|
||||
AsyncAdminAPI as AsyncAdminAPI,
|
||||
AsyncServerAPI as AsyncServerAPI,
|
||||
)
|
||||
|
||||
|
||||
class BaseAPI(ABC):
|
||||
@abstractmethod
|
||||
def heartbeat(self) -> int:
|
||||
"""Get the current time in nanoseconds since epoch.
|
||||
Used to check if the server is alive.
|
||||
|
||||
Returns:
|
||||
int: The current time in nanoseconds since epoch
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
#
|
||||
# COLLECTION METHODS
|
||||
#
|
||||
@abstractmethod
|
||||
def count_collections(self) -> int:
|
||||
"""Count the number of collections.
|
||||
|
||||
Returns:
|
||||
int: The number of collections.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
client.count_collections()
|
||||
# 1
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
def _modify(
|
||||
self,
|
||||
id: UUID,
|
||||
new_name: Optional[str] = None,
|
||||
new_metadata: Optional[CollectionMetadata] = None,
|
||||
new_configuration: Optional[UpdateCollectionConfiguration] = None,
|
||||
) -> None:
|
||||
"""[Internal] Modify a collection by UUID. Can update the name and/or metadata.
|
||||
|
||||
Args:
|
||||
id: The internal UUID of the collection to modify.
|
||||
new_name: The new name of the collection.
|
||||
If None, the existing name will remain. Defaults to None.
|
||||
new_metadata: The new metadata to associate with the collection.
|
||||
Defaults to None.
|
||||
new_configuration: The new configuration to associate with the collection.
|
||||
Defaults to None.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_collection(
|
||||
self,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Delete a collection with the given name.
|
||||
Args:
|
||||
name: The name of the collection to delete.
|
||||
|
||||
Raises:
|
||||
ValueError: If the collection does not exist.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
client.delete_collection("my_collection")
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
#
|
||||
# ITEM METHODS
|
||||
#
|
||||
|
||||
@abstractmethod
|
||||
def _add(
|
||||
self,
|
||||
ids: IDs,
|
||||
collection_id: UUID,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
) -> bool:
|
||||
"""[Internal] Add embeddings to a collection specified by UUID.
|
||||
If (some) ids already exist, only the new embeddings will be added.
|
||||
|
||||
Args:
|
||||
ids: The ids to associate with the embeddings.
|
||||
collection_id: The UUID of the collection to add the embeddings to.
|
||||
embedding: The sequence of embeddings to add.
|
||||
metadata: The metadata to associate with the embeddings. Defaults to None.
|
||||
documents: The documents to associate with the embeddings. Defaults to None.
|
||||
uris: URIs of data sources for each embedding. Defaults to None.
|
||||
|
||||
Returns:
|
||||
True if the embeddings were added successfully.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _update(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Optional[Embeddings] = None,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
) -> bool:
|
||||
"""[Internal] Update entries in a collection specified by UUID.
|
||||
|
||||
Args:
|
||||
collection_id: The UUID of the collection to update the embeddings in.
|
||||
ids: The IDs of the entries to update.
|
||||
embeddings: The sequence of embeddings to update. Defaults to None.
|
||||
metadatas: The metadata to associate with the embeddings. Defaults to None.
|
||||
documents: The documents to associate with the embeddings. Defaults to None.
|
||||
uris: URIs of data sources for each embedding. Defaults to None.
|
||||
Returns:
|
||||
True if the embeddings were updated successfully.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _upsert(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
) -> bool:
|
||||
"""[Internal] Add or update entries in the a collection specified by UUID.
|
||||
If an entry with the same id already exists, it will be updated,
|
||||
otherwise it will be added.
|
||||
|
||||
Args:
|
||||
collection_id: The collection to add the embeddings to
|
||||
ids: The ids to associate with the embeddings. Defaults to None.
|
||||
embeddings: The sequence of embeddings to add
|
||||
metadatas: The metadata to associate with the embeddings. Defaults to None.
|
||||
documents: The documents to associate with the embeddings. Defaults to None.
|
||||
uris: URIs of data sources for each embedding. Defaults to None.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _count(self, collection_id: UUID) -> int:
|
||||
"""[Internal] Returns the number of entries in a collection specified by UUID.
|
||||
|
||||
Args:
|
||||
collection_id: The UUID of the collection to count the embeddings in.
|
||||
|
||||
Returns:
|
||||
int: The number of embeddings in the collection
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _peek(self, collection_id: UUID, n: int = 10) -> GetResult:
|
||||
"""[Internal] Returns the first n entries in a collection specified by UUID.
|
||||
|
||||
Args:
|
||||
collection_id: The UUID of the collection to peek into.
|
||||
n: The number of entries to peek. Defaults to 10.
|
||||
|
||||
Returns:
|
||||
GetResult: The first n entries in the collection.
|
||||
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocuments,
|
||||
) -> GetResult:
|
||||
"""[Internal] Returns entries from a collection specified by UUID.
|
||||
|
||||
Args:
|
||||
ids: The IDs of the entries to get. Defaults to None.
|
||||
where: Conditional filtering on metadata. Defaults to None.
|
||||
limit: The maximum number of entries to return. Defaults to None.
|
||||
offset: The number of entries to skip before returning. Defaults to None.
|
||||
where_document: Conditional filtering on documents. Defaults to None.
|
||||
include: The fields to include in the response.
|
||||
Defaults to ["metadatas", "documents"].
|
||||
Returns:
|
||||
GetResult: The entries in the collection that match the query.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _delete(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs],
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
) -> None:
|
||||
"""[Internal] Deletes entries from a collection specified by UUID.
|
||||
|
||||
Args:
|
||||
collection_id: The UUID of the collection to delete the entries from.
|
||||
ids: The IDs of the entries to delete. Defaults to None.
|
||||
where: Conditional filtering on metadata. Defaults to None.
|
||||
where_document: Conditional filtering on documents. Defaults to None.
|
||||
|
||||
Returns:
|
||||
IDs: The list of IDs of the entries that were deleted.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _query(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
query_embeddings: Embeddings,
|
||||
ids: Optional[IDs] = None,
|
||||
n_results: int = 10,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocumentsDistances,
|
||||
) -> QueryResult:
|
||||
"""[Internal] Performs a nearest neighbors query on a collection specified by UUID.
|
||||
|
||||
Args:
|
||||
collection_id: The UUID of the collection to query.
|
||||
query_embeddings: The embeddings to use as the query.
|
||||
ids: The IDs to filter by during the query. Defaults to None.
|
||||
n_results: The number of results to return. Defaults to 10.
|
||||
where: Conditional filtering on metadata. Defaults to None.
|
||||
where_document: Conditional filtering on documents. Defaults to None.
|
||||
include: The fields to include in the response.
|
||||
Defaults to ["metadatas", "documents", "distances"].
|
||||
|
||||
Returns:
|
||||
QueryResult: The results of the query.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def reset(self) -> bool:
|
||||
"""Resets the database. This will delete all collections and entries.
|
||||
|
||||
Returns:
|
||||
bool: True if the database was reset successfully.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_version(self) -> str:
|
||||
"""Get the version of Chroma.
|
||||
|
||||
Returns:
|
||||
str: The version of Chroma
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_settings(self) -> Settings:
|
||||
"""Get the settings used to initialize.
|
||||
|
||||
Returns:
|
||||
Settings: The settings used to initialize.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_max_batch_size(self) -> int:
|
||||
"""Return the maximum number of records that can be created or mutated in a single call."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_user_identity(self) -> UserIdentity:
|
||||
"""Resolve the tenant and databases for the client. Returns the default
|
||||
values if can't be resolved.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ClientAPI(BaseAPI, ABC):
|
||||
tenant: str
|
||||
database: str
|
||||
|
||||
@abstractmethod
|
||||
def list_collections(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
) -> Sequence[Collection]:
|
||||
"""List all collections.
|
||||
Args:
|
||||
limit: The maximum number of entries to return. Defaults to None.
|
||||
offset: The number of entries to skip before returning. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Sequence[Collection]: A list of collections
|
||||
|
||||
Examples:
|
||||
```python
|
||||
client.list_collections()
|
||||
# [collection(name="my_collection", metadata={})]
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
embedding_function: Optional[
|
||||
EmbeddingFunction[Embeddable]
|
||||
] = DefaultEmbeddingFunction(), # type: ignore
|
||||
data_loader: Optional[DataLoader[Loadable]] = None,
|
||||
get_or_create: bool = False,
|
||||
) -> Collection:
|
||||
"""Create a new collection with the given name and metadata.
|
||||
Args:
|
||||
name: The name of the collection to create.
|
||||
metadata: Optional metadata to associate with the collection.
|
||||
embedding_function: Optional function to use to embed documents.
|
||||
Uses the default embedding function if not provided.
|
||||
get_or_create: If True, return the existing collection if it exists.
|
||||
data_loader: Optional function to use to load records (documents, images, etc.)
|
||||
|
||||
Returns:
|
||||
Collection: The newly created collection.
|
||||
|
||||
Raises:
|
||||
ValueError: If the collection already exists and get_or_create is False.
|
||||
ValueError: If the collection name is invalid.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
client.create_collection("my_collection")
|
||||
# collection(name="my_collection", metadata={})
|
||||
|
||||
client.create_collection("my_collection", metadata={"foo": "bar"})
|
||||
# collection(name="my_collection", metadata={"foo": "bar"})
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_collection(
|
||||
self,
|
||||
name: str,
|
||||
embedding_function: Optional[
|
||||
EmbeddingFunction[Embeddable]
|
||||
] = DefaultEmbeddingFunction(), # type: ignore
|
||||
data_loader: Optional[DataLoader[Loadable]] = None,
|
||||
) -> Collection:
|
||||
"""Get a collection with the given name.
|
||||
Args:
|
||||
name: The name of the collection to get
|
||||
embedding_function: Optional function to use to embed documents.
|
||||
Uses the default embedding function if not provided.
|
||||
data_loader: Optional function to use to load records (documents, images, etc.)
|
||||
|
||||
Returns:
|
||||
Collection: The collection
|
||||
|
||||
Raises:
|
||||
ValueError: If the collection does not exist
|
||||
|
||||
Examples:
|
||||
```python
|
||||
client.get_collection("my_collection")
|
||||
# collection(name="my_collection", metadata={})
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_or_create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
embedding_function: Optional[
|
||||
EmbeddingFunction[Embeddable]
|
||||
] = DefaultEmbeddingFunction(), # type: ignore
|
||||
data_loader: Optional[DataLoader[Loadable]] = None,
|
||||
) -> Collection:
|
||||
"""Get or create a collection with the given name and metadata.
|
||||
Args:
|
||||
name: The name of the collection to get or create
|
||||
metadata: Optional metadata to associate with the collection. If
|
||||
the collection already exists, the metadata provided is ignored.
|
||||
If the collection does not exist, the new collection will be created
|
||||
with the provided metadata.
|
||||
embedding_function: Optional function to use to embed documents
|
||||
data_loader: Optional function to use to load records (documents, images, etc.)
|
||||
|
||||
Returns:
|
||||
The collection
|
||||
|
||||
Examples:
|
||||
```python
|
||||
client.get_or_create_collection("my_collection")
|
||||
# collection(name="my_collection", metadata={})
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_tenant(self, tenant: str, database: str = DEFAULT_DATABASE) -> None:
|
||||
"""Set the tenant and database for the client. Raises an error if the tenant or
|
||||
database does not exist.
|
||||
|
||||
Args:
|
||||
tenant: The tenant to set.
|
||||
database: The database to set.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_database(self, database: str) -> None:
|
||||
"""Set the database for the client. Raises an error if the database does not exist.
|
||||
|
||||
Args:
|
||||
database: The database to set.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def clear_system_cache() -> None:
|
||||
"""Clear the system cache so that new systems can be created for an existing path.
|
||||
This should only be used for testing purposes."""
|
||||
pass
|
||||
|
||||
|
||||
class AdminAPI(ABC):
|
||||
@abstractmethod
|
||||
def create_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
|
||||
"""Create a new database. Raises an error if the database already exists.
|
||||
|
||||
Args:
|
||||
database: The name of the database to create.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> Database:
|
||||
"""Get a database. Raises an error if the database does not exist.
|
||||
|
||||
Args:
|
||||
database: The name of the database to get.
|
||||
tenant: The tenant of the database to get.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
|
||||
"""Delete a database. Raises an error if the database does not exist.
|
||||
|
||||
Args:
|
||||
database: The name of the database to delete.
|
||||
tenant: The tenant of the database to delete.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def list_databases(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> Sequence[Database]:
|
||||
"""List all databases for a tenant. Raises an error if the tenant does not exist.
|
||||
|
||||
Args:
|
||||
tenant: The tenant to list databases for.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_tenant(self, name: str) -> None:
|
||||
"""Create a new tenant. Raises an error if the tenant already exists.
|
||||
|
||||
Args:
|
||||
tenant: The name of the tenant to create.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_tenant(self, name: str) -> Tenant:
|
||||
"""Get a tenant. Raises an error if the tenant does not exist.
|
||||
|
||||
Args:
|
||||
tenant: The name of the tenant to get.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ServerAPI(BaseAPI, AdminAPI, Component):
|
||||
"""An API instance that extends the relevant Base API methods by passing
|
||||
in a tenant and database. This is the root component of the Chroma System"""
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def count_collections(
|
||||
self, tenant: str = DEFAULT_TENANT, database: str = DEFAULT_DATABASE
|
||||
) -> int:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def list_collections(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> Sequence[CollectionModel]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
get_or_create: bool = False,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_collection(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_or_create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def delete_collection(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def _modify(
|
||||
self,
|
||||
id: UUID,
|
||||
new_name: Optional[str] = None,
|
||||
new_metadata: Optional[CollectionMetadata] = None,
|
||||
new_configuration: Optional[UpdateCollectionConfiguration] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _fork(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
new_name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _search(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
searches: List[Search],
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> SearchResult:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def _count(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> int:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def _peek(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
n: int = 10,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> GetResult:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def _get(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocuments,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> GetResult:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def _add(
|
||||
self,
|
||||
ids: IDs,
|
||||
collection_id: UUID,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def _update(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Optional[Embeddings] = None,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def _upsert(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def _query(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
query_embeddings: Embeddings,
|
||||
ids: Optional[IDs] = None,
|
||||
n_results: int = 10,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocumentsDistances,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> QueryResult:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
def _delete(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def attach_function(
|
||||
self,
|
||||
function_id: str,
|
||||
name: str,
|
||||
input_collection_id: UUID,
|
||||
output_collection: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> "AttachedFunction":
|
||||
"""Attach a function to a collection.
|
||||
|
||||
Args:
|
||||
function_id: Built-in function identifier
|
||||
name: Unique name for this attached function
|
||||
input_collection_id: Source collection that triggers the function
|
||||
output_collection: Target collection where function output is stored
|
||||
params: Optional dictionary with function-specific parameters
|
||||
tenant: The tenant name
|
||||
database: The database name
|
||||
|
||||
Returns:
|
||||
AttachedFunction: Object representing the attached function
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def detach_function(
|
||||
self,
|
||||
attached_function_id: UUID,
|
||||
delete_output: bool = False,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
"""Detach a function and prevent any further runs.
|
||||
|
||||
Args:
|
||||
attached_function_id: ID of the attached function to remove
|
||||
delete_output: Whether to also delete the output collection
|
||||
tenant: The tenant name
|
||||
database: The database name
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
pass
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,770 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Sequence, Optional, List
|
||||
from uuid import UUID
|
||||
|
||||
from overrides import override
|
||||
from chromadb.api.collection_configuration import (
|
||||
CreateCollectionConfiguration,
|
||||
UpdateCollectionConfiguration,
|
||||
)
|
||||
from chromadb.auth import UserIdentity
|
||||
from chromadb.api.models.AsyncCollection import AsyncCollection
|
||||
from chromadb.config import DEFAULT_DATABASE, DEFAULT_TENANT
|
||||
from chromadb.api.types import (
|
||||
CollectionMetadata,
|
||||
Documents,
|
||||
Embeddable,
|
||||
EmbeddingFunction,
|
||||
DataLoader,
|
||||
Embeddings,
|
||||
IDs,
|
||||
Include,
|
||||
Loadable,
|
||||
Metadatas,
|
||||
Schema,
|
||||
URIs,
|
||||
Where,
|
||||
QueryResult,
|
||||
GetResult,
|
||||
WhereDocument,
|
||||
IncludeMetadataDocuments,
|
||||
IncludeMetadataDocumentsDistances,
|
||||
SearchResult,
|
||||
DefaultEmbeddingFunction,
|
||||
)
|
||||
from chromadb.execution.expression.plan import Search
|
||||
from chromadb.config import Component, Settings
|
||||
from chromadb.types import Database, Tenant, Collection as CollectionModel
|
||||
|
||||
|
||||
class AsyncBaseAPI(ABC):
|
||||
@abstractmethod
|
||||
async def heartbeat(self) -> int:
|
||||
"""Get the current time in nanoseconds since epoch.
|
||||
Used to check if the server is alive.
|
||||
|
||||
Returns:
|
||||
int: The current time in nanoseconds since epoch
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
#
|
||||
# COLLECTION METHODS
|
||||
#
|
||||
|
||||
@abstractmethod
|
||||
async def count_collections(self) -> int:
|
||||
"""Count the number of collections.
|
||||
|
||||
Returns:
|
||||
int: The number of collections.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
await client.count_collections()
|
||||
# 1
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _modify(
|
||||
self,
|
||||
id: UUID,
|
||||
new_name: Optional[str] = None,
|
||||
new_metadata: Optional[CollectionMetadata] = None,
|
||||
new_configuration: Optional[UpdateCollectionConfiguration] = None,
|
||||
) -> None:
|
||||
"""[Internal] Modify a collection by UUID. Can update the name and/or metadata.
|
||||
|
||||
Args:
|
||||
id: The internal UUID of the collection to modify.
|
||||
new_name: The new name of the collection.
|
||||
If None, the existing name will remain. Defaults to None.
|
||||
new_metadata: The new metadata to associate with the collection.
|
||||
Defaults to None.
|
||||
new_configuration: The new configuration to associate with the collection.
|
||||
Defaults to None.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete_collection(
|
||||
self,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Delete a collection with the given name.
|
||||
Args:
|
||||
name: The name of the collection to delete.
|
||||
|
||||
Raises:
|
||||
ValueError: If the collection does not exist.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
await client.delete_collection("my_collection")
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
#
|
||||
# ITEM METHODS
|
||||
#
|
||||
|
||||
@abstractmethod
|
||||
async def _add(
|
||||
self,
|
||||
ids: IDs,
|
||||
collection_id: UUID,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
) -> bool:
|
||||
"""[Internal] Add embeddings to a collection specified by UUID.
|
||||
If (some) ids already exist, only the new embeddings will be added.
|
||||
|
||||
Args:
|
||||
ids: The ids to associate with the embeddings.
|
||||
collection_id: The UUID of the collection to add the embeddings to.
|
||||
embedding: The sequence of embeddings to add.
|
||||
metadata: The metadata to associate with the embeddings. Defaults to None.
|
||||
documents: The documents to associate with the embeddings. Defaults to None.
|
||||
uris: URIs of data sources for each embedding. Defaults to None.
|
||||
|
||||
Returns:
|
||||
True if the embeddings were added successfully.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _update(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Optional[Embeddings] = None,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
) -> bool:
|
||||
"""[Internal] Update entries in a collection specified by UUID.
|
||||
|
||||
Args:
|
||||
collection_id: The UUID of the collection to update the embeddings in.
|
||||
ids: The IDs of the entries to update.
|
||||
embeddings: The sequence of embeddings to update. Defaults to None.
|
||||
metadatas: The metadata to associate with the embeddings. Defaults to None.
|
||||
documents: The documents to associate with the embeddings. Defaults to None.
|
||||
uris: URIs of data sources for each embedding. Defaults to None.
|
||||
Returns:
|
||||
True if the embeddings were updated successfully.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _upsert(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
) -> bool:
|
||||
"""[Internal] Add or update entries in the a collection specified by UUID.
|
||||
If an entry with the same id already exists, it will be updated,
|
||||
otherwise it will be added.
|
||||
|
||||
Args:
|
||||
collection_id: The collection to add the embeddings to
|
||||
ids: The ids to associate with the embeddings. Defaults to None.
|
||||
embeddings: The sequence of embeddings to add
|
||||
metadatas: The metadata to associate with the embeddings. Defaults to None.
|
||||
documents: The documents to associate with the embeddings. Defaults to None.
|
||||
uris: URIs of data sources for each embedding. Defaults to None.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _count(self, collection_id: UUID) -> int:
|
||||
"""[Internal] Returns the number of entries in a collection specified by UUID.
|
||||
|
||||
Args:
|
||||
collection_id: The UUID of the collection to count the embeddings in.
|
||||
|
||||
Returns:
|
||||
int: The number of embeddings in the collection
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _peek(self, collection_id: UUID, n: int = 10) -> GetResult:
|
||||
"""[Internal] Returns the first n entries in a collection specified by UUID.
|
||||
|
||||
Args:
|
||||
collection_id: The UUID of the collection to peek into.
|
||||
n: The number of entries to peek. Defaults to 10.
|
||||
|
||||
Returns:
|
||||
GetResult: The first n entries in the collection.
|
||||
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _get(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocuments,
|
||||
) -> GetResult:
|
||||
"""[Internal] Returns entries from a collection specified by UUID.
|
||||
|
||||
Args:
|
||||
ids: The IDs of the entries to get. Defaults to None.
|
||||
where: Conditional filtering on metadata. Defaults to None.
|
||||
limit: The maximum number of entries to return. Defaults to None.
|
||||
offset: The number of entries to skip before returning. Defaults to None.
|
||||
where_document: Conditional filtering on documents. Defaults to None.
|
||||
include: The fields to include in the response.
|
||||
Defaults to ["embeddings", "metadatas", "documents"].
|
||||
Returns:
|
||||
GetResult: The entries in the collection that match the query.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _delete(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs],
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
) -> None:
|
||||
"""[Internal] Deletes entries from a collection specified by UUID.
|
||||
|
||||
Args:
|
||||
collection_id: The UUID of the collection to delete the entries from.
|
||||
ids: The IDs of the entries to delete. Defaults to None.
|
||||
where: Conditional filtering on metadata. Defaults to None.
|
||||
where_document: Conditional filtering on documents. Defaults to None.
|
||||
|
||||
Returns:
|
||||
IDs: The list of IDs of the entries that were deleted.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _query(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
query_embeddings: Embeddings,
|
||||
ids: Optional[IDs] = None,
|
||||
n_results: int = 10,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocumentsDistances,
|
||||
) -> QueryResult:
|
||||
"""[Internal] Performs a nearest neighbors query on a collection specified by UUID.
|
||||
|
||||
Args:
|
||||
collection_id: The UUID of the collection to query.
|
||||
query_embeddings: The embeddings to use as the query.
|
||||
n_results: The number of results to return. Defaults to 10.
|
||||
where: Conditional filtering on metadata. Defaults to None.
|
||||
where_document: Conditional filtering on documents. Defaults to None.
|
||||
include: The fields to include in the response.
|
||||
Defaults to ["embeddings", "metadatas", "documents", "distances"].
|
||||
|
||||
Returns:
|
||||
QueryResult: The results of the query.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def reset(self) -> bool:
|
||||
"""Resets the database. This will delete all collections and entries.
|
||||
|
||||
Returns:
|
||||
bool: True if the database was reset successfully.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_version(self) -> str:
|
||||
"""Get the version of Chroma.
|
||||
|
||||
Returns:
|
||||
str: The version of Chroma
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_settings(self) -> Settings:
|
||||
"""Get the settings used to initialize.
|
||||
|
||||
Returns:
|
||||
Settings: The settings used to initialize.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_max_batch_size(self) -> int:
|
||||
"""Return the maximum number of records that can be created or mutated in a single call."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_user_identity(self) -> UserIdentity:
|
||||
"""Resolve the tenant and databases for the client. Returns the default
|
||||
values if can't be resolved.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class AsyncClientAPI(AsyncBaseAPI, ABC):
|
||||
tenant: str
|
||||
database: str
|
||||
|
||||
@abstractmethod
|
||||
async def list_collections(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
) -> Sequence[AsyncCollection]:
|
||||
"""List all collections.
|
||||
Args:
|
||||
limit: The maximum number of entries to return. Defaults to None.
|
||||
offset: The number of entries to skip before returning. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Sequence[AsyncCollection]: A list of collections.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
await client.list_collections()
|
||||
# [collection(name="my_collection", metadata={})]
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
embedding_function: Optional[
|
||||
EmbeddingFunction[Embeddable]
|
||||
] = DefaultEmbeddingFunction(), # type: ignore
|
||||
data_loader: Optional[DataLoader[Loadable]] = None,
|
||||
get_or_create: bool = False,
|
||||
) -> AsyncCollection:
|
||||
"""Create a new collection with the given name and metadata.
|
||||
Args:
|
||||
name: The name of the collection to create.
|
||||
metadata: Optional metadata to associate with the collection.
|
||||
embedding_function: Optional function to use to embed documents.
|
||||
Uses the default embedding function if not provided.
|
||||
get_or_create: If True, return the existing collection if it exists.
|
||||
data_loader: Optional function to use to load records (documents, images, etc.)
|
||||
|
||||
Returns:
|
||||
Collection: The newly created collection.
|
||||
|
||||
Raises:
|
||||
ValueError: If the collection already exists and get_or_create is False.
|
||||
ValueError: If the collection name is invalid.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
await client.create_collection("my_collection")
|
||||
# collection(name="my_collection", metadata={})
|
||||
|
||||
await client.create_collection("my_collection", metadata={"foo": "bar"})
|
||||
# collection(name="my_collection", metadata={"foo": "bar"})
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_collection(
|
||||
self,
|
||||
name: str,
|
||||
embedding_function: Optional[
|
||||
EmbeddingFunction[Embeddable]
|
||||
] = DefaultEmbeddingFunction(), # type: ignore
|
||||
data_loader: Optional[DataLoader[Loadable]] = None,
|
||||
) -> AsyncCollection:
|
||||
"""Get a collection with the given name.
|
||||
Args:
|
||||
name: The name of the collection to get
|
||||
embedding_function: Optional function to use to embed documents.
|
||||
Uses the default embedding function if not provided.
|
||||
data_loader: Optional function to use to load records (documents, images, etc.)
|
||||
|
||||
Returns:
|
||||
Collection: The collection
|
||||
|
||||
Raises:
|
||||
ValueError: If the collection does not exist
|
||||
|
||||
Examples:
|
||||
```python
|
||||
await client.get_collection("my_collection")
|
||||
# collection(name="my_collection", metadata={})
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_or_create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
embedding_function: Optional[
|
||||
EmbeddingFunction[Embeddable]
|
||||
] = DefaultEmbeddingFunction(), # type: ignore
|
||||
data_loader: Optional[DataLoader[Loadable]] = None,
|
||||
) -> AsyncCollection:
|
||||
"""Get or create a collection with the given name and metadata.
|
||||
Args:
|
||||
name: The name of the collection to get or create
|
||||
metadata: Optional metadata to associate with the collection. If
|
||||
the collection already exists, the metadata provided is ignored.
|
||||
If the collection does not exist, the new collection will be created
|
||||
with the provided metadata.
|
||||
embedding_function: Optional function to use to embed documents
|
||||
data_loader: Optional function to use to load records (documents, images, etc.)
|
||||
|
||||
Returns:
|
||||
The collection
|
||||
|
||||
Examples:
|
||||
```python
|
||||
await client.get_or_create_collection("my_collection")
|
||||
# collection(name="my_collection", metadata={})
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def set_tenant(self, tenant: str, database: str = DEFAULT_DATABASE) -> None:
|
||||
"""Set the tenant and database for the client. Raises an error if the tenant or
|
||||
database does not exist.
|
||||
|
||||
Args:
|
||||
tenant: The tenant to set.
|
||||
database: The database to set.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def set_database(self, database: str) -> None:
|
||||
"""Set the database for the client. Raises an error if the database does not exist.
|
||||
|
||||
Args:
|
||||
database: The database to set.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def clear_system_cache() -> None:
|
||||
"""Clear the system cache so that new systems can be created for an existing path.
|
||||
This should only be used for testing purposes."""
|
||||
pass
|
||||
|
||||
|
||||
class AsyncAdminAPI(ABC):
|
||||
@abstractmethod
|
||||
async def create_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
|
||||
"""Create a new database. Raises an error if the database already exists.
|
||||
|
||||
Args:
|
||||
database: The name of the database to create.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> Database:
|
||||
"""Get a database. Raises an error if the database does not exist.
|
||||
|
||||
Args:
|
||||
database: The name of the database to get.
|
||||
tenant: The tenant of the database to get.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
|
||||
"""Delete a database. Raises an error if the database does not exist.
|
||||
|
||||
Args:
|
||||
database: The name of the database to delete.
|
||||
tenant: The tenant of the database to delete.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_databases(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> Sequence[Database]:
|
||||
"""List all databases for a tenant. Raises an error if the tenant does not exist.
|
||||
|
||||
Args:
|
||||
tenant: The tenant to list databases for.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create_tenant(self, name: str) -> None:
|
||||
"""Create a new tenant. Raises an error if the tenant already exists.
|
||||
|
||||
Args:
|
||||
tenant: The name of the tenant to create.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_tenant(self, name: str) -> Tenant:
|
||||
"""Get a tenant. Raises an error if the tenant does not exist.
|
||||
|
||||
Args:
|
||||
tenant: The name of the tenant to get.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class AsyncServerAPI(AsyncBaseAPI, AsyncAdminAPI, Component):
|
||||
"""An API instance that extends the relevant Base API methods by passing
|
||||
in a tenant and database. This is the root component of the Chroma System"""
|
||||
|
||||
@abstractmethod
|
||||
async def list_collections(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> Sequence[CollectionModel]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
async def count_collections(
|
||||
self, tenant: str = DEFAULT_TENANT, database: str = DEFAULT_DATABASE
|
||||
) -> int:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
get_or_create: bool = False,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_collection(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_or_create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
async def delete_collection(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
async def _modify(
|
||||
self,
|
||||
id: UUID,
|
||||
new_name: Optional[str] = None,
|
||||
new_metadata: Optional[CollectionMetadata] = None,
|
||||
new_configuration: Optional[UpdateCollectionConfiguration] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _fork(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
new_name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def _search(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
searches: List[Search],
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> SearchResult:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
async def _count(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> int:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
async def _peek(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
n: int = 10,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> GetResult:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
async def _get(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocuments,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> GetResult:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
async def _add(
|
||||
self,
|
||||
ids: IDs,
|
||||
collection_id: UUID,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
async def _update(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Optional[Embeddings] = None,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
async def _upsert(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
async def _query(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
query_embeddings: Embeddings,
|
||||
ids: Optional[IDs] = None,
|
||||
n_results: int = 10,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocumentsDistances,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> QueryResult:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@override
|
||||
async def _delete(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
pass
|
||||
@@ -0,0 +1,526 @@
|
||||
import httpx
|
||||
from typing import Optional, Sequence
|
||||
from uuid import UUID
|
||||
from overrides import override
|
||||
|
||||
from chromadb.auth import UserIdentity
|
||||
from chromadb.auth.utils import maybe_set_tenant_and_database
|
||||
from chromadb.api import AsyncAdminAPI, AsyncClientAPI, AsyncServerAPI
|
||||
from chromadb.api.collection_configuration import (
|
||||
CreateCollectionConfiguration,
|
||||
UpdateCollectionConfiguration,
|
||||
validate_embedding_function_conflict_on_create,
|
||||
validate_embedding_function_conflict_on_get,
|
||||
)
|
||||
from chromadb.api.models.AsyncCollection import AsyncCollection
|
||||
from chromadb.api.shared_system_client import SharedSystemClient
|
||||
from chromadb.api.types import (
|
||||
CollectionMetadata,
|
||||
DataLoader,
|
||||
Documents,
|
||||
Embeddable,
|
||||
EmbeddingFunction,
|
||||
Embeddings,
|
||||
GetResult,
|
||||
IDs,
|
||||
Include,
|
||||
IncludeMetadataDocuments,
|
||||
IncludeMetadataDocumentsDistances,
|
||||
Loadable,
|
||||
Metadatas,
|
||||
QueryResult,
|
||||
Schema,
|
||||
URIs,
|
||||
DefaultEmbeddingFunction,
|
||||
)
|
||||
from chromadb.config import DEFAULT_DATABASE, DEFAULT_TENANT, Settings, System
|
||||
from chromadb.errors import ChromaError
|
||||
from chromadb.types import Database, Tenant, Where, WhereDocument
|
||||
|
||||
|
||||
class AsyncClient(SharedSystemClient, AsyncClientAPI):
|
||||
"""A client for Chroma. This is the main entrypoint for interacting with Chroma.
|
||||
A client internally stores its tenant and database and proxies calls to a
|
||||
Server API instance of Chroma. It treats the Server API and corresponding System
|
||||
as a singleton, so multiple clients connecting to the same resource will share the
|
||||
same API instance.
|
||||
|
||||
Client implementations should be implement their own API-caching strategies.
|
||||
"""
|
||||
|
||||
# An internal admin client for verifying that databases and tenants exist
|
||||
_admin_client: AsyncAdminAPI
|
||||
|
||||
tenant: str = DEFAULT_TENANT
|
||||
database: str = DEFAULT_DATABASE
|
||||
|
||||
_server: AsyncServerAPI
|
||||
|
||||
@classmethod
|
||||
async def create(
|
||||
cls,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
settings: Settings = Settings(),
|
||||
) -> "AsyncClient":
|
||||
# Create an admin client for verifying that databases and tenants exist
|
||||
self = cls(settings=settings)
|
||||
SharedSystemClient._populate_data_from_system(self._system)
|
||||
|
||||
self.tenant = tenant
|
||||
self.database = database
|
||||
|
||||
# Get the root system component we want to interact with
|
||||
self._server = self._system.instance(AsyncServerAPI)
|
||||
|
||||
user_identity = await self.get_user_identity()
|
||||
|
||||
maybe_tenant, maybe_database = maybe_set_tenant_and_database(
|
||||
user_identity,
|
||||
overwrite_singleton_tenant_database_access_from_auth=settings.chroma_overwrite_singleton_tenant_database_access_from_auth,
|
||||
user_provided_tenant=tenant,
|
||||
user_provided_database=database,
|
||||
)
|
||||
if maybe_tenant:
|
||||
self.tenant = maybe_tenant
|
||||
if maybe_database:
|
||||
self.database = maybe_database
|
||||
|
||||
self._admin_client = AsyncAdminClient.from_system(self._system)
|
||||
await self._validate_tenant_database(tenant=self.tenant, database=self.database)
|
||||
|
||||
self._submit_client_start_event()
|
||||
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
# (we can't override and use from_system() because it's synchronous)
|
||||
async def from_system_async(
|
||||
cls,
|
||||
system: System,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> "AsyncClient":
|
||||
"""Create a client from an existing system. This is useful for testing and debugging."""
|
||||
return await AsyncClient.create(tenant, database, system.settings)
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def from_system(
|
||||
cls,
|
||||
system: System,
|
||||
) -> "SharedSystemClient":
|
||||
"""AsyncClient cannot be created synchronously. Use .from_system_async() instead."""
|
||||
raise NotImplementedError(
|
||||
"AsyncClient cannot be created synchronously. Use .from_system_async() instead."
|
||||
)
|
||||
|
||||
@override
|
||||
async def get_user_identity(self) -> UserIdentity:
|
||||
return await self._server.get_user_identity()
|
||||
|
||||
@override
|
||||
async def set_tenant(self, tenant: str, database: str = DEFAULT_DATABASE) -> None:
|
||||
await self._validate_tenant_database(tenant=tenant, database=database)
|
||||
self.tenant = tenant
|
||||
self.database = database
|
||||
|
||||
@override
|
||||
async def set_database(self, database: str) -> None:
|
||||
await self._validate_tenant_database(tenant=self.tenant, database=database)
|
||||
self.database = database
|
||||
|
||||
async def _validate_tenant_database(self, tenant: str, database: str) -> None:
|
||||
try:
|
||||
await self._admin_client.get_tenant(name=tenant)
|
||||
except httpx.ConnectError:
|
||||
raise ValueError(
|
||||
"Could not connect to a Chroma server. Are you sure it is running?"
|
||||
)
|
||||
# Propagate ChromaErrors
|
||||
except ChromaError as e:
|
||||
raise e
|
||||
except Exception:
|
||||
raise ValueError(
|
||||
f"Could not connect to tenant {tenant}. Are you sure it exists?"
|
||||
)
|
||||
|
||||
try:
|
||||
await self._admin_client.get_database(name=database, tenant=tenant)
|
||||
except httpx.ConnectError:
|
||||
raise ValueError(
|
||||
"Could not connect to a Chroma server. Are you sure it is running?"
|
||||
)
|
||||
|
||||
# region BaseAPI Methods
|
||||
# Note - we could do this in less verbose ways, but they break type checking
|
||||
@override
|
||||
async def heartbeat(self) -> int:
|
||||
return await self._server.heartbeat()
|
||||
|
||||
@override
|
||||
async def list_collections(
|
||||
self, limit: Optional[int] = None, offset: Optional[int] = None
|
||||
) -> Sequence[AsyncCollection]:
|
||||
models = await self._server.list_collections(
|
||||
limit, offset, tenant=self.tenant, database=self.database
|
||||
)
|
||||
return [AsyncCollection(client=self._server, model=model) for model in models]
|
||||
|
||||
@override
|
||||
async def count_collections(self) -> int:
|
||||
return await self._server.count_collections(
|
||||
tenant=self.tenant, database=self.database
|
||||
)
|
||||
|
||||
@override
|
||||
async def create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
embedding_function: Optional[
|
||||
EmbeddingFunction[Embeddable]
|
||||
] = DefaultEmbeddingFunction(), # type: ignore
|
||||
data_loader: Optional[DataLoader[Loadable]] = None,
|
||||
get_or_create: bool = False,
|
||||
) -> AsyncCollection:
|
||||
if configuration is None:
|
||||
configuration = {}
|
||||
|
||||
configuration_ef = configuration.get("embedding_function")
|
||||
|
||||
validate_embedding_function_conflict_on_create(
|
||||
embedding_function, configuration_ef
|
||||
)
|
||||
|
||||
# If ef provided in function params and collection config ef is None,
|
||||
# set the collection config ef to the function params
|
||||
if embedding_function is not None and configuration_ef is None:
|
||||
configuration["embedding_function"] = embedding_function
|
||||
|
||||
model = await self._server.create_collection(
|
||||
name=name,
|
||||
schema=schema,
|
||||
configuration=configuration,
|
||||
metadata=metadata,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
get_or_create=get_or_create,
|
||||
)
|
||||
return AsyncCollection(
|
||||
client=self._server,
|
||||
model=model,
|
||||
embedding_function=embedding_function,
|
||||
data_loader=data_loader,
|
||||
)
|
||||
|
||||
@override
|
||||
async def get_collection(
|
||||
self,
|
||||
name: str,
|
||||
embedding_function: Optional[
|
||||
EmbeddingFunction[Embeddable]
|
||||
] = DefaultEmbeddingFunction(), # type: ignore
|
||||
data_loader: Optional[DataLoader[Loadable]] = None,
|
||||
) -> AsyncCollection:
|
||||
model = await self._server.get_collection(
|
||||
name=name,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
persisted_ef_config = model.configuration_json.get("embedding_function")
|
||||
|
||||
validate_embedding_function_conflict_on_get(
|
||||
embedding_function, persisted_ef_config
|
||||
)
|
||||
|
||||
return AsyncCollection(
|
||||
client=self._server,
|
||||
model=model,
|
||||
embedding_function=embedding_function,
|
||||
data_loader=data_loader,
|
||||
)
|
||||
|
||||
@override
|
||||
async def get_or_create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
embedding_function: Optional[
|
||||
EmbeddingFunction[Embeddable]
|
||||
] = DefaultEmbeddingFunction(), # type: ignore
|
||||
data_loader: Optional[DataLoader[Loadable]] = None,
|
||||
) -> AsyncCollection:
|
||||
if configuration is None:
|
||||
configuration = {}
|
||||
|
||||
configuration_ef = configuration.get("embedding_function")
|
||||
|
||||
validate_embedding_function_conflict_on_create(
|
||||
embedding_function, configuration_ef
|
||||
)
|
||||
|
||||
if embedding_function is not None and configuration_ef is None:
|
||||
configuration["embedding_function"] = embedding_function
|
||||
model = await self._server.get_or_create_collection(
|
||||
name=name,
|
||||
schema=schema,
|
||||
configuration=configuration,
|
||||
metadata=metadata,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
persisted_ef_config = model.configuration_json.get("embedding_function")
|
||||
|
||||
validate_embedding_function_conflict_on_get(
|
||||
embedding_function, persisted_ef_config
|
||||
)
|
||||
|
||||
return AsyncCollection(
|
||||
client=self._server,
|
||||
model=model,
|
||||
embedding_function=embedding_function,
|
||||
data_loader=data_loader,
|
||||
)
|
||||
|
||||
@override
|
||||
async def _modify(
|
||||
self,
|
||||
id: UUID,
|
||||
new_name: Optional[str] = None,
|
||||
new_metadata: Optional[CollectionMetadata] = None,
|
||||
new_configuration: Optional[UpdateCollectionConfiguration] = None,
|
||||
) -> None:
|
||||
return await self._server._modify(
|
||||
id=id,
|
||||
new_name=new_name,
|
||||
new_metadata=new_metadata,
|
||||
new_configuration=new_configuration,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
@override
|
||||
async def delete_collection(
|
||||
self,
|
||||
name: str,
|
||||
) -> None:
|
||||
return await self._server.delete_collection(
|
||||
name=name,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
#
|
||||
# ITEM METHODS
|
||||
#
|
||||
|
||||
@override
|
||||
async def _add(
|
||||
self,
|
||||
ids: IDs,
|
||||
collection_id: UUID,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
) -> bool:
|
||||
return await self._server._add(
|
||||
ids=ids,
|
||||
collection_id=collection_id,
|
||||
embeddings=embeddings,
|
||||
metadatas=metadatas,
|
||||
documents=documents,
|
||||
uris=uris,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
@override
|
||||
async def _update(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Optional[Embeddings] = None,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
) -> bool:
|
||||
return await self._server._update(
|
||||
collection_id=collection_id,
|
||||
ids=ids,
|
||||
embeddings=embeddings,
|
||||
metadatas=metadatas,
|
||||
documents=documents,
|
||||
uris=uris,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
@override
|
||||
async def _upsert(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
) -> bool:
|
||||
return await self._server._upsert(
|
||||
collection_id=collection_id,
|
||||
ids=ids,
|
||||
embeddings=embeddings,
|
||||
metadatas=metadatas,
|
||||
documents=documents,
|
||||
uris=uris,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
@override
|
||||
async def _count(self, collection_id: UUID) -> int:
|
||||
return await self._server._count(
|
||||
collection_id=collection_id,
|
||||
)
|
||||
|
||||
@override
|
||||
async def _peek(self, collection_id: UUID, n: int = 10) -> GetResult:
|
||||
return await self._server._peek(
|
||||
collection_id=collection_id,
|
||||
n=n,
|
||||
)
|
||||
|
||||
@override
|
||||
async def _get(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocuments,
|
||||
) -> GetResult:
|
||||
return await self._server._get(
|
||||
collection_id=collection_id,
|
||||
ids=ids,
|
||||
where=where,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
where_document=where_document,
|
||||
include=include,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
async def _delete(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs],
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
) -> None:
|
||||
await self._server._delete(
|
||||
collection_id=collection_id,
|
||||
ids=ids,
|
||||
where=where,
|
||||
where_document=where_document,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
@override
|
||||
async def _query(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
query_embeddings: Embeddings,
|
||||
ids: Optional[IDs] = None,
|
||||
n_results: int = 10,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocumentsDistances,
|
||||
) -> QueryResult:
|
||||
return await self._server._query(
|
||||
collection_id=collection_id,
|
||||
query_embeddings=query_embeddings,
|
||||
ids=ids,
|
||||
n_results=n_results,
|
||||
where=where,
|
||||
where_document=where_document,
|
||||
include=include,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
@override
|
||||
async def reset(self) -> bool:
|
||||
return await self._server.reset()
|
||||
|
||||
@override
|
||||
async def get_version(self) -> str:
|
||||
return await self._server.get_version()
|
||||
|
||||
@override
|
||||
def get_settings(self) -> Settings:
|
||||
return self._server.get_settings()
|
||||
|
||||
@override
|
||||
async def get_max_batch_size(self) -> int:
|
||||
return await self._server.get_max_batch_size()
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
class AsyncAdminClient(SharedSystemClient, AsyncAdminAPI):
|
||||
_server: AsyncServerAPI
|
||||
|
||||
def __init__(self, settings: Settings = Settings()) -> None:
|
||||
super().__init__(settings)
|
||||
self._server = self._system.instance(AsyncServerAPI)
|
||||
|
||||
@override
|
||||
async def create_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
|
||||
return await self._server.create_database(name=name, tenant=tenant)
|
||||
|
||||
@override
|
||||
async def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> Database:
|
||||
return await self._server.get_database(name=name, tenant=tenant)
|
||||
|
||||
@override
|
||||
async def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
|
||||
return await self._server.delete_database(name=name, tenant=tenant)
|
||||
|
||||
@override
|
||||
async def list_databases(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> Sequence[Database]:
|
||||
return await self._server.list_databases(
|
||||
limit=limit, offset=offset, tenant=tenant
|
||||
)
|
||||
|
||||
@override
|
||||
async def create_tenant(self, name: str) -> None:
|
||||
return await self._server.create_tenant(name=name)
|
||||
|
||||
@override
|
||||
async def get_tenant(self, name: str) -> Tenant:
|
||||
return await self._server.get_tenant(name=name)
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def from_system(
|
||||
cls,
|
||||
system: System,
|
||||
) -> "AsyncAdminClient":
|
||||
SharedSystemClient._populate_data_from_system(system)
|
||||
instance = cls(settings=system.settings)
|
||||
return instance
|
||||
@@ -0,0 +1,773 @@
|
||||
import asyncio
|
||||
from uuid import UUID
|
||||
import urllib.parse
|
||||
import orjson
|
||||
from typing import Any, Optional, cast, Tuple, Sequence, Dict, List
|
||||
import logging
|
||||
import httpx
|
||||
from overrides import override
|
||||
from chromadb import __version__
|
||||
from chromadb.auth import UserIdentity
|
||||
from chromadb.api.async_api import AsyncServerAPI
|
||||
from chromadb.api.base_http_client import BaseHTTPClient
|
||||
from chromadb.api.collection_configuration import (
|
||||
CreateCollectionConfiguration,
|
||||
UpdateCollectionConfiguration,
|
||||
create_collection_configuration_to_json,
|
||||
update_collection_configuration_to_json,
|
||||
)
|
||||
from chromadb.config import DEFAULT_DATABASE, DEFAULT_TENANT, System, Settings
|
||||
from chromadb.telemetry.opentelemetry import (
|
||||
OpenTelemetryClient,
|
||||
OpenTelemetryGranularity,
|
||||
trace_method,
|
||||
)
|
||||
from chromadb.telemetry.product import ProductTelemetryClient
|
||||
from chromadb.utils.async_to_sync import async_to_sync
|
||||
from chromadb.types import Database, Tenant, Collection as CollectionModel
|
||||
from chromadb.execution.expression.plan import Search
|
||||
|
||||
from chromadb.api.types import (
|
||||
Documents,
|
||||
Embeddings,
|
||||
IDs,
|
||||
Include,
|
||||
Schema,
|
||||
Metadatas,
|
||||
URIs,
|
||||
Where,
|
||||
WhereDocument,
|
||||
GetResult,
|
||||
QueryResult,
|
||||
SearchResult,
|
||||
CollectionMetadata,
|
||||
optional_embeddings_to_base64_strings,
|
||||
validate_batch,
|
||||
convert_np_embeddings_to_list,
|
||||
IncludeMetadataDocuments,
|
||||
IncludeMetadataDocumentsDistances,
|
||||
)
|
||||
|
||||
from chromadb.api.types import (
|
||||
IncludeMetadataDocumentsEmbeddings,
|
||||
serialize_metadata,
|
||||
deserialize_metadata,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncFastAPI(BaseHTTPClient, AsyncServerAPI):
|
||||
# We make one client per event loop to avoid unexpected issues if a client
|
||||
# is shared between event loops.
|
||||
# For example, if a client is constructed in the main thread, then passed
|
||||
# (or a returned Collection is passed) to a new thread, the client would
|
||||
# normally throw an obscure asyncio error.
|
||||
# Mixing asyncio and threading in this manner usually discouraged, but
|
||||
# this gives a better user experience with practically no downsides.
|
||||
# https://github.com/encode/httpx/issues/2058
|
||||
_clients: Dict[int, httpx.AsyncClient] = {}
|
||||
|
||||
def __init__(self, system: System):
|
||||
super().__init__(system)
|
||||
|
||||
system.settings.require("chroma_server_host")
|
||||
system.settings.require("chroma_server_http_port")
|
||||
|
||||
self._opentelemetry_client = self.require(OpenTelemetryClient)
|
||||
self._product_telemetry_client = self.require(ProductTelemetryClient)
|
||||
self._settings = system.settings
|
||||
|
||||
self._api_url = AsyncFastAPI.resolve_url(
|
||||
chroma_server_host=str(system.settings.chroma_server_host),
|
||||
chroma_server_http_port=system.settings.chroma_server_http_port,
|
||||
chroma_server_ssl_enabled=system.settings.chroma_server_ssl_enabled,
|
||||
default_api_path=system.settings.chroma_server_api_default_path,
|
||||
)
|
||||
|
||||
async def __aenter__(self) -> "AsyncFastAPI":
|
||||
self._get_client()
|
||||
return self
|
||||
|
||||
async def _cleanup(self) -> None:
|
||||
while len(self._clients) > 0:
|
||||
(_, client) = self._clients.popitem()
|
||||
await client.aclose()
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
||||
await self._cleanup()
|
||||
|
||||
@override
|
||||
def stop(self) -> None:
|
||||
super().stop()
|
||||
|
||||
@async_to_sync
|
||||
async def sync_cleanup() -> None:
|
||||
await self._cleanup()
|
||||
|
||||
sync_cleanup()
|
||||
|
||||
def _get_client(self) -> httpx.AsyncClient:
|
||||
# Ideally this would use anyio to be compatible with both
|
||||
# asyncio and trio, but anyio does not expose any way to identify
|
||||
# the current event loop.
|
||||
# We attempt to get the loop assuming the environment is asyncio, and
|
||||
# otherwise gracefully fall back to using a singleton client.
|
||||
loop_hash = None
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop_hash = loop.__hash__()
|
||||
except RuntimeError:
|
||||
loop_hash = 0
|
||||
|
||||
if loop_hash not in self._clients:
|
||||
headers = (self._settings.chroma_server_headers or {}).copy()
|
||||
headers["Content-Type"] = "application/json"
|
||||
headers["User-Agent"] = (
|
||||
"Chroma Python Client v"
|
||||
+ __version__
|
||||
+ " (https://github.com/chroma-core/chroma)"
|
||||
)
|
||||
|
||||
limits = httpx.Limits(keepalive_expiry=self.keepalive_secs)
|
||||
self._clients[loop_hash] = httpx.AsyncClient(
|
||||
timeout=None,
|
||||
headers=headers,
|
||||
verify=self._settings.chroma_server_ssl_verify or False,
|
||||
limits=limits,
|
||||
)
|
||||
|
||||
return self._clients[loop_hash]
|
||||
|
||||
async def _make_request(
|
||||
self, method: str, path: str, **kwargs: Dict[str, Any]
|
||||
) -> Any:
|
||||
# If the request has json in kwargs, use orjson to serialize it,
|
||||
# remove it from kwargs, and add it to the content parameter
|
||||
# This is because httpx uses a slower json serializer
|
||||
if "json" in kwargs:
|
||||
data = orjson.dumps(kwargs.pop("json"), option=orjson.OPT_SERIALIZE_NUMPY)
|
||||
kwargs["content"] = data
|
||||
|
||||
# Unlike requests, httpx does not automatically escape the path
|
||||
escaped_path = urllib.parse.quote(path, safe="/", encoding=None, errors=None)
|
||||
url = self._api_url + escaped_path
|
||||
|
||||
response = await self._get_client().request(method, url, **cast(Any, kwargs))
|
||||
BaseHTTPClient._raise_chroma_error(response)
|
||||
return orjson.loads(response.text)
|
||||
|
||||
@trace_method("AsyncFastAPI.heartbeat", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def heartbeat(self) -> int:
|
||||
response = await self._make_request("get", "")
|
||||
return int(response["nanosecond heartbeat"])
|
||||
|
||||
@trace_method("AsyncFastAPI.create_database", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def create_database(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> None:
|
||||
await self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases",
|
||||
json={"name": name},
|
||||
)
|
||||
|
||||
@trace_method("AsyncFastAPI.get_database", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def get_database(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> Database:
|
||||
response = await self._make_request(
|
||||
"get",
|
||||
f"/tenants/{tenant}/databases/{name}",
|
||||
params={"tenant": tenant},
|
||||
)
|
||||
|
||||
return Database(
|
||||
id=response["id"], name=response["name"], tenant=response["tenant"]
|
||||
)
|
||||
|
||||
@trace_method("AsyncFastAPI.delete_database", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def delete_database(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> None:
|
||||
await self._make_request(
|
||||
"delete",
|
||||
f"/tenants/{tenant}/databases/{name}",
|
||||
)
|
||||
|
||||
@trace_method("AsyncFastAPI.list_databases", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def list_databases(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> Sequence[Database]:
|
||||
response = await self._make_request(
|
||||
"get",
|
||||
f"/tenants/{tenant}/databases",
|
||||
params=BaseHTTPClient._clean_params(
|
||||
{
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
return [
|
||||
Database(id=db["id"], name=db["name"], tenant=db["tenant"])
|
||||
for db in response
|
||||
]
|
||||
|
||||
@trace_method("AsyncFastAPI.create_tenant", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def create_tenant(self, name: str) -> None:
|
||||
await self._make_request(
|
||||
"post",
|
||||
"/tenants",
|
||||
json={"name": name},
|
||||
)
|
||||
|
||||
@trace_method("AsyncFastAPI.get_tenant", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def get_tenant(self, name: str) -> Tenant:
|
||||
resp_json = await self._make_request(
|
||||
"get",
|
||||
"/tenants/" + name,
|
||||
)
|
||||
|
||||
return Tenant(name=resp_json["name"])
|
||||
|
||||
@trace_method("AsyncFastAPI.get_user_identity", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def get_user_identity(self) -> UserIdentity:
|
||||
return UserIdentity(**(await self._make_request("get", "/auth/identity")))
|
||||
|
||||
@trace_method("AsyncFastAPI.list_collections", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def list_collections(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> Sequence[CollectionModel]:
|
||||
resp_json = await self._make_request(
|
||||
"get",
|
||||
f"/tenants/{tenant}/databases/{database}/collections",
|
||||
params=BaseHTTPClient._clean_params(
|
||||
{
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
models = [
|
||||
CollectionModel.from_json(json_collection) for json_collection in resp_json
|
||||
]
|
||||
return models
|
||||
|
||||
@trace_method("AsyncFastAPI.count_collections", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def count_collections(
|
||||
self, tenant: str = DEFAULT_TENANT, database: str = DEFAULT_DATABASE
|
||||
) -> int:
|
||||
resp_json = await self._make_request(
|
||||
"get",
|
||||
f"/tenants/{tenant}/databases/{database}/collections_count",
|
||||
)
|
||||
|
||||
return cast(int, resp_json)
|
||||
|
||||
@trace_method("AsyncFastAPI.create_collection", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
get_or_create: bool = False,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
"""Creates a collection"""
|
||||
config_json = (
|
||||
create_collection_configuration_to_json(configuration, metadata)
|
||||
if configuration
|
||||
else None
|
||||
)
|
||||
serialized_schema = schema.serialize_to_json() if schema else None
|
||||
resp_json = await self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections",
|
||||
json={
|
||||
"name": name,
|
||||
"metadata": metadata,
|
||||
"configuration": config_json,
|
||||
"schema": serialized_schema,
|
||||
"get_or_create": get_or_create,
|
||||
},
|
||||
)
|
||||
model = CollectionModel.from_json(resp_json)
|
||||
|
||||
return model
|
||||
|
||||
@trace_method("AsyncFastAPI.get_collection", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def get_collection(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
resp_json = await self._make_request(
|
||||
"get",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{name}",
|
||||
)
|
||||
|
||||
model = CollectionModel.from_json(resp_json)
|
||||
|
||||
return model
|
||||
|
||||
@trace_method(
|
||||
"AsyncFastAPI.get_or_create_collection", OpenTelemetryGranularity.OPERATION
|
||||
)
|
||||
@override
|
||||
async def get_or_create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
return await self.create_collection(
|
||||
name=name,
|
||||
schema=schema,
|
||||
configuration=configuration,
|
||||
metadata=metadata,
|
||||
get_or_create=True,
|
||||
tenant=tenant,
|
||||
database=database,
|
||||
)
|
||||
|
||||
@trace_method("AsyncFastAPI._modify", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def _modify(
|
||||
self,
|
||||
id: UUID,
|
||||
new_name: Optional[str] = None,
|
||||
new_metadata: Optional[CollectionMetadata] = None,
|
||||
new_configuration: Optional[UpdateCollectionConfiguration] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
await self._make_request(
|
||||
"put",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{id}",
|
||||
json={
|
||||
"new_metadata": new_metadata,
|
||||
"new_name": new_name,
|
||||
"new_configuration": update_collection_configuration_to_json(
|
||||
new_configuration
|
||||
)
|
||||
if new_configuration
|
||||
else None,
|
||||
},
|
||||
)
|
||||
|
||||
@trace_method("AsyncFastAPI._fork", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def _fork(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
new_name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
resp_json = await self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{collection_id}/fork",
|
||||
json={"new_name": new_name},
|
||||
)
|
||||
model = CollectionModel.from_json(resp_json)
|
||||
return model
|
||||
|
||||
@trace_method("AsyncFastAPI._search", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def _search(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
searches: List[Search],
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> SearchResult:
|
||||
"""Performs hybrid search on a collection"""
|
||||
payload = {"searches": [s.to_dict() for s in searches]}
|
||||
|
||||
resp_json = await self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{collection_id}/search",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
metadata_batches = resp_json.get("metadatas", None)
|
||||
if metadata_batches is not None:
|
||||
resp_json["metadatas"] = [
|
||||
[
|
||||
deserialize_metadata(metadata) if metadata is not None else None
|
||||
for metadata in metadatas
|
||||
]
|
||||
if metadatas is not None
|
||||
else None
|
||||
for metadatas in metadata_batches
|
||||
]
|
||||
|
||||
return SearchResult(resp_json)
|
||||
|
||||
@trace_method("AsyncFastAPI.delete_collection", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def delete_collection(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
await self._make_request(
|
||||
"delete",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{name}",
|
||||
)
|
||||
|
||||
@trace_method("AsyncFastAPI._count", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def _count(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> int:
|
||||
"""Returns the number of embeddings in the database"""
|
||||
resp_json = await self._make_request(
|
||||
"get",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{collection_id}/count",
|
||||
)
|
||||
|
||||
return cast(int, resp_json)
|
||||
|
||||
@trace_method("AsyncFastAPI._peek", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def _peek(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
n: int = 10,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> GetResult:
|
||||
resp = await self._get(
|
||||
collection_id,
|
||||
tenant=tenant,
|
||||
database=database,
|
||||
limit=n,
|
||||
include=IncludeMetadataDocumentsEmbeddings,
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
@trace_method("AsyncFastAPI._get", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def _get(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocuments,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> GetResult:
|
||||
# Servers do not support the "data" include, as that is hydrated on the client side
|
||||
filtered_include = [i for i in include if i != "data"]
|
||||
|
||||
resp_json = await self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{collection_id}/get",
|
||||
json={
|
||||
"ids": ids,
|
||||
"where": where,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"where_document": where_document,
|
||||
"include": filtered_include,
|
||||
},
|
||||
)
|
||||
|
||||
metadatas = resp_json.get("metadatas", None)
|
||||
if metadatas is not None:
|
||||
metadatas = [
|
||||
deserialize_metadata(metadata) if metadata is not None else None
|
||||
for metadata in metadatas
|
||||
]
|
||||
|
||||
return GetResult(
|
||||
ids=resp_json["ids"],
|
||||
embeddings=resp_json.get("embeddings", None),
|
||||
metadatas=metadatas, # type: ignore
|
||||
documents=resp_json.get("documents", None),
|
||||
data=None,
|
||||
uris=resp_json.get("uris", None),
|
||||
included=include,
|
||||
)
|
||||
|
||||
@trace_method("AsyncFastAPI._delete", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def _delete(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
await self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{collection_id}/delete",
|
||||
json={"where": where, "ids": ids, "where_document": where_document},
|
||||
)
|
||||
return None
|
||||
|
||||
@trace_method("AsyncFastAPI._submit_batch", OpenTelemetryGranularity.ALL)
|
||||
async def _submit_batch(
|
||||
self,
|
||||
batch: Tuple[
|
||||
IDs,
|
||||
Optional[Embeddings],
|
||||
Optional[Metadatas],
|
||||
Optional[Documents],
|
||||
Optional[URIs],
|
||||
],
|
||||
url: str,
|
||||
) -> Any:
|
||||
"""
|
||||
Submits a batch of embeddings to the database
|
||||
"""
|
||||
supports_base64_encoding = await self.supports_base64_encoding()
|
||||
|
||||
serialized_metadatas = None
|
||||
if batch[2] is not None:
|
||||
serialized_metadatas = [
|
||||
serialize_metadata(metadata) if metadata is not None else None
|
||||
for metadata in batch[2]
|
||||
]
|
||||
|
||||
data = {
|
||||
"ids": batch[0],
|
||||
"embeddings": optional_embeddings_to_base64_strings(batch[1])
|
||||
if supports_base64_encoding
|
||||
else batch[1],
|
||||
"metadatas": serialized_metadatas,
|
||||
"documents": batch[3],
|
||||
"uris": batch[4],
|
||||
}
|
||||
|
||||
return await self._make_request(
|
||||
"post",
|
||||
url,
|
||||
json=data,
|
||||
)
|
||||
|
||||
@trace_method("AsyncFastAPI._add", OpenTelemetryGranularity.ALL)
|
||||
@override
|
||||
async def _add(
|
||||
self,
|
||||
ids: IDs,
|
||||
collection_id: UUID,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
batch = (
|
||||
ids,
|
||||
embeddings,
|
||||
metadatas,
|
||||
documents,
|
||||
uris,
|
||||
)
|
||||
validate_batch(batch, {"max_batch_size": await self.get_max_batch_size()})
|
||||
await self._submit_batch(
|
||||
batch,
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{str(collection_id)}/add",
|
||||
)
|
||||
return True
|
||||
|
||||
@trace_method("AsyncFastAPI._update", OpenTelemetryGranularity.ALL)
|
||||
@override
|
||||
async def _update(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Optional[Embeddings] = None,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
batch = (
|
||||
ids,
|
||||
embeddings if embeddings is not None else None,
|
||||
metadatas,
|
||||
documents,
|
||||
uris,
|
||||
)
|
||||
validate_batch(batch, {"max_batch_size": await self.get_max_batch_size()})
|
||||
|
||||
await self._submit_batch(
|
||||
batch,
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{str(collection_id)}/update",
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@trace_method("AsyncFastAPI._upsert", OpenTelemetryGranularity.ALL)
|
||||
@override
|
||||
async def _upsert(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
batch = (
|
||||
ids,
|
||||
embeddings,
|
||||
metadatas,
|
||||
documents,
|
||||
uris,
|
||||
)
|
||||
validate_batch(batch, {"max_batch_size": await self.get_max_batch_size()})
|
||||
await self._submit_batch(
|
||||
batch,
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{str(collection_id)}/upsert",
|
||||
)
|
||||
return True
|
||||
|
||||
@trace_method("AsyncFastAPI._query", OpenTelemetryGranularity.ALL)
|
||||
@override
|
||||
async def _query(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
query_embeddings: Embeddings,
|
||||
ids: Optional[IDs] = None,
|
||||
n_results: int = 10,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocumentsDistances,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> QueryResult:
|
||||
# Servers do not support the "data" include, as that is hydrated on the client side
|
||||
filtered_include = [i for i in include if i != "data"]
|
||||
|
||||
resp_json = await self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{collection_id}/query",
|
||||
json={
|
||||
"ids": ids,
|
||||
"query_embeddings": convert_np_embeddings_to_list(query_embeddings)
|
||||
if query_embeddings is not None
|
||||
else None,
|
||||
"n_results": n_results,
|
||||
"where": where,
|
||||
"where_document": where_document,
|
||||
"include": filtered_include,
|
||||
},
|
||||
)
|
||||
|
||||
metadata_batches = resp_json.get("metadatas", None)
|
||||
if metadata_batches is not None:
|
||||
metadata_batches = [
|
||||
[
|
||||
deserialize_metadata(metadata) if metadata is not None else None
|
||||
for metadata in metadatas
|
||||
]
|
||||
if metadatas is not None
|
||||
else None
|
||||
for metadatas in metadata_batches
|
||||
]
|
||||
|
||||
return QueryResult(
|
||||
ids=resp_json["ids"],
|
||||
distances=resp_json.get("distances", None),
|
||||
embeddings=resp_json.get("embeddings", None),
|
||||
metadatas=metadata_batches, # type: ignore
|
||||
documents=resp_json.get("documents", None),
|
||||
uris=resp_json.get("uris", None),
|
||||
data=None,
|
||||
included=include,
|
||||
)
|
||||
|
||||
@trace_method("AsyncFastAPI.reset", OpenTelemetryGranularity.ALL)
|
||||
@override
|
||||
async def reset(self) -> bool:
|
||||
resp_json = await self._make_request("post", "/reset")
|
||||
return cast(bool, resp_json)
|
||||
|
||||
@trace_method("AsyncFastAPI.get_version", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def get_version(self) -> str:
|
||||
resp_json = await self._make_request("get", "/version")
|
||||
return cast(str, resp_json)
|
||||
|
||||
@override
|
||||
def get_settings(self) -> Settings:
|
||||
return self._settings
|
||||
|
||||
@trace_method(
|
||||
"AsyncFastAPI.get_pre_flight_checks", OpenTelemetryGranularity.OPERATION
|
||||
)
|
||||
async def get_pre_flight_checks(self) -> Any:
|
||||
if self.pre_flight_checks is None:
|
||||
resp_json = await self._make_request("get", "/pre-flight-checks")
|
||||
self.pre_flight_checks = resp_json
|
||||
return self.pre_flight_checks
|
||||
|
||||
@trace_method(
|
||||
"AsyncFastAPI.supports_base64_encoding", OpenTelemetryGranularity.OPERATION
|
||||
)
|
||||
async def supports_base64_encoding(self) -> bool:
|
||||
pre_flight_checks = await self.get_pre_flight_checks()
|
||||
b64_encoding_enabled = cast(
|
||||
bool, pre_flight_checks.get("supports_base64_encoding", False)
|
||||
)
|
||||
return b64_encoding_enabled
|
||||
|
||||
@trace_method("AsyncFastAPI.get_max_batch_size", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
async def get_max_batch_size(self) -> int:
|
||||
pre_flight_checks = await self.get_pre_flight_checks()
|
||||
max_batch_size = cast(int, pre_flight_checks.get("max_batch_size", -1))
|
||||
return max_batch_size
|
||||
@@ -0,0 +1,105 @@
|
||||
from typing import Any, Dict, Optional, TypeVar
|
||||
from urllib.parse import quote, urlparse, urlunparse
|
||||
import logging
|
||||
import orjson as json
|
||||
import httpx
|
||||
|
||||
import chromadb.errors as errors
|
||||
from chromadb.config import Settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseHTTPClient:
|
||||
_settings: Settings
|
||||
pre_flight_checks: Any = None
|
||||
keepalive_secs: int = 40
|
||||
|
||||
@staticmethod
|
||||
def _validate_host(host: str) -> None:
|
||||
parsed = urlparse(host)
|
||||
if "/" in host and parsed.scheme not in {"http", "https"}:
|
||||
raise ValueError(
|
||||
"Invalid URL. " f"Unrecognized protocol - {parsed.scheme}."
|
||||
)
|
||||
if "/" in host and (not host.startswith("http")):
|
||||
raise ValueError(
|
||||
"Invalid URL. "
|
||||
"Seems that you are trying to pass URL as a host but without \
|
||||
specifying the protocol. "
|
||||
"Please add http:// or https:// to the host."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def resolve_url(
|
||||
chroma_server_host: str,
|
||||
chroma_server_ssl_enabled: Optional[bool] = False,
|
||||
default_api_path: Optional[str] = "",
|
||||
chroma_server_http_port: Optional[int] = 8000,
|
||||
) -> str:
|
||||
_skip_port = False
|
||||
_chroma_server_host = chroma_server_host
|
||||
BaseHTTPClient._validate_host(_chroma_server_host)
|
||||
if _chroma_server_host.startswith("http"):
|
||||
logger.debug("Skipping port as the user is passing a full URL")
|
||||
_skip_port = True
|
||||
parsed = urlparse(_chroma_server_host)
|
||||
|
||||
scheme = "https" if chroma_server_ssl_enabled else parsed.scheme or "http"
|
||||
net_loc = parsed.netloc or parsed.hostname or chroma_server_host
|
||||
port = (
|
||||
":" + str(parsed.port or chroma_server_http_port) if not _skip_port else ""
|
||||
)
|
||||
path = parsed.path or default_api_path
|
||||
|
||||
if not path or path == net_loc:
|
||||
path = default_api_path if default_api_path else ""
|
||||
if not path.endswith(default_api_path or ""):
|
||||
path = path + default_api_path if default_api_path else ""
|
||||
full_url = urlunparse(
|
||||
(scheme, f"{net_loc}{port}", quote(path.replace("//", "/")), "", "", "")
|
||||
)
|
||||
|
||||
return full_url
|
||||
|
||||
# requests removes None values from the built query string, but httpx includes it as an empty value
|
||||
T = TypeVar("T", bound=Dict[Any, Any])
|
||||
|
||||
@staticmethod
|
||||
def _clean_params(params: T) -> T:
|
||||
"""Remove None values from provided dict."""
|
||||
return {k: v for k, v in params.items() if v is not None} # type: ignore
|
||||
|
||||
@staticmethod
|
||||
def _raise_chroma_error(resp: httpx.Response) -> None:
|
||||
"""Raises an error if the response is not ok, using a ChromaError if possible."""
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
return
|
||||
except httpx.HTTPStatusError:
|
||||
pass
|
||||
|
||||
chroma_error = None
|
||||
try:
|
||||
body = json.loads(resp.text)
|
||||
if "error" in body:
|
||||
if body["error"] in errors.error_types:
|
||||
chroma_error = errors.error_types[body["error"]](body["message"])
|
||||
|
||||
trace_id = resp.headers.get("chroma-trace-id")
|
||||
if trace_id:
|
||||
chroma_error.trace_id = trace_id
|
||||
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
if chroma_error:
|
||||
raise chroma_error
|
||||
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except httpx.HTTPStatusError:
|
||||
trace_id = resp.headers.get("chroma-trace-id")
|
||||
if trace_id:
|
||||
raise Exception(f"{resp.text} (trace ID: {trace_id})")
|
||||
raise (Exception(resp.text))
|
||||
@@ -0,0 +1,545 @@
|
||||
from typing import Optional, Sequence
|
||||
from uuid import UUID
|
||||
|
||||
from overrides import override
|
||||
import httpx
|
||||
from chromadb.api import AdminAPI, ClientAPI, ServerAPI
|
||||
from chromadb.api.collection_configuration import (
|
||||
CreateCollectionConfiguration,
|
||||
UpdateCollectionConfiguration,
|
||||
validate_embedding_function_conflict_on_create,
|
||||
validate_embedding_function_conflict_on_get,
|
||||
)
|
||||
from chromadb.api.shared_system_client import SharedSystemClient
|
||||
from chromadb.api.types import (
|
||||
CollectionMetadata,
|
||||
DataLoader,
|
||||
Documents,
|
||||
Embeddable,
|
||||
EmbeddingFunction,
|
||||
Embeddings,
|
||||
GetResult,
|
||||
IDs,
|
||||
Include,
|
||||
Loadable,
|
||||
Metadatas,
|
||||
QueryResult,
|
||||
Schema,
|
||||
URIs,
|
||||
IncludeMetadataDocuments,
|
||||
IncludeMetadataDocumentsDistances,
|
||||
DefaultEmbeddingFunction,
|
||||
)
|
||||
from chromadb.auth import UserIdentity
|
||||
from chromadb.auth.utils import maybe_set_tenant_and_database
|
||||
from chromadb.config import Settings, System
|
||||
from chromadb.config import DEFAULT_TENANT, DEFAULT_DATABASE
|
||||
from chromadb.api.models.Collection import Collection
|
||||
from chromadb.errors import ChromaAuthError, ChromaError
|
||||
from chromadb.types import Database, Tenant, Where, WhereDocument
|
||||
|
||||
|
||||
class Client(SharedSystemClient, ClientAPI):
|
||||
"""A client for Chroma. This is the main entrypoint for interacting with Chroma.
|
||||
A client internally stores its tenant and database and proxies calls to a
|
||||
Server API instance of Chroma. It treats the Server API and corresponding System
|
||||
as a singleton, so multiple clients connecting to the same resource will share the
|
||||
same API instance.
|
||||
|
||||
Client implementations should be implement their own API-caching strategies.
|
||||
"""
|
||||
|
||||
tenant: str = DEFAULT_TENANT
|
||||
database: str = DEFAULT_DATABASE
|
||||
|
||||
_server: ServerAPI
|
||||
# An internal admin client for verifying that databases and tenants exist
|
||||
_admin_client: AdminAPI
|
||||
|
||||
# region Initialization
|
||||
def __init__(
|
||||
self,
|
||||
tenant: Optional[str] = DEFAULT_TENANT,
|
||||
database: Optional[str] = DEFAULT_DATABASE,
|
||||
settings: Settings = Settings(),
|
||||
) -> None:
|
||||
super().__init__(settings=settings)
|
||||
if tenant is not None:
|
||||
self.tenant = tenant
|
||||
if database is not None:
|
||||
self.database = database
|
||||
|
||||
# Get the root system component we want to interact with
|
||||
self._server = self._system.instance(ServerAPI)
|
||||
|
||||
user_identity = self.get_user_identity()
|
||||
|
||||
maybe_tenant, maybe_database = maybe_set_tenant_and_database(
|
||||
user_identity,
|
||||
overwrite_singleton_tenant_database_access_from_auth=settings.chroma_overwrite_singleton_tenant_database_access_from_auth,
|
||||
user_provided_tenant=tenant,
|
||||
user_provided_database=database,
|
||||
)
|
||||
|
||||
# this should not happen unless types are invalidated
|
||||
if maybe_tenant is None and tenant is None:
|
||||
raise ChromaAuthError(
|
||||
"Could not determine a tenant from the current authentication method. Please provide a tenant."
|
||||
)
|
||||
if maybe_database is None and database is None:
|
||||
raise ChromaAuthError(
|
||||
"Could not determine a database name from the current authentication method. Please provide a database name."
|
||||
)
|
||||
|
||||
if maybe_tenant:
|
||||
self.tenant = maybe_tenant
|
||||
if maybe_database:
|
||||
self.database = maybe_database
|
||||
|
||||
# Create an admin client for verifying that databases and tenants exist
|
||||
self._admin_client = AdminClient.from_system(self._system)
|
||||
self._validate_tenant_database(tenant=self.tenant, database=self.database)
|
||||
|
||||
self._submit_client_start_event()
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def from_system(
|
||||
cls,
|
||||
system: System,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> "Client":
|
||||
SharedSystemClient._populate_data_from_system(system)
|
||||
instance = cls(tenant=tenant, database=database, settings=system.settings)
|
||||
return instance
|
||||
|
||||
# endregion
|
||||
|
||||
@override
|
||||
def get_user_identity(self) -> UserIdentity:
|
||||
try:
|
||||
return self._server.get_user_identity()
|
||||
except httpx.ConnectError:
|
||||
raise ValueError(
|
||||
"Could not connect to a Chroma server. Are you sure it is running?"
|
||||
)
|
||||
# Propagate ChromaErrors
|
||||
except ChromaError as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise ValueError(str(e))
|
||||
|
||||
# region BaseAPI Methods
|
||||
# Note - we could do this in less verbose ways, but they break type checking
|
||||
@override
|
||||
def heartbeat(self) -> int:
|
||||
return self._server.heartbeat()
|
||||
|
||||
@override
|
||||
def list_collections(
|
||||
self, limit: Optional[int] = None, offset: Optional[int] = None
|
||||
) -> Sequence[Collection]:
|
||||
return [
|
||||
Collection(client=self._server, model=model)
|
||||
for model in self._server.list_collections(
|
||||
limit, offset, tenant=self.tenant, database=self.database
|
||||
)
|
||||
]
|
||||
|
||||
@override
|
||||
def count_collections(self) -> int:
|
||||
return self._server.count_collections(
|
||||
tenant=self.tenant, database=self.database
|
||||
)
|
||||
|
||||
@override
|
||||
def create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
embedding_function: Optional[
|
||||
EmbeddingFunction[Embeddable]
|
||||
] = DefaultEmbeddingFunction(), # type: ignore
|
||||
data_loader: Optional[DataLoader[Loadable]] = None,
|
||||
get_or_create: bool = False,
|
||||
) -> Collection:
|
||||
if configuration is None:
|
||||
configuration = {}
|
||||
|
||||
configuration_ef = configuration.get("embedding_function")
|
||||
|
||||
validate_embedding_function_conflict_on_create(
|
||||
embedding_function, configuration_ef
|
||||
)
|
||||
|
||||
# If ef provided in function params and collection config ef is None,
|
||||
# set the collection config ef to the function params
|
||||
if embedding_function is not None and configuration_ef is None:
|
||||
configuration["embedding_function"] = embedding_function
|
||||
|
||||
model = self._server.create_collection(
|
||||
name=name,
|
||||
schema=schema,
|
||||
metadata=metadata,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
get_or_create=get_or_create,
|
||||
configuration=configuration,
|
||||
)
|
||||
return Collection(
|
||||
client=self._server,
|
||||
model=model,
|
||||
embedding_function=embedding_function,
|
||||
data_loader=data_loader,
|
||||
)
|
||||
|
||||
@override
|
||||
def get_collection(
|
||||
self,
|
||||
name: str,
|
||||
embedding_function: Optional[
|
||||
EmbeddingFunction[Embeddable]
|
||||
] = DefaultEmbeddingFunction(), # type: ignore
|
||||
data_loader: Optional[DataLoader[Loadable]] = None,
|
||||
) -> Collection:
|
||||
model = self._server.get_collection(
|
||||
name=name,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
persisted_ef_config = model.configuration_json.get("embedding_function")
|
||||
|
||||
validate_embedding_function_conflict_on_get(
|
||||
embedding_function, persisted_ef_config
|
||||
)
|
||||
|
||||
return Collection(
|
||||
client=self._server,
|
||||
model=model,
|
||||
embedding_function=embedding_function,
|
||||
data_loader=data_loader,
|
||||
)
|
||||
|
||||
@override
|
||||
def get_or_create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
embedding_function: Optional[
|
||||
EmbeddingFunction[Embeddable]
|
||||
] = DefaultEmbeddingFunction(), # type: ignore
|
||||
data_loader: Optional[DataLoader[Loadable]] = None,
|
||||
) -> Collection:
|
||||
if configuration is None:
|
||||
configuration = {}
|
||||
|
||||
configuration_ef = configuration.get("embedding_function")
|
||||
|
||||
validate_embedding_function_conflict_on_create(
|
||||
embedding_function, configuration_ef
|
||||
)
|
||||
|
||||
if embedding_function is not None and configuration_ef is None:
|
||||
configuration["embedding_function"] = embedding_function
|
||||
model = self._server.get_or_create_collection(
|
||||
name=name,
|
||||
schema=schema,
|
||||
metadata=metadata,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
configuration=configuration,
|
||||
)
|
||||
|
||||
persisted_ef_config = model.configuration_json.get("embedding_function")
|
||||
|
||||
validate_embedding_function_conflict_on_get(
|
||||
embedding_function, persisted_ef_config
|
||||
)
|
||||
|
||||
return Collection(
|
||||
client=self._server,
|
||||
model=model,
|
||||
embedding_function=embedding_function,
|
||||
data_loader=data_loader,
|
||||
)
|
||||
|
||||
@override
|
||||
def _modify(
|
||||
self,
|
||||
id: UUID,
|
||||
new_name: Optional[str] = None,
|
||||
new_metadata: Optional[CollectionMetadata] = None,
|
||||
new_configuration: Optional[UpdateCollectionConfiguration] = None,
|
||||
) -> None:
|
||||
return self._server._modify(
|
||||
id=id,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
new_name=new_name,
|
||||
new_metadata=new_metadata,
|
||||
new_configuration=new_configuration,
|
||||
)
|
||||
|
||||
@override
|
||||
def delete_collection(
|
||||
self,
|
||||
name: str,
|
||||
) -> None:
|
||||
return self._server.delete_collection(
|
||||
name=name,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
#
|
||||
# ITEM METHODS
|
||||
#
|
||||
|
||||
@override
|
||||
def _add(
|
||||
self,
|
||||
ids: IDs,
|
||||
collection_id: UUID,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
) -> bool:
|
||||
return self._server._add(
|
||||
ids=ids,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
collection_id=collection_id,
|
||||
embeddings=embeddings,
|
||||
metadatas=metadatas,
|
||||
documents=documents,
|
||||
uris=uris,
|
||||
)
|
||||
|
||||
@override
|
||||
def _update(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Optional[Embeddings] = None,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
) -> bool:
|
||||
return self._server._update(
|
||||
collection_id=collection_id,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
ids=ids,
|
||||
embeddings=embeddings,
|
||||
metadatas=metadatas,
|
||||
documents=documents,
|
||||
uris=uris,
|
||||
)
|
||||
|
||||
@override
|
||||
def _upsert(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
) -> bool:
|
||||
return self._server._upsert(
|
||||
collection_id=collection_id,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
ids=ids,
|
||||
embeddings=embeddings,
|
||||
metadatas=metadatas,
|
||||
documents=documents,
|
||||
uris=uris,
|
||||
)
|
||||
|
||||
@override
|
||||
def _count(self, collection_id: UUID) -> int:
|
||||
return self._server._count(
|
||||
collection_id=collection_id,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
@override
|
||||
def _peek(self, collection_id: UUID, n: int = 10) -> GetResult:
|
||||
return self._server._peek(
|
||||
collection_id=collection_id,
|
||||
n=n,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
@override
|
||||
def _get(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocuments,
|
||||
) -> GetResult:
|
||||
return self._server._get(
|
||||
collection_id=collection_id,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
ids=ids,
|
||||
where=where,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
where_document=where_document,
|
||||
include=include,
|
||||
)
|
||||
|
||||
def _delete(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs],
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
) -> None:
|
||||
self._server._delete(
|
||||
collection_id=collection_id,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
ids=ids,
|
||||
where=where,
|
||||
where_document=where_document,
|
||||
)
|
||||
|
||||
@override
|
||||
def _query(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
query_embeddings: Embeddings,
|
||||
ids: Optional[IDs] = None,
|
||||
n_results: int = 10,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocumentsDistances,
|
||||
) -> QueryResult:
|
||||
return self._server._query(
|
||||
collection_id=collection_id,
|
||||
ids=ids,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
query_embeddings=query_embeddings,
|
||||
n_results=n_results,
|
||||
where=where,
|
||||
where_document=where_document,
|
||||
include=include,
|
||||
)
|
||||
|
||||
@override
|
||||
def reset(self) -> bool:
|
||||
return self._server.reset()
|
||||
|
||||
@override
|
||||
def get_version(self) -> str:
|
||||
return self._server.get_version()
|
||||
|
||||
@override
|
||||
def get_settings(self) -> Settings:
|
||||
return self._server.get_settings()
|
||||
|
||||
@override
|
||||
def get_max_batch_size(self) -> int:
|
||||
return self._server.get_max_batch_size()
|
||||
|
||||
# endregion
|
||||
|
||||
# region ClientAPI Methods
|
||||
|
||||
@override
|
||||
def set_tenant(self, tenant: str, database: str = DEFAULT_DATABASE) -> None:
|
||||
self._validate_tenant_database(tenant=tenant, database=database)
|
||||
self.tenant = tenant
|
||||
self.database = database
|
||||
|
||||
@override
|
||||
def set_database(self, database: str) -> None:
|
||||
self._validate_tenant_database(tenant=self.tenant, database=database)
|
||||
self.database = database
|
||||
|
||||
def _validate_tenant_database(self, tenant: str, database: str) -> None:
|
||||
try:
|
||||
self._admin_client.get_tenant(name=tenant)
|
||||
except httpx.ConnectError:
|
||||
raise ValueError(
|
||||
"Could not connect to a Chroma server. Are you sure it is running?"
|
||||
)
|
||||
# Propagate ChromaErrors
|
||||
except ChromaError as e:
|
||||
raise e
|
||||
except Exception:
|
||||
raise ValueError(
|
||||
f"Could not connect to tenant {tenant}. Are you sure it exists?"
|
||||
)
|
||||
|
||||
try:
|
||||
self._admin_client.get_database(name=database, tenant=tenant)
|
||||
except httpx.ConnectError:
|
||||
raise ValueError(
|
||||
"Could not connect to a Chroma server. Are you sure it is running?"
|
||||
)
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
class AdminClient(SharedSystemClient, AdminAPI):
|
||||
_server: ServerAPI
|
||||
|
||||
def __init__(self, settings: Settings = Settings()) -> None:
|
||||
super().__init__(settings)
|
||||
self._server = self._system.instance(ServerAPI)
|
||||
|
||||
@override
|
||||
def create_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
|
||||
return self._server.create_database(name=name, tenant=tenant)
|
||||
|
||||
@override
|
||||
def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> Database:
|
||||
return self._server.get_database(name=name, tenant=tenant)
|
||||
|
||||
@override
|
||||
def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
|
||||
return self._server.delete_database(name=name, tenant=tenant)
|
||||
|
||||
@override
|
||||
def list_databases(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> Sequence[Database]:
|
||||
return self._server.list_databases(limit, offset, tenant=tenant)
|
||||
|
||||
@override
|
||||
def create_tenant(self, name: str) -> None:
|
||||
return self._server.create_tenant(name=name)
|
||||
|
||||
@override
|
||||
def get_tenant(self, name: str) -> Tenant:
|
||||
return self._server.get_tenant(name=name)
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def from_system(
|
||||
cls,
|
||||
system: System,
|
||||
) -> "AdminClient":
|
||||
SharedSystemClient._populate_data_from_system(system)
|
||||
instance = cls(settings=system.settings)
|
||||
return instance
|
||||
@@ -0,0 +1,882 @@
|
||||
from typing import TypedDict, Dict, Any, Optional, cast, get_args
|
||||
import json
|
||||
from chromadb.api.types import (
|
||||
Space,
|
||||
CollectionMetadata,
|
||||
UpdateMetadata,
|
||||
EmbeddingFunction,
|
||||
)
|
||||
from chromadb.utils.embedding_functions import (
|
||||
known_embedding_functions,
|
||||
register_embedding_function,
|
||||
)
|
||||
from multiprocessing import cpu_count
|
||||
import warnings
|
||||
|
||||
from chromadb.api.types import Schema
|
||||
|
||||
|
||||
class HNSWConfiguration(TypedDict, total=False):
|
||||
space: Space
|
||||
ef_construction: int
|
||||
max_neighbors: int
|
||||
ef_search: int
|
||||
num_threads: int
|
||||
batch_size: int
|
||||
sync_threshold: int
|
||||
resize_factor: float
|
||||
|
||||
|
||||
class SpannConfiguration(TypedDict, total=False):
|
||||
search_nprobe: int
|
||||
write_nprobe: int
|
||||
space: Space
|
||||
ef_construction: int
|
||||
ef_search: int
|
||||
max_neighbors: int
|
||||
reassign_neighbor_count: int
|
||||
split_threshold: int
|
||||
merge_threshold: int
|
||||
|
||||
|
||||
class CollectionConfiguration(TypedDict, total=True):
|
||||
hnsw: Optional[HNSWConfiguration]
|
||||
spann: Optional[SpannConfiguration]
|
||||
embedding_function: Optional[EmbeddingFunction] # type: ignore
|
||||
|
||||
|
||||
def load_collection_configuration_from_json_str(
|
||||
config_json_str: str,
|
||||
) -> CollectionConfiguration:
|
||||
config_json_map = json.loads(config_json_str)
|
||||
return load_collection_configuration_from_json(config_json_map)
|
||||
|
||||
|
||||
# TODO: make warnings prettier and add link to migration docs
|
||||
def load_collection_configuration_from_json(
|
||||
config_json_map: Dict[str, Any]
|
||||
) -> CollectionConfiguration:
|
||||
if (
|
||||
config_json_map.get("spann") is not None
|
||||
and config_json_map.get("hnsw") is not None
|
||||
):
|
||||
raise ValueError("hnsw and spann cannot both be provided")
|
||||
|
||||
hnsw_config = None
|
||||
spann_config = None
|
||||
ef_config = None
|
||||
|
||||
# Process vector index configuration (HNSW or SPANN)
|
||||
if config_json_map.get("hnsw") is not None:
|
||||
hnsw_config = cast(HNSWConfiguration, config_json_map["hnsw"])
|
||||
if config_json_map.get("spann") is not None:
|
||||
spann_config = cast(SpannConfiguration, config_json_map["spann"])
|
||||
|
||||
# Process embedding function configuration
|
||||
if config_json_map.get("embedding_function") is not None:
|
||||
ef_config = config_json_map["embedding_function"]
|
||||
if ef_config["type"] == "legacy":
|
||||
warnings.warn(
|
||||
"legacy embedding function config",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
ef = None
|
||||
else:
|
||||
try:
|
||||
ef_name = ef_config["name"]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
f"Embedding function name not found in config: {ef_config}"
|
||||
)
|
||||
try:
|
||||
ef = known_embedding_functions[ef_name]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
f"Embedding function {ef_name} not found. Add @register_embedding_function decorator to the class definition."
|
||||
)
|
||||
try:
|
||||
ef = ef.build_from_config(ef_config["config"]) # type: ignore
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"Could not build embedding function {ef_config['name']} from config {ef_config['config']}: {e}"
|
||||
)
|
||||
else:
|
||||
ef = None
|
||||
|
||||
return CollectionConfiguration(
|
||||
hnsw=hnsw_config,
|
||||
spann=spann_config,
|
||||
embedding_function=ef, # type: ignore
|
||||
)
|
||||
|
||||
|
||||
def collection_configuration_to_json_str(config: CollectionConfiguration) -> str:
|
||||
return json.dumps(collection_configuration_to_json(config))
|
||||
|
||||
|
||||
def collection_configuration_to_json(config: CollectionConfiguration) -> Dict[str, Any]:
|
||||
if isinstance(config, dict):
|
||||
hnsw_config = config.get("hnsw")
|
||||
spann_config = config.get("spann")
|
||||
ef = config.get("embedding_function")
|
||||
else:
|
||||
try:
|
||||
hnsw_config = config.get_parameter("hnsw").value
|
||||
except ValueError:
|
||||
hnsw_config = None
|
||||
try:
|
||||
spann_config = config.get_parameter("spann").value
|
||||
except ValueError:
|
||||
spann_config = None
|
||||
try:
|
||||
ef = config.get_parameter("embedding_function").value
|
||||
except ValueError:
|
||||
ef = None
|
||||
|
||||
ef_config: Dict[str, Any] | None = None
|
||||
if hnsw_config is not None:
|
||||
try:
|
||||
hnsw_config = cast(HNSWConfiguration, hnsw_config)
|
||||
except Exception as e:
|
||||
raise ValueError(f"not a valid hnsw config: {e}")
|
||||
if spann_config is not None:
|
||||
try:
|
||||
spann_config = cast(SpannConfiguration, spann_config)
|
||||
except Exception as e:
|
||||
raise ValueError(f"not a valid spann config: {e}")
|
||||
|
||||
if ef is None:
|
||||
ef = None
|
||||
ef_config = {"type": "legacy"}
|
||||
|
||||
if ef is not None:
|
||||
try:
|
||||
if ef.is_legacy():
|
||||
ef_config = {"type": "legacy"}
|
||||
else:
|
||||
ef_config = {
|
||||
"name": ef.name(),
|
||||
"type": "known",
|
||||
"config": ef.get_config(),
|
||||
}
|
||||
register_embedding_function(type(ef)) # type: ignore
|
||||
except Exception as e:
|
||||
warnings.warn(
|
||||
f"legacy embedding function config: {e}",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
ef = None
|
||||
ef_config = {"type": "legacy"}
|
||||
|
||||
return {
|
||||
"hnsw": hnsw_config,
|
||||
"spann": spann_config,
|
||||
"embedding_function": ef_config,
|
||||
}
|
||||
|
||||
|
||||
class CreateHNSWConfiguration(TypedDict, total=False):
|
||||
space: Space
|
||||
ef_construction: int
|
||||
max_neighbors: int
|
||||
ef_search: int
|
||||
num_threads: int
|
||||
batch_size: int
|
||||
sync_threshold: int
|
||||
resize_factor: float
|
||||
|
||||
|
||||
def json_to_create_hnsw_configuration(
|
||||
json_map: Dict[str, Any]
|
||||
) -> CreateHNSWConfiguration:
|
||||
config: CreateHNSWConfiguration = {}
|
||||
if "space" in json_map:
|
||||
space_value = json_map["space"]
|
||||
if space_value in get_args(Space):
|
||||
config["space"] = space_value
|
||||
else:
|
||||
raise ValueError(f"not a valid space: {space_value}")
|
||||
if "ef_construction" in json_map:
|
||||
config["ef_construction"] = json_map["ef_construction"]
|
||||
if "max_neighbors" in json_map:
|
||||
config["max_neighbors"] = json_map["max_neighbors"]
|
||||
if "ef_search" in json_map:
|
||||
config["ef_search"] = json_map["ef_search"]
|
||||
if "num_threads" in json_map:
|
||||
config["num_threads"] = json_map["num_threads"]
|
||||
if "batch_size" in json_map:
|
||||
config["batch_size"] = json_map["batch_size"]
|
||||
if "sync_threshold" in json_map:
|
||||
config["sync_threshold"] = json_map["sync_threshold"]
|
||||
if "resize_factor" in json_map:
|
||||
config["resize_factor"] = json_map["resize_factor"]
|
||||
return config
|
||||
|
||||
|
||||
class CreateSpannConfiguration(TypedDict, total=False):
|
||||
search_nprobe: int
|
||||
write_nprobe: int
|
||||
space: Space
|
||||
ef_construction: int
|
||||
ef_search: int
|
||||
max_neighbors: int
|
||||
reassign_neighbor_count: int
|
||||
split_threshold: int
|
||||
merge_threshold: int
|
||||
|
||||
|
||||
def json_to_create_spann_configuration(
|
||||
json_map: Dict[str, Any]
|
||||
) -> CreateSpannConfiguration:
|
||||
config: CreateSpannConfiguration = {}
|
||||
if "search_nprobe" in json_map:
|
||||
config["search_nprobe"] = json_map["search_nprobe"]
|
||||
if "write_nprobe" in json_map:
|
||||
config["write_nprobe"] = json_map["write_nprobe"]
|
||||
if "space" in json_map:
|
||||
space_value = json_map["space"]
|
||||
if space_value in get_args(Space):
|
||||
config["space"] = space_value
|
||||
else:
|
||||
raise ValueError(f"not a valid space: {space_value}")
|
||||
if "ef_construction" in json_map:
|
||||
config["ef_construction"] = json_map["ef_construction"]
|
||||
if "ef_search" in json_map:
|
||||
config["ef_search"] = json_map["ef_search"]
|
||||
if "max_neighbors" in json_map:
|
||||
config["max_neighbors"] = json_map["max_neighbors"]
|
||||
return config
|
||||
|
||||
|
||||
class CreateCollectionConfiguration(TypedDict, total=False):
|
||||
hnsw: Optional[CreateHNSWConfiguration]
|
||||
spann: Optional[CreateSpannConfiguration]
|
||||
embedding_function: Optional[EmbeddingFunction] # type: ignore
|
||||
|
||||
|
||||
def create_collection_configuration_from_legacy_collection_metadata(
|
||||
metadata: CollectionMetadata,
|
||||
) -> CreateCollectionConfiguration:
|
||||
"""Create a CreateCollectionConfiguration from legacy collection metadata"""
|
||||
return create_collection_configuration_from_legacy_metadata_dict(metadata)
|
||||
|
||||
|
||||
def create_collection_configuration_from_legacy_metadata_dict(
|
||||
metadata: Dict[str, Any],
|
||||
) -> CreateCollectionConfiguration:
|
||||
"""Create a CreateCollectionConfiguration from legacy collection metadata"""
|
||||
old_to_new = {
|
||||
"hnsw:space": "space",
|
||||
"hnsw:construction_ef": "ef_construction",
|
||||
"hnsw:M": "max_neighbors",
|
||||
"hnsw:search_ef": "ef_search",
|
||||
"hnsw:num_threads": "num_threads",
|
||||
"hnsw:batch_size": "batch_size",
|
||||
"hnsw:sync_threshold": "sync_threshold",
|
||||
"hnsw:resize_factor": "resize_factor",
|
||||
}
|
||||
json_map = {}
|
||||
for name, value in metadata.items():
|
||||
if name in old_to_new:
|
||||
json_map[old_to_new[name]] = value
|
||||
hnsw_config = json_to_create_hnsw_configuration(json_map)
|
||||
hnsw_config = populate_create_hnsw_defaults(hnsw_config)
|
||||
|
||||
return CreateCollectionConfiguration(hnsw=hnsw_config)
|
||||
|
||||
|
||||
# TODO: make warnings prettier and add link to migration docs
|
||||
def load_create_collection_configuration_from_json(
|
||||
json_map: Dict[str, Any]
|
||||
) -> CreateCollectionConfiguration:
|
||||
if json_map.get("hnsw") is not None and json_map.get("spann") is not None:
|
||||
raise ValueError("hnsw and spann cannot both be provided")
|
||||
|
||||
result = CreateCollectionConfiguration()
|
||||
|
||||
# Handle vector index configuration
|
||||
if json_map.get("hnsw") is not None:
|
||||
result["hnsw"] = json_to_create_hnsw_configuration(json_map["hnsw"])
|
||||
|
||||
if json_map.get("spann") is not None:
|
||||
result["spann"] = json_to_create_spann_configuration(json_map["spann"])
|
||||
|
||||
# Handle embedding function configuration
|
||||
if json_map.get("embedding_function") is not None:
|
||||
ef_config = json_map["embedding_function"]
|
||||
if ef_config["type"] == "legacy":
|
||||
warnings.warn(
|
||||
"legacy embedding function config",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
ef = known_embedding_functions[ef_config["name"]]
|
||||
result["embedding_function"] = ef.build_from_config(ef_config["config"])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def create_collection_configuration_to_json_str(
|
||||
config: CreateCollectionConfiguration,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
) -> str:
|
||||
"""Convert a CreateCollection configuration to a JSON-serializable string"""
|
||||
return json.dumps(create_collection_configuration_to_json(config, metadata))
|
||||
|
||||
|
||||
# TODO: make warnings prettier and add link to migration docs
|
||||
def create_collection_configuration_to_json(
|
||||
config: CreateCollectionConfiguration,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Convert a CreateCollection configuration to a JSON-serializable dict"""
|
||||
ef_config: Dict[str, Any] | None = None
|
||||
hnsw_config = config.get("hnsw")
|
||||
spann_config = config.get("spann")
|
||||
if hnsw_config is not None:
|
||||
try:
|
||||
hnsw_config = cast(CreateHNSWConfiguration, hnsw_config)
|
||||
except Exception as e:
|
||||
raise ValueError(f"not a valid hnsw config: {e}")
|
||||
if spann_config is not None:
|
||||
try:
|
||||
spann_config = cast(CreateSpannConfiguration, spann_config)
|
||||
except Exception as e:
|
||||
raise ValueError(f"not a valid spann config: {e}")
|
||||
|
||||
if hnsw_config is not None and spann_config is not None:
|
||||
raise ValueError("hnsw and spann cannot both be provided")
|
||||
|
||||
if config.get("embedding_function") is None:
|
||||
ef = None
|
||||
ef_config = {"type": "legacy"}
|
||||
return {
|
||||
"hnsw": hnsw_config,
|
||||
"spann": spann_config,
|
||||
"embedding_function": ef_config,
|
||||
}
|
||||
|
||||
try:
|
||||
ef = cast(EmbeddingFunction, config.get("embedding_function")) # type: ignore
|
||||
if ef.is_legacy():
|
||||
ef_config = {"type": "legacy"}
|
||||
else:
|
||||
# default space logic: if neither hnsw nor spann config is provided and metadata doesn't have space,
|
||||
# then populate space from ef
|
||||
# otherwise dont use default space from ef
|
||||
|
||||
# then validate the space afterwards based on the supported spaces of the embedding function,
|
||||
# warn if space is not supported
|
||||
|
||||
if hnsw_config is None and spann_config is None:
|
||||
if metadata is None or metadata.get("hnsw:space") is None:
|
||||
# this populates space from ef if not provided in either config
|
||||
hnsw_config = CreateHNSWConfiguration(space=ef.default_space())
|
||||
|
||||
# if hnsw config or spann config exists but space is not provided, populate it from ef
|
||||
if hnsw_config is not None and hnsw_config.get("space") is None:
|
||||
hnsw_config["space"] = ef.default_space()
|
||||
if spann_config is not None and spann_config.get("space") is None:
|
||||
spann_config["space"] = ef.default_space()
|
||||
|
||||
# Validate space compatibility with embedding function
|
||||
if hnsw_config is not None:
|
||||
if hnsw_config.get("space") not in ef.supported_spaces():
|
||||
warnings.warn(
|
||||
f"space {hnsw_config.get('space')} is not supported by {ef.name()}. Supported spaces: {ef.supported_spaces()}",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if spann_config is not None:
|
||||
if spann_config.get("space") not in ef.supported_spaces():
|
||||
warnings.warn(
|
||||
f"space {spann_config.get('space')} is not supported by {ef.name()}. Supported spaces: {ef.supported_spaces()}",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# only validate space from metadata if config is not provided
|
||||
if (
|
||||
hnsw_config is None
|
||||
and spann_config is None
|
||||
and metadata is not None
|
||||
and metadata.get("hnsw:space") is not None
|
||||
):
|
||||
if metadata.get("hnsw:space") not in ef.supported_spaces():
|
||||
warnings.warn(
|
||||
f"space {metadata.get('hnsw:space')} is not supported by {ef.name()}. Supported spaces: {ef.supported_spaces()}",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
ef_config = {
|
||||
"name": ef.name(),
|
||||
"type": "known",
|
||||
"config": ef.get_config(),
|
||||
}
|
||||
register_embedding_function(type(ef)) # type: ignore
|
||||
except Exception as e:
|
||||
warnings.warn(
|
||||
f"legacy embedding function config: {e}",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
ef = None
|
||||
ef_config = {"type": "legacy"}
|
||||
|
||||
return {
|
||||
"hnsw": hnsw_config,
|
||||
"spann": spann_config,
|
||||
"embedding_function": ef_config,
|
||||
}
|
||||
|
||||
|
||||
def populate_create_hnsw_defaults(
|
||||
config: CreateHNSWConfiguration, ef: Optional[EmbeddingFunction] = None # type: ignore
|
||||
) -> CreateHNSWConfiguration:
|
||||
"""Populate a CreateHNSW configuration with default values"""
|
||||
if config.get("space") is None:
|
||||
config["space"] = ef.default_space() if ef else "l2"
|
||||
if config.get("ef_construction") is None:
|
||||
config["ef_construction"] = 100
|
||||
if config.get("max_neighbors") is None:
|
||||
config["max_neighbors"] = 16
|
||||
if config.get("ef_search") is None:
|
||||
config["ef_search"] = 100
|
||||
if config.get("num_threads") is None:
|
||||
config["num_threads"] = cpu_count()
|
||||
if config.get("batch_size") is None:
|
||||
config["batch_size"] = 100
|
||||
if config.get("sync_threshold") is None:
|
||||
config["sync_threshold"] = 1000
|
||||
if config.get("resize_factor") is None:
|
||||
config["resize_factor"] = 1.2
|
||||
return config
|
||||
|
||||
|
||||
class UpdateHNSWConfiguration(TypedDict, total=False):
|
||||
ef_search: int
|
||||
num_threads: int
|
||||
batch_size: int
|
||||
sync_threshold: int
|
||||
resize_factor: float
|
||||
|
||||
|
||||
def json_to_update_hnsw_configuration(
|
||||
json_map: Dict[str, Any]
|
||||
) -> UpdateHNSWConfiguration:
|
||||
config: UpdateHNSWConfiguration = {}
|
||||
if "ef_search" in json_map:
|
||||
config["ef_search"] = json_map["ef_search"]
|
||||
if "num_threads" in json_map:
|
||||
config["num_threads"] = json_map["num_threads"]
|
||||
if "batch_size" in json_map:
|
||||
config["batch_size"] = json_map["batch_size"]
|
||||
if "sync_threshold" in json_map:
|
||||
config["sync_threshold"] = json_map["sync_threshold"]
|
||||
if "resize_factor" in json_map:
|
||||
config["resize_factor"] = json_map["resize_factor"]
|
||||
return config
|
||||
|
||||
|
||||
class UpdateSpannConfiguration(TypedDict, total=False):
|
||||
search_nprobe: int
|
||||
ef_search: int
|
||||
|
||||
|
||||
def json_to_update_spann_configuration(
|
||||
json_map: Dict[str, Any]
|
||||
) -> UpdateSpannConfiguration:
|
||||
config: UpdateSpannConfiguration = {}
|
||||
if "search_nprobe" in json_map:
|
||||
config["search_nprobe"] = json_map["search_nprobe"]
|
||||
if "ef_search" in json_map:
|
||||
config["ef_search"] = json_map["ef_search"]
|
||||
return config
|
||||
|
||||
|
||||
class UpdateCollectionConfiguration(TypedDict, total=False):
|
||||
hnsw: Optional[UpdateHNSWConfiguration]
|
||||
spann: Optional[UpdateSpannConfiguration]
|
||||
embedding_function: Optional[EmbeddingFunction] # type: ignore
|
||||
|
||||
|
||||
def update_collection_configuration_from_legacy_collection_metadata(
|
||||
metadata: CollectionMetadata,
|
||||
) -> UpdateCollectionConfiguration:
|
||||
"""Create an UpdateCollectionConfiguration from legacy collection metadata"""
|
||||
old_to_new = {
|
||||
"hnsw:search_ef": "ef_search",
|
||||
"hnsw:num_threads": "num_threads",
|
||||
"hnsw:batch_size": "batch_size",
|
||||
"hnsw:sync_threshold": "sync_threshold",
|
||||
"hnsw:resize_factor": "resize_factor",
|
||||
}
|
||||
json_map = {}
|
||||
for name, value in metadata.items():
|
||||
if name in old_to_new:
|
||||
json_map[old_to_new[name]] = value
|
||||
hnsw_config = json_to_update_hnsw_configuration(json_map)
|
||||
return UpdateCollectionConfiguration(hnsw=hnsw_config)
|
||||
|
||||
|
||||
def update_collection_configuration_from_legacy_update_metadata(
|
||||
metadata: UpdateMetadata,
|
||||
) -> UpdateCollectionConfiguration:
|
||||
"""Create an UpdateCollectionConfiguration from legacy update metadata"""
|
||||
old_to_new = {
|
||||
"hnsw:search_ef": "ef_search",
|
||||
"hnsw:num_threads": "num_threads",
|
||||
"hnsw:batch_size": "batch_size",
|
||||
"hnsw:sync_threshold": "sync_threshold",
|
||||
"hnsw:resize_factor": "resize_factor",
|
||||
}
|
||||
json_map = {}
|
||||
for name, value in metadata.items():
|
||||
if name in old_to_new:
|
||||
json_map[old_to_new[name]] = value
|
||||
hnsw_config = json_to_update_hnsw_configuration(json_map)
|
||||
return UpdateCollectionConfiguration(hnsw=hnsw_config)
|
||||
|
||||
|
||||
def update_collection_configuration_to_json_str(
|
||||
config: UpdateCollectionConfiguration,
|
||||
) -> str:
|
||||
"""Convert an UpdateCollectionConfiguration to a JSON-serializable string"""
|
||||
json_dict = update_collection_configuration_to_json(config)
|
||||
return json.dumps(json_dict)
|
||||
|
||||
|
||||
def update_collection_configuration_to_json(
|
||||
config: UpdateCollectionConfiguration,
|
||||
) -> Dict[str, Any]:
|
||||
"""Convert an UpdateCollectionConfiguration to a JSON-serializable dict"""
|
||||
hnsw_config = config.get("hnsw")
|
||||
spann_config = config.get("spann")
|
||||
ef = config.get("embedding_function")
|
||||
if hnsw_config is None and spann_config is None and ef is None:
|
||||
return {}
|
||||
|
||||
if hnsw_config is not None:
|
||||
try:
|
||||
hnsw_config = cast(UpdateHNSWConfiguration, hnsw_config)
|
||||
except Exception as e:
|
||||
raise ValueError(f"not a valid hnsw config: {e}")
|
||||
|
||||
if spann_config is not None:
|
||||
try:
|
||||
spann_config = cast(UpdateSpannConfiguration, spann_config)
|
||||
except Exception as e:
|
||||
raise ValueError(f"not a valid spann config: {e}")
|
||||
|
||||
ef_config: Dict[str, Any] | None = None
|
||||
if ef is not None:
|
||||
if ef.is_legacy():
|
||||
ef_config = {"type": "legacy"}
|
||||
else:
|
||||
ef.validate_config(ef.get_config())
|
||||
ef_config = {
|
||||
"name": ef.name(),
|
||||
"type": "known",
|
||||
"config": ef.get_config(),
|
||||
}
|
||||
register_embedding_function(type(ef)) # type: ignore
|
||||
else:
|
||||
ef_config = None
|
||||
|
||||
return {
|
||||
"hnsw": hnsw_config,
|
||||
"spann": spann_config,
|
||||
"embedding_function": ef_config,
|
||||
}
|
||||
|
||||
|
||||
def load_update_collection_configuration_from_json_str(
|
||||
json_str: str,
|
||||
) -> UpdateCollectionConfiguration:
|
||||
json_map = json.loads(json_str)
|
||||
return load_update_collection_configuration_from_json(json_map)
|
||||
|
||||
|
||||
# TODO: make warnings prettier and add link to migration docs
|
||||
def load_update_collection_configuration_from_json(
|
||||
json_map: Dict[str, Any]
|
||||
) -> UpdateCollectionConfiguration:
|
||||
"""Convert a JSON dict to an UpdateCollectionConfiguration"""
|
||||
if json_map.get("hnsw") is not None and json_map.get("spann") is not None:
|
||||
raise ValueError("hnsw and spann cannot both be provided")
|
||||
|
||||
result = UpdateCollectionConfiguration()
|
||||
|
||||
# Handle vector index configurations
|
||||
if json_map.get("hnsw") is not None:
|
||||
result["hnsw"] = json_to_update_hnsw_configuration(json_map["hnsw"])
|
||||
|
||||
if json_map.get("spann") is not None:
|
||||
result["spann"] = json_to_update_spann_configuration(json_map["spann"])
|
||||
|
||||
# Handle embedding function
|
||||
if json_map.get("embedding_function") is not None:
|
||||
if json_map["embedding_function"]["type"] == "legacy":
|
||||
warnings.warn(
|
||||
"legacy embedding function config",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
ef = known_embedding_functions[json_map["embedding_function"]["name"]]
|
||||
result["embedding_function"] = ef.build_from_config(
|
||||
json_map["embedding_function"]["config"]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def overwrite_hnsw_configuration(
|
||||
existing_hnsw_config: HNSWConfiguration, update_hnsw_config: UpdateHNSWConfiguration
|
||||
) -> HNSWConfiguration:
|
||||
"""Overwrite a HNSWConfiguration with a new configuration"""
|
||||
# Create a copy of the existing config and update with new values
|
||||
result = dict(existing_hnsw_config)
|
||||
update_fields = [
|
||||
"ef_search",
|
||||
"num_threads",
|
||||
"batch_size",
|
||||
"sync_threshold",
|
||||
"resize_factor",
|
||||
]
|
||||
|
||||
for field in update_fields:
|
||||
if field in update_hnsw_config:
|
||||
result[field] = update_hnsw_config[field] # type: ignore
|
||||
|
||||
return cast(HNSWConfiguration, result)
|
||||
|
||||
|
||||
def overwrite_spann_configuration(
|
||||
existing_spann_config: SpannConfiguration,
|
||||
update_spann_config: UpdateSpannConfiguration,
|
||||
) -> SpannConfiguration:
|
||||
"""Overwrite a SpannConfiguration with a new configuration"""
|
||||
result = dict(existing_spann_config)
|
||||
update_fields = [
|
||||
"search_nprobe",
|
||||
"ef_search",
|
||||
]
|
||||
|
||||
for field in update_fields:
|
||||
if field in update_spann_config:
|
||||
result[field] = update_spann_config[field] # type: ignore
|
||||
|
||||
return cast(SpannConfiguration, result)
|
||||
|
||||
|
||||
# TODO: make warnings prettier and add link to migration docs
|
||||
def overwrite_embedding_function(
|
||||
existing_embedding_function: EmbeddingFunction, # type: ignore
|
||||
update_embedding_function: EmbeddingFunction, # type: ignore
|
||||
) -> EmbeddingFunction: # type: ignore
|
||||
"""Overwrite an EmbeddingFunction with a new configuration"""
|
||||
# Check for legacy embedding functions
|
||||
if existing_embedding_function.is_legacy() or update_embedding_function.is_legacy():
|
||||
warnings.warn(
|
||||
"cannot update legacy embedding function config",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return existing_embedding_function
|
||||
|
||||
# Validate function compatibility
|
||||
if existing_embedding_function.name() != update_embedding_function.name():
|
||||
raise ValueError(
|
||||
f"Cannot update embedding function: incompatible types "
|
||||
f"({existing_embedding_function.name()} vs {update_embedding_function.name()})"
|
||||
)
|
||||
|
||||
# Validate and apply the configuration update
|
||||
update_embedding_function.validate_config_update(
|
||||
existing_embedding_function.get_config(), update_embedding_function.get_config()
|
||||
)
|
||||
return update_embedding_function
|
||||
|
||||
|
||||
def overwrite_collection_configuration(
|
||||
existing_config: CollectionConfiguration,
|
||||
update_config: UpdateCollectionConfiguration,
|
||||
) -> CollectionConfiguration:
|
||||
"""Overwrite a CollectionConfiguration with a new configuration"""
|
||||
update_spann = update_config.get("spann")
|
||||
update_hnsw = update_config.get("hnsw")
|
||||
if update_spann is not None and update_hnsw is not None:
|
||||
raise ValueError("hnsw and spann cannot both be provided")
|
||||
|
||||
# Handle HNSW configuration update
|
||||
|
||||
updated_hnsw_config = existing_config.get("hnsw")
|
||||
if updated_hnsw_config is not None and update_hnsw is not None:
|
||||
updated_hnsw_config = overwrite_hnsw_configuration(
|
||||
updated_hnsw_config, update_hnsw
|
||||
)
|
||||
|
||||
# Handle SPANN configuration update
|
||||
updated_spann_config = existing_config.get("spann")
|
||||
if updated_spann_config is not None and update_spann is not None:
|
||||
updated_spann_config = overwrite_spann_configuration(
|
||||
updated_spann_config, update_spann
|
||||
)
|
||||
|
||||
# Handle embedding function update
|
||||
updated_embedding_function = existing_config.get("embedding_function")
|
||||
update_ef = update_config.get("embedding_function")
|
||||
if update_ef is not None:
|
||||
if updated_embedding_function is not None:
|
||||
updated_embedding_function = overwrite_embedding_function(
|
||||
updated_embedding_function, update_ef
|
||||
)
|
||||
else:
|
||||
updated_embedding_function = update_ef
|
||||
|
||||
return CollectionConfiguration(
|
||||
hnsw=updated_hnsw_config,
|
||||
spann=updated_spann_config,
|
||||
embedding_function=updated_embedding_function,
|
||||
)
|
||||
|
||||
|
||||
def validate_embedding_function_conflict_on_create(
|
||||
embedding_function: Optional[EmbeddingFunction], # type: ignore
|
||||
configuration_ef: Optional[EmbeddingFunction], # type: ignore
|
||||
) -> None:
|
||||
"""
|
||||
Validates that there are no conflicting embedding functions between function parameter
|
||||
and collection configuration.
|
||||
|
||||
Args:
|
||||
embedding_function: The embedding function provided as a parameter
|
||||
configuration_ef: The embedding function from collection configuration
|
||||
|
||||
Returns:
|
||||
bool: True if there is a conflict, False otherwise
|
||||
"""
|
||||
# If ef provided in function params and collection config, check if they are the same
|
||||
# If not, there's a conflict
|
||||
# ef is by default "default" if not provided, so ignore that case.
|
||||
if embedding_function is not None and configuration_ef is not None:
|
||||
if (
|
||||
embedding_function.name() != "default"
|
||||
and embedding_function.name() != configuration_ef.name()
|
||||
):
|
||||
raise ValueError(
|
||||
f"Multiple embedding functions provided. Please provide only one. Embedding function conflict: {embedding_function.name()} vs {configuration_ef.name()}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
# The reason to use the config on get, rather than build the ef is because
|
||||
# if there is an issue with deserializing the config, an error shouldn't be raised
|
||||
# at get time. CollectionCommon.py will raise an error at _embed time if there is an issue deserializing.
|
||||
def validate_embedding_function_conflict_on_get(
|
||||
embedding_function: Optional[EmbeddingFunction], # type: ignore
|
||||
persisted_ef_config: Optional[Dict[str, Any]],
|
||||
) -> None:
|
||||
"""
|
||||
Validates that there are no conflicting embedding functions between function parameter
|
||||
and collection configuration.
|
||||
"""
|
||||
if persisted_ef_config is not None and embedding_function is not None:
|
||||
if (
|
||||
embedding_function.name() != "default"
|
||||
and persisted_ef_config.get("name") is not None
|
||||
and persisted_ef_config.get("name") != embedding_function.name()
|
||||
):
|
||||
raise ValueError(
|
||||
f"An embedding function already exists in the collection configuration, and a new one is provided. If this is intentional, please embed documents separately. Embedding function conflict: new: {embedding_function.name()} vs persisted: {persisted_ef_config.get('name')}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def update_schema_from_collection_configuration(
|
||||
schema: "Schema", configuration: "UpdateCollectionConfiguration"
|
||||
) -> "Schema":
|
||||
"""
|
||||
Updates a schema with configuration changes.
|
||||
Only updates fields that are present in the configuration update.
|
||||
|
||||
Args:
|
||||
schema: The existing Schema object
|
||||
configuration: The configuration updates to apply
|
||||
|
||||
Returns:
|
||||
Updated Schema object
|
||||
"""
|
||||
|
||||
# Get the vector index from defaults and #embedding key
|
||||
if (
|
||||
schema.defaults.float_list is None
|
||||
or schema.defaults.float_list.vector_index is None
|
||||
):
|
||||
raise ValueError("Schema is missing defaults.float_list.vector_index")
|
||||
|
||||
embedding_key = "#embedding"
|
||||
if embedding_key not in schema.keys:
|
||||
raise ValueError(f"Schema is missing keys[{embedding_key}]")
|
||||
|
||||
embedding_value_types = schema.keys[embedding_key]
|
||||
if (
|
||||
embedding_value_types.float_list is None
|
||||
or embedding_value_types.float_list.vector_index is None
|
||||
):
|
||||
raise ValueError(
|
||||
f"Schema is missing keys[{embedding_key}].float_list.vector_index"
|
||||
)
|
||||
|
||||
# Update vector index config in both locations
|
||||
for vector_index in [
|
||||
schema.defaults.float_list.vector_index,
|
||||
embedding_value_types.float_list.vector_index,
|
||||
]:
|
||||
if "hnsw" in configuration and configuration["hnsw"] is not None:
|
||||
# Update HNSW config
|
||||
if vector_index.config.hnsw is None:
|
||||
raise ValueError("Trying to update HNSW config but schema has SPANN")
|
||||
|
||||
hnsw_config = vector_index.config.hnsw
|
||||
update_hnsw = configuration["hnsw"]
|
||||
|
||||
# Only update fields that are present in the update
|
||||
if "ef_search" in update_hnsw:
|
||||
hnsw_config.ef_search = update_hnsw["ef_search"]
|
||||
if "num_threads" in update_hnsw:
|
||||
hnsw_config.num_threads = update_hnsw["num_threads"]
|
||||
if "batch_size" in update_hnsw:
|
||||
hnsw_config.batch_size = update_hnsw["batch_size"]
|
||||
if "sync_threshold" in update_hnsw:
|
||||
hnsw_config.sync_threshold = update_hnsw["sync_threshold"]
|
||||
if "resize_factor" in update_hnsw:
|
||||
hnsw_config.resize_factor = update_hnsw["resize_factor"]
|
||||
|
||||
elif "spann" in configuration and configuration["spann"] is not None:
|
||||
# Update SPANN config
|
||||
if vector_index.config.spann is None:
|
||||
raise ValueError("Trying to update SPANN config but schema has HNSW")
|
||||
|
||||
spann_config = vector_index.config.spann
|
||||
update_spann = configuration["spann"]
|
||||
|
||||
# Only update fields that are present in the update
|
||||
if "search_nprobe" in update_spann:
|
||||
spann_config.search_nprobe = update_spann["search_nprobe"]
|
||||
if "ef_search" in update_spann:
|
||||
spann_config.ef_search = update_spann["ef_search"]
|
||||
|
||||
# Update embedding function if present
|
||||
if (
|
||||
"embedding_function" in configuration
|
||||
and configuration["embedding_function"] is not None
|
||||
):
|
||||
vector_index.config.embedding_function = configuration["embedding_function"]
|
||||
|
||||
return schema
|
||||
@@ -0,0 +1,410 @@
|
||||
from abc import abstractmethod
|
||||
import json
|
||||
from overrides import override
|
||||
from typing import (
|
||||
Any,
|
||||
ClassVar,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Protocol,
|
||||
Union,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
from multiprocessing import cpu_count
|
||||
|
||||
from chromadb.serde import JSONSerializable
|
||||
|
||||
# TODO: move out of API
|
||||
|
||||
|
||||
class StaticParameterError(Exception):
|
||||
"""Represents an error that occurs when a static parameter is set."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidConfigurationError(ValueError):
|
||||
"""Represents an error that occurs when a configuration is invalid."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
ParameterValue = Union[str, int, float, bool, "ConfigurationInternal"]
|
||||
|
||||
|
||||
class ParameterValidator(Protocol):
|
||||
"""Represents an abstract parameter validator."""
|
||||
|
||||
@abstractmethod
|
||||
def __call__(self, value: ParameterValue) -> bool:
|
||||
"""Returns whether the given value is valid."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ConfigurationDefinition:
|
||||
"""Represents the definition of a configuration."""
|
||||
|
||||
name: str
|
||||
validator: ParameterValidator
|
||||
is_static: bool
|
||||
default_value: ParameterValue
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
validator: ParameterValidator,
|
||||
is_static: bool,
|
||||
default_value: ParameterValue,
|
||||
):
|
||||
self.name = name
|
||||
self.validator = validator
|
||||
self.is_static = is_static
|
||||
self.default_value = default_value
|
||||
|
||||
|
||||
class ConfigurationParameter:
|
||||
"""Represents a parameter of a configuration."""
|
||||
|
||||
name: str
|
||||
value: ParameterValue
|
||||
|
||||
def __init__(self, name: str, value: ParameterValue):
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ConfigurationParameter({self.name}, {self.value})"
|
||||
|
||||
def __eq__(self, __value: object) -> bool:
|
||||
if not isinstance(__value, ConfigurationParameter):
|
||||
return NotImplemented
|
||||
return self.name == __value.name and self.value == __value.value
|
||||
|
||||
|
||||
T = TypeVar("T", bound="ConfigurationInternal")
|
||||
|
||||
|
||||
class ConfigurationInternal(JSONSerializable["ConfigurationInternal"]):
|
||||
"""Represents an abstract configuration, used internally by Chroma."""
|
||||
|
||||
# The internal data structure used to store the parameters
|
||||
# All expected parameters must be present with defaults or None values at initialization
|
||||
parameter_map: Dict[str, ConfigurationParameter]
|
||||
definitions: ClassVar[Dict[str, ConfigurationDefinition]]
|
||||
|
||||
def __init__(self, parameters: Optional[List[ConfigurationParameter]] = None):
|
||||
"""Initializes a new instance of the Configuration class. Respecting defaults and
|
||||
validators."""
|
||||
self.parameter_map = {}
|
||||
if parameters is not None:
|
||||
for parameter in parameters:
|
||||
if parameter.name not in self.definitions:
|
||||
raise ValueError(f"Invalid parameter name: {parameter.name}")
|
||||
|
||||
definition = self.definitions[parameter.name]
|
||||
# Handle the case where we have a recursive configuration definition
|
||||
if isinstance(parameter.value, dict):
|
||||
child_type = globals().get(parameter.value.get("_type", None))
|
||||
if child_type is None:
|
||||
raise ValueError(
|
||||
f"Invalid configuration type: {parameter.value}"
|
||||
)
|
||||
parameter.value = child_type.from_json(parameter.value)
|
||||
if not isinstance(parameter.value, type(definition.default_value)):
|
||||
raise ValueError(f"Invalid parameter value: {parameter.value}")
|
||||
|
||||
parameter_validator = definition.validator
|
||||
if not parameter_validator(parameter.value):
|
||||
raise ValueError(f"Invalid parameter value: {parameter.value}")
|
||||
self.parameter_map[parameter.name] = parameter
|
||||
# Apply the defaults for any missing parameters
|
||||
for name, definition in self.definitions.items():
|
||||
if name not in self.parameter_map:
|
||||
self.parameter_map[name] = ConfigurationParameter(
|
||||
name=name, value=definition.default_value
|
||||
)
|
||||
|
||||
self.configuration_validator()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Configuration({self.parameter_map.values()})"
|
||||
|
||||
def __eq__(self, __value: object) -> bool:
|
||||
if not isinstance(__value, ConfigurationInternal):
|
||||
return NotImplemented
|
||||
return self.parameter_map == __value.parameter_map
|
||||
|
||||
@abstractmethod
|
||||
def configuration_validator(self) -> None:
|
||||
"""Perform custom validation when parameters are dependent on each other.
|
||||
|
||||
Raises an InvalidConfigurationError if the configuration is invalid.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_parameters(self) -> List[ConfigurationParameter]:
|
||||
"""Returns the parameters of the configuration."""
|
||||
return list(self.parameter_map.values())
|
||||
|
||||
def get_parameter(self, name: str) -> ConfigurationParameter:
|
||||
"""Returns the parameter with the given name, or except if it doesn't exist."""
|
||||
if name not in self.parameter_map:
|
||||
raise ValueError(
|
||||
f"Invalid parameter name: {name} for configuration {self.__class__.__name__}"
|
||||
)
|
||||
param_value = cast(ConfigurationParameter, self.parameter_map.get(name))
|
||||
return param_value
|
||||
|
||||
def set_parameter(self, name: str, value: Union[str, int, float, bool]) -> None:
|
||||
"""Sets the parameter with the given name to the given value."""
|
||||
if name not in self.definitions:
|
||||
raise ValueError(f"Invalid parameter name: {name}")
|
||||
definition = self.definitions[name]
|
||||
parameter = self.parameter_map[name]
|
||||
if definition.is_static:
|
||||
raise StaticParameterError(f"Cannot set static parameter: {name}")
|
||||
if not definition.validator(value):
|
||||
raise ValueError(f"Invalid value for parameter {name}: {value}")
|
||||
parameter.value = value
|
||||
|
||||
@override
|
||||
def to_json_str(self) -> str:
|
||||
"""Returns the JSON representation of the configuration."""
|
||||
return json.dumps(self.to_json())
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def from_json_str(cls, json_str: str) -> Self:
|
||||
"""Returns a configuration from the given JSON string."""
|
||||
try:
|
||||
config_json = json.loads(json_str)
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError(
|
||||
f"Unable to decode configuration from JSON string: {json_str}"
|
||||
)
|
||||
return cls.from_json(config_json) if config_json else cls()
|
||||
|
||||
@override
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
"""Returns the JSON compatible dictionary representation of the configuration."""
|
||||
json_dict = {
|
||||
name: parameter.value.to_json()
|
||||
if isinstance(parameter.value, ConfigurationInternal)
|
||||
else parameter.value
|
||||
for name, parameter in self.parameter_map.items()
|
||||
}
|
||||
# What kind of configuration is this?
|
||||
json_dict["_type"] = self.__class__.__name__
|
||||
return json_dict
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def from_json(cls, json_map: Dict[str, Any]) -> Self:
|
||||
"""Returns a configuration from the given JSON string."""
|
||||
if cls.__name__ != json_map.get("_type", None):
|
||||
raise ValueError(
|
||||
f"Trying to instantiate configuration of type {cls.__name__} from JSON with type {json_map['_type']}"
|
||||
)
|
||||
parameters = []
|
||||
for name, value in json_map.items():
|
||||
# Type value is only for storage
|
||||
if name == "_type":
|
||||
continue
|
||||
parameters.append(ConfigurationParameter(name=name, value=value))
|
||||
return cls(parameters=parameters)
|
||||
|
||||
|
||||
class HNSWConfigurationInternal(ConfigurationInternal):
|
||||
"""Internal representation of the HNSW configuration.
|
||||
Used for validation, defaults, serialization and deserialization."""
|
||||
|
||||
definitions = {
|
||||
"space": ConfigurationDefinition(
|
||||
name="space",
|
||||
validator=lambda value: isinstance(value, str)
|
||||
and value in ["l2", "ip", "cosine"],
|
||||
is_static=True,
|
||||
default_value="l2",
|
||||
),
|
||||
"ef_construction": ConfigurationDefinition(
|
||||
name="ef_construction",
|
||||
validator=lambda value: isinstance(value, int) and value >= 1,
|
||||
is_static=True,
|
||||
default_value=100,
|
||||
),
|
||||
"ef_search": ConfigurationDefinition(
|
||||
name="ef_search",
|
||||
validator=lambda value: isinstance(value, int) and value >= 1,
|
||||
is_static=False,
|
||||
default_value=100,
|
||||
),
|
||||
"num_threads": ConfigurationDefinition(
|
||||
name="num_threads",
|
||||
validator=lambda value: isinstance(value, int) and value >= 1,
|
||||
is_static=False,
|
||||
default_value=cpu_count(), # By default use all cores available
|
||||
),
|
||||
"M": ConfigurationDefinition(
|
||||
name="M",
|
||||
validator=lambda value: isinstance(value, int) and value >= 1,
|
||||
is_static=True,
|
||||
default_value=16,
|
||||
),
|
||||
"resize_factor": ConfigurationDefinition(
|
||||
name="resize_factor",
|
||||
validator=lambda value: isinstance(value, float) and value >= 1,
|
||||
is_static=True,
|
||||
default_value=1.2,
|
||||
),
|
||||
"batch_size": ConfigurationDefinition(
|
||||
name="batch_size",
|
||||
validator=lambda value: isinstance(value, int) and value >= 1,
|
||||
is_static=True,
|
||||
default_value=100,
|
||||
),
|
||||
"sync_threshold": ConfigurationDefinition(
|
||||
name="sync_threshold",
|
||||
validator=lambda value: isinstance(value, int) and value >= 1,
|
||||
is_static=True,
|
||||
default_value=1000,
|
||||
),
|
||||
}
|
||||
|
||||
@override
|
||||
def configuration_validator(self) -> None:
|
||||
batch_size = self.parameter_map.get("batch_size")
|
||||
sync_threshold = self.parameter_map.get("sync_threshold")
|
||||
|
||||
if (
|
||||
batch_size
|
||||
and sync_threshold
|
||||
and cast(int, batch_size.value) > cast(int, sync_threshold.value)
|
||||
):
|
||||
raise InvalidConfigurationError(
|
||||
"batch_size must be less than or equal to sync_threshold"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_legacy_params(cls, params: Dict[str, Any]) -> Self:
|
||||
"""Returns an HNSWConfiguration from a metadata dict containing legacy HNSW parameters. Used for migration."""
|
||||
|
||||
# We maintain this map to avoid a circular import with HnswParams, and
|
||||
# because then names won't change since we intend to deprecate HNSWParams
|
||||
# in favor of this type of configuration.
|
||||
old_to_new = {
|
||||
"hnsw:space": "space",
|
||||
"hnsw:construction_ef": "ef_construction",
|
||||
"hnsw:search_ef": "ef_search",
|
||||
"hnsw:M": "M",
|
||||
"hnsw:num_threads": "num_threads",
|
||||
"hnsw:resize_factor": "resize_factor",
|
||||
"hnsw:batch_size": "batch_size",
|
||||
"hnsw:sync_threshold": "sync_threshold",
|
||||
}
|
||||
|
||||
parameters = []
|
||||
for name, value in params.items():
|
||||
if name not in old_to_new:
|
||||
raise ValueError(f"Invalid legacy HNSW parameter name: {name}")
|
||||
parameters.append(
|
||||
ConfigurationParameter(name=old_to_new[name], value=value)
|
||||
)
|
||||
return cls(parameters)
|
||||
|
||||
|
||||
# This is the user-facing interface for HNSW index configuration parameters.
|
||||
# Internally, we pass around HNSWConfigurationInternal objects, which perform
|
||||
# validation, serialization and deserialization. Users don't need to know
|
||||
# about that and instead get a clean constructor with default arguments.
|
||||
class HNSWConfigurationInterface(HNSWConfigurationInternal):
|
||||
"""HNSW index configuration parameters.
|
||||
See https://docs.trychroma.com/guides#changing-the-distance-function for more information.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
space: str = "l2",
|
||||
ef_construction: int = 100,
|
||||
ef_search: int = 100,
|
||||
num_threads: int = cpu_count(),
|
||||
M: int = 16,
|
||||
resize_factor: float = 1.2,
|
||||
batch_size: int = 100,
|
||||
sync_threshold: int = 1000,
|
||||
):
|
||||
parameters = [
|
||||
ConfigurationParameter(name="space", value=space),
|
||||
ConfigurationParameter(name="ef_construction", value=ef_construction),
|
||||
ConfigurationParameter(name="ef_search", value=ef_search),
|
||||
ConfigurationParameter(name="num_threads", value=num_threads),
|
||||
ConfigurationParameter(name="M", value=M),
|
||||
ConfigurationParameter(name="resize_factor", value=resize_factor),
|
||||
ConfigurationParameter(name="batch_size", value=batch_size),
|
||||
ConfigurationParameter(name="sync_threshold", value=sync_threshold),
|
||||
]
|
||||
|
||||
super().__init__(parameters=parameters)
|
||||
|
||||
|
||||
# Alias for user convenience - the user doesn't need to know this is an 'Interface'
|
||||
HNSWConfiguration = HNSWConfigurationInterface
|
||||
|
||||
|
||||
class CollectionConfigurationInternal(ConfigurationInternal):
|
||||
"""Internal representation of the collection configuration.
|
||||
Used for validation, defaults, and serialization / deserialization."""
|
||||
|
||||
definitions = {
|
||||
"hnsw_configuration": ConfigurationDefinition(
|
||||
name="hnsw_configuration",
|
||||
validator=lambda value: isinstance(value, HNSWConfigurationInternal),
|
||||
is_static=True,
|
||||
default_value=HNSWConfigurationInternal(),
|
||||
),
|
||||
}
|
||||
|
||||
@override
|
||||
def configuration_validator(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# This is the user-facing interface for HNSW index configuration parameters.
|
||||
# Internally, we pass around HNSWConfigurationInternal objects, which perform
|
||||
# validation, serialization and deserialization. Users don't need to know
|
||||
# about that and instead get a clean constructor with default arguments.
|
||||
class CollectionConfigurationInterface(CollectionConfigurationInternal):
|
||||
"""Configuration parameters for creating a collection."""
|
||||
|
||||
def __init__(self, hnsw_configuration: Optional[HNSWConfigurationInternal]):
|
||||
"""Initializes a new instance of the CollectionConfiguration class.
|
||||
Args:
|
||||
hnsw_configuration: The HNSW configuration to use for the collection.
|
||||
"""
|
||||
if hnsw_configuration is None:
|
||||
hnsw_configuration = HNSWConfigurationInternal()
|
||||
parameters = [
|
||||
ConfigurationParameter(name="hnsw_configuration", value=hnsw_configuration)
|
||||
]
|
||||
super().__init__(parameters=parameters)
|
||||
|
||||
|
||||
# Alias for user convenience - the user doesn't need to know this is an 'Interface'.
|
||||
CollectionConfiguration = CollectionConfigurationInterface
|
||||
|
||||
|
||||
class EmbeddingsQueueConfigurationInternal(ConfigurationInternal):
|
||||
definitions = {
|
||||
"automatically_purge": ConfigurationDefinition(
|
||||
name="automatically_purge",
|
||||
validator=lambda value: isinstance(value, bool),
|
||||
is_static=False,
|
||||
default_value=True,
|
||||
),
|
||||
}
|
||||
|
||||
@override
|
||||
def configuration_validator(self) -> None:
|
||||
pass
|
||||
@@ -0,0 +1,806 @@
|
||||
import orjson
|
||||
import logging
|
||||
from typing import Any, Dict, Optional, cast, Tuple, List
|
||||
from typing import Sequence
|
||||
from uuid import UUID
|
||||
import httpx
|
||||
import urllib.parse
|
||||
from overrides import override
|
||||
|
||||
from chromadb.api.models.AttachedFunction import AttachedFunction
|
||||
|
||||
from chromadb.api.collection_configuration import (
|
||||
CreateCollectionConfiguration,
|
||||
UpdateCollectionConfiguration,
|
||||
update_collection_configuration_to_json,
|
||||
create_collection_configuration_to_json,
|
||||
)
|
||||
from chromadb import __version__
|
||||
from chromadb.api.base_http_client import BaseHTTPClient
|
||||
from chromadb.types import Database, Tenant, Collection as CollectionModel
|
||||
from chromadb.api import ServerAPI
|
||||
from chromadb.execution.expression.plan import Search
|
||||
|
||||
from chromadb.api.types import (
|
||||
Documents,
|
||||
Embeddings,
|
||||
IDs,
|
||||
Include,
|
||||
Schema,
|
||||
Metadatas,
|
||||
URIs,
|
||||
Where,
|
||||
WhereDocument,
|
||||
GetResult,
|
||||
QueryResult,
|
||||
SearchResult,
|
||||
CollectionMetadata,
|
||||
validate_batch,
|
||||
convert_np_embeddings_to_list,
|
||||
IncludeMetadataDocuments,
|
||||
IncludeMetadataDocumentsDistances,
|
||||
)
|
||||
|
||||
from chromadb.api.types import (
|
||||
IncludeMetadataDocumentsEmbeddings,
|
||||
optional_embeddings_to_base64_strings,
|
||||
serialize_metadata,
|
||||
deserialize_metadata,
|
||||
)
|
||||
from chromadb.auth import UserIdentity
|
||||
from chromadb.auth import (
|
||||
ClientAuthProvider,
|
||||
)
|
||||
from chromadb.config import DEFAULT_DATABASE, DEFAULT_TENANT, Settings, System
|
||||
from chromadb.telemetry.opentelemetry import (
|
||||
OpenTelemetryClient,
|
||||
OpenTelemetryGranularity,
|
||||
trace_method,
|
||||
)
|
||||
from chromadb.telemetry.product import ProductTelemetryClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FastAPI(BaseHTTPClient, ServerAPI):
|
||||
def __init__(self, system: System):
|
||||
super().__init__(system)
|
||||
system.settings.require("chroma_server_host")
|
||||
system.settings.require("chroma_server_http_port")
|
||||
|
||||
self._opentelemetry_client = self.require(OpenTelemetryClient)
|
||||
self._product_telemetry_client = self.require(ProductTelemetryClient)
|
||||
self._settings = system.settings
|
||||
|
||||
self._api_url = FastAPI.resolve_url(
|
||||
chroma_server_host=str(system.settings.chroma_server_host),
|
||||
chroma_server_http_port=system.settings.chroma_server_http_port,
|
||||
chroma_server_ssl_enabled=system.settings.chroma_server_ssl_enabled,
|
||||
default_api_path=system.settings.chroma_server_api_default_path,
|
||||
)
|
||||
|
||||
limits = httpx.Limits(keepalive_expiry=self.keepalive_secs)
|
||||
self._session = httpx.Client(timeout=None, limits=limits)
|
||||
|
||||
self._header = system.settings.chroma_server_headers or {}
|
||||
self._header["Content-Type"] = "application/json"
|
||||
self._header["User-Agent"] = (
|
||||
"Chroma Python Client v"
|
||||
+ __version__
|
||||
+ " (https://github.com/chroma-core/chroma)"
|
||||
)
|
||||
|
||||
if self._settings.chroma_server_ssl_verify is not None:
|
||||
self._session = httpx.Client(verify=self._settings.chroma_server_ssl_verify)
|
||||
if self._header is not None:
|
||||
self._session.headers.update(self._header)
|
||||
|
||||
if system.settings.chroma_client_auth_provider:
|
||||
self._auth_provider = self.require(ClientAuthProvider)
|
||||
_headers = self._auth_provider.authenticate()
|
||||
for header, value in _headers.items():
|
||||
self._session.headers[header] = value.get_secret_value()
|
||||
|
||||
def _make_request(self, method: str, path: str, **kwargs: Dict[str, Any]) -> Any:
|
||||
# If the request has json in kwargs, use orjson to serialize it,
|
||||
# remove it from kwargs, and add it to the content parameter
|
||||
# This is because httpx uses a slower json serializer
|
||||
if "json" in kwargs:
|
||||
data = orjson.dumps(kwargs.pop("json"), option=orjson.OPT_SERIALIZE_NUMPY)
|
||||
kwargs["content"] = data
|
||||
|
||||
# Unlike requests, httpx does not automatically escape the path
|
||||
escaped_path = urllib.parse.quote(path, safe="/", encoding=None, errors=None)
|
||||
url = self._api_url + escaped_path
|
||||
|
||||
response = self._session.request(method, url, **cast(Any, kwargs))
|
||||
BaseHTTPClient._raise_chroma_error(response)
|
||||
return orjson.loads(response.text)
|
||||
|
||||
@trace_method("FastAPI.heartbeat", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def heartbeat(self) -> int:
|
||||
"""Returns the current server time in nanoseconds to check if the server is alive"""
|
||||
resp_json = self._make_request("get", "/heartbeat")
|
||||
return int(resp_json["nanosecond heartbeat"])
|
||||
|
||||
# Migrated to rust in distributed.
|
||||
@trace_method("FastAPI.create_database", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def create_database(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> None:
|
||||
"""Creates a database"""
|
||||
self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases",
|
||||
json={"name": name},
|
||||
)
|
||||
|
||||
# Migrated to rust in distributed.
|
||||
@trace_method("FastAPI.get_database", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def get_database(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> Database:
|
||||
"""Returns a database"""
|
||||
resp_json = self._make_request(
|
||||
"get",
|
||||
f"/tenants/{tenant}/databases/{name}",
|
||||
)
|
||||
return Database(
|
||||
id=resp_json["id"], name=resp_json["name"], tenant=resp_json["tenant"]
|
||||
)
|
||||
|
||||
@trace_method("FastAPI.delete_database", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def delete_database(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> None:
|
||||
"""Deletes a database"""
|
||||
self._make_request(
|
||||
"delete",
|
||||
f"/tenants/{tenant}/databases/{name}",
|
||||
)
|
||||
|
||||
@trace_method("FastAPI.list_databases", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def list_databases(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> Sequence[Database]:
|
||||
"""Returns a list of all databases"""
|
||||
json_databases = self._make_request(
|
||||
"get",
|
||||
f"/tenants/{tenant}/databases",
|
||||
params=BaseHTTPClient._clean_params(
|
||||
{
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
),
|
||||
)
|
||||
databases = [
|
||||
Database(id=db["id"], name=db["name"], tenant=db["tenant"])
|
||||
for db in json_databases
|
||||
]
|
||||
return databases
|
||||
|
||||
@trace_method("FastAPI.create_tenant", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def create_tenant(self, name: str) -> None:
|
||||
self._make_request("post", "/tenants", json={"name": name})
|
||||
|
||||
@trace_method("FastAPI.get_tenant", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def get_tenant(self, name: str) -> Tenant:
|
||||
resp_json = self._make_request("get", "/tenants/" + name)
|
||||
return Tenant(name=resp_json["name"])
|
||||
|
||||
@trace_method("FastAPI.get_user_identity", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def get_user_identity(self) -> UserIdentity:
|
||||
return UserIdentity(**self._make_request("get", "/auth/identity"))
|
||||
|
||||
@trace_method("FastAPI.list_collections", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def list_collections(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> Sequence[CollectionModel]:
|
||||
"""Returns a list of all collections"""
|
||||
json_collections = self._make_request(
|
||||
"get",
|
||||
f"/tenants/{tenant}/databases/{database}/collections",
|
||||
params=BaseHTTPClient._clean_params(
|
||||
{
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
),
|
||||
)
|
||||
collection_models = [
|
||||
CollectionModel.from_json(json_collection)
|
||||
for json_collection in json_collections
|
||||
]
|
||||
|
||||
return collection_models
|
||||
|
||||
@trace_method("FastAPI.count_collections", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def count_collections(
|
||||
self, tenant: str = DEFAULT_TENANT, database: str = DEFAULT_DATABASE
|
||||
) -> int:
|
||||
"""Returns a count of collections"""
|
||||
resp_json = self._make_request(
|
||||
"get",
|
||||
f"/tenants/{tenant}/databases/{database}/collections_count",
|
||||
)
|
||||
return cast(int, resp_json)
|
||||
|
||||
@trace_method("FastAPI.create_collection", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
get_or_create: bool = False,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
"""Creates a collection"""
|
||||
config_json = (
|
||||
create_collection_configuration_to_json(configuration, metadata)
|
||||
if configuration
|
||||
else None
|
||||
)
|
||||
serialized_schema = schema.serialize_to_json() if schema else None
|
||||
resp_json = self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections",
|
||||
json={
|
||||
"name": name,
|
||||
"metadata": metadata,
|
||||
"configuration": config_json,
|
||||
"schema": serialized_schema,
|
||||
"get_or_create": get_or_create,
|
||||
},
|
||||
)
|
||||
model = CollectionModel.from_json(resp_json)
|
||||
|
||||
return model
|
||||
|
||||
@trace_method("FastAPI.get_collection", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def get_collection(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
"""Returns a collection"""
|
||||
resp_json = self._make_request(
|
||||
"get",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{name}",
|
||||
)
|
||||
|
||||
model = CollectionModel.from_json(resp_json)
|
||||
return model
|
||||
|
||||
@trace_method(
|
||||
"FastAPI.get_or_create_collection", OpenTelemetryGranularity.OPERATION
|
||||
)
|
||||
@override
|
||||
def get_or_create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
return self.create_collection(
|
||||
name=name,
|
||||
metadata=metadata,
|
||||
configuration=configuration,
|
||||
schema=schema,
|
||||
get_or_create=True,
|
||||
tenant=tenant,
|
||||
database=database,
|
||||
)
|
||||
|
||||
@trace_method("FastAPI._modify", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def _modify(
|
||||
self,
|
||||
id: UUID,
|
||||
new_name: Optional[str] = None,
|
||||
new_metadata: Optional[CollectionMetadata] = None,
|
||||
new_configuration: Optional[UpdateCollectionConfiguration] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
"""Updates a collection"""
|
||||
self._make_request(
|
||||
"put",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{id}",
|
||||
json={
|
||||
"new_metadata": new_metadata,
|
||||
"new_name": new_name,
|
||||
"new_configuration": update_collection_configuration_to_json(
|
||||
new_configuration
|
||||
)
|
||||
if new_configuration
|
||||
else None,
|
||||
},
|
||||
)
|
||||
|
||||
@trace_method("FastAPI._fork", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def _fork(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
new_name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
"""Forks a collection"""
|
||||
resp_json = self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{collection_id}/fork",
|
||||
json={"new_name": new_name},
|
||||
)
|
||||
model = CollectionModel.from_json(resp_json)
|
||||
return model
|
||||
|
||||
@trace_method("FastAPI._search", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def _search(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
searches: List[Search],
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> SearchResult:
|
||||
"""Performs hybrid search on a collection"""
|
||||
# Convert Search objects to dictionaries
|
||||
payload = {"searches": [s.to_dict() for s in searches]}
|
||||
|
||||
resp_json = self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{collection_id}/search",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
# Deserialize metadatas: convert transport format to SparseVector instances
|
||||
metadata_batches = resp_json.get("metadatas", None)
|
||||
if metadata_batches is not None:
|
||||
# SearchResult has nested structure: List[Optional[List[Optional[Metadata]]]]
|
||||
resp_json["metadatas"] = [
|
||||
[
|
||||
deserialize_metadata(metadata) if metadata is not None else None
|
||||
for metadata in metadatas
|
||||
]
|
||||
if metadatas is not None
|
||||
else None
|
||||
for metadatas in metadata_batches
|
||||
]
|
||||
|
||||
return SearchResult(resp_json)
|
||||
|
||||
@trace_method("FastAPI.delete_collection", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def delete_collection(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
"""Deletes a collection"""
|
||||
self._make_request(
|
||||
"delete",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{name}",
|
||||
)
|
||||
|
||||
@trace_method("FastAPI._count", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def _count(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> int:
|
||||
"""Returns the number of embeddings in the database"""
|
||||
resp_json = self._make_request(
|
||||
"get",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{collection_id}/count",
|
||||
)
|
||||
return cast(int, resp_json)
|
||||
|
||||
@trace_method("FastAPI._peek", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def _peek(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
n: int = 10,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> GetResult:
|
||||
return cast(
|
||||
GetResult,
|
||||
self._get(
|
||||
collection_id,
|
||||
tenant=tenant,
|
||||
database=database,
|
||||
limit=n,
|
||||
include=IncludeMetadataDocumentsEmbeddings,
|
||||
),
|
||||
)
|
||||
|
||||
@trace_method("FastAPI._get", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def _get(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocuments,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> GetResult:
|
||||
# Servers do not support receiving "data", as that is hydrated by the client as a loadable
|
||||
filtered_include = [i for i in include if i != "data"]
|
||||
|
||||
resp_json = self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{collection_id}/get",
|
||||
json={
|
||||
"ids": ids,
|
||||
"where": where,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"where_document": where_document,
|
||||
"include": filtered_include,
|
||||
},
|
||||
)
|
||||
|
||||
# Deserialize metadatas: convert transport format to SparseVector instances
|
||||
metadatas = resp_json.get("metadatas", None)
|
||||
if metadatas is not None:
|
||||
metadatas = [
|
||||
deserialize_metadata(metadata) if metadata is not None else None
|
||||
for metadata in metadatas
|
||||
]
|
||||
|
||||
return GetResult(
|
||||
ids=resp_json["ids"],
|
||||
embeddings=resp_json.get("embeddings", None),
|
||||
metadatas=metadatas, # type: ignore
|
||||
documents=resp_json.get("documents", None),
|
||||
data=None,
|
||||
uris=resp_json.get("uris", None),
|
||||
included=include,
|
||||
)
|
||||
|
||||
@trace_method("FastAPI._delete", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def _delete(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
"""Deletes embeddings from the database"""
|
||||
self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{collection_id}/delete",
|
||||
json={
|
||||
"ids": ids,
|
||||
"where": where,
|
||||
"where_document": where_document,
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
@trace_method("FastAPI._submit_batch", OpenTelemetryGranularity.ALL)
|
||||
def _submit_batch(
|
||||
self,
|
||||
batch: Tuple[
|
||||
IDs,
|
||||
Optional[Embeddings],
|
||||
Optional[Metadatas],
|
||||
Optional[Documents],
|
||||
Optional[URIs],
|
||||
],
|
||||
url: str,
|
||||
) -> None:
|
||||
"""
|
||||
Submits a batch of embeddings to the database
|
||||
"""
|
||||
# Serialize metadatas: convert SparseVector instances to transport format
|
||||
serialized_metadatas = None
|
||||
if batch[2] is not None:
|
||||
serialized_metadatas = [
|
||||
serialize_metadata(metadata) if metadata is not None else None
|
||||
for metadata in batch[2]
|
||||
]
|
||||
|
||||
data = {
|
||||
"ids": batch[0],
|
||||
"embeddings": optional_embeddings_to_base64_strings(batch[1])
|
||||
if self.supports_base64_encoding()
|
||||
else batch[1],
|
||||
"metadatas": serialized_metadatas,
|
||||
"documents": batch[3],
|
||||
"uris": batch[4],
|
||||
}
|
||||
|
||||
self._make_request("post", url, json=data)
|
||||
|
||||
@trace_method("FastAPI._add", OpenTelemetryGranularity.ALL)
|
||||
@override
|
||||
def _add(
|
||||
self,
|
||||
ids: IDs,
|
||||
collection_id: UUID,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
"""
|
||||
Adds a batch of embeddings to the database
|
||||
- pass in column oriented data lists
|
||||
"""
|
||||
batch = (
|
||||
ids,
|
||||
embeddings,
|
||||
metadatas,
|
||||
documents,
|
||||
uris,
|
||||
)
|
||||
validate_batch(batch, {"max_batch_size": self.get_max_batch_size()})
|
||||
self._submit_batch(
|
||||
batch,
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{str(collection_id)}/add",
|
||||
)
|
||||
return True
|
||||
|
||||
@trace_method("FastAPI._update", OpenTelemetryGranularity.ALL)
|
||||
@override
|
||||
def _update(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Optional[Embeddings] = None,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
"""
|
||||
Updates a batch of embeddings in the database
|
||||
- pass in column oriented data lists
|
||||
"""
|
||||
batch = (
|
||||
ids,
|
||||
embeddings if embeddings is not None else None,
|
||||
metadatas,
|
||||
documents,
|
||||
uris,
|
||||
)
|
||||
validate_batch(batch, {"max_batch_size": self.get_max_batch_size()})
|
||||
self._submit_batch(
|
||||
batch,
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{str(collection_id)}/update",
|
||||
)
|
||||
return True
|
||||
|
||||
@trace_method("FastAPI._upsert", OpenTelemetryGranularity.ALL)
|
||||
@override
|
||||
def _upsert(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
"""
|
||||
Upserts a batch of embeddings in the database
|
||||
- pass in column oriented data lists
|
||||
"""
|
||||
batch = (
|
||||
ids,
|
||||
embeddings,
|
||||
metadatas,
|
||||
documents,
|
||||
uris,
|
||||
)
|
||||
validate_batch(batch, {"max_batch_size": self.get_max_batch_size()})
|
||||
self._submit_batch(
|
||||
batch,
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{str(collection_id)}/upsert",
|
||||
)
|
||||
return True
|
||||
|
||||
@trace_method("FastAPI._query", OpenTelemetryGranularity.ALL)
|
||||
@override
|
||||
def _query(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
query_embeddings: Embeddings,
|
||||
ids: Optional[IDs] = None,
|
||||
n_results: int = 10,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocumentsDistances,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> QueryResult:
|
||||
# Clients do not support receiving "data", as that is hydrated by the client as a loadable
|
||||
filtered_include = [i for i in include if i != "data"]
|
||||
|
||||
"""Gets the nearest neighbors of a single embedding"""
|
||||
resp_json = self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{collection_id}/query",
|
||||
json={
|
||||
"ids": ids,
|
||||
"query_embeddings": convert_np_embeddings_to_list(query_embeddings)
|
||||
if query_embeddings is not None
|
||||
else None,
|
||||
"n_results": n_results,
|
||||
"where": where,
|
||||
"where_document": where_document,
|
||||
"include": filtered_include,
|
||||
},
|
||||
)
|
||||
|
||||
# Deserialize metadatas: convert transport format to SparseVector instances
|
||||
metadata_batches = resp_json.get("metadatas", None)
|
||||
if metadata_batches is not None:
|
||||
metadata_batches = [
|
||||
[
|
||||
deserialize_metadata(metadata) if metadata is not None else None
|
||||
for metadata in metadatas
|
||||
]
|
||||
if metadatas is not None
|
||||
else None
|
||||
for metadatas in metadata_batches
|
||||
]
|
||||
|
||||
return QueryResult(
|
||||
ids=resp_json["ids"],
|
||||
distances=resp_json.get("distances", None),
|
||||
embeddings=resp_json.get("embeddings", None),
|
||||
metadatas=metadata_batches, # type: ignore
|
||||
documents=resp_json.get("documents", None),
|
||||
uris=resp_json.get("uris", None),
|
||||
data=None,
|
||||
included=include,
|
||||
)
|
||||
|
||||
@trace_method("FastAPI.reset", OpenTelemetryGranularity.ALL)
|
||||
@override
|
||||
def reset(self) -> bool:
|
||||
"""Resets the database"""
|
||||
resp_json = self._make_request("post", "/reset")
|
||||
return cast(bool, resp_json)
|
||||
|
||||
@trace_method("FastAPI.get_version", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def get_version(self) -> str:
|
||||
"""Returns the version of the server"""
|
||||
resp_json = self._make_request("get", "/version")
|
||||
return cast(str, resp_json)
|
||||
|
||||
@override
|
||||
def get_settings(self) -> Settings:
|
||||
"""Returns the settings of the client"""
|
||||
return self._settings
|
||||
|
||||
@trace_method("FastAPI.get_pre_flight_checks", OpenTelemetryGranularity.OPERATION)
|
||||
def get_pre_flight_checks(self) -> Any:
|
||||
if self.pre_flight_checks is None:
|
||||
resp_json = self._make_request("get", "/pre-flight-checks")
|
||||
self.pre_flight_checks = resp_json
|
||||
return self.pre_flight_checks
|
||||
|
||||
@trace_method(
|
||||
"FastAPI.supports_base64_encoding", OpenTelemetryGranularity.OPERATION
|
||||
)
|
||||
def supports_base64_encoding(self) -> bool:
|
||||
pre_flight_checks = self.get_pre_flight_checks()
|
||||
b64_encoding_enabled = cast(
|
||||
bool, pre_flight_checks.get("supports_base64_encoding", False)
|
||||
)
|
||||
return b64_encoding_enabled
|
||||
|
||||
@trace_method("FastAPI.get_max_batch_size", OpenTelemetryGranularity.OPERATION)
|
||||
@override
|
||||
def get_max_batch_size(self) -> int:
|
||||
pre_flight_checks = self.get_pre_flight_checks()
|
||||
max_batch_size = cast(int, pre_flight_checks.get("max_batch_size", -1))
|
||||
return max_batch_size
|
||||
|
||||
@trace_method("FastAPI.attach_function", OpenTelemetryGranularity.ALL)
|
||||
@override
|
||||
def attach_function(
|
||||
self,
|
||||
function_id: str,
|
||||
name: str,
|
||||
input_collection_id: UUID,
|
||||
output_collection: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> "AttachedFunction":
|
||||
"""Attach a function to a collection."""
|
||||
resp_json = self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/collections/{input_collection_id}/functions/attach",
|
||||
json={
|
||||
"name": name,
|
||||
"function_id": function_id,
|
||||
"output_collection": output_collection,
|
||||
"params": params,
|
||||
},
|
||||
)
|
||||
|
||||
return AttachedFunction(
|
||||
client=self,
|
||||
id=UUID(resp_json["attached_function"]["id"]),
|
||||
name=resp_json["attached_function"]["name"],
|
||||
function_id=resp_json["attached_function"]["function_id"],
|
||||
input_collection_id=input_collection_id,
|
||||
output_collection=output_collection,
|
||||
params=params,
|
||||
tenant=tenant,
|
||||
database=database,
|
||||
)
|
||||
|
||||
@trace_method("FastAPI.detach_function", OpenTelemetryGranularity.ALL)
|
||||
@override
|
||||
def detach_function(
|
||||
self,
|
||||
attached_function_id: UUID,
|
||||
delete_output: bool = False,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
"""Detach a function and prevent any further runs."""
|
||||
resp_json = self._make_request(
|
||||
"post",
|
||||
f"/tenants/{tenant}/databases/{database}/attached_functions/{attached_function_id}/detach",
|
||||
json={
|
||||
"delete_output": delete_output,
|
||||
},
|
||||
)
|
||||
return cast(bool, resp_json["success"])
|
||||
@@ -0,0 +1,492 @@
|
||||
from typing import TYPE_CHECKING, Optional, Union, List, cast
|
||||
|
||||
from chromadb.api.types import (
|
||||
URI,
|
||||
CollectionMetadata,
|
||||
Embedding,
|
||||
PyEmbedding,
|
||||
Include,
|
||||
Metadata,
|
||||
Document,
|
||||
Image,
|
||||
Where,
|
||||
IDs,
|
||||
GetResult,
|
||||
QueryResult,
|
||||
ID,
|
||||
OneOrMany,
|
||||
WhereDocument,
|
||||
SearchResult,
|
||||
maybe_cast_one_to_many,
|
||||
)
|
||||
|
||||
from chromadb.api.models.CollectionCommon import CollectionCommon
|
||||
from chromadb.api.collection_configuration import UpdateCollectionConfiguration
|
||||
from chromadb.execution.expression.plan import Search
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from chromadb.api import AsyncServerAPI # noqa: F401
|
||||
|
||||
|
||||
class AsyncCollection(CollectionCommon["AsyncServerAPI"]):
|
||||
async def add(
|
||||
self,
|
||||
ids: OneOrMany[ID],
|
||||
embeddings: Optional[
|
||||
Union[
|
||||
OneOrMany[Embedding],
|
||||
OneOrMany[PyEmbedding],
|
||||
]
|
||||
] = None,
|
||||
metadatas: Optional[OneOrMany[Metadata]] = None,
|
||||
documents: Optional[OneOrMany[Document]] = None,
|
||||
images: Optional[OneOrMany[Image]] = None,
|
||||
uris: Optional[OneOrMany[URI]] = None,
|
||||
) -> None:
|
||||
"""Add embeddings to the data store.
|
||||
Args:
|
||||
ids: The ids of the embeddings you wish to add
|
||||
embeddings: The embeddings to add. If None, embeddings will be computed based on the documents or images using the embedding_function set for the Collection. Optional.
|
||||
metadatas: The metadata to associate with the embeddings. When querying, you can filter on this metadata. Optional.
|
||||
documents: The documents to associate with the embeddings. Optional.
|
||||
images: The images to associate with the embeddings. Optional.
|
||||
uris: The uris of the images to associate with the embeddings. Optional.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
ValueError: If you don't provide either embeddings or documents
|
||||
ValueError: If the length of ids, embeddings, metadatas, or documents don't match
|
||||
ValueError: If you don't provide an embedding function and don't provide embeddings
|
||||
ValueError: If you provide both embeddings and documents
|
||||
ValueError: If you provide an id that already exists
|
||||
|
||||
"""
|
||||
add_request = self._validate_and_prepare_add_request(
|
||||
ids=ids,
|
||||
embeddings=embeddings,
|
||||
metadatas=metadatas,
|
||||
documents=documents,
|
||||
images=images,
|
||||
uris=uris,
|
||||
)
|
||||
|
||||
await self._client._add(
|
||||
collection_id=self.id,
|
||||
ids=add_request["ids"],
|
||||
embeddings=add_request["embeddings"],
|
||||
metadatas=add_request["metadatas"],
|
||||
documents=add_request["documents"],
|
||||
uris=add_request["uris"],
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
async def count(self) -> int:
|
||||
"""The total number of embeddings added to the database
|
||||
|
||||
Returns:
|
||||
int: The total number of embeddings added to the database
|
||||
|
||||
"""
|
||||
return await self._client._count(
|
||||
collection_id=self.id,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
async def get(
|
||||
self,
|
||||
ids: Optional[OneOrMany[ID]] = None,
|
||||
where: Optional[Where] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = ["metadatas", "documents"],
|
||||
) -> GetResult:
|
||||
"""Get embeddings and their associate data from the data store. If no ids or where filter is provided returns
|
||||
all embeddings up to limit starting at offset.
|
||||
|
||||
Args:
|
||||
ids: The ids of the embeddings to get. Optional.
|
||||
where: A Where type dict used to filter results by. E.g. `{"$and": [{"color" : "red"}, {"price": {"$gte": 4.20}}]}`. Optional.
|
||||
limit: The number of documents to return. Optional.
|
||||
offset: The offset to start returning results from. Useful for paging results with limit. Optional.
|
||||
where_document: A WhereDocument type dict used to filter by the documents. E.g. `{"$contains": "hello"}`. Optional.
|
||||
include: A list of what to include in the results. Can contain `"embeddings"`, `"metadatas"`, `"documents"`. Ids are always included. Defaults to `["metadatas", "documents"]`. Optional.
|
||||
|
||||
Returns:
|
||||
GetResult: A GetResult object containing the results.
|
||||
|
||||
"""
|
||||
get_request = self._validate_and_prepare_get_request(
|
||||
ids=ids,
|
||||
where=where,
|
||||
where_document=where_document,
|
||||
include=include,
|
||||
)
|
||||
|
||||
get_results = await self._client._get(
|
||||
collection_id=self.id,
|
||||
ids=get_request["ids"],
|
||||
where=get_request["where"],
|
||||
where_document=get_request["where_document"],
|
||||
include=get_request["include"],
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
return self._transform_get_response(
|
||||
response=get_results, include=get_request["include"]
|
||||
)
|
||||
|
||||
async def peek(self, limit: int = 10) -> GetResult:
|
||||
"""Get the first few results in the database up to limit
|
||||
|
||||
Args:
|
||||
limit: The number of results to return.
|
||||
|
||||
Returns:
|
||||
GetResult: A GetResult object containing the results.
|
||||
"""
|
||||
return self._transform_peek_response(
|
||||
await self._client._peek(
|
||||
collection_id=self.id,
|
||||
n=limit,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
)
|
||||
|
||||
async def query(
|
||||
self,
|
||||
query_embeddings: Optional[
|
||||
Union[
|
||||
OneOrMany[Embedding],
|
||||
OneOrMany[PyEmbedding],
|
||||
]
|
||||
] = None,
|
||||
query_texts: Optional[OneOrMany[Document]] = None,
|
||||
query_images: Optional[OneOrMany[Image]] = None,
|
||||
query_uris: Optional[OneOrMany[URI]] = None,
|
||||
ids: Optional[OneOrMany[ID]] = None,
|
||||
n_results: int = 10,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = [
|
||||
"metadatas",
|
||||
"documents",
|
||||
"distances",
|
||||
],
|
||||
) -> QueryResult:
|
||||
"""Get the n_results nearest neighbor embeddings for provided query_embeddings or query_texts.
|
||||
|
||||
Args:
|
||||
query_embeddings: The embeddings to get the closes neighbors of. Optional.
|
||||
query_texts: The document texts to get the closes neighbors of. Optional.
|
||||
query_images: The images to get the closes neighbors of. Optional.
|
||||
ids: A subset of ids to search within. Optional.
|
||||
n_results: The number of neighbors to return for each query_embedding or query_texts. Optional.
|
||||
where: A Where type dict used to filter results by. E.g. `{"$and": [{"color" : "red"}, {"price": {"$gte": 4.20}}]}`. Optional.
|
||||
where_document: A WhereDocument type dict used to filter by the documents. E.g. `{"$contains": "hello"}`. Optional.
|
||||
include: A list of what to include in the results. Can contain `"embeddings"`, `"metadatas"`, `"documents"`, `"distances"`. Ids are always included. Defaults to `["metadatas", "documents", "distances"]`. Optional.
|
||||
|
||||
Returns:
|
||||
QueryResult: A QueryResult object containing the results.
|
||||
|
||||
Raises:
|
||||
ValueError: If you don't provide either query_embeddings, query_texts, or query_images
|
||||
ValueError: If you provide both query_embeddings and query_texts
|
||||
ValueError: If you provide both query_embeddings and query_images
|
||||
ValueError: If you provide both query_texts and query_images
|
||||
|
||||
"""
|
||||
|
||||
query_request = self._validate_and_prepare_query_request(
|
||||
query_embeddings=query_embeddings,
|
||||
query_texts=query_texts,
|
||||
query_images=query_images,
|
||||
query_uris=query_uris,
|
||||
ids=ids,
|
||||
n_results=n_results,
|
||||
where=where,
|
||||
where_document=where_document,
|
||||
include=include,
|
||||
)
|
||||
|
||||
query_results = await self._client._query(
|
||||
collection_id=self.id,
|
||||
ids=query_request["ids"],
|
||||
query_embeddings=query_request["embeddings"],
|
||||
n_results=query_request["n_results"],
|
||||
where=query_request["where"],
|
||||
where_document=query_request["where_document"],
|
||||
include=query_request["include"],
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
return self._transform_query_response(
|
||||
response=query_results, include=query_request["include"]
|
||||
)
|
||||
|
||||
async def modify(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
configuration: Optional[UpdateCollectionConfiguration] = None,
|
||||
) -> None:
|
||||
"""Modify the collection name or metadata
|
||||
|
||||
Args:
|
||||
name: The updated name for the collection. Optional.
|
||||
metadata: The updated metadata for the collection. Optional.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
self._validate_modify_request(metadata)
|
||||
|
||||
# Note there is a race condition here where the metadata can be updated
|
||||
# but another thread sees the cached local metadata.
|
||||
# TODO: fixme
|
||||
await self._client._modify(
|
||||
id=self.id,
|
||||
new_name=name,
|
||||
new_metadata=metadata,
|
||||
new_configuration=configuration,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
self._update_model_after_modify_success(name, metadata, configuration)
|
||||
|
||||
async def fork(
|
||||
self,
|
||||
new_name: str,
|
||||
) -> "AsyncCollection":
|
||||
"""Fork the current collection under a new name. The returning collection should contain identical data to the current collection.
|
||||
This is an experimental API that only works for Hosted Chroma for now.
|
||||
|
||||
Args:
|
||||
new_name: The name of the new collection.
|
||||
|
||||
Returns:
|
||||
Collection: A new collection with the specified name and containing identical data to the current collection.
|
||||
"""
|
||||
model = await self._client._fork(
|
||||
collection_id=self.id,
|
||||
new_name=new_name,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
return AsyncCollection(
|
||||
client=self._client,
|
||||
model=model,
|
||||
embedding_function=self._embedding_function,
|
||||
data_loader=self._data_loader,
|
||||
)
|
||||
|
||||
async def search(
|
||||
self,
|
||||
searches: OneOrMany[Search],
|
||||
) -> SearchResult:
|
||||
"""Perform hybrid search on the collection.
|
||||
This is an experimental API that only works for Hosted Chroma for now.
|
||||
|
||||
Args:
|
||||
searches: A single Search object or a list of Search objects, each containing:
|
||||
- where: Where expression for filtering
|
||||
- rank: Ranking expression for hybrid search (defaults to Val(0.0))
|
||||
- limit: Limit configuration for pagination (defaults to no limit)
|
||||
- select: Select configuration for keys to return (defaults to empty)
|
||||
|
||||
Returns:
|
||||
SearchResult: Column-major format response with:
|
||||
- ids: List of result IDs for each search payload
|
||||
- documents: Optional documents for each payload
|
||||
- embeddings: Optional embeddings for each payload
|
||||
- metadatas: Optional metadata for each payload
|
||||
- scores: Optional scores for each payload
|
||||
- select: List of selected keys for each payload
|
||||
|
||||
Raises:
|
||||
NotImplementedError: For local/segment API implementations
|
||||
|
||||
Examples:
|
||||
# Using builder pattern with Key constants
|
||||
from chromadb.execution.expression import (
|
||||
Search, Key, K, Knn, Val
|
||||
)
|
||||
|
||||
# Note: K is an alias for Key, so K.DOCUMENT == Key.DOCUMENT
|
||||
search = (Search()
|
||||
.where((K("category") == "science") & (K("score") > 0.5))
|
||||
.rank(Knn(query=[0.1, 0.2, 0.3]) * 0.8 + Val(0.5) * 0.2)
|
||||
.limit(10, offset=0)
|
||||
.select(K.DOCUMENT, K.SCORE, "title"))
|
||||
|
||||
# Direct construction
|
||||
from chromadb.execution.expression import (
|
||||
Search, Eq, And, Gt, Knn, Limit, Select, Key
|
||||
)
|
||||
|
||||
search = Search(
|
||||
where=And([Eq("category", "science"), Gt("score", 0.5)]),
|
||||
rank=Knn(query=[0.1, 0.2, 0.3]),
|
||||
limit=Limit(offset=0, limit=10),
|
||||
select=Select(keys={Key.DOCUMENT, Key.SCORE, "title"})
|
||||
)
|
||||
|
||||
# Single search
|
||||
result = await collection.search(search)
|
||||
|
||||
# Multiple searches at once
|
||||
searches = [
|
||||
Search().where(K("type") == "article").rank(Knn(query=[0.1, 0.2])),
|
||||
Search().where(K("type") == "paper").rank(Knn(query=[0.3, 0.4]))
|
||||
]
|
||||
results = await collection.search(searches)
|
||||
"""
|
||||
# Convert single search to list for consistent handling
|
||||
searches_list = maybe_cast_one_to_many(searches)
|
||||
if searches_list is None:
|
||||
searches_list = []
|
||||
|
||||
# Embed any string queries in Knn objects
|
||||
embedded_searches = [
|
||||
self._embed_search_string_queries(search) for search in searches_list
|
||||
]
|
||||
|
||||
return await self._client._search(
|
||||
collection_id=self.id,
|
||||
searches=cast(List[Search], embedded_searches),
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
async def update(
|
||||
self,
|
||||
ids: OneOrMany[ID],
|
||||
embeddings: Optional[
|
||||
Union[
|
||||
OneOrMany[Embedding],
|
||||
OneOrMany[PyEmbedding],
|
||||
]
|
||||
] = None,
|
||||
metadatas: Optional[OneOrMany[Metadata]] = None,
|
||||
documents: Optional[OneOrMany[Document]] = None,
|
||||
images: Optional[OneOrMany[Image]] = None,
|
||||
uris: Optional[OneOrMany[URI]] = None,
|
||||
) -> None:
|
||||
"""Update the embeddings, metadatas or documents for provided ids.
|
||||
|
||||
Args:
|
||||
ids: The ids of the embeddings to update
|
||||
embeddings: The embeddings to update. If None, embeddings will be computed based on the documents or images using the embedding_function set for the Collection. Optional.
|
||||
metadatas: The metadata to associate with the embeddings. When querying, you can filter on this metadata. Optional.
|
||||
documents: The documents to associate with the embeddings. Optional.
|
||||
images: The images to associate with the embeddings. Optional.
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
update_request = self._validate_and_prepare_update_request(
|
||||
ids=ids,
|
||||
embeddings=embeddings,
|
||||
metadatas=metadatas,
|
||||
documents=documents,
|
||||
images=images,
|
||||
uris=uris,
|
||||
)
|
||||
|
||||
await self._client._update(
|
||||
collection_id=self.id,
|
||||
ids=update_request["ids"],
|
||||
embeddings=update_request["embeddings"],
|
||||
metadatas=update_request["metadatas"],
|
||||
documents=update_request["documents"],
|
||||
uris=update_request["uris"],
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
async def upsert(
|
||||
self,
|
||||
ids: OneOrMany[ID],
|
||||
embeddings: Optional[
|
||||
Union[
|
||||
OneOrMany[Embedding],
|
||||
OneOrMany[PyEmbedding],
|
||||
]
|
||||
] = None,
|
||||
metadatas: Optional[OneOrMany[Metadata]] = None,
|
||||
documents: Optional[OneOrMany[Document]] = None,
|
||||
images: Optional[OneOrMany[Image]] = None,
|
||||
uris: Optional[OneOrMany[URI]] = None,
|
||||
) -> None:
|
||||
"""Update the embeddings, metadatas or documents for provided ids, or create them if they don't exist.
|
||||
|
||||
Args:
|
||||
ids: The ids of the embeddings to update
|
||||
embeddings: The embeddings to add. If None, embeddings will be computed based on the documents using the embedding_function set for the Collection. Optional.
|
||||
metadatas: The metadata to associate with the embeddings. When querying, you can filter on this metadata. Optional.
|
||||
documents: The documents to associate with the embeddings. Optional.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
upsert_request = self._validate_and_prepare_upsert_request(
|
||||
ids=ids,
|
||||
embeddings=embeddings,
|
||||
metadatas=metadatas,
|
||||
documents=documents,
|
||||
images=images,
|
||||
uris=uris,
|
||||
)
|
||||
|
||||
await self._client._upsert(
|
||||
collection_id=self.id,
|
||||
ids=upsert_request["ids"],
|
||||
embeddings=upsert_request["embeddings"],
|
||||
metadatas=upsert_request["metadatas"],
|
||||
documents=upsert_request["documents"],
|
||||
uris=upsert_request["uris"],
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
async def delete(
|
||||
self,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
) -> None:
|
||||
"""Delete the embeddings based on ids and/or a where filter
|
||||
|
||||
Args:
|
||||
ids: The ids of the embeddings to delete
|
||||
where: A Where type dict used to filter the delection by. E.g. `{"$and": [{"color" : "red"}, {"price": {"$gte": 4.20}}]}`. Optional.
|
||||
where_document: A WhereDocument type dict used to filter the deletion by the document content. E.g. `{"$contains": "hello"}`. Optional.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
ValueError: If you don't provide either ids, where, or where_document
|
||||
"""
|
||||
delete_request = self._validate_and_prepare_delete_request(
|
||||
ids, where, where_document
|
||||
)
|
||||
|
||||
await self._client._delete(
|
||||
collection_id=self.id,
|
||||
ids=delete_request["ids"],
|
||||
where=delete_request["where"],
|
||||
where_document=delete_request["where_document"],
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
@@ -0,0 +1,101 @@
|
||||
from typing import TYPE_CHECKING, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from chromadb.api import ServerAPI # noqa: F401
|
||||
|
||||
|
||||
class AttachedFunction:
|
||||
"""Represents a function attached to a collection."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: "ServerAPI",
|
||||
id: UUID,
|
||||
name: str,
|
||||
function_id: str,
|
||||
input_collection_id: UUID,
|
||||
output_collection: str,
|
||||
params: Optional[Dict[str, Any]],
|
||||
tenant: str,
|
||||
database: str,
|
||||
):
|
||||
"""Initialize an AttachedFunction.
|
||||
|
||||
Args:
|
||||
client: The API client
|
||||
id: Unique identifier for this attached function
|
||||
name: Name of this attached function instance
|
||||
function_id: The function identifier (e.g., "record_counter")
|
||||
input_collection_id: ID of the input collection
|
||||
output_collection: Name of the output collection
|
||||
params: Function-specific parameters
|
||||
tenant: The tenant name
|
||||
database: The database name
|
||||
"""
|
||||
self._client = client
|
||||
self._id = id
|
||||
self._name = name
|
||||
self._function_id = function_id
|
||||
self._input_collection_id = input_collection_id
|
||||
self._output_collection = output_collection
|
||||
self._params = params
|
||||
self._tenant = tenant
|
||||
self._database = database
|
||||
|
||||
@property
|
||||
def id(self) -> UUID:
|
||||
"""The unique identifier of this attached function."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name of this attached function instance."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def function_id(self) -> str:
|
||||
"""The function identifier."""
|
||||
return self._function_id
|
||||
|
||||
@property
|
||||
def input_collection_id(self) -> UUID:
|
||||
"""The ID of the input collection."""
|
||||
return self._input_collection_id
|
||||
|
||||
@property
|
||||
def output_collection(self) -> str:
|
||||
"""The name of the output collection."""
|
||||
return self._output_collection
|
||||
|
||||
@property
|
||||
def params(self) -> Optional[Dict[str, Any]]:
|
||||
"""The function parameters."""
|
||||
return self._params
|
||||
|
||||
def detach(self, delete_output_collection: bool = False) -> bool:
|
||||
"""Detach this function and prevent any further runs.
|
||||
|
||||
Args:
|
||||
delete_output_collection: Whether to also delete the output collection. Defaults to False.
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
|
||||
Example:
|
||||
>>> success = attached_fn.detach(delete_output_collection=True)
|
||||
"""
|
||||
return self._client.detach_function(
|
||||
attached_function_id=self._id,
|
||||
delete_output=delete_output_collection,
|
||||
tenant=self._tenant,
|
||||
database=self._database,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"AttachedFunction(id={self._id}, name='{self._name}', "
|
||||
f"function_id='{self._function_id}', "
|
||||
f"input_collection_id={self._input_collection_id}, "
|
||||
f"output_collection='{self._output_collection}')"
|
||||
)
|
||||
@@ -0,0 +1,535 @@
|
||||
from typing import TYPE_CHECKING, Optional, Union, List, cast, Dict, Any
|
||||
|
||||
from chromadb.api.models.CollectionCommon import CollectionCommon
|
||||
from chromadb.api.types import (
|
||||
URI,
|
||||
CollectionMetadata,
|
||||
Embedding,
|
||||
PyEmbedding,
|
||||
Include,
|
||||
Metadata,
|
||||
Document,
|
||||
Image,
|
||||
Where,
|
||||
IDs,
|
||||
GetResult,
|
||||
QueryResult,
|
||||
ID,
|
||||
OneOrMany,
|
||||
WhereDocument,
|
||||
SearchResult,
|
||||
maybe_cast_one_to_many,
|
||||
)
|
||||
from chromadb.api.collection_configuration import UpdateCollectionConfiguration
|
||||
from chromadb.execution.expression.plan import Search
|
||||
|
||||
import logging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from chromadb.api.models.AttachedFunction import AttachedFunction
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from chromadb.api import ServerAPI # noqa: F401
|
||||
|
||||
|
||||
class Collection(CollectionCommon["ServerAPI"]):
|
||||
def count(self) -> int:
|
||||
"""The total number of embeddings added to the database
|
||||
|
||||
Returns:
|
||||
int: The total number of embeddings added to the database
|
||||
|
||||
"""
|
||||
return self._client._count(
|
||||
collection_id=self.id,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
def add(
|
||||
self,
|
||||
ids: OneOrMany[ID],
|
||||
embeddings: Optional[
|
||||
Union[
|
||||
OneOrMany[Embedding],
|
||||
OneOrMany[PyEmbedding],
|
||||
]
|
||||
] = None,
|
||||
metadatas: Optional[OneOrMany[Metadata]] = None,
|
||||
documents: Optional[OneOrMany[Document]] = None,
|
||||
images: Optional[OneOrMany[Image]] = None,
|
||||
uris: Optional[OneOrMany[URI]] = None,
|
||||
) -> None:
|
||||
"""Add embeddings to the data store.
|
||||
Args:
|
||||
ids: The ids of the embeddings you wish to add
|
||||
embeddings: The embeddings to add. If None, embeddings will be computed based on the documents or images using the embedding_function set for the Collection. Optional.
|
||||
metadatas: The metadata to associate with the embeddings. When querying, you can filter on this metadata. Optional.
|
||||
documents: The documents to associate with the embeddings. Optional.
|
||||
images: The images to associate with the embeddings. Optional.
|
||||
uris: The uris of the images to associate with the embeddings. Optional.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
ValueError: If you don't provide either embeddings or documents
|
||||
ValueError: If the length of ids, embeddings, metadatas, or documents don't match
|
||||
ValueError: If you don't provide an embedding function and don't provide embeddings
|
||||
ValueError: If you provide both embeddings and documents
|
||||
ValueError: If you provide an id that already exists
|
||||
|
||||
"""
|
||||
|
||||
add_request = self._validate_and_prepare_add_request(
|
||||
ids=ids,
|
||||
embeddings=embeddings,
|
||||
metadatas=metadatas,
|
||||
documents=documents,
|
||||
images=images,
|
||||
uris=uris,
|
||||
)
|
||||
|
||||
self._client._add(
|
||||
collection_id=self.id,
|
||||
ids=add_request["ids"],
|
||||
embeddings=add_request["embeddings"],
|
||||
metadatas=add_request["metadatas"],
|
||||
documents=add_request["documents"],
|
||||
uris=add_request["uris"],
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
def get(
|
||||
self,
|
||||
ids: Optional[OneOrMany[ID]] = None,
|
||||
where: Optional[Where] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = ["metadatas", "documents"],
|
||||
) -> GetResult:
|
||||
"""Get embeddings and their associate data from the data store. If no ids or where filter is provided returns
|
||||
all embeddings up to limit starting at offset.
|
||||
|
||||
Args:
|
||||
ids: The ids of the embeddings to get. Optional.
|
||||
where: A Where type dict used to filter results by. E.g. `{"$and": [{"color" : "red"}, {"price": {"$gte": 4.20}}]}`. Optional.
|
||||
limit: The number of documents to return. Optional.
|
||||
offset: The offset to start returning results from. Useful for paging results with limit. Optional.
|
||||
where_document: A WhereDocument type dict used to filter by the documents. E.g. `{"$contains": "hello"}`. Optional.
|
||||
include: A list of what to include in the results. Can contain `"embeddings"`, `"metadatas"`, `"documents"`. Ids are always included. Defaults to `["metadatas", "documents"]`. Optional.
|
||||
|
||||
Returns:
|
||||
GetResult: A GetResult object containing the results.
|
||||
|
||||
"""
|
||||
get_request = self._validate_and_prepare_get_request(
|
||||
ids=ids,
|
||||
where=where,
|
||||
where_document=where_document,
|
||||
include=include,
|
||||
)
|
||||
|
||||
get_results = self._client._get(
|
||||
collection_id=self.id,
|
||||
ids=get_request["ids"],
|
||||
where=get_request["where"],
|
||||
where_document=get_request["where_document"],
|
||||
include=get_request["include"],
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
return self._transform_get_response(
|
||||
response=get_results, include=get_request["include"]
|
||||
)
|
||||
|
||||
def peek(self, limit: int = 10) -> GetResult:
|
||||
"""Get the first few results in the database up to limit
|
||||
|
||||
Args:
|
||||
limit: The number of results to return.
|
||||
|
||||
Returns:
|
||||
GetResult: A GetResult object containing the results.
|
||||
"""
|
||||
return self._transform_peek_response(
|
||||
self._client._peek(
|
||||
collection_id=self.id,
|
||||
n=limit,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
)
|
||||
|
||||
def query(
|
||||
self,
|
||||
query_embeddings: Optional[
|
||||
Union[
|
||||
OneOrMany[Embedding],
|
||||
OneOrMany[PyEmbedding],
|
||||
]
|
||||
] = None,
|
||||
query_texts: Optional[OneOrMany[Document]] = None,
|
||||
query_images: Optional[OneOrMany[Image]] = None,
|
||||
query_uris: Optional[OneOrMany[URI]] = None,
|
||||
ids: Optional[OneOrMany[ID]] = None,
|
||||
n_results: int = 10,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = [
|
||||
"metadatas",
|
||||
"documents",
|
||||
"distances",
|
||||
],
|
||||
) -> QueryResult:
|
||||
"""Get the n_results nearest neighbor embeddings for provided query_embeddings or query_texts.
|
||||
|
||||
Args:
|
||||
query_embeddings: The embeddings to get the closes neighbors of. Optional.
|
||||
query_texts: The document texts to get the closes neighbors of. Optional.
|
||||
query_images: The images to get the closes neighbors of. Optional.
|
||||
query_uris: The URIs to be used with data loader. Optional.
|
||||
ids: A subset of ids to search within. Optional.
|
||||
n_results: The number of neighbors to return for each query_embedding or query_texts. Optional.
|
||||
where: A Where type dict used to filter results by. E.g. `{"$and": [{"color" : "red"}, {"price": {"$gte": 4.20}}]}`. Optional.
|
||||
where_document: A WhereDocument type dict used to filter by the documents. E.g. `{"$contains": "hello"}`. Optional.
|
||||
include: A list of what to include in the results. Can contain `"embeddings"`, `"metadatas"`, `"documents"`, `"distances"`. Ids are always included. Defaults to `["metadatas", "documents", "distances"]`. Optional.
|
||||
|
||||
Returns:
|
||||
QueryResult: A QueryResult object containing the results.
|
||||
|
||||
Raises:
|
||||
ValueError: If you don't provide either query_embeddings, query_texts, or query_images
|
||||
ValueError: If you provide both query_embeddings and query_texts
|
||||
ValueError: If you provide both query_embeddings and query_images
|
||||
ValueError: If you provide both query_texts and query_images
|
||||
|
||||
"""
|
||||
|
||||
query_request = self._validate_and_prepare_query_request(
|
||||
query_embeddings=query_embeddings,
|
||||
query_texts=query_texts,
|
||||
query_images=query_images,
|
||||
query_uris=query_uris,
|
||||
ids=ids,
|
||||
n_results=n_results,
|
||||
where=where,
|
||||
where_document=where_document,
|
||||
include=include,
|
||||
)
|
||||
|
||||
query_results = self._client._query(
|
||||
collection_id=self.id,
|
||||
ids=query_request["ids"],
|
||||
query_embeddings=query_request["embeddings"],
|
||||
n_results=query_request["n_results"],
|
||||
where=query_request["where"],
|
||||
where_document=query_request["where_document"],
|
||||
include=query_request["include"],
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
return self._transform_query_response(
|
||||
response=query_results, include=query_request["include"]
|
||||
)
|
||||
|
||||
def modify(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
configuration: Optional[UpdateCollectionConfiguration] = None,
|
||||
) -> None:
|
||||
"""Modify the collection name or metadata
|
||||
|
||||
Args:
|
||||
name: The updated name for the collection. Optional.
|
||||
metadata: The updated metadata for the collection. Optional.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
self._validate_modify_request(metadata)
|
||||
|
||||
# Note there is a race condition here where the metadata can be updated
|
||||
# but another thread sees the cached local metadata.
|
||||
# TODO: fixme
|
||||
self._client._modify(
|
||||
id=self.id,
|
||||
new_name=name,
|
||||
new_metadata=metadata,
|
||||
new_configuration=configuration,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
self._update_model_after_modify_success(name, metadata, configuration)
|
||||
|
||||
def fork(
|
||||
self,
|
||||
new_name: str,
|
||||
) -> "Collection":
|
||||
"""Fork the current collection under a new name. The returning collection should contain identical data to the current collection.
|
||||
This is an experimental API that only works for Hosted Chroma for now.
|
||||
|
||||
Args:
|
||||
new_name: The name of the new collection.
|
||||
|
||||
Returns:
|
||||
Collection: A new collection with the specified name and containing identical data to the current collection.
|
||||
"""
|
||||
model = self._client._fork(
|
||||
collection_id=self.id,
|
||||
new_name=new_name,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
return Collection(
|
||||
client=self._client,
|
||||
model=model,
|
||||
embedding_function=self._embedding_function,
|
||||
data_loader=self._data_loader,
|
||||
)
|
||||
|
||||
def search(
|
||||
self,
|
||||
searches: OneOrMany[Search],
|
||||
) -> SearchResult:
|
||||
"""Perform hybrid search on the collection.
|
||||
This is an experimental API that only works for Hosted Chroma for now.
|
||||
|
||||
Args:
|
||||
searches: A single Search object or a list of Search objects, each containing:
|
||||
- where: Where expression for filtering
|
||||
- rank: Ranking expression for hybrid search (defaults to Val(0.0))
|
||||
- limit: Limit configuration for pagination (defaults to no limit)
|
||||
- select: Select configuration for keys to return (defaults to empty)
|
||||
|
||||
Returns:
|
||||
SearchResult: Column-major format response with:
|
||||
- ids: List of result IDs for each search payload
|
||||
- documents: Optional documents for each payload
|
||||
- embeddings: Optional embeddings for each payload
|
||||
- metadatas: Optional metadata for each payload
|
||||
- scores: Optional scores for each payload
|
||||
- select: List of selected keys for each payload
|
||||
|
||||
Raises:
|
||||
NotImplementedError: For local/segment API implementations
|
||||
|
||||
Examples:
|
||||
# Using builder pattern with Key constants
|
||||
from chromadb.execution.expression import (
|
||||
Search, Key, K, Knn, Val
|
||||
)
|
||||
|
||||
# Note: K is an alias for Key, so K.DOCUMENT == Key.DOCUMENT
|
||||
search = (Search()
|
||||
.where((K("category") == "science") & (K("score") > 0.5))
|
||||
.rank(Knn(query=[0.1, 0.2, 0.3]) * 0.8 + Val(0.5) * 0.2)
|
||||
.limit(10, offset=0)
|
||||
.select(K.DOCUMENT, K.SCORE, "title"))
|
||||
|
||||
# Direct construction
|
||||
from chromadb.execution.expression import (
|
||||
Search, Eq, And, Gt, Knn, Limit, Select, Key
|
||||
)
|
||||
|
||||
search = Search(
|
||||
where=And([Eq("category", "science"), Gt("score", 0.5)]),
|
||||
rank=Knn(query=[0.1, 0.2, 0.3]),
|
||||
limit=Limit(offset=0, limit=10),
|
||||
select=Select(keys={Key.DOCUMENT, Key.SCORE, "title"})
|
||||
)
|
||||
|
||||
# Single search
|
||||
result = collection.search(search)
|
||||
|
||||
# Multiple searches at once
|
||||
searches = [
|
||||
Search().where(K("type") == "article").rank(Knn(query=[0.1, 0.2])),
|
||||
Search().where(K("type") == "paper").rank(Knn(query=[0.3, 0.4]))
|
||||
]
|
||||
results = collection.search(searches)
|
||||
"""
|
||||
# Convert single search to list for consistent handling
|
||||
searches_list = maybe_cast_one_to_many(searches)
|
||||
if searches_list is None:
|
||||
searches_list = []
|
||||
|
||||
# Embed any string queries in Knn objects
|
||||
embedded_searches = [
|
||||
self._embed_search_string_queries(search) for search in searches_list
|
||||
]
|
||||
|
||||
return self._client._search(
|
||||
collection_id=self.id,
|
||||
searches=cast(List[Search], embedded_searches),
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
def update(
|
||||
self,
|
||||
ids: OneOrMany[ID],
|
||||
embeddings: Optional[
|
||||
Union[
|
||||
OneOrMany[Embedding],
|
||||
OneOrMany[PyEmbedding],
|
||||
]
|
||||
] = None,
|
||||
metadatas: Optional[OneOrMany[Metadata]] = None,
|
||||
documents: Optional[OneOrMany[Document]] = None,
|
||||
images: Optional[OneOrMany[Image]] = None,
|
||||
uris: Optional[OneOrMany[URI]] = None,
|
||||
) -> None:
|
||||
"""Update the embeddings, metadatas or documents for provided ids.
|
||||
|
||||
Args:
|
||||
ids: The ids of the embeddings to update
|
||||
embeddings: The embeddings to update. If None, embeddings will be computed based on the documents or images using the embedding_function set for the Collection. Optional.
|
||||
metadatas: The metadata to associate with the embeddings. When querying, you can filter on this metadata. Optional.
|
||||
documents: The documents to associate with the embeddings. Optional.
|
||||
images: The images to associate with the embeddings. Optional.
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
update_request = self._validate_and_prepare_update_request(
|
||||
ids=ids,
|
||||
embeddings=embeddings,
|
||||
metadatas=metadatas,
|
||||
documents=documents,
|
||||
images=images,
|
||||
uris=uris,
|
||||
)
|
||||
|
||||
self._client._update(
|
||||
collection_id=self.id,
|
||||
ids=update_request["ids"],
|
||||
embeddings=update_request["embeddings"],
|
||||
metadatas=update_request["metadatas"],
|
||||
documents=update_request["documents"],
|
||||
uris=update_request["uris"],
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
def upsert(
|
||||
self,
|
||||
ids: OneOrMany[ID],
|
||||
embeddings: Optional[
|
||||
Union[
|
||||
OneOrMany[Embedding],
|
||||
OneOrMany[PyEmbedding],
|
||||
]
|
||||
] = None,
|
||||
metadatas: Optional[OneOrMany[Metadata]] = None,
|
||||
documents: Optional[OneOrMany[Document]] = None,
|
||||
images: Optional[OneOrMany[Image]] = None,
|
||||
uris: Optional[OneOrMany[URI]] = None,
|
||||
) -> None:
|
||||
"""Update the embeddings, metadatas or documents for provided ids, or create them if they don't exist.
|
||||
|
||||
Args:
|
||||
ids: The ids of the embeddings to update
|
||||
embeddings: The embeddings to add. If None, embeddings will be computed based on the documents using the embedding_function set for the Collection. Optional.
|
||||
metadatas: The metadata to associate with the embeddings. When querying, you can filter on this metadata. Optional.
|
||||
documents: The documents to associate with the embeddings. Optional.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
upsert_request = self._validate_and_prepare_upsert_request(
|
||||
ids=ids,
|
||||
embeddings=embeddings,
|
||||
metadatas=metadatas,
|
||||
documents=documents,
|
||||
images=images,
|
||||
uris=uris,
|
||||
)
|
||||
|
||||
self._client._upsert(
|
||||
collection_id=self.id,
|
||||
ids=upsert_request["ids"],
|
||||
embeddings=upsert_request["embeddings"],
|
||||
metadatas=upsert_request["metadatas"],
|
||||
documents=upsert_request["documents"],
|
||||
uris=upsert_request["uris"],
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
) -> None:
|
||||
"""Delete the embeddings based on ids and/or a where filter
|
||||
|
||||
Args:
|
||||
ids: The ids of the embeddings to delete
|
||||
where: A Where type dict used to filter the delection by. E.g. `{"$and": [{"color" : "red"}, {"price": {"$gte": 4.20}]}}`. Optional.
|
||||
where_document: A WhereDocument type dict used to filter the deletion by the document content. E.g. `{"$contains": "hello"}`. Optional.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
ValueError: If you don't provide either ids, where, or where_document
|
||||
"""
|
||||
delete_request = self._validate_and_prepare_delete_request(
|
||||
ids, where, where_document
|
||||
)
|
||||
|
||||
self._client._delete(
|
||||
collection_id=self.id,
|
||||
ids=delete_request["ids"],
|
||||
where=delete_request["where"],
|
||||
where_document=delete_request["where_document"],
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
|
||||
def attach_function(
|
||||
self,
|
||||
function_id: str,
|
||||
name: str,
|
||||
output_collection: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
) -> "AttachedFunction":
|
||||
"""Attach a function to this collection.
|
||||
|
||||
Args:
|
||||
function_id: Built-in function identifier (e.g., "record_counter")
|
||||
name: Unique name for this attached function
|
||||
output_collection: Name of the collection where function output will be stored
|
||||
params: Optional dictionary with function-specific parameters
|
||||
|
||||
Returns:
|
||||
AttachedFunction: Object representing the attached function
|
||||
|
||||
Example:
|
||||
>>> attached_fn = collection.attach_function(
|
||||
... function_id="record_counter",
|
||||
... name="mycoll_stats_fn",
|
||||
... output_collection="mycoll_stats",
|
||||
... params={"threshold": 100}
|
||||
... )
|
||||
"""
|
||||
return self._client.attach_function(
|
||||
function_id=function_id,
|
||||
name=name,
|
||||
input_collection_id=self.id,
|
||||
output_collection=output_collection,
|
||||
params=params,
|
||||
tenant=self.tenant,
|
||||
database=self.database,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,644 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from chromadb import (
|
||||
CollectionMetadata,
|
||||
Embeddings,
|
||||
GetResult,
|
||||
IDs,
|
||||
Where,
|
||||
WhereDocument,
|
||||
Include,
|
||||
Documents,
|
||||
Metadatas,
|
||||
QueryResult,
|
||||
URIs,
|
||||
)
|
||||
from chromadb.api import ServerAPI
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from chromadb.api.models.AttachedFunction import AttachedFunction
|
||||
from chromadb.api.collection_configuration import (
|
||||
CreateCollectionConfiguration,
|
||||
UpdateCollectionConfiguration,
|
||||
create_collection_configuration_to_json_str,
|
||||
update_collection_configuration_to_json_str,
|
||||
)
|
||||
from chromadb.auth import UserIdentity
|
||||
from chromadb.config import DEFAULT_DATABASE, DEFAULT_TENANT, Settings, System
|
||||
from chromadb.telemetry.product import ProductTelemetryClient
|
||||
from chromadb.telemetry.product.events import (
|
||||
CollectionAddEvent,
|
||||
CollectionDeleteEvent,
|
||||
CollectionGetEvent,
|
||||
CollectionUpdateEvent,
|
||||
CollectionQueryEvent,
|
||||
ClientCreateCollectionEvent,
|
||||
)
|
||||
|
||||
from chromadb.api.types import (
|
||||
IncludeMetadataDocuments,
|
||||
IncludeMetadataDocumentsDistances,
|
||||
IncludeMetadataDocumentsEmbeddings,
|
||||
Schema,
|
||||
SearchResult,
|
||||
)
|
||||
|
||||
# TODO(hammadb): Unify imports across types vs root __init__.py
|
||||
from chromadb.types import Database, Tenant, Collection as CollectionModel
|
||||
from chromadb.execution.expression.plan import Search
|
||||
import chromadb_rust_bindings
|
||||
|
||||
|
||||
from typing import Optional, Sequence, List, Dict, Any
|
||||
from overrides import override
|
||||
from uuid import UUID
|
||||
import json
|
||||
import platform
|
||||
|
||||
if platform.system() != "Windows":
|
||||
import resource
|
||||
elif platform.system() == "Windows":
|
||||
import ctypes
|
||||
|
||||
|
||||
# RustBindingsAPI is an implementation of ServerAPI which shims
|
||||
# the Rust bindings to the Python API, providing a full implementation
|
||||
# of the API. It could be that bindings was a direct implementation of
|
||||
# ServerAPI, but in order to prevent propagating the bindings types
|
||||
# into the Python API, we have to shim it here so we can convert into
|
||||
# the legacy Python types.
|
||||
# TODO(hammadb): Propagate the types from the bindings into the Python API
|
||||
# and remove the python-level types entirely.
|
||||
class RustBindingsAPI(ServerAPI):
|
||||
bindings: chromadb_rust_bindings.Bindings
|
||||
hnsw_cache_size: int
|
||||
product_telemetry_client: ProductTelemetryClient
|
||||
|
||||
def __init__(self, system: System):
|
||||
super().__init__(system)
|
||||
self.product_telemetry_client = self.require(ProductTelemetryClient)
|
||||
|
||||
if platform.system() != "Windows":
|
||||
max_file_handles = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
|
||||
else:
|
||||
max_file_handles = ctypes.windll.msvcrt._getmaxstdio() # type: ignore
|
||||
self.hnsw_cache_size = (
|
||||
max_file_handles
|
||||
# This is integer division in Python 3, and not a comment.
|
||||
# Each HNSW index has 4 data files and 1 metadata file
|
||||
// 5
|
||||
)
|
||||
|
||||
@override
|
||||
def start(self) -> None:
|
||||
# Construct the SqliteConfig
|
||||
# TOOD: We should add a "config converter"
|
||||
if self._system.settings.require("is_persistent"):
|
||||
persist_path = self._system.settings.require("persist_directory")
|
||||
sqlite_persist_path = persist_path + "/chroma.sqlite3"
|
||||
else:
|
||||
persist_path = None
|
||||
sqlite_persist_path = None
|
||||
hash_type = self._system.settings.require("migrations_hash_algorithm")
|
||||
hash_type_bindings = (
|
||||
chromadb_rust_bindings.MigrationHash.MD5
|
||||
if hash_type == "md5"
|
||||
else chromadb_rust_bindings.MigrationHash.SHA256
|
||||
)
|
||||
migration_mode = self._system.settings.require("migrations")
|
||||
migration_mode_bindings = (
|
||||
chromadb_rust_bindings.MigrationMode.Apply
|
||||
if migration_mode == "apply"
|
||||
else chromadb_rust_bindings.MigrationMode.Validate
|
||||
)
|
||||
sqlite_config = chromadb_rust_bindings.SqliteDBConfig(
|
||||
hash_type=hash_type_bindings,
|
||||
migration_mode=migration_mode_bindings,
|
||||
url=sqlite_persist_path,
|
||||
)
|
||||
|
||||
self.bindings = chromadb_rust_bindings.Bindings(
|
||||
allow_reset=self._system.settings.require("allow_reset"),
|
||||
sqlite_db_config=sqlite_config,
|
||||
persist_path=persist_path,
|
||||
hnsw_cache_size=self.hnsw_cache_size,
|
||||
)
|
||||
|
||||
@override
|
||||
def stop(self) -> None:
|
||||
del self.bindings
|
||||
|
||||
# ////////////////////////////// Admin API //////////////////////////////
|
||||
|
||||
@override
|
||||
def create_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
|
||||
return self.bindings.create_database(name, tenant)
|
||||
|
||||
@override
|
||||
def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> Database:
|
||||
database = self.bindings.get_database(name, tenant)
|
||||
return {
|
||||
"id": database.id,
|
||||
"name": database.name,
|
||||
"tenant": database.tenant,
|
||||
}
|
||||
|
||||
@override
|
||||
def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
|
||||
return self.bindings.delete_database(name, tenant)
|
||||
|
||||
@override
|
||||
def list_databases(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
) -> Sequence[Database]:
|
||||
databases = self.bindings.list_databases(limit, offset, tenant)
|
||||
return [
|
||||
{
|
||||
"id": database.id,
|
||||
"name": database.name,
|
||||
"tenant": database.tenant,
|
||||
}
|
||||
for database in databases
|
||||
]
|
||||
|
||||
@override
|
||||
def create_tenant(self, name: str) -> None:
|
||||
return self.bindings.create_tenant(name)
|
||||
|
||||
@override
|
||||
def get_tenant(self, name: str) -> Tenant:
|
||||
tenant = self.bindings.get_tenant(name)
|
||||
return Tenant(name=tenant.name)
|
||||
|
||||
# ////////////////////////////// Base API //////////////////////////////
|
||||
|
||||
@override
|
||||
def heartbeat(self) -> int:
|
||||
return self.bindings.heartbeat()
|
||||
|
||||
@override
|
||||
def count_collections(
|
||||
self, tenant: str = DEFAULT_TENANT, database: str = DEFAULT_DATABASE
|
||||
) -> int:
|
||||
return self.bindings.count_collections(tenant, database)
|
||||
|
||||
@override
|
||||
def list_collections(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> Sequence[CollectionModel]:
|
||||
collections = self.bindings.list_collections(limit, offset, tenant, database)
|
||||
return [
|
||||
CollectionModel(
|
||||
id=collection.id,
|
||||
name=collection.name,
|
||||
serialized_schema=collection.schema,
|
||||
configuration_json=collection.configuration,
|
||||
metadata=collection.metadata,
|
||||
dimension=collection.dimension,
|
||||
tenant=collection.tenant,
|
||||
database=collection.database,
|
||||
)
|
||||
for collection in collections
|
||||
]
|
||||
|
||||
@override
|
||||
def create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
get_or_create: bool = False,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
# TODO: This event doesn't capture the get_or_create case appropriately
|
||||
# TODO: Re-enable embedding function tracking in create_collection
|
||||
self.product_telemetry_client.capture(
|
||||
ClientCreateCollectionEvent(
|
||||
collection_uuid=str(id),
|
||||
# embedding_function=embedding_function.__class__.__name__,
|
||||
)
|
||||
)
|
||||
if configuration:
|
||||
configuration_json_str = create_collection_configuration_to_json_str(
|
||||
configuration, metadata
|
||||
)
|
||||
else:
|
||||
configuration_json_str = None
|
||||
|
||||
if schema:
|
||||
schema_str = json.dumps(schema.serialize_to_json())
|
||||
else:
|
||||
schema_str = None
|
||||
|
||||
collection = self.bindings.create_collection(
|
||||
name,
|
||||
configuration_json_str,
|
||||
schema_str,
|
||||
metadata,
|
||||
get_or_create,
|
||||
tenant,
|
||||
database,
|
||||
)
|
||||
collection_model = CollectionModel(
|
||||
id=collection.id,
|
||||
name=collection.name,
|
||||
configuration_json=collection.configuration,
|
||||
serialized_schema=collection.schema,
|
||||
metadata=collection.metadata,
|
||||
dimension=collection.dimension,
|
||||
tenant=collection.tenant,
|
||||
database=collection.database,
|
||||
)
|
||||
return collection_model
|
||||
|
||||
@override
|
||||
def get_collection(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
collection = self.bindings.get_collection(name, tenant, database)
|
||||
return CollectionModel(
|
||||
id=collection.id,
|
||||
name=collection.name,
|
||||
configuration_json=collection.configuration,
|
||||
serialized_schema=collection.schema,
|
||||
metadata=collection.metadata,
|
||||
dimension=collection.dimension,
|
||||
tenant=collection.tenant,
|
||||
database=collection.database,
|
||||
)
|
||||
|
||||
@override
|
||||
def get_or_create_collection(
|
||||
self,
|
||||
name: str,
|
||||
schema: Optional[Schema] = None,
|
||||
configuration: Optional[CreateCollectionConfiguration] = None,
|
||||
metadata: Optional[CollectionMetadata] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
return self.create_collection(
|
||||
name, schema, configuration, metadata, True, tenant, database
|
||||
)
|
||||
|
||||
@override
|
||||
def delete_collection(
|
||||
self,
|
||||
name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
self.bindings.delete_collection(name, tenant, database)
|
||||
|
||||
@override
|
||||
def _modify(
|
||||
self,
|
||||
id: UUID,
|
||||
new_name: Optional[str] = None,
|
||||
new_metadata: Optional[CollectionMetadata] = None,
|
||||
new_configuration: Optional[UpdateCollectionConfiguration] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
if new_configuration:
|
||||
new_configuration_json_str = update_collection_configuration_to_json_str(
|
||||
new_configuration
|
||||
)
|
||||
else:
|
||||
new_configuration_json_str = None
|
||||
self.bindings.update_collection(
|
||||
str(id), new_name, new_metadata, new_configuration_json_str
|
||||
)
|
||||
|
||||
@override
|
||||
def _fork(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
new_name: str,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> CollectionModel:
|
||||
raise NotImplementedError(
|
||||
"Collection forking is not implemented for Local Chroma"
|
||||
)
|
||||
|
||||
@override
|
||||
def _search(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
searches: List[Search],
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> SearchResult:
|
||||
raise NotImplementedError("Search is not implemented for Local Chroma")
|
||||
|
||||
@override
|
||||
def _count(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> int:
|
||||
return self.bindings.count(str(collection_id), tenant, database)
|
||||
|
||||
@override
|
||||
def _peek(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
n: int = 10,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> GetResult:
|
||||
return self._get(
|
||||
collection_id,
|
||||
limit=n,
|
||||
tenant=tenant,
|
||||
database=database,
|
||||
include=IncludeMetadataDocumentsEmbeddings,
|
||||
)
|
||||
|
||||
@override
|
||||
def _get(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocuments,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> GetResult:
|
||||
ids_amount = len(ids) if ids else 0
|
||||
self.product_telemetry_client.capture(
|
||||
CollectionGetEvent(
|
||||
collection_uuid=str(collection_id),
|
||||
ids_count=ids_amount,
|
||||
limit=limit if limit else 0,
|
||||
include_metadata=ids_amount if "metadatas" in include else 0,
|
||||
include_documents=ids_amount if "documents" in include else 0,
|
||||
include_uris=ids_amount if "uris" in include else 0,
|
||||
)
|
||||
)
|
||||
|
||||
rust_response = self.bindings.get(
|
||||
str(collection_id),
|
||||
ids,
|
||||
json.dumps(where) if where else None,
|
||||
limit,
|
||||
offset or 0,
|
||||
json.dumps(where_document) if where_document else None,
|
||||
include,
|
||||
tenant,
|
||||
database,
|
||||
)
|
||||
|
||||
return GetResult(
|
||||
ids=rust_response.ids,
|
||||
embeddings=rust_response.embeddings,
|
||||
documents=rust_response.documents,
|
||||
uris=rust_response.uris,
|
||||
included=include,
|
||||
data=None,
|
||||
metadatas=rust_response.metadatas,
|
||||
)
|
||||
|
||||
@override
|
||||
def _add(
|
||||
self,
|
||||
ids: IDs,
|
||||
collection_id: UUID,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
self.product_telemetry_client.capture(
|
||||
CollectionAddEvent(
|
||||
collection_uuid=str(collection_id),
|
||||
add_amount=len(ids),
|
||||
with_metadata=len(ids) if metadatas is not None else 0,
|
||||
with_documents=len(ids) if documents is not None else 0,
|
||||
with_uris=len(ids) if uris is not None else 0,
|
||||
)
|
||||
)
|
||||
|
||||
return self.bindings.add(
|
||||
ids,
|
||||
str(collection_id),
|
||||
embeddings,
|
||||
metadatas,
|
||||
documents,
|
||||
uris,
|
||||
tenant,
|
||||
database,
|
||||
)
|
||||
|
||||
@override
|
||||
def _update(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Optional[Embeddings] = None,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
self.product_telemetry_client.capture(
|
||||
CollectionUpdateEvent(
|
||||
collection_uuid=str(collection_id),
|
||||
update_amount=len(ids),
|
||||
with_embeddings=len(embeddings) if embeddings else 0,
|
||||
with_metadata=len(metadatas) if metadatas else 0,
|
||||
with_documents=len(documents) if documents else 0,
|
||||
with_uris=len(uris) if uris else 0,
|
||||
)
|
||||
)
|
||||
|
||||
return self.bindings.update(
|
||||
str(collection_id),
|
||||
ids,
|
||||
embeddings,
|
||||
metadatas,
|
||||
documents,
|
||||
uris,
|
||||
tenant,
|
||||
database,
|
||||
)
|
||||
|
||||
@override
|
||||
def _upsert(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: IDs,
|
||||
embeddings: Embeddings,
|
||||
metadatas: Optional[Metadatas] = None,
|
||||
documents: Optional[Documents] = None,
|
||||
uris: Optional[URIs] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
return self.bindings.upsert(
|
||||
str(collection_id),
|
||||
ids,
|
||||
embeddings,
|
||||
metadatas,
|
||||
documents,
|
||||
uris,
|
||||
tenant,
|
||||
database,
|
||||
)
|
||||
|
||||
@override
|
||||
def _query(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
query_embeddings: Embeddings,
|
||||
ids: Optional[IDs] = None,
|
||||
n_results: int = 10,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
include: Include = IncludeMetadataDocumentsDistances,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> QueryResult:
|
||||
query_amount = len(query_embeddings)
|
||||
filtered_ids_amount = len(ids) if ids else 0
|
||||
self.product_telemetry_client.capture(
|
||||
CollectionQueryEvent(
|
||||
collection_uuid=str(collection_id),
|
||||
query_amount=query_amount,
|
||||
filtered_ids_amount=filtered_ids_amount,
|
||||
n_results=n_results,
|
||||
with_metadata_filter=query_amount if where is not None else 0,
|
||||
with_document_filter=query_amount if where_document is not None else 0,
|
||||
include_metadatas=query_amount if "metadatas" in include else 0,
|
||||
include_documents=query_amount if "documents" in include else 0,
|
||||
include_uris=query_amount if "uris" in include else 0,
|
||||
include_distances=query_amount if "distances" in include else 0,
|
||||
)
|
||||
)
|
||||
|
||||
rust_response = self.bindings.query(
|
||||
str(collection_id),
|
||||
ids,
|
||||
query_embeddings,
|
||||
n_results,
|
||||
json.dumps(where) if where else None,
|
||||
json.dumps(where_document) if where_document else None,
|
||||
include,
|
||||
tenant,
|
||||
database,
|
||||
)
|
||||
|
||||
return QueryResult(
|
||||
ids=rust_response.ids,
|
||||
embeddings=rust_response.embeddings,
|
||||
documents=rust_response.documents,
|
||||
uris=rust_response.uris,
|
||||
included=include,
|
||||
data=None,
|
||||
metadatas=rust_response.metadatas,
|
||||
distances=rust_response.distances,
|
||||
)
|
||||
|
||||
@override
|
||||
def _delete(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
ids: Optional[IDs] = None,
|
||||
where: Optional[Where] = None,
|
||||
where_document: Optional[WhereDocument] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> None:
|
||||
self.product_telemetry_client.capture(
|
||||
CollectionDeleteEvent(
|
||||
# NOTE: the delete amount is not observable from python
|
||||
# TODO: Fix this when posthog is pushed into Rust frontend
|
||||
collection_uuid=str(collection_id),
|
||||
delete_amount=0,
|
||||
)
|
||||
)
|
||||
|
||||
return self.bindings.delete(
|
||||
str(collection_id),
|
||||
ids,
|
||||
json.dumps(where) if where else None,
|
||||
json.dumps(where_document) if where_document else None,
|
||||
tenant,
|
||||
database,
|
||||
)
|
||||
|
||||
@override
|
||||
def reset(self) -> bool:
|
||||
return self.bindings.reset()
|
||||
|
||||
@override
|
||||
def get_version(self) -> str:
|
||||
return self.bindings.get_version()
|
||||
|
||||
@override
|
||||
def get_settings(self) -> Settings:
|
||||
return self._system.settings
|
||||
|
||||
@override
|
||||
def get_max_batch_size(self) -> int:
|
||||
return self.bindings.get_max_batch_size()
|
||||
|
||||
@override
|
||||
def attach_function(
|
||||
self,
|
||||
function_id: str,
|
||||
name: str,
|
||||
input_collection_id: UUID,
|
||||
output_collection: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> "AttachedFunction":
|
||||
"""Attached functions are not supported in the Rust bindings (local embedded mode)."""
|
||||
raise NotImplementedError(
|
||||
"Attached functions are only supported when connecting to a Chroma server via HttpClient. "
|
||||
"The Rust bindings (embedded mode) do not support attached function operations."
|
||||
)
|
||||
|
||||
@override
|
||||
def detach_function(
|
||||
self,
|
||||
attached_function_id: UUID,
|
||||
delete_output: bool = False,
|
||||
tenant: str = DEFAULT_TENANT,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
) -> bool:
|
||||
"""Attached functions are not supported in the Rust bindings (local embedded mode)."""
|
||||
raise NotImplementedError(
|
||||
"Attached functions are only supported when connecting to a Chroma server via HttpClient. "
|
||||
"The Rust bindings (embedded mode) do not support attached function operations."
|
||||
)
|
||||
|
||||
# TODO: Remove this if it's not planned to be used
|
||||
@override
|
||||
def get_user_identity(self) -> UserIdentity:
|
||||
return UserIdentity(
|
||||
user_id="",
|
||||
tenant=DEFAULT_TENANT,
|
||||
databases=[DEFAULT_DATABASE],
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
from typing import ClassVar, Dict
|
||||
import uuid
|
||||
|
||||
from chromadb.api import ServerAPI
|
||||
from chromadb.config import Settings, System
|
||||
from chromadb.telemetry.product import ProductTelemetryClient
|
||||
from chromadb.telemetry.product.events import ClientStartEvent
|
||||
|
||||
|
||||
class SharedSystemClient:
|
||||
_identifier_to_system: ClassVar[Dict[str, System]] = {}
|
||||
_identifier: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
settings: Settings = Settings(),
|
||||
) -> None:
|
||||
self._identifier = SharedSystemClient._get_identifier_from_settings(settings)
|
||||
SharedSystemClient._create_system_if_not_exists(self._identifier, settings)
|
||||
|
||||
@classmethod
|
||||
def _create_system_if_not_exists(
|
||||
cls, identifier: str, settings: Settings
|
||||
) -> System:
|
||||
if identifier not in cls._identifier_to_system:
|
||||
new_system = System(settings)
|
||||
cls._identifier_to_system[identifier] = new_system
|
||||
|
||||
new_system.instance(ProductTelemetryClient)
|
||||
new_system.instance(ServerAPI)
|
||||
|
||||
new_system.start()
|
||||
else:
|
||||
previous_system = cls._identifier_to_system[identifier]
|
||||
|
||||
# For now, the settings must match
|
||||
if previous_system.settings != settings:
|
||||
raise ValueError(
|
||||
f"An instance of Chroma already exists for {identifier} with different settings"
|
||||
)
|
||||
|
||||
return cls._identifier_to_system[identifier]
|
||||
|
||||
@staticmethod
|
||||
def _get_identifier_from_settings(settings: Settings) -> str:
|
||||
identifier = ""
|
||||
api_impl = settings.chroma_api_impl
|
||||
|
||||
if api_impl is None:
|
||||
raise ValueError("Chroma API implementation must be set in settings")
|
||||
elif api_impl in [
|
||||
"chromadb.api.segment.SegmentAPI",
|
||||
"chromadb.api.rust.RustBindingsAPI",
|
||||
]:
|
||||
if settings.is_persistent:
|
||||
identifier = settings.persist_directory
|
||||
else:
|
||||
identifier = (
|
||||
"ephemeral" # TODO: support pathing and multiple ephemeral clients
|
||||
)
|
||||
elif api_impl in [
|
||||
"chromadb.api.fastapi.FastAPI",
|
||||
"chromadb.api.async_fastapi.AsyncFastAPI",
|
||||
]:
|
||||
# FastAPI clients can all use unique system identifiers since their configurations can be independent, e.g. different auth tokens
|
||||
identifier = str(uuid.uuid4())
|
||||
else:
|
||||
raise ValueError(f"Unsupported Chroma API implementation {api_impl}")
|
||||
|
||||
return identifier
|
||||
|
||||
@staticmethod
|
||||
def _populate_data_from_system(system: System) -> str:
|
||||
identifier = SharedSystemClient._get_identifier_from_settings(system.settings)
|
||||
SharedSystemClient._identifier_to_system[identifier] = system
|
||||
return identifier
|
||||
|
||||
@classmethod
|
||||
def from_system(cls, system: System) -> "SharedSystemClient":
|
||||
"""Create a client from an existing system. This is useful for testing and debugging."""
|
||||
|
||||
SharedSystemClient._populate_data_from_system(system)
|
||||
instance = cls(system.settings)
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def clear_system_cache() -> None:
|
||||
SharedSystemClient._identifier_to_system = {}
|
||||
|
||||
@property
|
||||
def _system(self) -> System:
|
||||
return SharedSystemClient._identifier_to_system[self._identifier]
|
||||
|
||||
def _submit_client_start_event(self) -> None:
|
||||
telemetry_client = self._system.instance(ProductTelemetryClient)
|
||||
telemetry_client.capture(ClientStartEvent())
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user