chore: 添加虚拟环境到仓库

- 添加 backend_service/venv 虚拟环境
- 包含所有Python依赖包
- 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
2025-12-03 10:19:25 +08:00
parent a6c2027caa
commit c4f851d387
12655 changed files with 3009376 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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,
)

View File

@@ -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}')"
)

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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