# Chroma Cookbook > Recipes and operational guides for building with Chroma. This llms.txt index points to the Chroma Cookbook pages that are most useful for agents and LLM-based tooling. # Getting Started # Home This is a collection of small guides and recipes to help you get started with Chroma. Latest ChromaDB version: [1.5.3](https://github.com/chroma-core/chroma/releases/tag/1.5.3) API Changelog (1.5.3, 1.5.2, 1.5.1, and 1.5.0) **Version [1.5.3](https://github.com/chroma-core/chroma/releases/tag/1.5.3) (March 7, 2026)** | Area | API-facing change | Reference | | ------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | Collections | Added delete operations with `limit` in server and client APIs | [#6573](https://github.com/chroma-core/chroma/pull/6573), [#6578](https://github.com/chroma-core/chroma/pull/6578) | | Python Client | Updated Python packaging compatibility by dropping Pydantic v1 compatibility layer for Python 3.14 support | [#6356](https://github.com/chroma-core/chroma/pull/6356) | **Version [1.5.2](https://github.com/chroma-core/chroma/releases/tag/1.5.2) (February 27, 2026)** | Area | API-facing change | Reference | | ------------------- | ------------------------------------------------------------------ | -------------------------------------------------------- | | Python Client | Added `Client.close()` and context manager support (`with` syntax) | [#6373](https://github.com/chroma-core/chroma/pull/6373) | | Embedding Functions | Added Perplexity embedding function support (`Pplx EF`) | [#6511](https://github.com/chroma-core/chroma/pull/6511) | **Version [1.5.1](https://github.com/chroma-core/chroma/releases/tag/1.5.1) (February 19, 2026)** | Area | API-facing change | Reference | | --------------- | --------------------------------------------------- | -------------------------------------------------------- | | Advanced Search | Removed beta label from Advanced Search API | [#6396](https://github.com/chroma-core/chroma/pull/6396) | | Collections | Reject `fork_collection` for multi-region databases | [#6400](https://github.com/chroma-core/chroma/pull/6400) | | Schema / FTS | Added option to disable FTS in schema | [#6214](https://github.com/chroma-core/chroma/pull/6214) | **Version [1.5.0](https://github.com/chroma-core/chroma/releases/tag/1.5.0) (February 9, 2026)** | Area | API-facing change | Reference | | ----------- | -------------------------------------------------- | -------------------------------------------------------- | | Search | Exported search options parameter | [#6160](https://github.com/chroma-core/chroma/pull/6160) | | Collections | Rust sysdb impl for `get collections` | [#6146](https://github.com/chroma-core/chroma/pull/6146) | | Collections | Rust sysdb impl for `get collection with segments` | [#6147](https://github.com/chroma-core/chroma/pull/6147) | | Collections | Rust sysdb impl for `update collection` | [#6163](https://github.com/chroma-core/chroma/pull/6163) | | Schema | Added option to enable quantization in schema | [#6295](https://github.com/chroma-core/chroma/pull/6295) | ## New and Noteworthy - 🧨 [Go Local/Persistent Client Example](https://cookbook.chromadb.dev/strategies/go-local-markdown-cli/index.md) - A new local/persistent Go client is available, and this page includes a complete example for markdown indexing and search - πŸ“…`05-Mar-2026` - πŸ–ΌοΈ [Image Search](https://cookbook.chromadb.dev/strategies/image-search/index.md) - Added a multimodal retrieval walkthrough with a full runnable Python example for text-to-image and image-to-image search using OpenCLIP - πŸ“…`26-Feb-2026` - πŸ”Ž [Keyword Search](https://cookbook.chromadb.dev/strategies/keyword-search/index.md) - Expanded with Rust/Go guidance, regex (`$regex`/`$not_regex`) patterns, composition hints, and full runnable Python/TypeScript/Go/Rust examples - πŸ“…`26-Feb-2026` - βœ… [Metadata Schema Validation](https://cookbook.chromadb.dev/strategies/metadata-schema-validation/index.md) - Added app-layer metadata contract patterns with short snippets and runnable Python/TypeScript/Go/Rust examples - πŸ“…`26-Feb-2026` - 🧩 [Deployment Patterns](https://cookbook.chromadb.dev/running/deployment-patterns/index.md) - Added two practical deployment walkthroughs: embed Chroma directly in a Python app, or run it as a standalone server and connect with `HttpClient` - πŸ“…`24-Feb-2026` - πŸ“Š [Resource Requirements](https://cookbook.chromadb.dev/core/resources/index.md) - Added an interactive sizing calculator, clearer RAM formulas, and explicit disk caveats for large documents and FTS index overhead - πŸ“…`21-Feb-2026` - πŸš€ [Running Chroma](https://cookbook.chromadb.dev/running/running-chroma/index.md) - Refreshed CLI/Docker/Compose/Minikube guidance, aligned Helm chart notes, and added collapsed optional YAML config examples - πŸ“…`20-Feb-2026` - 🧭 [Core Concepts](https://cookbook.chromadb.dev/core/concepts/index.md) - Reworked into General vs Power Users tracks, with interactive local/distributed execution diagrams and data-flow visuals - πŸ“…`19-Feb-2026` - 🎯 [Collections Query IDs](https://cookbook.chromadb.dev/core/collections/#constrain-query-candidates-by-id) - Documented `query(..., ids=...)` for restricting similarity search to specific records - πŸ“…`17-Feb-2026` - πŸ” [Filters](https://cookbook.chromadb.dev/core/filters/index.md) - Added multi-language filter examples and `$regex`/`$not_regex` operators - πŸ“…`17-Feb-2026` - πŸ”§ [Installation](https://cookbook.chromadb.dev/core/install/index.md) - Updated package names and added Go/Rust install examples - πŸ“…`17-Feb-2026` - βš’οΈ [Configuration](https://cookbook.chromadb.dev/core/configuration/index.md) - Added 1.0 docs for HNSW, SPANN index, and embedding functions - πŸ“…`17-Feb-2026` - πŸ“¦ [Clients](https://cookbook.chromadb.dev/core/clients/#cloud-client) - Added Cloud Client section and updated client examples - πŸ“…`17-Feb-2026` - πŸ“š [Collections](https://cookbook.chromadb.dev/core/collections/index.md) - Updated to current APIs with multi-language examples - πŸ“…`17-Feb-2026` - 🏷️ [Array Metadata Filters](https://cookbook.chromadb.dev/core/filters/#array-metadata) - Chroma 1.5.0 adds support for array metadata with `$contains`/`$not_contains` operators - πŸ“…`17-Feb-2026` - πŸ”‘ [Authentication in Chroma v1.0.x](https://cookbook.chromadb.dev/security/auth-1.0.x/index.md) - Chroma 1.0.x does not support native Authentication, in this article we cover how to secure your Chroma 1.0.x instance - πŸ“…`28-May-2025` ## Getting Started We suggest you first head to the [Concepts](https://cookbook.chromadb.dev/core/concepts/index.md) section. It now has two tracks: - [For General Users](https://cookbook.chromadb.dev/core/concepts/#for-general-users) - tenancy, collections, metadata, embeddings, and cloud data-flow basics - [For Power Users](https://cookbook.chromadb.dev/core/concepts/#for-power-users) - local SQLite + HNSW path, distributed frontend dispatch path, and core internals Once you're comfortable with the concepts, you can jump to the [Installation](https://cookbook.chromadb.dev/core/install/index.md) section to install ChromaDB. **Core Topics:** - [Filters](https://cookbook.chromadb.dev/core/filters/index.md) - Learn to filter data in ChromaDB using metadata and document filters - [Resource Requirements](https://cookbook.chromadb.dev/core/resources/index.md) - Understand the resource requirements for running ChromaDB - ✨[Multi-Tenancy](https://cookbook.chromadb.dev/strategies/multi-tenancy/index.md) - Learn how to implement multi-tenancy in ChromaDB ## Running ChromaDB - [Running Chroma](https://cookbook.chromadb.dev/running/running-chroma/index.md) - Main guide for running ChromaDB via CLI, Docker, Docker Compose, and Kubernetes - [Deployment Patterns](https://cookbook.chromadb.dev/running/deployment-patterns/index.md) - Decide between embedded (`PersistentClient`) and standalone server (`HttpClient`) with copy/paste examples - [Docker](https://cookbook.chromadb.dev/running/running-chroma/#docker) - Running ChromaDB in Docker - [Docker Compose](https://cookbook.chromadb.dev/running/running-chroma/#docker-compose) - Running ChromaDB in Docker Compose - [CLI](https://cookbook.chromadb.dev/running/running-chroma/#chroma-cli) - Running ChromaDB via the CLI - [Kubernetes](https://cookbook.chromadb.dev/running/running-chroma/#minikube-with-helm-chart) - Running ChromaDB in Kubernetes (Minikube) ## Integrations - ✨[LangChain](https://cookbook.chromadb.dev/integrations/langchain/index.md) - Integrating ChromaDB with LangChain - ✨[LlamaIndex](https://cookbook.chromadb.dev/integrations/llamaindex/index.md) - Integrating ChromaDB with LlamaIndex - ✨[Ollama](https://cookbook.chromadb.dev/integrations/ollama/index.md) - Integrating ChromaDB with Ollama ## The Ecosystem ### Clients Below is a list of available clients for ChromaDB. - [Python Client](https://cookbook.chromadb.dev/ecosystem/clients/#python) (Official Chroma client) - [JavaScript Client](https://cookbook.chromadb.dev/ecosystem/clients/#javascript) (Official Chroma client) - [Ruby Client](https://cookbook.chromadb.dev/ecosystem/clients/#ruby-client) (Community maintained) - [Java Client](https://cookbook.chromadb.dev/ecosystem/clients/#java-client) (Community maintained) - [Go Client](https://cookbook.chromadb.dev/ecosystem/clients/#go-client) (Community maintained) - [C# Client](https://cookbook.chromadb.dev/ecosystem/clients/#c-client) (Microsoft maintained) - [Rust Client](https://cookbook.chromadb.dev/ecosystem/clients/#rust-client) (Community maintained) - [Elixir Client](https://cookbook.chromadb.dev/ecosystem/clients/#elixir-client) (Community maintained) - [Dart Client](https://cookbook.chromadb.dev/ecosystem/clients/#dart-client) (Community maintained) - [PHP Client](https://cookbook.chromadb.dev/ecosystem/clients/#php-client) (Community maintained) - [PHP (Laravel)](https://cookbook.chromadb.dev/ecosystem/clients/#php-laravel-client) Client (Community maintained) ### User Interfaces - [VectorAdmin](https://github.com/Mintplex-Labs/vector-admin) (MintPlex Labs) - An open-source web-based admin interface for vector databases, including ChromaDB - [ChromaDB UI](https://github.com/thakkaryash94/chroma-ui) (Community maintained) - A web-based UI for ChromaDB - [phpMyChroma](https://github.com/pari/phpMyChroma) (Community maintained) - A tiny PHP 8+ web client that allows you to browse Chroma and perform semantic search ### CLI Tooling - [Chroma CLI](https://github.com/amikos-tech/chroma-cli) (Community maintained) - Early Alpha - [Chroma Data Pipes](https://github.com/amikos-tech/chromadb-data-pipes) (Community maintained) - A CLI tool for importing and exporting data from ChromaDB - [Chroma Ops](https://github.com/amikos-tech/chromadb-ops) (Community maintained) - A maintenance CLI tool for ChromaDB ## Strategies - [Backup](https://cookbook.chromadb.dev/strategies/backup/index.md) - Backing up ChromaDB data - [Batch Imports](https://cookbook.chromadb.dev/strategies/batching/index.md) - Importing data in batches - [Multi-Tenancy](https://cookbook.chromadb.dev/strategies/multi-tenancy/index.md) - Running multiple ChromaDB instances - [Go Local Markdown CLI](https://cookbook.chromadb.dev/strategies/go-local-markdown-cli/index.md) - Build a local-only Go CLI with `PersistentClient`, `default_ef`, markdown chunking, and metadata-filtered search - [Image Search](https://cookbook.chromadb.dev/strategies/image-search/index.md) - Multimodal retrieval with OpenCLIP (`query_texts` and `query_uris`) plus a runnable Python example - [Keyword Search](https://cookbook.chromadb.dev/strategies/keyword-search/index.md) - Keyword and regex document filtering with concise snippets plus runnable Python/TypeScript/Go/Rust examples - [Memory Management](https://cookbook.chromadb.dev/strategies/memory-management/index.md) - Managing memory in ChromaDB - [Metadata Schema Validation](https://cookbook.chromadb.dev/strategies/metadata-schema-validation/index.md) - Enforcing metadata contracts in your app layer (Pydantic/Zod/Go validator/Rust validator) - [Time-based Queries](https://cookbook.chromadb.dev/strategies/time-based-queries/index.md) - Querying data based on timestamps - ✨ `Coming Soon` Testing with Chroma - learn how to test your GenAI apps that include Chroma. - ✨ `Coming Soon` Monitoring Chroma - learn how to monitor your Chroma instance. - ✨ `Coming Soon` Building Chroma clients - learn how to build clients for Chroma. - ✨ `Coming Soon` Creating the perfect Embedding Function (wrapper) - learn the best practices for creating your own embedding function. - ✨ [Multi-User Basic Auth Plugin](https://cookbook.chromadb.dev/strategies/multi-tenancy/multi-user-basic-auth/index.md) - learn how to build a multi-user basic authentication plugin for Chroma. - ✨ [CORS Configuration For JS Browser apps](https://cookbook.chromadb.dev/strategies/cors/index.md) - learn how to configure CORS for Chroma. - ✨ [Running Chroma with SystemD](https://cookbook.chromadb.dev/running/systemd-service/index.md) - learn how to start Chroma upon system boot. ## Get Help Missing something? Let us know by [opening an issue](https://github.com/amikos-tech/chroma-cookbook/issues/new), reach out on [Discord](https://discord.gg/MMeYNTmh3x) (look for `@taz`), or message us on [Twitter](https://twitter.com/AmikosTech). # Getting Started with Contributing to Chroma ## Overview Here are some steps to follow: - Fork the repository (if you are part of an organization to which you cannot grant permissions it might be advisable to fork under your own user account to allow other community members to contribute by granting them permissions, something that is a bit more difficult at organizational level) - Clone your forked repo locally (git clone ...) under a dir with an apt name for the change you want to make e.g. `my_awesome_feature` - Create a branch for your change (git checkout -b my_awesome_feature) - Make your changes - Test (see [Testing](#testing)) - Lint (see [Linting](#linting)) - Commit your changes (git commit -am 'Added some feature') - Push to the branch (git push origin my_awesome_feature) - Create a new Pull Request (PR) from your forked repository to the main Chroma repository ## Testing It is generally good to test your changes before submitting a PR. To run the full test suite: ```bash pip install -r requirements_dev.txt pytest ``` To run a specific test: ```bash pytest chromadb/tests/test_api.py::test_get_collection ``` If you want to see the output of print statements in the tests, you can run: ```bash pytest -s ``` If you want your pytest to stop on first failure, you can run: ```bash pytest -x ``` ### Integration Tests You can only run the integration tests by running: ```bash sh bin/bin/integration-test ``` The above will create a docker container and will run the integration tests against it. This will also include JS client. ## Linting # Useful Shortcuts for Contributors ## Git ## Aliases ### Create venv and install dependencies Add the following to your `.bashrc`, `.zshrc` or `.profile`: ```bash alias chroma-init='python -m virtualenv venv && source venv/bin/activate && pip install -r requirements.txt && pip install -r requirements_dev.txt' ``` # Core Concepts # Chroma Core: Concepts and APIs This section is the fastest way to understand Chroma's core data model, client choices, and day-to-day APIs. If you are new to Chroma, use the path below in order. If you are already building, jump to the API map. ## Recommended Learning Path 1. [Concepts](https://cookbook.chromadb.dev/core/concepts/index.md): Understand tenants, databases, collections, documents, metadata, embeddings, and query flow. 1. [Installation](https://cookbook.chromadb.dev/core/install/index.md): Install Chroma for local development, server usage, or cloud-connected workflows. 1. [Clients](https://cookbook.chromadb.dev/core/clients/index.md): Pick the right client (`PersistentClient`, `HttpClient`, `AsyncHttpClient`, `CloudClient`) for your architecture. 1. [Collections](https://cookbook.chromadb.dev/core/collections/index.md): Learn collection lifecycle and core CRUD/query operations. 1. [Filters](https://cookbook.chromadb.dev/core/filters/index.md): Add precise metadata and document filtering to retrieval. 1. [Configuration](https://cookbook.chromadb.dev/core/configuration/index.md): Tune index, runtime, and environment options. 1. [Resources](https://cookbook.chromadb.dev/core/resources/index.md): Estimate memory/CPU/disk needs before scaling. 1. [Advanced Search](https://cookbook.chromadb.dev/core/advanced/queries/index.md): Learn query-stage semantics, ranking, and execution tradeoffs. ## API Map ### Client-Level APIs - Connect to Chroma: `PersistentClient`, `HttpClient`, `AsyncHttpClient`, `CloudClient` - Scope data isolation: tenant + database selection - Perform admin/list operations (for example collection listing and lifecycle actions) Start in [Clients](https://cookbook.chromadb.dev/core/clients/index.md), then use [Tenants and Databases](https://cookbook.chromadb.dev/core/tenants-and-databases/index.md) for multi-tenant setups. ### Collection-Level APIs - Create/get collections: `create_collection`, `get_or_create_collection`, `get_collection` - Write data: `add`, `upsert`, `update`, `delete` - Read data: `get`, `query`, `count` - Manage collection settings: metadata, index configuration, and cloning/forking patterns Start in [Collections](https://cookbook.chromadb.dev/core/collections/index.md), then pair with [Filters](https://cookbook.chromadb.dev/core/filters/index.md) and [Document IDs](https://cookbook.chromadb.dev/core/document-ids/index.md). ### HTTP/OpenAPI Surface - Explore server endpoints and OpenAPI: [API](https://cookbook.chromadb.dev/core/api/index.md) - Generate custom clients when needed ## Minimal End-to-End Flow (Python) ```python import chromadb client = chromadb.PersistentClient(path="./chroma") collection = client.get_or_create_collection("quickstart") collection.upsert( ids=["doc-1", "doc-2"], documents=["Chroma stores vectors and metadata.", "Filters narrow candidate sets before ranking."], metadatas=[{"topic": "basics"}, {"topic": "search"}], ) result = collection.query( query_texts=["How do filters affect retrieval?"], where={"topic": "search"}, n_results=2, ) print(result["ids"]) ``` ## Where to Go Next - Operating local persistent deployments: [Storage Layout](https://cookbook.chromadb.dev/core/storage-layout/index.md), [WAL](https://cookbook.chromadb.dev/core/advanced/wal/index.md), [WAL Pruning](https://cookbook.chromadb.dev/core/advanced/wal-pruning/index.md) - Production guardrails and limits: [System Constraints](https://cookbook.chromadb.dev/core/system_constraints/index.md), [Resources](https://cookbook.chromadb.dev/core/resources/index.md) # Chroma API In this article we will cover the Chroma API in an indepth details. ## Accessing the API If you are running a Chroma server you can access its API at - `http://:/docs` ( e.g. `http://localhost:8000/docs`). Alternatively you can take a peek at the latest API from Chroma Cloud - https://api.trychroma.com:8000/docs ## API Endpoints TBD ## Generating Clients While Chroma ecosystem has client implementations for many languages, it may be the case you want to roll out your own. Below we explain some of the options available to you: ### Using OpenAPI Generator The fastest way to build a client is to use the OpenAPI Generator with the API spec. Chroma provides an OpenAPI specification that can be used to generate clients in various programming languages. #### Prerequisites 1. Install the OpenAPI Generator CLI: ```bash # Using npm npm install @openapitools/openapi-generator-cli -g # Using Docker docker pull openapitools/openapi-generator-cli ``` 1. Get the OpenAPI specification: 1. From a running Chroma server: `http://:/openapi.json` 1. From Chroma Cloud: `https://api.trychroma.com:8000/openapi.json` #### Generating Clients Here are examples for generating clients in different languages: **Python Client:** ```bash # Using npm CLI openapi-generator-cli generate \ -i https://api.trychroma.com:8000/openapi.json \ -g python \ -o ./chroma-python-client \ --additional-properties=packageName=chroma_client,packageVersion=1.0.0 # Using Docker docker run --rm \ -v ${PWD}:/local openapitools/openapi-generator-cli generate \ -i https://api.trychroma.com:8000/openapi.json \ -g python \ -o /local/chroma-python-client \ --additional-properties=packageName=chroma_client,packageVersion=1.0.0 ``` **TypeScript/JavaScript Client:** ```bash # Using npm CLI openapi-generator-cli generate \ -i https://api.trychroma.com:8000/openapi.json \ -g typescript-fetch \ -o ./chroma-typescript-client \ --additional-properties=npmName=@chroma/client,npmVersion=1.0.0 # Using Docker docker run --rm \ -v ${PWD}:/local openapitools/openapi-generator-cli generate \ -i http://localhost:8000/openapi.json \ -g typescript-fetch \ -o /local/chroma-typescript-client \ --additional-properties=npmName=@chroma/client,npmVersion=1.0.0 ``` **Java Client:** ```bash # Using npm CLI openapi-generator-cli generate \ -i http://localhost:8000/openapi.json \ -g java \ -o ./chroma-java-client \ --additional-properties=groupId=com.chroma,artifactId=chroma-client,artifactVersion=1.0.0 # Using Docker docker run --rm \ -v ${PWD}:/local openapitools/openapi-generator-cli generate \ -i http://localhost:8000/openapi.json \ -g java \ -o /local/chroma-java-client \ --additional-properties=groupId=com.chroma,artifactId=chroma-client,artifactVersion=1.0.0 ``` **Go Client:** ```bash # Using npm CLI openapi-generator-cli generate \ -i http://localhost:8000/openapi.json \ -g go \ -o ./chroma-go-client \ --additional-properties=packageName=chroma,packageVersion=1.0.0 # Using Docker docker run --rm \ -v ${PWD}:/local openapitools/openapi-generator-cli generate \ -i http://localhost:8000/openapi.json \ -g go \ -o /local/chroma-go-client \ --additional-properties=packageName=chroma,packageVersion=1.0.0 ``` #### Using the Generated Client After generating the client, you can use it in your code. Here's an example with the Python client: ```python # Install the generated client cd chroma-python-client pip install -e . # Use the client from chroma_client import ApiClient, DefaultApi # Create API client client = ApiClient(host="http://localhost:8000") api = DefaultApi(client) # List collections collections = api.list_collections() print(f"Found {len(collections)} collections") ``` #### Available Generators The OpenAPI Generator supports many languages and frameworks. Some popular options include: - `python` - Python client - `typescript-fetch` - TypeScript client using fetch - `typescript-axios` - TypeScript client using axios - `java` - Java client - `go` - Go client - `csharp` - C# client - `php` - PHP client - `ruby` - Ruby client - `rust` - Rust client For a complete list of available generators, run: ```bash openapi-generator-cli list ``` ### Manually Creating a Client If you more control over things, you can create your own client by using the API spec as guideline. ##### Python ##### Typescript ##### Golang ##### Java ##### Rust ##### Elixir # Chroma Clients Chroma Settings Object The below is only a partial list of Chroma configuration options. For full list check the code [`chromadb.config.Settings`](https://github.com/chroma-core/chroma/blob/main/chromadb/config.py) or the [ChromaDB Configuration](https://cookbook.chromadb.dev/core/configuration/index.md) page. ## Client implementations and source repos | Language | Constructors covered on this page | Source repository | | ---------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Python | `PersistentClient`, `HttpClient`, `AsyncHttpClient`, `CloudClient`, `EphemeralClient`, `Client` | [`chroma-core/chroma`](https://github.com/chroma-core/chroma/tree/main/chromadb) | | TypeScript | `ChromaClient`, `CloudClient` | [`chroma-core/chroma` (JS client)](https://github.com/chroma-core/chroma/tree/main/clients/js) | | Go | `NewHTTPClient`, `NewCloudClient` | [`amikos-tech/chroma-go`](https://github.com/amikos-tech/chroma-go/tree/main/pkg/api/v2) | | Rust | `ChromaHttpClient` | [`chroma-core/chroma` (Rust crate)](https://github.com/chroma-core/chroma/tree/main/rust/chroma) | ## Persistent Client To create a local persistent client, use the `PersistentClient` class. This client stores data locally in a directory on your machine at the path you specify. Authentication For authentication details see the [Chroma-native Authentication](https://cookbook.chromadb.dev/security/auth-1.0.x/index.md) section. ```python import chromadb from chromadb.config import DEFAULT_TENANT, DEFAULT_DATABASE, Settings client = chromadb.PersistentClient( path="test", settings=Settings(), tenant=DEFAULT_TENANT, database=DEFAULT_DATABASE, ) ``` **Parameters**: | Parameter | Type | Description | Default / Allowed values | | ---------- | ------------------ | ------------------------------------------------------------------------------------------------------- | -------------------------- | | `path` | `str \| Path` | Local path on the machine where Chroma runs. Created if it does not exist. Can be relative or absolute. | `./chroma` | | `settings` | `Settings \| None` | Chroma settings object. | `None` (uses `Settings()`) | | `tenant` | `str` | Tenant to use. | `default_tenant` | | `database` | `str` | Database to use. | `default_database` | Positional Parameters Chroma `PersistentClient` parameters are positional, unless keyword arguments are used. ### Uses of Persistent Client The persistent client is useful for: - **Local development**: You can use the persistent client to develop locally and test out ChromaDB. - **Embedded applications**: You can use the persistent client to embed ChromaDB in your application. This means that you can ship Chroma bundled with your product or services, thus simplifying the deployment process. - **Simplicity**: If you do not wish to incur the complexities associated with setting up and operating a Chroma server (arguably Hosted-Chroma will resolve this). - **Data privacy**: If you are working with sensitive data and do not want to store it on a remote server. - **Optimize performance**: If you want to reduce latency. The right tool for the job When evaluating the use of local `PersistentClient` one should always factor in the scale of the application. Similar to SQLite vs Postgres/MySQL, `PersistentClient` vs `HTTPClient` with Chroma server, application architectural characteristics (such as complexity, scale, performance etc) should be considered when deciding to use one or the other. ## HTTP Client Chroma also provides HTTP Client, suitable for use in a client-server mode. This client can be used to connect to a remote ChromaDB server. The HTTP client can operate in synchronous or asynchronous mode (see examples below). Authentication For authentication details see the [Chroma-native Authentication](https://cookbook.chromadb.dev/security/auth-1.0.x/index.md) section. ```python import chromadb from chromadb.config import DEFAULT_TENANT, DEFAULT_DATABASE, Settings client = chromadb.HttpClient( host="localhost", port=8000, ssl=False, headers=None, settings=Settings(), tenant=DEFAULT_TENANT, database=DEFAULT_DATABASE, ) ``` **Parameters**: | Parameter | Type | Description | Default / Allowed values | | ---------- | ------------------------ | -------------------------------------------------------------------------------------- | -------------------------- | | `host` | `str` | Hostname of the remote server. You can also pass a full URL (including a path prefix). | `localhost` | | `port` | `int` | Port of the remote server. | `8000` | | `ssl` | `bool` | Uses HTTPS when `True`. | `False` | | `headers` | `dict[str, str] \| None` | Additional headers sent with each request (for example auth headers). | `None` | | `settings` | `Settings \| None` | Chroma settings object. | `None` (uses `Settings()`) | | `tenant` | `str` | Tenant to use. | `default_tenant` | | `database` | `str` | Database to use. | `default_database` | Positional Parameters Chroma `HttpClient` parameters are positional, unless keyword arguments are used. ```python import asyncio import chromadb from chromadb.config import DEFAULT_TENANT, DEFAULT_DATABASE, Settings # Apply nest_asyncio to allow running nested event loops in jupyter notebook # import nest_asyncio # import this if running in jupyter notebook # nest_asyncio.apply() # apply this if running in jupyter notebook async def list_collections(): client = await chromadb.AsyncHttpClient( host="localhost", port=8000, ssl=False, headers=None, settings=Settings(), tenant=DEFAULT_TENANT, database=DEFAULT_DATABASE, ) return await client.list_collections() result = asyncio.run(list_collections()) print(result) ``` **Parameters**: | Parameter | Type | Description | Default / Allowed values | | ---------- | ------------------------ | -------------------------------------------------------------------------------------- | -------------------------- | | `host` | `str` | Hostname of the remote server. You can also pass a full URL (including a path prefix). | `localhost` | | `port` | `int` | Port of the remote server. | `8000` | | `ssl` | `bool` | Uses HTTPS when `True`. | `False` | | `headers` | `dict[str, str] \| None` | Additional headers sent with each request (for example auth headers). | `None` | | `settings` | `Settings \| None` | Chroma settings object. | `None` (uses `Settings()`) | | `tenant` | `str` | Tenant to use. | `default_tenant` | | `database` | `str` | Database to use. | `default_database` | Positional Parameters Chroma `AsyncHttpClient` parameters are positional, unless keyword arguments are used. ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient({ path: "http://localhost:8000", auth: { provider: "token", credentials: "your_token_here", tokenHeaderType: "X_CHROMA_TOKEN", }, tenant: "default_tenant", database: "default_database", }); ``` **Parameters**: | Parameter | Type | Description | Default / Allowed values | | -------------- | ------------- | ---------------------------------------------------------------- | --------------------------------------------------- | | `path` | `string` | Base URL for the Chroma API. | `http://localhost:8000` | | `auth` | `AuthOptions` | Authentication config. | Optional. `provider` values: `"basic"` or `"token"` | | `fetchOptions` | `RequestInit` | Fetch options passed to HTTP calls (for example custom headers). | Optional | | `tenant` | `string` | Tenant to use. | `default_tenant` | | `database` | `string` | Database to use. | `default_database` | ```bash go get github.com/amikos-tech/chroma-go@latest ``` ```go package main import ( "context" "log" chroma "github.com/amikos-tech/chroma-go/pkg/api/v2" ) func main() { client, err := chroma.NewHTTPClient( chroma.WithBaseURL("http://localhost:8000"), chroma.WithDefaultDatabaseAndTenant(), ) if err != nil { log.Fatalf("Error creating client: %s \n", err) } if err := client.Heartbeat(context.TODO()); err != nil { log.Fatalf("Error connecting: %s \n", err) } } ``` **Parameters**: | Option | Type | Description | Default / Allowed values | | -------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------- | | `WithBaseURL()` | `func(string) ClientOption` | Sets Chroma endpoint URL. `/api/v2` is appended if missing. | Default base URL is `http://localhost:8000/api/v2` | | `WithAuth()` | `func(CredentialsProvider) ClientOption` | Sets auth provider (see [Go auth docs](https://go-client.chromadb.dev/auth/)). | Optional | | `WithDatabaseAndTenant()` | `func(database string, tenant string) ClientOption` | Sets database and tenant explicitly. | Optional | | `WithDatabaseAndTenantFromEnv()` | `func() ClientOption` | Reads `CHROMA_DATABASE` and `CHROMA_TENANT` when present. | Applied by default in `NewHTTPClient` | | `WithDefaultDatabaseAndTenant()` | `func() ClientOption` | Fills missing values with defaults. | `default_database` and `default_tenant` | | `WithTimeout()` | `func(time.Duration) ClientOption` | Sets request timeout. | Optional | ```bash cargo add chroma ``` ```rust use chroma::{ChromaHttpClient, ChromaHttpClientOptions}; #[tokio::main] async fn main() -> anyhow::Result<()> { let client = ChromaHttpClient::new(ChromaHttpClientOptions::default()); let heartbeat = client.heartbeat().await?; println!("Server timestamp: {}", heartbeat); Ok(()) } ``` **Parameters** (`ChromaHttpClientOptions`): | Parameter | Type | Description | Default / Allowed values | | --------------- | -------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------ | | `endpoint` | `reqwest::Url` | Server base URL. | `http://localhost:8000` | | `auth_method` | `ChromaAuthMethod` | Authentication strategy. | `ChromaAuthMethod::None` | | `retry_options` | `ChromaRetryOptions` | Retry/backoff behavior for failed requests. | `max_retries=3`, `min_delay=200ms`, `max_delay=5s`, `jitter=true` | | `tenant_id` | `Option` | Tenant identifier override. | `None` (resolved from identity when possible) | | `database_name` | `Option` | Database name override. | `None` (resolved when possible; explicit value recommended if multiple DBs are accessible) | You can also construct a client from environment variables (`CHROMA_ENDPOINT`, `CHROMA_TENANT`, `CHROMA_DATABASE`): ```rust let client = ChromaHttpClient::from_env()?; ``` ### Uses of HTTP Client The HTTP client is ideal for when you want to scale your application or move off of local machine storage. It is important to note that there are trade-offs associated with using HTTP client: - Network latency - The time it takes to send a request to the server and receive a response. - Serialization and deserialization overhead - The time it takes to convert data to a format that can be sent over the network and then convert it back to its original format. - Security - The data is sent over the network, so it is important to ensure that the connection is secure (we recommend using both HTTPS and authentication). - Availability - The server must be available for the client to connect to it. - Bandwidth usage - The amount of data sent over the network. - Data privacy and compliance - Storing data on a remote server may require compliance with data protection laws and regulations. - Difficulty in debugging - Debugging network issues can be more difficult than debugging local issues. The same applies to server-side issues. ### Host parameter special cases (Python-only) The `host` parameter supports a more advanced syntax than just the hostname. You can specify the whole endpoint URL ( without the API paths), e.g. `https://chromadb.example.com:8000/my_server/path/`. This is useful when you want to use a reverse proxy or load balancer in front of your ChromaDB server. ## Cloud Client The `CloudClient` connects to [Chroma Cloud](https://trychroma.com). It handles authentication and endpoint configuration automatically. Environment Variables Cloud environment variable handling differs by language: | Language | API key | Tenant / database | | ---------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Python | `api_key` arg or `CHROMA_API_KEY` | `tenant`/`database` args, or `CHROMA_TENANT`/`CHROMA_DATABASE`, or auto-resolved from scoped credentials | | TypeScript | `apiKey` arg or `CHROMA_API_KEY` | Constructor args (`tenant`, `database`) | | Go | `WithCloudAPIKey()` or `CHROMA_API_KEY` | `WithDatabaseAndTenant()` or `CHROMA_TENANT` + `CHROMA_DATABASE` | | Rust | `ChromaHttpClientOptions::cloud(...)` or `CHROMA_API_KEY` | Explicit options or `CHROMA_TENANT`/`CHROMA_DATABASE`; otherwise resolved from identity when possible | ```python import chromadb # Minimal β€” auto-resolves tenant/database from API key client = chromadb.CloudClient(api_key="ck-your-api-key") # Explicit tenant and database client = chromadb.CloudClient( tenant="your-tenant-id", database="your-database-name", api_key="ck-your-api-key", ) ``` **Parameters**: | Parameter | Type | Description | Default / Allowed values | | ------------ | ------------------ | --------------------------------------------------------- | ----------------------------------------------------------- | | `api_key` | `str \| None` | Chroma Cloud API key. | Required. Falls back to `CHROMA_API_KEY` | | `tenant` | `str \| None` | Tenant identifier. | Falls back to `CHROMA_TENANT`, then auth-based resolution | | `database` | `str \| None` | Database name. | Falls back to `CHROMA_DATABASE`, then auth-based resolution | | `settings` | `Settings \| None` | Settings override. | `None` (uses `Settings()`) | | `cloud_host` | `str` | Cloud API hostname (keyword-only; primarily for testing). | `api.trychroma.com` | | `cloud_port` | `int` | Cloud API port (keyword-only; primarily for testing). | `443` | | `enable_ssl` | `bool` | Enables TLS (keyword-only; primarily for testing). | `True` | ```typescript import { CloudClient } from "chromadb"; const client = new CloudClient({ apiKey: "ck-your-api-key", tenant: "your-tenant-id", database: "your-database-name", }); ``` **Parameters**: | Parameter | Type | Description | Default / Allowed values | | ----------- | --------------------- | --------------------- | --------------------------------------------------------------------- | | `apiKey` | `string \| undefined` | Chroma Cloud API key. | Required. Falls back to `CHROMA_API_KEY` | | `tenant` | `string \| undefined` | Tenant identifier. | Optional. Defaults to `default_tenant` in underlying `ChromaClient` | | `database` | `string \| undefined` | Database name. | Optional. Defaults to `default_database` in underlying `ChromaClient` | | `cloudHost` | `string \| undefined` | Cloud host prefix. | `https://api.trychroma.com` | | `cloudPort` | `string \| undefined` | Cloud port suffix. | `8000` | ```go package main import ( "context" "log" chroma "github.com/amikos-tech/chroma-go/pkg/api/v2" ) func main() { client, err := chroma.NewCloudClient( chroma.WithCloudAPIKey("ck-your-api-key"), chroma.WithDatabaseAndTenant("your-database", "your-tenant-id"), ) if err != nil { log.Fatalf("Error creating cloud client: %s \n", err) } if err := client.Heartbeat(context.TODO()); err != nil { log.Fatalf("Error connecting: %s \n", err) } } ``` **Parameters**: | Option | Type | Description | Default / Allowed values | | -------------------------------- | --------------------------------------------------- | -------------------------------------------- | -------------------------------------- | | `WithCloudAPIKey()` | `func(string) ClientOption` | Sets Chroma Cloud API key. | Falls back to `CHROMA_API_KEY` | | `WithDatabaseAndTenant()` | `func(database string, tenant string) ClientOption` | Sets database and tenant explicitly. | Required unless both env vars are set | | `WithDatabaseAndTenantFromEnv()` | `func() ClientOption` | Reads `CHROMA_DATABASE` and `CHROMA_TENANT`. | Applied by default in `NewCloudClient` | | `WithTimeout()` | `func(time.Duration) ClientOption` | Sets request timeout. | Optional | `NewCloudClient` requires non-default tenant and database values and also requires an API key. Go Client Package The Go client is maintained at [`amikos-tech/chroma-go`](https://github.com/amikos-tech/chroma-go) and has not yet been moved to `chroma-core`. Use the `github.com/amikos-tech/chroma-go/pkg/api/v2` import path. ```rust use chroma::{ChromaHttpClient, ChromaHttpClientOptions}; #[tokio::main] async fn main() -> anyhow::Result<()> { // Explicit API key and database let options = ChromaHttpClientOptions::cloud( "ck-your-api-key", "your-database-name", )?; let client = ChromaHttpClient::new(options); // Or from environment variables (CHROMA_API_KEY, CHROMA_DATABASE, etc.) let client = ChromaHttpClient::cloud()?; let heartbeat = client.heartbeat().await?; println!("Server timestamp: {}", heartbeat); Ok(()) } ``` **Parameters** (`ChromaHttpClientOptions::cloud()`): | Parameter | Type | Description | Default / Allowed values | | --------------- | ------------------- | --------------------------------------------- | ------------------------ | | `api_key` | `impl Into` | Chroma Cloud API key. | Required | | `database_name` | `impl Into` | Database name used for collection operations. | Required | **Environment-based** (`ChromaHttpClient::cloud()`): | Variable | Required | Description | Default / Allowed values | | ----------------- | -------- | ------------------------ | ------------------------------------------------ | | `CHROMA_API_KEY` | Yes | Cloud API key. | None | | `CHROMA_ENDPOINT` | No | Cloud endpoint override. | `https://api.trychroma.com` | | `CHROMA_TENANT` | No | Tenant override. | If omitted, resolved from identity when possible | | `CHROMA_DATABASE` | No | Database override. | If omitted, resolved from identity when possible | ## Ephemeral Client Ephemeral client is a client that does not store any data on disk. It is useful for fast prototyping and testing. To get started with an ephemeral client, use the `EphemeralClient` class. ```python import chromadb from chromadb.config import DEFAULT_TENANT, DEFAULT_DATABASE, Settings client = chromadb.EphemeralClient( settings=Settings(), tenant=DEFAULT_TENANT, database=DEFAULT_DATABASE, ) ``` **Parameters**: | Parameter | Type | Description | Default / Allowed values | | ---------- | ------------------ | ----------------------- | -------------------------- | | `settings` | `Settings \| None` | Chroma settings object. | `None` (uses `Settings()`) | | `tenant` | `str` | Tenant to use. | `default_tenant` | | `database` | `str` | Database to use. | `default_database` | Positional Parameters Chroma `EphemeralClient` parameters are positional, unless keyword arguments are used. ## Environment Variable Configured Client You can also configure the client using environment variables. This is useful when you want to configure any of the client options listed above via environment variables. ```python import chromadb # Uses configured defaults from environment/.env/settings. client = chromadb.Client() ``` **Parameters**: | Parameter | Type | Description | Default / Allowed values | | ---------- | ---------- | ---------------------------------------------------------- | --------------------------------------------------- | | `settings` | `Settings` | Settings object for environment and runtime configuration. | Current global settings (`chromadb.get_settings()`) | | `tenant` | `str` | Tenant to use. | `default_tenant` | | `database` | `str` | Database to use. | `default_database` | Positional Parameters Chroma `Client` parameters are positional, unless keyword arguments are used. # Collections Collections are the grouping mechanism for embeddings, documents, and metadata. Runnable Examples Complete, runnable collection examples for each language are available in the [examples/collections](https://github.com/amikos-tech/chroma-cookbook/tree/main/examples/collections) directory: - [Python](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/collections/python/collection_examples.py) - [TypeScript](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/collections/typescript/collection_examples.ts) - [Go](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/collections/go/main.go) - [Rust](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/collections/rust/src/main.rs) All examples require a running Chroma server: `docker run -p 8000:8000 chromadb/chroma` ## Collection Basics ### Collection Properties Each collection is characterized by the following properties: - `name`: The name of the collection. The name can be changed as long as it is unique within the database ( use `collection.modify(name="new_name")` to change the name of the collection - `metadata`: A dictionary of metadata associated with the collection. The metadata is a dictionary of key-value pairs. Keys can be strings, values can be strings, integers, floats, or booleans. Metadata can be changed using `collection.modify(metadata={"key": "value"})` (Note: Metadata is always overwritten when modified) - `configuration`: A dictionary of HNSW index configuration options. Configuration is set at collection creation time via the `configuration` parameter. See the example below. - `embedding_function`: The embedding function used to embed documents in the collection. Defaults: - Embedding Function - by default if `embedding_function` parameter is not provided at `create_collection()` or `get_or_create_collection()` time, Chroma uses `chromadb.utils.embedding_functions.DefaultEmbeddingFunction` to embed documents. The default embedding function uses [Onnx Runtime](https://onnxruntime.ai/) with [`all-MiniLM-L6-v2`](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) model. - Distance metric - by default Chroma uses L2 (Euclidean Distance Squared) distance metric for newly created collections. You can change it at creation time using the `configuration` parameter: `configuration={"hnsw": {"space": "cosine"}}`. Possible values are `l2`, `cosine`, and `ip` (inner product). (Note: `cosine` value returns `cosine distance` rather than `cosine similarity`. I.e. values close to 0 means the embeddings are more similar.) - Batch size, defined by `configuration={"hnsw": {"batch_size": 100}}`. Default is 100. The batch size defines the size of the in-memory bruteforce index. Once the threshold is reached, vectors are added to the HNSW index and the bruteforce index is cleared. Greater values may improve ingest performance. When updating also consider changing sync threshold. - Sync threshold, defined by `configuration={"hnsw": {"sync_threshold": 1000}}`. Default is 1000. The sync threshold defines the limit at which the HNSW index is synced to disk. This limit only applies to newly added vectors. Keep in Mind Collection distance metric cannot be changed after the collection is created. To change the distance metric see [Cloning a Collection](#cloning-a-collection). Embedding Function Persistence Since Chroma v1.1.13, the embedding function configuration (EF) is persisted server-side. You no longer need to pass `embedding_function` when calling `get_collection` β€” Chroma will use the EF that was set at collection creation time. Name Restrictions Collection names in Chroma must adhere to the following restrictions: (1) contains 3-512 characters (2) starts and ends with a lowercase letter or a digit (3) can contain dots, dashes, and underscores in between (4) cannot contain two consecutive periods (`..`) (5) is not a valid IPv4 address ### Creating a collection Official Docs For more information on the `create_collection` or `get_or_create_collection` methods, see the [official ChromaDB documentation](https://docs.trychroma.com/reference/python/client#getorcreatecollection). Parameters: | Name | Description | Default Value | Type | | -------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------- | ----------------- | | `name` | Name of the collection to create. Parameter is required | N/A | String | | `metadata` | Metadata associated with the collection. This is an optional parameter | `None` | Dictionary | | `configuration` | HNSW index configuration for the collection. This is an optional parameter | `None` | Dictionary | | `embedding_function` | Embedding function to use for the collection. This is an optional parameter | `chromadb.utils.embedding_functions.DefaultEmbeddingFunction` | EmbeddingFunction | ```python import chromadb client = chromadb.PersistentClient(path="test") # or HttpClient() col = client.create_collection("test") ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient(); const collection = await client.createCollection({ name: "test" }); ``` ```go package main import ( "context" chroma "github.com/amikos-tech/chroma-go" ) func main() { ctx := context.Background() client, _ := chroma.NewHTTPClient(ctx, chroma.WithDefaultDatabase("default_database"), chroma.WithDefaultTenant("default_tenant")) col, _ := client.CreateCollection(ctx, "test", false) } ``` ```rust use chroma::{ChromaHttpClient, ChromaHttpClientOptions}; #[tokio::main] async fn main() { let client = ChromaHttpClient::new(ChromaHttpClientOptions::default()); let collection = client.create_collection("test", None, None).await.unwrap(); } ``` Alternatively you can use the `get_or_create_collection` method to create a collection if it doesn't exist already. ```python import chromadb client = chromadb.PersistentClient(path="test") # or HttpClient() col = client.get_or_create_collection("test", metadata={"key": "value"}) ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient(); const collection = await client.getOrCreateCollection({ name: "test", metadata: { key: "value" }, }); ``` ```go col, _ := client.GetOrCreateCollection(ctx, "test") ``` ```rust let collection = client.get_or_create_collection("test", None, None).await.unwrap(); ``` Creating a collection with custom HNSW configuration: ```python import chromadb client = chromadb.PersistentClient(path="test") # or HttpClient() col = client.create_collection( "test", configuration={ "hnsw": { "space": "cosine", "ef_construction": 200, "max_neighbors": 32, } }, ) ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient(); const collection = await client.createCollection({ name: "test", configuration: { hnsw: { space: "cosine", ef_construction: 200, max_neighbors: 32, }, }, }); ``` ### Embedding Function Configuration and Persistence Starting with Chroma v1.1.13, embedding functions are persisted server-side in the collection configuration. After you create a collection, later `get_collection` / `getCollection` calls will auto-resolve the persisted embedding function. You can configure embedding functions in two ways: 1. Pass `embedding_function` when creating a collection 1. Set `configuration.embedding_function` with `name` and `config` API keys are auto-discovered from provider standard environment variables (for example `OPENAI_API_KEY`). If you use a non-standard variable, set `api_key_env_var` (Python) or `apiKeyEnvVar` (TypeScript). The persisted `embedding_function` payload follows provider schemas in the upstream Chroma registry: - [Embedding Function Schemas](https://github.com/chroma-core/chroma/tree/main/schemas/embedding_functions) - [OpenAI Schema Example](https://github.com/chroma-core/chroma/blob/main/schemas/embedding_functions/openai.json) - [Schema README](https://github.com/chroma-core/chroma/blob/main/schemas/embedding_functions/README.md) Cross-checked dense provider/package mapping: | Provider | Python | TypeScript (NPM Β· GitHub) | Go (pkg.go.dev Β· GitHub) | | ----------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | OpenAI | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/openai) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/openai) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/openai) | | Google Gemini | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/google-gemini) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/gemini) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/gemini) | | Cohere | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/cohere) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/cohere) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/cohere) | | Cloudflare Workers AI | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/cloudflare-worker-ai) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/cloudflare) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/cloudflare) | | Hugging Face | βœ… | - | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/hf) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/hf) | | Hugging Face Embedding Server | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/huggingface-server) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/hf) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/hf) | | Instructor | βœ… | - | - | | Jina AI | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/jina) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/jina) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/jina) | | Mistral | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/mistral) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/mistral) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/mistral) | | Morph | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/morph) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/morph) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/morph) | | Ollama | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/ollama) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/ollama) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/ollama) | | Nomic | βœ… | - | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/nomic) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/nomic) | | OpenCLIP (Multimodal) | βœ… | - | - | | Roboflow (Multimodal) | βœ… | - | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/roboflow) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/roboflow) | | Sentence Transformers | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/sentence-transformer) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | - | | Text2Vec | βœ… | - | - | | Together AI | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/together-ai) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/together) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/together) | | VoyageAI | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/voyageai) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/voyage) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/voyage) | | Amazon Bedrock | βœ… | - | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/bedrock) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/bedrock) | | Baseten | βœ… | - | βœ… [pkg](https://pkg.go.dev/github.com/amikos-tech/chroma-go/v2/pkg/embeddings/baseten) Β· [src](https://github.com/amikos-tech/chroma-go/tree/main/pkg/embeddings/baseten) | | Chroma Cloud Qwen | βœ… | βœ… [npm](https://www.npmjs.com/package/@chroma-core/chroma-cloud-qwen) Β· [src](https://github.com/chroma-core/chroma/tree/main/clients/js/packages/chromadb-core/src/embeddings) | - | Sparse embedding function integrations include: - Chroma BM25 - Chroma Cloud Splade - Hugging Face sparse For broader language/provider support, see: - [Chroma Ecosystem Clients](https://cookbook.chromadb.dev/ecosystem/clients/index.md) - [Chroma Integrations](https://docs.trychroma.com/integrations/chroma-integrations) - [Upstream embedding functions reference](https://github.com/chroma-core/chroma/blob/main/docs/mintlify/docs/embeddings/embedding-functions.mdx) - [Upstream collection configuration reference](https://github.com/chroma-core/chroma/blob/main/docs/mintlify/docs/collections/configure.mdx) Cross-check Scope Python/TypeScript support was cross-checked against Chroma Docs integrations and embedding functions pages. Go package mappings were cross-checked against `github.com/amikos-tech/chroma-go/v2/pkg/embeddings/*`. Configure a persisted EF at collection creation: ```python import chromadb from chromadb.utils.embedding_functions import OpenAIEmbeddingFunction client = chromadb.HttpClient() # 1) Set via embedding_function argument ef = OpenAIEmbeddingFunction(model_name="text-embedding-3-small") col = client.create_collection("with_openai_ef", embedding_function=ef) # 2) Later calls auto-resolve persisted EF (no ef needed here) same_col = client.get_collection("with_openai_ef") ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient(); const collection = await client.createCollection({ name: "with_openai_ef", configuration: { embedding_function: { name: "openai", config: { model_name: "text-embedding-3-small", apiKeyEnvVar: "OPENAI_API_KEY", }, }, }, }); const sameCollection = await client.getCollection({ name: "with_openai_ef" }); ``` ```go package main import ( "context" "os" chroma "github.com/amikos-tech/chroma-go/v2" v2 "github.com/amikos-tech/chroma-go/v2/pkg/api/v2" openai "github.com/amikos-tech/chroma-go/v2/pkg/embeddings/openai" ) func main() { ctx := context.Background() client, _ := chroma.NewHTTPClient(ctx, chroma.WithDefaultDatabase("default_database"), chroma.WithDefaultTenant("default_tenant"), ) ef, _ := openai.NewOpenAIEmbeddingFunction(os.Getenv("OPENAI_API_KEY")) _, _ = client.CreateCollection(ctx, "with_openai_ef", v2.WithEmbeddingFunctionCreate(ef)) // Persisted EF is auto-resolved server-side _, _ = client.GetCollection(ctx, "with_openai_ef") } ``` Custom API key environment variable names: ```python from chromadb.utils.embedding_functions import OpenAIEmbeddingFunction ef = OpenAIEmbeddingFunction( model_name="text-embedding-3-small", api_key_env_var="MY_CUSTOM_OPENAI_KEY", ) ``` ```typescript import { OpenAIEmbeddingFunction } from "@chroma-core/openai"; const ef = new OpenAIEmbeddingFunction({ modelName: "text-embedding-3-small", apiKeyEnvVar: "MY_CUSTOM_OPENAI_KEY", }); ``` Custom embedding function patterns: ```python from typing import Any, Dict from chromadb import Documents, EmbeddingFunction, Embeddings from chromadb.utils.embedding_functions import register_embedding_function @register_embedding_function class MyEmbeddingFunction(EmbeddingFunction): def __call__(self, input: Documents) -> Embeddings: # Produce embeddings for input documents return [[0.0] * 3 for _ in input] @staticmethod def name() -> str: return "my-embedding-function" def get_config(self) -> Dict[str, Any]: return {"model": "my-model-v1"} @staticmethod def build_from_config(config: Dict[str, Any]) -> "MyEmbeddingFunction": return MyEmbeddingFunction() ``` ```typescript import type { ChromaClient, EmbeddingFunction } from "chromadb"; type MyConfig = { model: string }; class MyEmbeddingFunction implements EmbeddingFunction { public readonly name = "my-embedding-function"; constructor(private readonly config: MyConfig) {} async generate(texts: string[]): Promise { return texts.map(() => [0, 0, 0]); } getConfig(): MyConfig { return this.config; } validateConfigUpdate(next: Record) { if ("model" in next) { throw new Error("Model cannot be updated"); } } static buildFromConfig(config: MyConfig, _client?: ChromaClient): MyEmbeddingFunction { return new MyEmbeddingFunction(config); } } ``` ```go package myef import ( "context" "github.com/amikos-tech/chroma-go/v2/pkg/embeddings" ) type MyEmbeddingFunction struct{} func (m *MyEmbeddingFunction) EmbedDocuments(_ context.Context, texts []string) ([]embeddings.Embedding, error) { out := make([]embeddings.Embedding, len(texts)) for i := range texts { out[i] = embeddings.NewEmbeddingFromFloat32([]float32{0, 0, 0}) } return out, nil } func (m *MyEmbeddingFunction) EmbedQuery(_ context.Context, _ string) (embeddings.Embedding, error) { return embeddings.NewEmbeddingFromFloat32([]float32{0, 0, 0}), nil } func (m *MyEmbeddingFunction) Name() string { return "my-embedding-function" } func (m *MyEmbeddingFunction) GetConfig() embeddings.EmbeddingFunctionConfig { return embeddings.EmbeddingFunctionConfig{"model": "my-model-v1"} } func (m *MyEmbeddingFunction) DefaultSpace() embeddings.DistanceMetric { return embeddings.COSINE } func (m *MyEmbeddingFunction) SupportedSpaces() []embeddings.DistanceMetric { return []embeddings.DistanceMetric{embeddings.COSINE} } func newMyEmbeddingFunctionFromConfig(_ embeddings.EmbeddingFunctionConfig) (embeddings.EmbeddingFunction, error) { return &MyEmbeddingFunction{}, nil } func init() { _ = embeddings.RegisterDense("my-embedding-function", newMyEmbeddingFunctionFromConfig) } ``` Metadata with `get_or_create_collection()` If the collection exists and metadata is provided in the method it will attempt to overwrite the existing metadata. ### Deleting a collection Official Docs For more information on the `delete_collection` method, see the [official ChromaDB documentation](https://docs.trychroma.com/reference/python/client#deletecollection). Destructive Operation Deleting a collection permanently removes all its data (embeddings, documents, and metadata). This action cannot be undone. Parameters: | Name | Description | Default Value | Type | | ------ | ------------------------------------------------------- | ------------- | ------ | | `name` | Name of the collection to delete. Parameter is required | N/A | String | ```python import chromadb client = chromadb.PersistentClient(path="test") # or HttpClient() client.delete_collection("test") ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient(); await client.deleteCollection({ name: "test" }); ``` ```go _, err := client.DeleteCollection(ctx, "test") ``` ```rust client.delete_collection("test").await.unwrap(); ``` ### Listing all collections Official Docs For more information on the `list_collections` method, see the [official ChromaDB documentation](https://docs.trychroma.com/reference/python/client#listcollections). The `list_collections` method returns `Collection` objects (name, metadata, configuration, and counts). Use `offset` and `limit` to paginate through large tenants or databases. Parameters: | Name | Description | Default Value | Type | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------- | | `offset` | The starting offset for listing collections. This is an optional parameter | `None` | Positive Integer | | `limit` | The number of collections to return. If the remaining collections from `offset` are fewer than this number then returned collection will also be fewer. This is an optional parameter | `None` | Positive Integer | ```python import chromadb client = chromadb.PersistentClient(path="test") # or HttpClient() collections = client.list_collections() # returns list of collection names # with pagination collections = client.list_collections(limit=10, offset=0) ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient(); const collections = await client.listCollections({ limit: 10, offset: 0 }); // fetch the next page by advancing the offset const nextPage = await client.listCollections({ limit: 10, offset: 10 }); ``` ```go collections, _ := client.ListCollections(ctx) // with pagination collections, _ = client.ListCollections(ctx, chroma.ListWithLimit(10), chroma.ListWithOffset(0)) ``` ```rust let collections = client.list_collections(100, None).await.unwrap(); // with pagination let collections = client.list_collections(10, Some(0)).await.unwrap(); ``` ### Getting a collection Official Docs For more information on the `get_collection` method, see the [official ChromaDB documentation](https://docs.trychroma.com/reference/python/client#getcollection). Embedding Function Persistence Since Chroma v1.1.13, the embedding function is persisted server-side. You no longer need to pass `embedding_function` when calling `get_collection`. If you do pass one, it will override the persisted configuration for that client session. Parameters: | Name | Description | Default Value | Type | | -------------------- | ------------------------------------------------------------------------------------------------ | ------------- | ----------------- | | `name` | Name of the collection to get. Parameter is required | N/A | String | | `embedding_function` | Embedding function override for the collection. Optional β€” uses the persisted EF if not provided | `None` | EmbeddingFunction | ```python import chromadb client = chromadb.PersistentClient(path="test") # or HttpClient() col = client.get_collection("test") ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient(); const collection = await client.getCollection({ name: "test" }); ``` ```go col, _ := client.GetCollection(ctx, "test") ``` ```rust let collection = client.get_collection("test").await.unwrap(); ``` ### Modifying a collection Official Docs For more information on the `modify` method, see the [official ChromaDB documentation](https://docs.trychroma.com/reference/python/collection#modify). Modify method on collection The `modify` method is called on the collection and not on the client, unlike the rest of the collection lifecycle methods. Metadata Overwrite Metadata is always overwritten when modified. If you want to add a new key-value pair to the metadata, you must first get the existing metadata and then add the new key-value pair to it. Changing HNSW parameters HNSW configuration parameters (space, M, ef_construction, etc.) cannot be changed after the collection is created. To change these parameters, clone the collection β€” see [Cloning a Collection](#cloning-a-collection). Parameters: | Name | Description | Default Value | Type | | ---------- | ---------------------------------------------------------------------- | ------------- | ---------- | | `name` | The new name of the collection. Parameter is required | N/A | String | | `metadata` | Metadata associated with the collection. This is an optional parameter | `None` | Dictionary | Both collection properties (`name` and `metadata`) can be modified, separately or together. ```python import chromadb client = chromadb.PersistentClient(path="test") # or HttpClient() col = client.get_collection("test") col.modify(name="test2", metadata={"key": "value"}) ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient(); const collection = await client.getCollection({ name: "test" }); await collection.modify({ name: "test2", metadata: { key: "value" } }); ``` ### Counting Collections Returns the number of collections for the currently configured tenant and database. Official Docs For more information on the `count_collections` method, see the [official ChromaDB documentation](https://docs.trychroma.com/reference/python/client#countcollections). ```python import chromadb client = chromadb.PersistentClient(path="test") # or HttpClient() col = client.get_or_create_collection("test") # create a new collection collections_count = client.count_collections() # int ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient(); const count = await client.countCollections(); ``` ### Convenience Methods The following methods are available on a collection instance: ```python import chromadb client = chromadb.PersistentClient(path="test") # or HttpClient() col = client.get_or_create_collection("test") col.add(ids=["1", "2"], documents=["hello world", "hello chroma"]) # peek at the first N items in the collection (default 10) col.peek() col.peek(limit=5) # count the number of items in the collection col.count() ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient(); const collection = await client.getOrCreateCollection({ name: "test" }); await collection.add({ ids: ["1", "2"], documents: ["hello world", "hello chroma"], }); // peek at the first N items in the collection (default 10) await collection.peek(); await collection.peek({ limit: 5 }); // count the number of items in the collection await collection.count(); ``` ### Deleting records from a collection Use `collection.delete(...)` to remove records while keeping the collection itself. To remove the entire collection object, use [Deleting a collection](#deleting-a-collection). Destructive Operation Deleting records permanently removes their embeddings, documents, and metadata from the collection. Record deletion is selected by IDs and/or filters: - `ids` targets specific records - `where` targets records by metadata - `where_document` / `whereDocument` targets records by document content on clients that expose it Merged Core vs Released Clients Core changes from [#6573](https://github.com/chroma-core/chroma/pull/6573) and [#6582](https://github.com/chroma-core/chroma/pull/6582) added a delete response payload with a `deleted` count and filter-based `limit` support. Released client support is still not uniform: - Rust `chroma = "0.13.2"` already returns `response.deleted` and supports `limit` with `where` - The runnable Rust example in this repository is currently locked to `chroma 0.13.0`, so it still uses the older `delete(ids, where)` surface - Current Python releases on PyPI also expose `limit` and return `DeleteResult` with `deleted` - Current TypeScript and Go releases support selector-based deletes, but do not yet expose delete counts or `limit` ```python collection.delete(ids=["doc-1", "doc-2"]) collection.delete(where={"status": "archived"}) collection.delete(where_document={"$contains": "deprecated"}) deleted = collection.delete(where={"status": "deleted"}, limit=10)["deleted"] ``` Builds that include [#6573](https://github.com/chroma-core/chroma/pull/6573) and [#6582](https://github.com/chroma-core/chroma/pull/6582) also add `limit` for filter-based deletes and return a payload such as `{"deleted": 2}`. ```typescript await collection.delete({ ids: ["doc-1", "doc-2"] }); await collection.delete({ where: { status: "archived" } }); await collection.delete({ whereDocument: { $contains: "deprecated" } }); ``` ```go err := collection.Delete(ctx, chroma.WithIDs("doc-1", "doc-2")) if err != nil { panic(err) } err = collection.Delete(ctx, chroma.WithWhere(chroma.EqString("status", "archived"))) if err != nil { panic(err) } err = collection.Delete(ctx, chroma.WithWhereDocument(chroma.Contains("deprecated"))) if err != nil { panic(err) } ``` ```rust use chroma::types::{ MetadataComparison, MetadataExpression, MetadataValue, PrimitiveOperator, Where, }; let by_id = collection .delete( Some(vec!["doc-1".to_string(), "doc-2".to_string()]), None, None, ) .await?; println!("Deleted {} records by ID", by_id.deleted); let delete_result = collection .delete( None, Some(Where::Metadata(MetadataExpression { key: "status".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::Equal, MetadataValue::Str("deleted".to_string()), ), })), Some(10), ) .await?; println!( "Deleted {} records matching status=deleted", delete_result.deleted ); let deleted = delete_result.deleted; ``` ## Query and Get Results `collection.get()` and `collection.query()` return column-oriented results. - Column values are aligned by index. For `get()`, `ids[i]` refers to the same record as `documents[i]`, `metadatas[i]`, and `embeddings[i]` (if included). - `query()` adds one level of nesting. `ids[q][k]` is the `k`-th match for query `q`, and aligns with `documents[q][k]`, `metadatas[q][k]`, and `distances[q][k]` (if included). - Use `include` to control which optional fields are returned. - Default `include` fields for `get()`: `documents` and `metadatas` (order may vary by client). - Default `include` fields for `query()`: `documents`, `metadatas`, and `distances` (order may vary by client). - `ids` are always returned, even when `include=[]`. ### Constrain Query Candidates By ID Use the `ids` argument on `query()` to search only within a known subset of records. Provide one query input (`query_texts` or `query_embeddings`) and an `ids` list. By default, Chroma returns up to 10 results per query, capped by matching IDs. ```python collection.query( query_texts=["climate"], ids=["doc-1", "doc-2", "doc-3"], ) ``` ```typescript await collection.query({ queryTexts: ["climate"], ids: ["doc-1", "doc-2", "doc-3"], }); ``` ```go _, err := collection.Query(ctx, chroma.WithQueryTexts("climate"), chroma.WithIDs("doc-1", "doc-2", "doc-3"), ) if err != nil { panic(err) } ``` ```rust let _results = collection .query( vec![vec![0.1, 0.2, 0.3]], None, None, Some(vec![ "doc-1".to_string(), "doc-2".to_string(), "doc-3".to_string(), ]), None, ) .await?; ``` ### Result Type Shapes ```python class GetResult(TypedDict): ids: List[ID] embeddings: Optional[Union[Embeddings, PyEmbeddings, NDArray[Union[np.int32, np.float32]]]] documents: Optional[List[Document]] uris: Optional[URIs] data: Optional[Loadable] metadatas: Optional[List[Metadata]] included: Include class QueryResult(TypedDict): ids: List[IDs] embeddings: Optional[ Union[ List[Embeddings], List[PyEmbeddings], List[NDArray[Union[np.int32, np.float32]]], ] ] documents: Optional[List[List[Document]]] uris: Optional[List[List[URI]]] data: Optional[List[Loadable]] metadatas: Optional[List[List[Metadata]]] distances: Optional[List[List[float]]] included: Include ``` ```typescript class GetResult { readonly ids: string[]; readonly documents: (string | null)[]; readonly metadatas: (TMeta | null)[]; readonly embeddings: number[][]; readonly uris: (string | null)[]; readonly include: Include[]; rows(): Array<{ id: string; document?: string | null; metadata?: TMeta | null; embedding?: number[]; uri?: string | null; }>; } class QueryResult { readonly ids: string[][]; readonly documents: (string | null)[][]; readonly metadatas: (TMeta | null)[][]; readonly embeddings: (number[] | null)[][]; readonly distances: (number | null)[][]; readonly uris: (string | null)[][]; readonly include: Include[]; rows(): QueryRowResult[][]; } ``` ```go // Selected methods shown for brevity. type GetResult interface { GetIDs() DocumentIDs GetDocuments() Documents GetMetadatas() DocumentMetadatas GetEmbeddings() embeddings.Embeddings } type QueryResult interface { GetIDGroups() []DocumentIDs GetDocumentsGroups() []Documents GetMetadatasGroups() []DocumentMetadatas GetEmbeddingsGroups() []embeddings.Embeddings GetDistancesGroups() []embeddings.Distances } type GetResultImpl struct { Ids DocumentIDs Documents Documents Metadatas DocumentMetadatas Embeddings embeddings.Embeddings Include []Include } type QueryResultImpl struct { IDLists []DocumentIDs DocumentsLists []Documents MetadatasLists []DocumentMetadatas EmbeddingsLists []embeddings.Embeddings DistancesLists []embeddings.Distances Include []Include } // Row helpers for iteration func (r *GetResultImpl) Rows() []ResultRow func (r *GetResultImpl) At(index int) (ResultRow, bool) // Query.Rows() returns the first query group; use RowGroups() for all groups. func (r *QueryResultImpl) Rows() []ResultRow func (r *QueryResultImpl) RowGroups() [][]ResultRow func (r *QueryResultImpl) At(group, index int) (ResultRow, bool) ``` ```rust pub struct GetResponse { pub ids: Vec, pub embeddings: Option>>, pub documents: Option>>, pub uris: Option>>, pub metadatas: Option>>, pub include: Vec, } pub struct QueryResponse { pub ids: Vec>, pub embeddings: Option>>>>, pub documents: Option>>>, pub uris: Option>>>, pub metadatas: Option>>>, pub distances: Option>>>, pub include: Vec, } ``` ### Iteration Patterns ```python # GET: zip aligned columns result = collection.get(include=["documents", "metadatas"]) if result["documents"] is None or result["metadatas"] is None: raise ValueError("include must contain documents and metadatas") for doc_id, doc, meta in zip( result["ids"], result["documents"], result["metadatas"], ): print(doc_id, doc, meta) # QUERY: nested loop (queries -> matches) q = collection.query(query_texts=["climate"], n_results=3, include=["documents", "distances"]) if q["documents"] is None or q["distances"] is None: raise ValueError("include must contain documents and distances") for q_idx, ids in enumerate(q["ids"]): docs = q["documents"][q_idx] distances = q["distances"][q_idx] for doc_id, doc, distance in zip(ids, docs, distances): print(q_idx, doc_id, distance, doc) ``` ```typescript // Metadata type inference with generics const getResult = await collection.get<{ page: number }>({ include: ["documents", "metadatas"], }); for (const row of getResult.rows()) { console.log(row.id, row.metadata?.page, row.document); } const queryResult = await collection.query<{ page: number }>({ queryTexts: ["climate"], nResults: 3, include: ["documents", "metadatas", "distances"], }); for (const [queryIndex, rows] of queryResult.rows().entries()) { for (const row of rows) { console.log(queryIndex, row.id, row.distance, row.metadata?.page); } } ``` ```go // Keep example compact: panic on unexpected errors/types. getResult, err := collection.Get(ctx, chroma.WithInclude(chroma.IncludeDocuments, chroma.IncludeMetadatas)) if err != nil { panic(err) } getRows, ok := getResult.(*chroma.GetResultImpl) if !ok { panic(fmt.Sprintf("unexpected get result type %T", getResult)) } for _, row := range getRows.Rows() { fmt.Println(row.ID, row.Document, row.Metadata) } if row, ok := getRows.At(0); ok { fmt.Println("first get row:", row.ID) } queryResult, err := collection.Query(ctx, chroma.WithQueryTexts("climate"), chroma.WithNResults(3), chroma.WithInclude(chroma.IncludeDocuments, chroma.IncludeMetadatas, chroma.IncludeDistances), ) if err != nil { panic(err) } queryRows, ok := queryResult.(*chroma.QueryResultImpl) if !ok { panic(fmt.Sprintf("unexpected query result type %T", queryResult)) } // Query.Rows() gives rows for the first query group. for _, row := range queryRows.Rows() { fmt.Println("q0", row.ID, row.Score, row.Document) } if row, ok := queryRows.At(0, 0); ok { fmt.Println("first query row:", row.ID) } // Query.RowGroups() gives all query groups (useful for multi-query inputs). for queryIndex, rows := range queryRows.RowGroups() { for _, row := range rows { fmt.Println(queryIndex, row.ID, row.Score, row.Document) } } ``` ```rust // `None` include uses the Rust defaults: // IncludeList::default_get() and IncludeList::default_query(). let get_result = collection.get(None, None, Some(10), Some(0), None).await?; for (i, id) in get_result.ids.iter().enumerate() { let doc = get_result .documents .as_ref() .and_then(|docs| docs.get(i)) .and_then(|doc| doc.as_deref()); println!("{id}: {:?}", doc); } let query_result = collection .query(vec![vec![0.1, 0.2, 0.3]], Some(3), None, None, None) .await?; for (i, ids) in query_result.ids.iter().enumerate() { println!("query {i} has {} neighbors", ids.len()); } for (query_index, ids) in query_result.ids.iter().enumerate() { for (rank, id) in ids.iter().enumerate() { let distance = query_result .distances .as_ref() .and_then(|groups| groups.get(query_index)) .and_then(|group| group.get(rank)) .and_then(|v| *v); println!("query={query_index} rank={rank} id={id} distance={distance:?}"); } } ``` ## Iterating over a Collection ```python import chromadb client = chromadb.PersistentClient(path="my_local_data") # or HttpClient() collection = client.get_or_create_collection("local_collection") collection.add( ids=[f"{i}" for i in range(1000)], documents=[f"document {i}" for i in range(1000)], metadatas=[{"doc_id": i} for i in range(1000)]) existing_count = collection.count() batch_size = 10 for i in range(0, existing_count, batch_size): batch = collection.get( include=["metadatas", "documents", "embeddings"], limit=batch_size, offset=i) print(batch) # do something with the batch ``` ## Collection Utilities ### Copying Collections The following example demonstrates how to copy a local collection to a remote ChromaDB server. (it also works in reverse) ```python import chromadb client = chromadb.PersistentClient(path="my_local_data") remote_client = chromadb.HttpClient() collection = client.get_or_create_collection("local_collection") collection.add( ids=["1", "2"], documents=["hello world", "hello ChromaDB"], metadatas=[{"a": 1}, {"b": 2}]) remote_collection = remote_client.get_or_create_collection("remote_collection", metadata=collection.metadata) existing_count = collection.count() batch_size = 10 for i in range(0, existing_count, batch_size): batch = collection.get( include=["metadatas", "documents", "embeddings"], limit=batch_size, offset=i) remote_collection.add( ids=batch["ids"], documents=batch["documents"], metadatas=batch["metadatas"], embeddings=batch["embeddings"]) ``` Using ChromaDB Data Pipes Using [ChromaDB Data Pipes](https://datapipes.chromadb.dev) package you can achieve the same result. ```bash pip install chromadb-data-pipes cdp export "file://path/to_local_data/local_collection" | \ cdp import "http://remote_chromadb:port/remote_collection" --create ``` Following shows an example of how to copy a collection from one local persistent DB to another local persistent DB. ```python import chromadb local_client = chromadb.PersistentClient(path="source") remote_client = chromadb.PersistentClient(path="target") collection = local_client.get_or_create_collection("my_source_collection") collection.add( ids=["1", "2"], documents=["hello world", "hello ChromaDB"], metadatas=[{"a": 1}, {"b": 2}]) remote_collection = remote_client.get_or_create_collection("my_target_collection", metadata=collection.metadata) existing_count = collection.count() batch_size = 10 for i in range(0, existing_count, batch_size): batch = collection.get( include=["metadatas", "documents", "embeddings"], limit=batch_size, offset=i) remote_collection.add( ids=batch["ids"], documents=batch["documents"], metadatas=batch["metadatas"], embeddings=batch["embeddings"]) ``` Using ChromaDB Data Pipes You can achieve the above with [ChromaDB Data Pipes](https://datapipes.chromadb.dev) package. ```bash pip install chromadb-data-pipes cdp export "file://source_persist_dir/target_collection" | \ cdp import "file://target_persist_dir/target_collection" --create ``` ### Cloning a collection Here are some reasons why you might want to clone a collection: - Change distance function (via `configuration` β€” `hnsw.space`) - Change HNSW hyper parameters (`max_neighbors`, `ef_construction`, `search_ef`) ```python import chromadb client = chromadb.PersistentClient(path="test") # or HttpClient() col = client.get_or_create_collection("test") # create a new collection with L2 (default) col.add(ids=[f"{i}" for i in range(1000)], documents=[f"document {i}" for i in range(1000)]) newCol = client.get_or_create_collection("test1", configuration={ "hnsw": {"space": "cosine"}}) # change the distance function to cosine existing_count = col.count() batch_size = 10 for i in range(0, existing_count, batch_size): batch = col.get(include=["metadatas", "documents", "embeddings"], limit=batch_size, offset=i) newCol.add(ids=batch["ids"], documents=batch["documents"], metadatas=batch["metadatas"], embeddings=batch["embeddings"]) print(newCol.count()) print(newCol.get(offset=0, limit=10)) # get first 10 documents ``` #### Changing the embedding function To change the embedding function of a collection, it must be cloned to a new collection with the desired embedding function. External API Dependency This example requires an OpenAI API key (`OPENAI_API_KEY` environment variable). The [runnable example](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/collections/python/collection_examples.py) skips this section gracefully when the key is not set. ```python import os import chromadb from chromadb.utils.embedding_functions import OpenAIEmbeddingFunction, DefaultEmbeddingFunction client = chromadb.PersistentClient(path="test") # or HttpClient() default_ef = DefaultEmbeddingFunction() col = client.create_collection("default_ef_collection",embedding_function=default_ef) openai_ef = OpenAIEmbeddingFunction(api_key=os.getenv("OPENAI_API_KEY"), model_name="text-embedding-3-small") col.add(ids=[f"{i}" for i in range(1000)], documents=[f"document {i}" for i in range(1000)]) newCol = client.get_or_create_collection("openai_ef_collection", embedding_function=openai_ef) existing_count = col.count() batch_size = 10 for i in range(0, existing_count, batch_size): batch = col.get(include=["metadatas", "documents"], limit=batch_size, offset=i) newCol.add(ids=batch["ids"], documents=batch["documents"], metadatas=batch["metadatas"]) # get first 10 documents with their OpenAI embeddings print(newCol.get(offset=0, limit=10,include=["metadatas", "documents", "embeddings"])) ``` #### Cloning a subset of a collection with query The below example demonstrates how to select a slice of an existing collection by using `where` and `where_document` query and creating a new collection with the selected slice. Race Condition The below example is not atomic and if data is changed between the initial selection query (`select_ids = col.get(...)` and the subsequent insertion query (`batch = col.get(...)`) the new collection may not contain the expected data. ```python import chromadb client = chromadb.PersistentClient(path="test") # or HttpClient() col = client.get_or_create_collection("test") # create a new collection with L2 (default) col.add(ids=[f"{i}" for i in range(1000)], documents=[f"document {i}" for i in range(1000)]) newCol = client.get_or_create_collection("test1", configuration={ "hnsw": {"space": "cosine", "max_neighbors": 32}}) query_where = {"metadata_key": "value"} query_where_document = {"$contains": "document"} select_ids = col.get(where_document=query_where_document, where=query_where, include=[]) # get only IDs batch_size = 10 for i in range(0, len(select_ids["ids"]), batch_size): batch = col.get(include=["metadatas", "documents", "embeddings"], limit=batch_size, offset=i, where=query_where, where_document=query_where_document) newCol.add(ids=batch["ids"], documents=batch["documents"], metadatas=batch["metadatas"], embeddings=batch["embeddings"]) print(newCol.count()) print(newCol.get(offset=0, limit=10)) # get first 10 documents ``` ### Updating Document/Record Metadata In this example we loop through all documents of a collection and strip all metadata fields of leading and trailing whitespace. Change the `update_metadata` function to suit your needs. ```python import chromadb client = chromadb.PersistentClient(path="test") col = client.get_or_create_collection("test") count = col.count() def update_metadata(metadata: dict): return {k: v.strip() for k, v in metadata.items()} for i in range(0, count, 10): batch = col.get(include=["metadatas"], limit=10, offset=i) col.update(ids=batch["ids"], metadatas=[update_metadata(metadata) for metadata in batch["metadatas"]]) ``` ## Tips and Tricks ### Getting IDs Only The below example demonstrates how to get only the IDs of a collection. This is useful if you need to work with IDs without the need to fetch any additional data. Chroma will accept and empty `include` array indicating that no other data than the IDs is returned. ```python import chromadb client = chromadb.PersistentClient(path="test") col = client.get_or_create_collection("my_collection") ids_only_result = col.get(include=[]) print(ids_only_result['ids']) ``` # Concepts This page has two tracks: - [For General Users](#for-general-users) - [For Power Users](#for-power-users) If you're new to Chroma, start with **For General Users**. ## For General Users ### Tenancy and DB Hierarchies The following picture illustrates the tenancy and DB hierarchy in Chroma: Quick mental model: - **Tenant** = the top-level account or organization boundary - **Database** = a project/app space inside that tenant - **Collection** = a dataset (your searchable records) inside that database Storage In Chroma single-node, all data about tenancy, databases, collections and documents is stored in a single SQLite database. ### Tenants A tenant is the top-level container for data isolation. In practice, this is usually one team, company, or app owner. Example: `acme-inc` as one tenant. ### Databases A database is a project space inside a tenant. One database can hold many collections. Example: inside tenant `acme-inc`, you might have databases `support-bot`, `website-search`, and `analytics-rag`. ### Collections A collection is the dataset you query. It stores records (IDs, documents, metadata, embeddings) together. Example: `support_articles_v1`. ### Documents Chunks of text Documents in ChromaDB lingo are chunks of text that fit within the embedding model's context window. Unlike other frameworks that use the term "document" to mean a file, ChromaDB uses the term "document" to mean a chunk of text. Documents are raw chunks of text that are associated with an embedding. Documents are stored in the database and can be queried. Example document: ```text "To reset SSO, rotate your IdP certificate and re-run domain verification." ``` ### Metadata Metadata is a dictionary of key-value pairs associated with an embedding. Example metadata: ```json { "product_area": "auth", "status": "published", "year": 2025 } ``` Metadata values can be: - strings - integers - floats (`float32`) - booleans - arrays of strings, integers, floats, or booleans (`Chroma >= 1.5.0`) Array metadata constraints: - all elements must be the same type - empty arrays are not allowed - nested arrays are not supported No Built-In Metadata Contract Enforcement Chroma validates metadata value types, but it does not enforce a per-collection metadata schema contract. If you need required keys, enums, or numeric ranges, validate metadata in your app before `add()` / `upsert()`. See [Metadata Schema Validation](https://cookbook.chromadb.dev/strategies/metadata-schema-validation/index.md). See [Array Metadata](https://cookbook.chromadb.dev/core/filters/#array-metadata) for examples with `$contains` / `$not_contains`. Runnable filter examples: - [Python](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/filtering/python/filter_examples.py) - [TypeScript](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/filtering/typescript/filter_examples.ts) - [Rust](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/filtering/rust/src/main.rs) ### Embedding Function An embedding function is the model/API that turns text into vectors. You run it on: - documents (when writing data) - queries (when searching) See Chroma's official [embedding integrations](https://docs.trychroma.com/integrations#%F0%9F%A7%AC-embedding-integrations). ### Embeddings An embedding is the numeric vector representation of text, typically a list of `float32` values. You can think of it as a machine-friendly fingerprint of meaning. ### Distance Function Distance functions define similarity between vectors: - Cosine: common for semantic text similarity - Euclidean (`l2`): geometric distance - Inner Product (`ip`): common in recommendation-like scenarios In most query outputs, **lower distance means closer match**. ### Search Concepts Most search systems follow this flow: **eligible pool -> ranked list -> optional fusion -> optional grouping -> returned page**. Use this running example: - Query intent: "Find troubleshooting docs about SSO login failures." - Constraints: only `status=published` from `year >= 2024`. - Output goal: top 20 results (`title` + `score`), without one product area dominating. | Stage | What it decides | Chroma concept | | ------------------------------- | -------------------------------------- | ----------------------------- | | 1. Candidate selection | Which records are allowed to compete | `where`, `where_document` | | 2. Relevance ranking | Which eligible records appear first | `Knn` / ranking expressions | | 3. Hybrid fusion (optional) | How multiple ranked lists are combined | `Rrf` | | 4. Diversity / dedup (optional) | How many records to keep per bucket | `GroupBy` + `MinK` / `MaxK` | | 5. Response shaping | How much and which fields to return | `limit`, pagination, `select` | #### 1) Filters decide eligibility (not relevance) Filters answer: "Can this record be considered?" - `where` filters metadata (for example `status=published`, `year >= 2024`). - `where_document` filters document text content. - Filters remove non-matching records, but they do not define final ranking order. #### 2) Ranking decides order among eligible records Ranking answers: "Among the records that passed filters, which are most relevant?" - `Knn` computes similarity/distance ordering for eligible records. - In Search API scoring, lower scores represent better matches. For the running example, ranking pushes SSO/login-related incidents closest to the top after the eligibility filters are applied. #### 3) RRF fuses multiple rankings when score scales differ Hybrid fusion answers: "How do we merge strong semantic matches with strong keyword matches?" - `Rrf` combines rankings by position, not raw score magnitude. - This is useful when dense and sparse ranking scores are on different scales. In the running example, dense retrieval might catch "authentication outage", while sparse retrieval catches exact tokens like "SSO" and "SAML"; `Rrf` blends both lists. #### 4) Grouping/aggregation shapes the final mix Grouping answers: "How do we avoid one category dominating the top results?" - `GroupBy` partitions ranked results by one or more keys. - `MinK` / `MaxK` keep top-k rows per group before flattening. In the running example, grouping by `product_area` with `k=2` can prevent ten near-duplicate auth incidents from crowding out other useful categories. #### 5) Response shaping controls what comes back Response shaping answers: "How much should the API return, and which fields do you actually need?" - Pagination controls page size and offset. - `select` controls the returned payload. This keeps responses smaller and focused for downstream UI or agent use. Example: if search found 2,000 matches, you might return only the first 20 IDs, titles, and scores. See: - [Filters](https://cookbook.chromadb.dev/core/filters/index.md) for `where` / `where_document` syntax and operators. - [Advanced Search Semantics](https://cookbook.chromadb.dev/core/advanced/queries/#advanced-search-semantics) for execution-stage behavior and tradeoffs. - [Search API Overview](https://docs.trychroma.com/cloud/search-api/overview) for the full query model. - [Ranking and Scoring](https://docs.trychroma.com/cloud/search-api/ranking), [Hybrid Search with RRF](https://docs.trychroma.com/cloud/search-api/hybrid-search), and [Group By & Aggregation](https://docs.trychroma.com/cloud/search-api/group-by) for ranking primitives and grouping concepts. - [Examples & Patterns](https://docs.trychroma.com/cloud/search-api/examples) for concrete Search API implementations. ### How Data Flows Through Chroma Cloud (Distributed Chroma) The animated flows below model Chroma Cloud / distributed Chroma, where gateway, WAL, compaction, and query execution are separate services. In local or single-node deployments, the same logical stages still apply but are often co-located in one process. What this means: - On writes: Chroma saves changes durably first, then updates indexes in the background. - On reads: Chroma combines indexed data and recent log data so results stay up to date. #### Write Path (Add / Update / Upsert / Delete) Write Path Cloud/distributed: durable first, indexed asynchronously Client Gateway WAL Service Compactor Indexes In distributed Chroma, writes are acknowledged after WAL durability. Compaction materializes new index versions in the background. #### Query Path (Get / Query / Search) Query Path Cloud/distributed: index + WAL consistency Client Gateway Query Engine Filter + Plan Vector / FTS / Metadata Ranked Results In distributed Chroma, strongly consistent reads combine indexed state with recent WAL state. The detailed query pipeline is described in [Advanced Queries](https://cookbook.chromadb.dev/core/advanced/queries/index.md). Implementation-level (code-backed) local-vs-distributed query diagrams are in the [For Power Users](#for-power-users) section below. ## For Power Users This section is a code-oriented map of distributed Chroma, based on the Rust workspace (`rust/`) and the distributed architecture docs. If you mostly care about product behavior, you can skip this section. This part is for readers who want to connect concepts to Rust implementation details. ### Execution Paths (Code-Backed) These diagrams are traced from the Rust frontend/segment/log implementation (`rust/frontend`, `rust/segment`, `rust/log`). Tip: treat these as "what service does what" maps, not required reading for everyday app development. #### Interactive Local Query Path (Single-Node SQLite + HNSW) Interactive Local Query Pipeline Single-node: SQLite + HNSW Local Click any stage to inspect what happens in the local executor path. First read triggers backfill/purge into local metadata + HNSW segments. Validation + ResolveMetadata Pre-Filter get SQLite MetadataKNN Search query HNSW LocalMetadata Fetch get SQLite MetadataResult Aggregation Selected stage Validation + Segment Resolve **Service:** FrontendServer + ServiceBasedFrontend Request validation/auth happens in frontend, then collection + segment ids are resolved before plan execution. #### Distributed Frontend Dispatch Path (Cloud) Distributed Query Path in Frontend Cloud: frontend dispatches, workers execute Client Frontend API Resolver + Executor gRPC Query Worker Worker Indexes Results In distributed mode, frontend routes Knn/Get/Search plans to query workers over gRPC. Worker-side execution uses distributed segment/index types (HnswDistributed or Spann with blockfile segments), not frontend-local SQLite + HNSW providers. Code references for the two paths: - Local segment types on collection create: [`Executor::Local` creates `HnswLocalPersisted` + `Sqlite`](https://github.com/chroma-core/chroma/blob/main/rust/frontend/src/impls/service_based_frontend.rs) - Local query execution: [`LocalExecutor` uses `SqliteMetadataReader` + `LocalSegmentManager::get_hnsw_reader`](https://github.com/chroma-core/chroma/blob/main/rust/frontend/src/executor/local.rs) - Distributed query execution: [`DistributedExecutor` dispatches `knn/get/search` via gRPC query clients](https://github.com/chroma-core/chroma/blob/main/rust/frontend/src/executor/distributed.rs) ### Distributed Architecture (Main Services) - Gateway / frontend API service: `rust/frontend` (receives API calls and dispatches work) ([server](https://github.com/chroma-core/chroma/blob/main/rust/frontend/src/server.rs)) - Query executor service: `rust/worker` (runs query operators and orchestrators) ([query entrypoint](https://github.com/chroma-core/chroma/blob/main/rust/worker/src/lib.rs), [query server](https://github.com/chroma-core/chroma/blob/main/rust/worker/src/server.rs)) - Compaction service: `rust/worker` (turns WAL/log history into read-optimized segment versions) ([compaction orchestrator](https://github.com/chroma-core/chroma/blob/main/rust/worker/src/execution/orchestration/compact.rs)) - Write-ahead log: `rust/wal3` (durable append-only change log) ([design README](https://github.com/chroma-core/chroma/blob/main/rust/wal3/README.md)) - Garbage collector service: `rust/garbage_collector` (cleans old index/log artifacts safely) ([orchestrator](https://github.com/chroma-core/chroma/blob/main/rust/garbage_collector/src/garbage_collector_orchestrator_v2.rs)) See also the official architecture doc: [Distributed Chroma Architecture](https://github.com/chroma-core/chroma/blob/main/docs/mintlify/docs/overview/architecture.mdx). ### Main Primitives and Index Families At the segment/type level (`rust/types/src/segment.rs`), distributed Chroma uses segment types such as: - `BlockfileMetadata`, `BlockfileRecord` - `HnswDistributed` - `Spann`, `QuantizedSpann` - `Sqlite` In simple terms: - `Blockfile*` segments store compacted record/metadata data. - `HnswDistributed` / `Spann` / `QuantizedSpann` are vector-search structures. - `Sqlite` is still used for specific metadata/system concerns. And at the index crate level (`rust/index/src`), major families include: - Vector ANN: `hnsw`, `spann`, `quantized_spann` - Full text: `fulltext` - Metadata: `metadata` - Sparse retrieval support: `sparse` ### SPANN in Distributed Chroma Core implementation: `rust/index/src/spann/types.rs`. Quick intuition: instead of searching one giant graph, SPANN first searches cluster centers, then looks inside the best matching posting lists. Operationally, SPANN combines: - a head/center ANN structure (HNSW over centers) - posting lists keyed by center/head id (blockfile-backed) - a versions map (`doc_offset_id -> version`) to filter stale entries - a persisted `max_head_id` for deterministic head allocation across compactions The write-side behavior includes: - add/update/delete on posting lists and versions map - splitting oversized posting lists into new heads - reassigning points to nearby heads after split/merge operations - optional garbage collection policies: - posting-list random-sample cleanup - HNSW full rebuild or delete-percentage-triggered rebuild ### Blockfile Format and Update Model Core implementation: `rust/blockstore/src/arrow`. Quick intuition: blockfiles are immutable Arrow-backed data blocks with copy-on-write updates, so reads stay stable while writes build new versions. Production blockfiles are Arrow-backed and use: - immutable blocks for persisted data - an in-memory sparse index mapping key ranges to block ids - writer-side deltas for mutation batching (`set`/`delete`) - copy-on-write for updates via `fork(...)` Update lifecycle: 1. Writer mutates deltas and may split blocks when over target block size. 1. Sparse index is updated to point at new block ids. 1. `commit()` converts deltas into immutable blocks and prepares a flusher. 1. `flush()` persists blocks, then atomically persists root metadata/sparse index. Relevant code: - [provider](https://github.com/chroma-core/chroma/blob/main/rust/blockstore/src/arrow/provider.rs) - [blockfile writer](https://github.com/chroma-core/chroma/blob/main/rust/blockstore/src/arrow/blockfile.rs) - [flusher](https://github.com/chroma-core/chroma/blob/main/rust/blockstore/src/arrow/flusher.rs) - [root + sparse index](https://github.com/chroma-core/chroma/blob/main/rust/blockstore/src/arrow/root.rs) ### Compaction and Registration Compaction in distributed mode (`rust/worker/src/execution/orchestration/compact.rs`) follows an explicit staged flow: - fetch and materialize logs - apply to segment writers (record/metadata/vector) - commit and flush segment artifacts - register new segment metadata and offsets in sysdb/log metadata This is the core bridge from WAL durability to read-optimized segment versions. What this means: compaction is the "make recent writes fast to read" job. ### Garbage Collection (Index Files + WAL) Two GC tracks run in the Rust implementation: - Segment/index artifact GC in `rust/garbage_collector`: - construct collection version graph (including fork dependencies) - compute versions to delete using cutoff + min-versions retention - compute unreferenced files and clean up (dry-run / rename / delete) - WAL GC in `wal3`: - three-phase GC flow (compute garbage, manifest synchronization, delete) - cursor-driven safety so required log ranges remain pinned What this means: GC removes storage that is no longer needed, but only after safety checks confirm active readers won't break. Relevant code: - [version graph construction](https://github.com/chroma-core/chroma/blob/main/rust/garbage_collector/src/construct_version_graph_orchestrator.rs) - [version deletion policy](https://github.com/chroma-core/chroma/blob/main/rust/garbage_collector/src/operators/compute_versions_to_delete_from_graph.rs) - [unused file cleanup](https://github.com/chroma-core/chroma/blob/main/rust/garbage_collector/src/operators/delete_unused_files.rs) - [unused WAL cleanup](https://github.com/chroma-core/chroma/blob/main/rust/garbage_collector/src/operators/delete_unused_logs.rs) # Configuration ## 1.0 Configuration (Current) Starting with Chroma 1.0, collection index settings are configured via the `configuration` dict parameter at collection creation time. This replaces the legacy `metadata`-based approach. ```python collection = client.create_collection( "my_collection", configuration={ "hnsw": { "space": "cosine", "ef_construction": 200, "max_neighbors": 32, } }, ) ``` Configuration vs Metadata The `configuration` dict is separate from `metadata`. Metadata is for user-defined key-value pairs. Configuration controls the vector index behavior. You cannot specify both `hnsw` and `spann` in the same configuration - only one index type is allowed per collection. ### HNSW Index Configuration HNSW (Hierarchical Navigable Small World) is the default vector index for Chroma. It provides fast approximate nearest neighbor search for single-node and self-hosted deployments. #### Parameters | Parameter | Description | Default | Constraints | Mutable | | ----------------- | ------------------------------------------------- | --------- | -------------------- | ------- | | `space` | Distance metric | `l2` | `l2`, `cosine`, `ip` | No | | `ef_construction` | Neighbors explored during index build | `100` | Positive integer | No | | `ef_search` | Neighbors explored during search | `100` | Positive integer | Yes | | `max_neighbors` | Max connections per node (M parameter) | `16` | Positive integer | No | | `num_threads` | Threads used by HNSW | CPU cores | Positive integer | Yes | | `resize_factor` | Graph growth rate when capacity is reached | `1.2` | Positive float | Yes | | `batch_size` | In-memory bruteforce index size before HNSW flush | `100` | >= 2 | Yes | | `sync_threshold` | Threshold for syncing HNSW index to disk | `1000` | >= 2 | Yes | #### Examples **Create with configuration:** ```python import chromadb client = chromadb.HttpClient() # or PersistentClient() collection = client.create_collection( "my_collection", configuration={ "hnsw": { "space": "cosine", "ef_construction": 200, "ef_search": 100, "max_neighbors": 32, "num_threads": 4, "resize_factor": 1.2, "batch_size": 100, "sync_threshold": 1000, } }, ) ``` **Update mutable parameters after creation:** ```python collection.modify( configuration={ "hnsw": { "ef_search": 200, "num_threads": 8, } } ) ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient(); const collection = await client.createCollection({ name: "my_collection", configuration: { hnsw: { space: "cosine", ef_construction: 200, ef_search: 100, max_neighbors: 32, num_threads: 4, resize_factor: 1.2, batch_size: 100, sync_threshold: 1000, }, }, }); ``` ```go package main import ( "context" chroma "github.com/amikos-tech/chroma-go" "github.com/amikos-tech/chroma-go/types" ) func main() { ctx := context.Background() client, _ := chroma.NewHTTPClient(ctx, chroma.WithDefaultDatabase("default_database"), chroma.WithDefaultTenant("default_tenant"), ) col, _ := client.CreateCollection(ctx, "my_collection", false, types.WithHNSWConfiguration( types.WithSpace("cosine"), types.WithEfConstruction(200), types.WithEfSearch(100), types.WithMaxNeighbors(32), ), ) } ``` ### SPANN Index Configuration Chroma Cloud Only SPANN is the vector index used in **Chroma Cloud** and distributed Chroma deployments. It is not available in single-node self-hosted Chroma. If you are running Chroma locally, use HNSW configuration instead. SPANN (Space Partition tree AND graph based Nearest neighbor search) is Chroma's distributed vector index, based on the [SPFresh](https://arxiv.org/abs/2206.14286) paper. It is designed for large-scale datasets where the full index cannot fit in a single machine's memory. #### How SPANN Works ```text SPANN Architecture β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Query: "find similar" β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ HNSW Centroid β”‚ O(log C) lookup β”‚ β”‚ β”‚ Index β”‚ C = num centroids β”‚ β”‚ β”‚ β”Œβ”€β”€β” β”Œβ”€β”€β” β”Œβ”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚c1│─│c2│─│c3β”‚Β·Β·Β· β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”˜ β””β”€β”€β”˜ β””β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ top-K centroids β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Probe posting β”‚ search_nprobe β”‚ β”‚ β”‚ lists for each β”‚ centroids probed β”‚ β”‚ β”‚ selected centroid β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β–Ό β–Ό β–Ό β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚Posting β”‚ β”‚Posting β”‚ β”‚Posting β”‚ β”‚ β”‚ β”‚List c1 β”‚ β”‚List c2 β”‚ β”‚List c3 β”‚ Blockfile β”‚ β”‚ β”‚β”Œβ”€β”β”Œβ”€β” β”‚ β”‚β”Œβ”€β”β”Œβ”€β” β”‚ β”‚β”Œβ”€β”β”Œβ”€β” β”‚ storage β”‚ β”‚ β”‚β”‚dβ”‚β”‚d│…│ β”‚β”‚dβ”‚β”‚d│…│ β”‚β”‚dβ”‚β”‚d│…│ β”‚ β”‚ β”‚β””β”€β”˜β””β”€β”˜ β”‚ β”‚β””β”€β”˜β””β”€β”˜ β”‚ β”‚β””β”€β”˜β””β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Score, dedupe, β”‚ β”‚ β”‚ β”‚ return results β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ SPFresh Maintenance (automatic): β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Posting list too large (> split_threshold)? β”‚ β”‚ β†’ Split via balanced 2-means clustering β”‚ β”‚ β†’ New centroid added to HNSW index β”‚ β”‚ β†’ Nearby points reassigned for better recall β”‚ β”‚ β”‚ β”‚ Posting list too small (< merge_threshold)? β”‚ β”‚ β†’ Merge with nearest neighbor centroid β”‚ β”‚ β†’ Old centroid removed from HNSW index β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` **Key concepts:** - **Centroids** are cluster representatives stored in a small HNSW graph for fast lookup - **Posting lists** store the actual document embeddings grouped by their nearest centroid - **Multi-posting** allows a document to appear in multiple posting lists for better recall - **SPFresh maintenance** automatically splits large clusters and merges small ones to keep cluster sizes balanced #### Parameters | Parameter | Description | Default | Constraints | Mutable | | ------------------------- | ------------------------------------------------- | ------- | -------------------- | ------- | | `space` | Distance metric | `l2` | `l2`, `cosine`, `ip` | No | | `search_nprobe` | Number of centroids probed during search | `64` | Max 128 | Yes | | `write_nprobe` | Number of centroids considered during insert | `32` | Max 128 | No | | `ef_construction` | HNSW build effort for centroid index | `200` | Max 200 | No | | `ef_search` | HNSW search effort for centroid index | `200` | Max 200 | Yes | | `max_neighbors` | Max connections in centroid HNSW graph | `64` | Max 64 | No | | `reassign_neighbor_count` | Nearby clusters checked during split reassignment | `64` | Max 64 | No | | `split_threshold` | Posting list size that triggers a split | `50` | 25 - 200 | No | | `merge_threshold` | Posting list size that triggers a merge | `25` | 12 - 100 | No | Tuning Guidance - **`search_nprobe`** is the primary knob for search quality vs latency. Higher values improve recall but increase search time. Start with the default (64) and adjust based on your recall requirements. - **`split_threshold`** and **`merge_threshold`** control cluster granularity. Smaller split thresholds create more, smaller clusters (better for high-dimensional data). The merge threshold should always be less than the split threshold. - **`space`** must match your embedding model's expected distance metric. Use `cosine` for normalized embeddings (most common), `l2` for Euclidean distance, or `ip` for inner product. #### Examples **Create with SPANN configuration:** ```python import chromadb client = chromadb.CloudClient( tenant="my-tenant", database="my-database", ) collection = client.create_collection( "my_collection", configuration={ "spann": { "space": "cosine", "search_nprobe": 64, "ef_search": 200, } }, ) ``` **Update mutable parameters after creation:** ```python collection.modify( configuration={ "spann": { "search_nprobe": 96, "ef_search": 200, } } ) ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient({ // Cloud client configuration }); const collection = await client.createCollection({ name: "my_collection", configuration: { spann: { space: "cosine", search_nprobe: 64, ef_search: 200, }, }, }); ``` ```go package main import ( "context" chroma "github.com/amikos-tech/chroma-go/v2" v2 "github.com/amikos-tech/chroma-go/v2/pkg/api/v2" ) func main() { ctx := context.Background() client, _ := chroma.NewCloudClient(ctx, chroma.WithTenant("my-tenant"), chroma.WithDatabase("my-database"), ) col, _ := client.CreateCollection(ctx, "my_collection", v2.WithVectorIndexCreate(v2.NewVectorIndexConfig( v2.WithSpace(v2.SpaceCosine), v2.WithSpann(v2.NewSpannConfig( v2.WithSpannSearchNprobe(64), v2.WithSpannEfSearch(200), )), )), ) } ``` ### Embedding Function Configuration Starting with Chroma v1.1.13, embedding functions are persisted server-side. You can configure the embedding function at collection creation time, and it will be automatically used on subsequent `get_collection` calls. Embedding function `name`/`config` payloads are defined by upstream schemas: - [Embedding Function Schemas](https://github.com/chroma-core/chroma/tree/main/schemas/embedding_functions) - [Schema README](https://github.com/chroma-core/chroma/blob/main/schemas/embedding_functions/README.md) **Set via argument (recommended):** ```python from chromadb.utils.embedding_functions import OpenAIEmbeddingFunction ef = OpenAIEmbeddingFunction(model_name="text-embedding-3-small") collection = client.create_collection("my_collection", embedding_function=ef) # On subsequent access, the EF is auto-resolved: collection = client.get_collection("my_collection") # no ef needed ``` **Set via configuration dict:** ```python collection = client.create_collection( "my_collection", configuration={ "embedding_function": { "name": "openai", "config": { "model_name": "text-embedding-3-small", "api_key_env_var": "OPENAI_API_KEY", }, } }, ) ``` **Custom API key environment variable:** ```python ef = OpenAIEmbeddingFunction( model_name="text-embedding-3-small", api_key_env_var="MY_CUSTOM_OPENAI_KEY", # defaults to OPENAI_API_KEY ) collection = client.create_collection("my_collection", embedding_function=ef) ``` ```typescript const collection = await client.createCollection({ name: "my_collection", configuration: { embedding_function: { name: "openai", config: { model_name: "text-embedding-3-small", apiKeyEnvVar: "OPENAI_API_KEY", }, }, }, }); ``` ```go package main import ( "context" "os" chroma "github.com/amikos-tech/chroma-go/v2" v2 "github.com/amikos-tech/chroma-go/v2/pkg/api/v2" openai "github.com/amikos-tech/chroma-go/v2/pkg/embeddings/openai" ) func main() { ctx := context.Background() client, _ := chroma.NewHTTPClient(ctx, chroma.WithDefaultDatabase("default_database"), chroma.WithDefaultTenant("default_tenant"), ) ef, _ := openai.NewOpenAIEmbeddingFunction(os.Getenv("OPENAI_API_KEY"), openai.WithModel(openai.TextEmbedding3Small), ) col, _ := client.CreateCollection(ctx, "my_collection", v2.WithEmbeddingFunctionCreate(ef), ) } ``` ______________________________________________________________________ ## 1.x Server Configuration Chroma 1.x is configured via a YAML configuration file. The server loads configuration from the path specified by the `CONFIG_PATH` environment variable. Individual settings can be overridden with `CHROMA_`-prefixed environment variables (use `__` for nested properties). ```bash # Start with a custom config file CONFIG_PATH=/etc/chroma/config.yaml chroma run # Override individual settings via environment variables CHROMA_PORT=9000 chroma run CHROMA_CORS_ALLOW_ORIGINS='["*"]' chroma run CHROMA_ALLOW_RESET=true chroma run ``` ### HTTP Server Settings | Parameter | Description | Default | Env Override | | ------------------------ | ------------------------------------------------------------- | -------------------------- | ------------------------------- | | `port` | HTTP server port | `8000` | `CHROMA_PORT` | | `listen_address` | Network interface to bind to | `0.0.0.0` (all interfaces) | `CHROMA_LISTEN_ADDRESS` | | `max_payload_size_bytes` | Maximum request body size in bytes | `41943040` (40 MB) | `CHROMA_MAX_PAYLOAD_SIZE_BYTES` | | `cors_allow_origins` | List of allowed CORS origins. Use `["*"]` to allow any origin | None (CORS disabled) | `CHROMA_CORS_ALLOW_ORIGINS` | | `persist_path` | Directory for Chroma data files (SQLite DB, HNSW indices) | `./chroma` | `CHROMA_PERSIST_PATH` | ### General Settings | Parameter | Description | Default | Env Override | | ------------------- | -------------------------------------------------------------------------------------------------------- | ------- | -------------------------- | | `allow_reset` | Enable the `DELETE /reset` endpoint, which deletes all data. Should be `false` in production | `false` | `CHROMA_ALLOW_RESET` | | `default_knn_index` | Default vector index type for new collections. Values: `hnsw` (single-node), `spann` (distributed/cloud) | `hnsw` | `CHROMA_DEFAULT_KNN_INDEX` | | `enable_schema` | Enable server-side schema validation for collection operations | `true` | `CHROMA_ENABLE_SCHEMA` | ### SQLite Settings | Parameter | Description | Default | Env Override | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------- | --------------------------------- | | `sqlitedb.hash_type` | Hash algorithm for verifying migration file integrity. Values: `md5`, `sha256`. Use `sha256` if your organization prohibits MD5 | `md5` | `CHROMA_SQLITEDB__HASH_TYPE` | | `sqlitedb.migration_mode` | How schema migrations are handled on startup. Values: `apply` (run pending migrations), `validate` (check migrations are applied, fail if not) | `apply` | `CHROMA_SQLITEDB__MIGRATION_MODE` | ### OpenTelemetry Chroma 1.x emits traces via OpenTelemetry (OTLP gRPC). Tracing is disabled by default. Set the `open_telemetry` section to enable it. | Parameter | Description | Default | Env Override | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------- | | `open_telemetry.endpoint` | OTLP gRPC collector endpoint. Tracing is disabled when not set | None (disabled) | `CHROMA_OPEN_TELEMETRY__ENDPOINT` | | `open_telemetry.service_name` | Service name attached to all emitted spans | `chromadb` | `CHROMA_OPEN_TELEMETRY__SERVICE_NAME` | | `open_telemetry.filters` | Per-crate log level filters. Each entry has `crate_name` (Rust crate to filter) and `filter_level`. Levels: `trace`, `debug`, `info`, `warn`, `error` | `[{crate_name: "chroma_frontend", filter_level: "trace"}]` | β€” | ### Example Configurations **Minimal single-node:** ```yaml persist_path: "./chroma" ``` **Single-node with all common options:** ```yaml # HTTP server port: 8000 listen_address: "0.0.0.0" max_payload_size_bytes: 41943040 cors_allow_origins: ["*"] # Storage persist_path: "./chroma" allow_reset: false # SQLite sqlitedb: hash_type: "md5" migration_mode: "apply" # Telemetry (optional) open_telemetry: service_name: "chroma" endpoint: "http://otel-collector:4317" filters: - crate_name: "chroma_frontend" filter_level: "trace" ``` **Docker single-node:** ```yaml persist_path: "/data" ``` Environment Variable Nesting Nested YAML properties are overridden using `__` as a separator. For example, `sqlitedb.hash_type` becomes `CHROMA_SQLITEDB__HASH_TYPE`. ______________________________________________________________________ ## Pre-1.0 Configuration (Legacy) Legacy Configuration The configuration options below apply to Chroma versions prior to 1.0. For Chroma 1.0+, use the `configuration` dict parameter described above. Legacy `metadata`-based HNSW configuration (e.g. `metadata={"hnsw:space": "cosine"}`) is still supported for backwards compatibility but is deprecated. ### Common Configurations Options ### Server Configuration #### Core ##### `IS_PERSISTENT` Defines whether Chroma should persist data or not. Possible values: - `TRUE` - `FALSE` Default: `FALSE` **How to use**: ```bash export IS_PERSISTENT=TRUE chroma run --path ./chroma ``` ```python from chromadb.config import Settings settings = Settings(is_persistent=True) # run the server with the settings ``` ```bash docker run -d --rm --name chromadb -v ./chroma:/chroma/chroma -e IS_PERSISTENT=TRUE chromadb/chroma:0.6.3 ``` ##### `PERSIST_DIRECTORY` Defines the directory where Chroma should persist data. This can be relative or absolute path. The directory must be writeable to Chroma process. Default: `./chroma` ##### `ALLOW_RESET` Defines whether Chroma should allow resetting the index (delete all data). Possible values: - `TRUE` - `FALSE` Default: `FALSE` ##### `CHROMA_MEMORY_LIMIT_BYTES` ##### `CHROMA_SEGMENT_CACHE_POLICY` #### Telemetry and Observability In the current Chroma version (as of time or writing `0.6.3`) the only type of telemetry supported are traces. The following configuration options allow you to configure the tracing service that accepts OpenTelemetry traces via the OLTP GRPC endpoint. In addition to traces Chroma also performs anonymized product telemetry. The product telemetry is enabled by default. ##### `CHROMA_OTEL_COLLECTION_ENDPOINT` Defines the endpoint of the tracing service that accepts OpenTelemetry traces via the OLTP GRPC endpoint. Value type: `Valid URL` Default: None **Example**: ```bash export CHROMA_OTEL_COLLECTION_ENDPOINT=http://localhost:4317 ``` ##### `CHROMA_OTEL_SERVICE_NAME` Defines the name of the service that will be used in the tracing service. Default: `chroma` **Example**: ```bash export CHROMA_OTEL_SERVICE_NAME=chroma-dev ``` ##### `CHROMA_OTEL_COLLECTION_HEADERS` Defines the headers that will be sent with each trace/span. Default: None **Example**: ```bash export CHROMA_OTEL_COLLECTION_HEADERS='{"X-API-KEY":"1234567890"}' ``` ##### `CHROMA_OTEL_GRANULARITY` Defines the granularity of the traces. Possible values: - `none` - No spans are emitted. - `operation` - Spans are emitted for each operation. - `operation_and_segment` - Spans are emitted for almost all method calls. - `all` - Spans are emitted for almost all method calls. Default: `none` **Example**: ```bash export CHROMA_OTEL_GRANULARITY=all ``` ##### `CHROMA_PRODUCT_TELEMETRY_IMPL` Do not change Do not change the default implementation as it may impact Chroma stability, instead use the `ANONYMIZED_TELEMETRY` configuration. Defines the implementation of the product telemetry. Default: `chromadb.telemetry.product.posthog.Posthog` ##### `CHROMA_TELEMETRY_IMPL` This is identical to `CHROMA_PRODUCT_TELEMETRY_IMPL` but for the anonymized telemetry but is kept for backwards compatibility. ##### `ANONYMIZED_TELEMETRY` Enables or disables anonymized product telemetry. Possible values: - `TRUE` - Enables anonymized telemetry. - `FALSE` - Disables anonymized telemetry. Default: `TRUE` (enabled) Read more about how Chroma uses telemetry [here](https://docs.trychroma.com/telemetry). **Example**: ```bash export ANONYMIZED_TELEMETRY=FALSE ``` #### Maintenance ##### `MIGRATIONS` Defines how schema migrations are handled in Chroma. Possible values: - `none` - No migrations are applied. - `validate` - Existing schema is validated. - `apply` - Migrations are applied. Default: `apply` ##### `MIGRATIONS_HASH_ALGORITHM` Defines the algorithm used to hash the migrations. This configuration was introduces as some organizations have strict policies around use of cryptographic algorithms, considering the default `md5` being a weak hashing algorithm. Possible values: - `sha256` - Uses SHA-256 to hash the migrations. - `md5` - Uses MD5 to hash the migrations. Default: `md5` **Example**: ```bash export MIGRATIONS_HASH_ALGORITHM=sha256 ``` #### Operations and Distributed ##### `CHROMA_SYSDB_IMPL` ##### `CHROMA_PRODUCER_IMPL` ##### `CHROMA_CONSUMER_IMPL` ##### `CHROMA_SEGMENT_MANAGER_IMPL` ##### `CHROMA_SEGMENT_DIRECTORY_IMPL` ##### `CHROMA_MEMBERLIST_PROVIDER_IMPL` ##### `WORKER_MEMBERLIST_NAME` ##### `CHROMA_COORDINATOR_HOST` ##### `CHROMA_SERVER_GRPC_PORT` ##### `CHROMA_LOGSERVICE_HOST` ##### `CHROMA_LOGSERVICE_PORT` ##### `CHROMA_QUOTA_PROVIDER_IMPL` ##### `CHROMA_RATE_LIMITING_PROVIDER_IMPL` #### Authentication ##### `CHROMA_AUTH_TOKEN_TRANSPORT_HEADER` ##### `CHROMA_CLIENT_AUTH_PROVIDER` ##### `CHROMA_CLIENT_AUTH_CREDENTIALS` ##### `CHROMA_SERVER_AUTH_IGNORE_PATHS` ##### `CHROMA_OVERWRITE_SINGLETON_TENANT_DATABASE_ACCESS_FROM_AUTH` ##### `CHROMA_SERVER_AUTHN_PROVIDER` ##### `CHROMA_SERVER_AUTHN_CREDENTIALS` ##### `CHROMA_SERVER_AUTHN_CREDENTIALS_FILE` #### Authorization ##### `CHROMA_SERVER_AUTHZ_PROVIDER` ##### `CHROMA_SERVER_AUTHZ_CONFIG` ##### `CHROMA_SERVER_AUTHZ_CONFIG_FILE` ### Client Configuration #### Authentication ### HNSW Configuration (Legacy) HNSW parameters were previously configured as collection metadata with the `hnsw:` prefix. This approach still works for backwards compatibility but is deprecated in favor of the `configuration` dict. Changing HNSW parameters Some HNSW parameters cannot be changed after index creation via the standard method shown below. If you which to change these parameters, you will need to clone the collection see an example [here](https://cookbook.chromadb.dev/core/collections/#cloning-a-collection). #### `hnsw:space` **Description**: Controls the distance metric of the HNSW index. The space cannot be changed after index creation. **Default**: `l2` **Constraints**: - Possible values: `l2`, `cosine`, `ip` - Parameter ***cannot*** be changed after index creation. **Example**: ```python res = client.create_collection("my_collection", metadata={ "hnsw:space": "cosine"}) ``` #### `hnsw:construction_ef` **Description**: Controls the number of neighbours in the HNSW graph to explore when adding new vectors. The more neighbours HNSW explores the better and more exhaustive the results will be. Increasing the value will also increase memory consumption. **Default**: `100` **Constraints**: - Values must be positive integers. - Parameter ***cannot*** be changed after index creation. **Example**: ```python client.create_collection( "my_collection", metadata={ "hnsw:construction_ef": 100} ) ``` #### `hnsw:M` **Description**: Controls the maximum number of neighbour connections (M), a newly inserted vector. A higher value results in a mode densely connected graph. The impact on this is slower but more accurate searches with increased memory consumption. **Default**: `16` **Constraints**: - Values must be positive integers. - Parameter ***cannot*** be changed after index creation. **Example**: ```python client.create_collection( "my_collection", metadata={ "hnsw:M": 16} ) ``` #### `hnsw:search_ef` **Description**: Controls the number of neighbours in the HNSW graph to explore when searching. Increasing this requires more memory for the HNSW algo to explore the nodes during knn search. **Default**: `10` **Constraints**: - Values must be positive integers. - Parameter ***can*** be changed after index creation. **Example**: ```python client.create_collection( "my_collection", metadata={ "hnsw:search_ef": 10} ) ``` #### `hnsw:num_threads` **Description**: Controls how many threads HNSW algo use. **Default**: `` **Constraints**: - Values must be positive integers. - Parameter ***can*** be changed after index creation. **Example**: ```python client.create_collection( "my_collection", metadata={ "hnsw:num_threads": 4} ) ``` #### `hnsw:resize_factor` **Description**: Controls the rate of growth of the graph (e.g. how many node capacity will be added) whenever the current graph capacity is reached. **Default**: `1.2` **Constraints**: - Values must be positive floating point numbers. - Parameter ***can*** be changed after index creation. **Example**: ```python client.create_collection( "my_collection", metadata={ "hnsw:resize_factor": 1.2} ) ``` #### `hnsw:batch_size` **Description**: Controls the size of the Bruteforce (in-memory) index. Once this threshold is crossed vectors from BF gets transferred to HNSW index. This value can be changed after index creation. The value must be less than `hnsw:sync_threshold`. **Default**: `100` **Constraints**: - Values must be positive integers. - Parameter ***can*** be changed after index creation. **Example**: ```python client.create_collection( "my_collection", metadata={ "hnsw:batch_size": 100} ) ``` #### `hnsw:sync_threshold` **Description**: Controls the threshold when using HNSW index is written to disk. **Default**: `1000` **Constraints**: - Values must be positive integers. - Parameter ***can*** be changed after index creation. #### Examples Configuring HNSW parameters at creation time ```python import chromadb client = chromadb.HttpClient() # Adjust as per your client client.create_collection( "my_collection", metadata={ "hnsw:space": "cosine", "hnsw:construction_ef": 100, "hnsw:M": 16, "hnsw:search_ef": 10, "hnsw:num_threads": 4, "hnsw:resize_factor": 1.2, "hnsw:batch_size": 100, "hnsw:sync_threshold": 1000, } ) ``` Updating HNSW parameters after creation Updating HNSW parameters Updating HNSW parameters after index creation is not supported as of version `0.5.5`. # Document IDs Chroma is unopinionated about document IDs and delegates those decisions to the user. This frees users to build semantics around their IDs. ## Note on Compound IDs While you can choose to use IDs that are composed of multiple sub-IDs (e.g. `user_id` + `document_id`), it is important to highlight that Chroma does not support querying by partial ID. ## Common Practices chromadbx We provide a convinient wrapper for in the form of `chromadbx` package that provides ID generators for UUIDs, ULIDs, NonoIDs, and Hashes, among others functions. You can install it with `pip install chromadbx`. ### UUIDs UUIDs are a common choice for document IDs. They are unique, and can be generated in a distributed fashion. They are also opaque, which means that they do not contain any information about the document itself. This can be a good thing, as it allows you to change the document without changing the ID. ```python import chromadb from chromadbx import UUIDGenerator client = chromadb.Client() col = client.get_or_create_collection("test") my_docs = [f"Document {_}" for _ in range(10)] col.add(ids=UUIDGenerator(len(my_docs)), documents=my_docs) ``` ```python import uuid import chromadb my_documents = [ "Hello, world!", "Hello, Chroma!" ] client = chromadb.Client() collection = client.get_or_create_collection("collection") collection.add(ids=[f"{uuid.uuid4()}" for _ in range(len(my_documents))], documents=my_documents) ``` #### Caveats Predictable Ordering UUIDs especially v4 are not lexicographically sortable. In its current version (0.4.x-0.5.10) Chroma orders responses of `get()` by the ID of the documents. Therefore, if you need predictable ordering, you may want to consider a different ID strategy. In version `0.5.11` ordering is done on [internal IDs](https://docs.trychroma.com/deployment/migration#v0.5.11) Storage and Performance Overhead Chroma stores Document IDs as strings and UUIDs are 36 characters long, which can be a lot of overhead if you have a large number of documents. If you are concerned about storage overhead, you may want to consider a different ID strategy. Additionally Chroma uses the document IDs when sorting results which also incurs a performance hit. ### ULIDs ULIDs are a variant of UUIDs that are lexicographically sortable. They are also 128 bits long, like UUIDs, but they are encoded in a way that makes them sortable. This can be useful if you need predictable ordering of your documents. ULIDs are also shorter than UUIDs, which can save you some storage space. They are also opaque, like UUIDs, which means that they do not contain any information about the document itself. Install the `ulid-py` package to generate ULIDs. ```bash pip install ulid-py ``` ```python import chromadb from chromadbx import ULIDGenerator import ulid client = chromadb.Client() col = client.get_or_create_collection("test") my_docs = [f"Document {_}" for _ in range(10)] col.add(ids=ULIDGenerator(len(my_docs)), documents=my_docs) ``` ```python from ulid import ULID import chromadb my_documents = [ "Hello, world!", "Hello, Chroma!" ] _ulid = ULID() client = chromadb.Client() collection = client.get_or_create_collection("name") collection.add(ids=[f"{_ulid.generate()}" for _ in range(len(my_documents))], documents=my_documents) ``` ### NanoIDs NanoIDs provide a way to generate unique IDs that are shorter than UUIDs. They are not lexically sortable, but they are unique and can be generated in a distributed fashion. They are also opaque, with low collision rates - (collision probability calculator)[https://zelark.github.io/nano-id-cc/] ```python import chromadb from chromadbx import NanoIDGenerator client = chromadb.Client() col = client.get_or_create_collection("test") my_docs = [f"Document {_}" for _ in range(10)] col.add(ids=NanoIDGenerator(len(my_docs)), documents=my_docs) ``` ```python from nanoid import generate import chromadb client = chromadb.Client() col = client.get_or_create_collection("test") my_docs = [f"Document {_}" for _ in range(10)] col.add(ids=[f"{generate()}" for _ in range(my_docs)], documents=my_docs) ``` ### Hashes Hashes are another common choice for document IDs. They are unique, and can be generated in a distributed fashion. They are also opaque, which means that they do not contain any information about the document itself. This can be a good thing, as it allows you to change the document without changing the ID. **Random SHA256:** ```python import chromadb from chromadbx import RandomSHA256Generator client = chromadb.Client() col = client.get_or_create_collection("test") my_docs = [f"Document {_}" for _ in range(10)] col.add(ids=RandomSHA256Generator(len(my_docs)), documents=my_docs) ``` **Document-based SHA256:** ```python import chromadb from chromadbx import DocumentSHA256Generator client = chromadb.Client() col = client.get_or_create_collection("test") my_docs = [f"Document {_}" for _ in range(10)] col.add(ids=DocumentSHA256Generator(documents=my_docs), documents=my_docs) ``` **Random SHA256:** ```python import hashlib import os import chromadb def generate_sha256_hash() -> str: # Generate a random number random_data = os.urandom(16) # Create a SHA256 hash object sha256_hash = hashlib.sha256() # Update the hash object with the random data sha256_hash.update(random_data) # Return the hexadecimal representation of the hash return sha256_hash.hexdigest() my_documents = [ "Hello, world!", "Hello, Chroma!" ] client = chromadb.Client() collection = client.get_or_create_collection("collection") collection.add(ids=[generate_sha256_hash() for _ in range(len(my_documents))], documents=my_documents) ``` **Document-based SHA256:** It is also possible to use the document as basis for the hash, the downside of that is that when the document changes, and you have a semantic around the text as relating to the hash, you may need to update the hash. ```python import hashlib import chromadb def generate_sha256_hash_from_text(text) -> str: # Create a SHA256 hash object sha256_hash = hashlib.sha256() # Update the hash object with the text encoded to bytes sha256_hash.update(text.encode('utf-8')) # Return the hexadecimal representation of the hash return sha256_hash.hexdigest() my_documents = [ "Hello, world!", "Hello, Chroma!" ] client = chromadb.Client() collection = client.get_or_create_collection("collection") collection.add(ids=[generate_sha256_hash_from_text(my_documents[i]) for i in range(len(my_documents))], documents=my_documents) ``` ## Semantic Strategies In this section we'll explore a few different use cases for building semantics around document IDs. - URL Slugs - if your docs are web pages with permalinks (e.g. blog posts), you can use the URL slug as the document ID. - File Paths - if your docs are files on disk, you can use the file path as the document ID. # Filters Chroma provides two types of filters: - Metadata - filter documents based on metadata using `where` clause in either `Collection.query()` or `Collection.get()` - Document - filter documents based on document content using `where_document` in `Collection.query()` or `Collection.get()`. Those familiar with MongoDB queries will find Chroma's filters very similar. Runnable Examples Complete, runnable filtering examples for each language are available in the [examples/filtering](https://github.com/amikos-tech/chroma-cookbook/tree/main/examples/filtering) directory: - [Python](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/filtering/python/filter_examples.py) - [TypeScript](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/filtering/typescript/filter_examples.ts) - [Go](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/filtering/go/main.go) - [Rust](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/filtering/rust/src/main.rs) Interactive playground Build filters interactively Compose `where` and `where_document`, preview payloads, and copy Cloud or Local starter code. [Open Interactive Playground](#interactive-filter-playground) ## Metadata Filters ### Schema Filter Schema vs Record Metadata Schema The JSON schema below validates `where` filter expressions, not the metadata contract of records you ingest. For application-layer metadata validation/enforcement patterns, see [Metadata Schema Validation](https://cookbook.chromadb.dev/strategies/metadata-schema-validation/index.md). You can use the following JSON schema to validate your `where` filters: ```json { "$schema": "https://json-schema.org/draft/2020-12/schema#", "title": "Chroma Metadata Where Filter Schema", "description": "Schema for Chroma metadata filters used in where clauses", "oneOf": [ { "type": "object", "patternProperties": { "^[^$].*$": { "oneOf": [ { "type": ["string", "number", "boolean"] }, { "type": "object", "properties": { "$eq": {"type": ["string", "number", "boolean"]}, "$ne": {"type": ["string", "number", "boolean"]}, "$gt": {"type": "number"}, "$gte": {"type": "number"}, "$lt": {"type": "number"}, "$lte": {"type": "number"}, "$in": { "oneOf": [ { "type": "array", "items": { "type": "string" }, "minItems": 1 }, { "type": "array", "items": { "type": "number" }, "minItems": 1 }, { "type": "array", "items": { "type": "boolean" }, "minItems": 1 } ] }, "$nin": { "oneOf": [ { "type": "array", "items": { "type": "string" }, "minItems": 1 }, { "type": "array", "items": { "type": "number" }, "minItems": 1 }, { "type": "array", "items": { "type": "boolean" }, "minItems": 1 } ] }, "$contains": {"type": ["string", "number", "boolean"]}, "$not_contains": {"type": ["string", "number", "boolean"]} }, "additionalProperties": false, "minProperties": 1, "maxProperties": 1 } ] } }, "minProperties": 1 }, { "type": "object", "properties": { "$and": { "type": "array", "items": {"$ref": "#"}, "minItems": 2 }, "$or": { "type": "array", "items": {"$ref": "#"}, "minItems": 2 } }, "additionalProperties": false, "minProperties": 1, "maxProperties": 1 } ] } ``` ### Equality (`$eq`) This filter matches attribute values that equal to a specified string, boolean, integer or float value. The value check is case-sensitive. Supported value types are: `string`, `boolean`, `integer` or `float` (or `number` in JS/TS) Simple equality: Single condition If you are using simple equality expression `{"metadata_field": "is_equal_to_this"}`, you can only specify a single condition. ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": "is_equal_to_this"} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { metadata_field: "is_equal_to_this" }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.EqString("metadata_field", "is_equal_to_this")), ) ``` ```rust let results = collection.get( None, Some(Where::Metadata(MetadataExpression { key: "metadata_field".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::Equal, MetadataValue::Str("is_equal_to_this".to_string()), ), })), None, None, None, ).await?; ``` Alternative syntax: ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": {"$eq": "is_equal_to_this"}} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { metadata_field: { $eq: "is_equal_to_this" } }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.EqString("metadata_field", "is_equal_to_this")), ) ``` ```rust let results = collection.get( None, Some(Where::Metadata(MetadataExpression { key: "metadata_field".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::Equal, MetadataValue::Str("is_equal_to_this".to_string()), ), })), None, None, None, ).await?; ``` Validation Failures When validation fails, similar to this message is expected to be returned by Chroma - `ValueError: Expected where value to be a str, int, float, or operator expression, got X in get.` with `X` refering to the inferred type of the data. ### Inequality (`$ne`) This filter matches attribute values that are not equal to a specified string, boolean, integer or float value. The value check is case-sensitive. Supported value types are: `string`, `boolean`, `integer` or `float` (or `number` in JS/TS) ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": {"$ne": "is_not_equal_to_this"}} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { metadata_field: { $ne: "is_not_equal_to_this" } }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.NotEqString("metadata_field", "is_not_equal_to_this")), ) ``` ```rust let results = collection.get( None, Some(Where::Metadata(MetadataExpression { key: "metadata_field".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::NotEqual, MetadataValue::Str("is_not_equal_to_this".to_string()), ), })), None, None, None, ).await?; ``` ### Greater Than (`$gt`) This filter matches attribute values that are strictly greater than a specified numeric (`integer` or `float`) value. Greater Than The `$gt` operator is only supported for numerical values - int or float values. ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": {"$gt": 5}} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { metadata_field: { $gt: 5 } }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.GtInt("metadata_field", 5)), ) ``` ```rust let results = collection.get( None, Some(Where::Metadata(MetadataExpression { key: "metadata_field".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::GreaterThan, MetadataValue::Int(5), ), })), None, None, None, ).await?; ``` ### Greater Than or Equal (`$gte`) This filter matches attribute values that are greater than or equal a specified numeric (`integer` or `float`) value. Greater Than or Equal The `$gte` operator is only supported for numerical values - int or float values. ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": {"$gte": 5.1}} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { metadata_field: { $gte: 5.1 } }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.GteFloat("metadata_field", 5.1)), ) ``` ```rust let results = collection.get( None, Some(Where::Metadata(MetadataExpression { key: "metadata_field".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::GreaterThanOrEqual, MetadataValue::Float(5.1), ), })), None, None, None, ).await?; ``` ### Less Than (`$lt`) This filter matches attribute values that are less than specified numeric (`integer` or `float`) value. Supported values: `integer` or `float` ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": {"$lt": 5}} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { metadata_field: { $lt: 5 } }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.LtInt("metadata_field", 5)), ) ``` ```rust let results = collection.get( None, Some(Where::Metadata(MetadataExpression { key: "metadata_field".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::LessThan, MetadataValue::Int(5), ), })), None, None, None, ).await?; ``` ### Less Than or Equal (`$lte`) This filter matches attribute values that are less than or equal specified numeric (`integer` or `float`) value. Supported values: `integer` or `float` ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": {"$lte": 5.1}} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { metadata_field: { $lte: 5.1 } }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.LteFloat("metadata_field", 5.1)), ) ``` ```rust let results = collection.get( None, Some(Where::Metadata(MetadataExpression { key: "metadata_field".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::LessThanOrEqual, MetadataValue::Float(5.1), ), })), None, None, None, ).await?; ``` ### In (`$in`) This filter matches attribute values that are in the given list of values. Supported value types are: `string`, `boolean`, `integer` or `float` (or `number` in JS/TS) In The `$in` operator is only supported for list of values of the same type. ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": {"$in": ["value1", "value2"]}} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { metadata_field: { $in: ["value1", "value2"] } }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.InString("metadata_field", "value1", "value2")), ) ``` ```rust let results = collection.get( None, Some(Where::Metadata(MetadataExpression { key: "metadata_field".to_string(), comparison: MetadataComparison::Set( SetOperator::In, MetadataSetValue::Str(vec!["value1".into(), "value2".into()]), ), })), None, None, None, ).await?; ``` ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": {"$in": [1, 2, 3]}} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { metadata_field: { $in: [1, 2, 3] } }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.InInt("metadata_field", 1, 2, 3)), ) ``` ```rust let results = collection.get( None, Some(Where::Metadata(MetadataExpression { key: "metadata_field".to_string(), comparison: MetadataComparison::Set( SetOperator::In, MetadataSetValue::Int(vec![1, 2, 3]), ), })), None, None, None, ).await?; ``` ```python # All values in $in must be the same type - this will fail results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": {"$in": [1, "2", 1.1]}} ) ``` ### Not In (`$nin`) This filter matches attribute that do not have the given key or the values of which are not in the given list of values. Supported value types are: `string`, `boolean`, `integer` or `float` (or `number` in JS/TS) Not In The `$nin` operator is only supported for list of values of the same type. ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": {"$nin": ["value1", "value2"]}} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { metadata_field: { $nin: ["value1", "value2"] } }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.NinString("metadata_field", "value1", "value2")), ) ``` ```rust let results = collection.get( None, Some(Where::Metadata(MetadataExpression { key: "metadata_field".to_string(), comparison: MetadataComparison::Set( SetOperator::NotIn, MetadataSetValue::Str(vec!["value1".into(), "value2".into()]), ), })), None, None, None, ).await?; ``` ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": {"$nin": [1, 2, 3]}} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { metadata_field: { $nin: [1, 2, 3] } }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.NinInt("metadata_field", 1, 2, 3)), ) ``` ```rust let results = collection.get( None, Some(Where::Metadata(MetadataExpression { key: "metadata_field".to_string(), comparison: MetadataComparison::Set( SetOperator::NotIn, MetadataSetValue::Int(vec![1, 2, 3]), ), })), None, None, None, ).await?; ``` ```python # All values in $nin must be the same type - this will fail results = collection.query( query_texts=["This is a query document"], n_results=2, where={"metadata_field": {"$nin": [1, "2", 1.1]}} ) ``` ### Array Metadata Chroma >= 1.5.0 Array metadata and the `$contains`/`$not_contains` operators require Chroma 1.5.0 or later. Chroma supports storing arrays in metadata fields. All elements in an array must be of the same type. Supported array element types: `string`, `integer`, `float`, `boolean` **Constraints:** - Empty arrays are not allowed - Nested arrays (arrays of arrays) are not supported - All elements must be the same type (no mixed-type arrays) Runnable Array Examples End-to-end array metadata examples are included in the filtering examples: - [Python](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/filtering/python/filter_examples.py) - [TypeScript](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/filtering/typescript/filter_examples.ts) - [Rust](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/filtering/rust/src/main.rs) #### Storing Array Metadata Here is an example of a research paper collection using array metadata to store multi-value fields like topics, authors, and review scores: ```python import chromadb client = chromadb.Client() collection = client.create_collection("research_papers") collection.add( ids=["paper-1", "paper-2", "paper-3"], documents=[ "We introduce a transformer-based architecture for low-resource language translation.", "A study on the effects of soil microbiome diversity on crop yield in arid climates.", "Applying reinforcement learning to optimize energy consumption in smart grid networks.", ], metadatas=[ { "authors": ["Chen", "Okafor", "MΓΌller"], "topics": ["nlp", "transformers", "low-resource"], "review_scores": [8, 7, 9], "year": 2024, }, { "authors": ["Patel", "Johansson"], "topics": ["agriculture", "microbiome", "climate"], "review_scores": [6, 7, 7], "year": 2023, }, { "authors": ["Chen", "Williams"], "topics": ["reinforcement-learning", "energy", "smart-grid"], "review_scores": [9, 8, 9], "year": 2024, }, ], ) ``` ```typescript import { ChromaClient } from "chromadb"; const client = new ChromaClient(); const collection = await client.createCollection({ name: "research_papers" }); await collection.add({ ids: ["paper-1", "paper-2", "paper-3"], documents: [ "We introduce a transformer-based architecture for low-resource language translation.", "A study on the effects of soil microbiome diversity on crop yield in arid climates.", "Applying reinforcement learning to optimize energy consumption in smart grid networks.", ], metadatas: [ { authors: ["Chen", "Okafor", "MΓΌller"], topics: ["nlp", "transformers", "low-resource"], review_scores: [8, 7, 9], year: 2024, }, { authors: ["Patel", "Johansson"], topics: ["agriculture", "microbiome", "climate"], review_scores: [6, 7, 7], year: 2023, }, { authors: ["Chen", "Williams"], topics: ["reinforcement-learning", "energy", "smart-grid"], review_scores: [9, 8, 9], year: 2024, }, ], }); ``` Go Client Array metadata support in the Go client is pending. See the [chroma-go repository](https://github.com/amikos-tech/chroma-go) for updates. ```rust let mut metadata1 = Metadata::new(); metadata1.insert("authors".into(), vec!["Chen", "Okafor", "MΓΌller"].into()); metadata1.insert("topics".into(), vec!["nlp", "transformers", "low-resource"].into()); metadata1.insert("review_scores".into(), vec![8i64, 7, 9].into()); metadata1.insert("year".into(), MetadataValue::Int(2024)); let mut metadata2 = Metadata::new(); metadata2.insert("authors".into(), vec!["Patel", "Johansson"].into()); metadata2.insert("topics".into(), vec!["agriculture", "microbiome", "climate"].into()); metadata2.insert("review_scores".into(), vec![6i64, 7, 7].into()); metadata2.insert("year".into(), MetadataValue::Int(2023)); let mut metadata3 = Metadata::new(); metadata3.insert("authors".into(), vec!["Chen", "Williams"].into()); metadata3.insert("topics".into(), vec!["reinforcement-learning", "energy", "smart-grid"].into()); metadata3.insert("review_scores".into(), vec![9i64, 8, 9].into()); metadata3.insert("year".into(), MetadataValue::Int(2024)); collection.add( vec!["paper-1".into(), "paper-2".into(), "paper-3".into()], vec![embed1, embed2, embed3], Some(vec![ Some("We introduce a transformer-based architecture for low-resource language translation.".into()), Some("A study on the effects of soil microbiome diversity on crop yield in arid climates.".into()), Some("Applying reinforcement learning to optimize energy consumption in smart grid networks.".into()), ]), None, Some(vec![Some(metadata1), Some(metadata2), Some(metadata3)]), ).await?; ``` #### Contains (`$contains`) Returns records where an array metadata field includes a specific value. The filter value must be a scalar matching the array's element type. ```python # Find papers authored by "Chen" results = collection.get( where={"authors": {"$contains": "Chen"}} ) # Returns paper-1 and paper-3 ``` ```typescript // Find papers authored by "Chen" const results = await collection.get({ where: { authors: { $contains: "Chen" } }, }); // Returns paper-1 and paper-3 ``` Go Client Array metadata `$contains` in the Go client is pending. See the [chroma-go repository](https://github.com/amikos-tech/chroma-go) for updates. ```rust // Find papers authored by "Chen" (Search API - Chroma Cloud) let results = collection.search(vec![ SearchPayload::default() .r#where(Key::field("authors").contains("Chen")) .limit(Some(10), 0), ]).await?; // Returns paper-1 and paper-3 ``` ```python # Find papers that received a review score of 9 results = collection.get( where={"review_scores": {"$contains": 9}} ) # Returns paper-1 and paper-3 ``` ```typescript // Find papers that received a review score of 9 const results = await collection.get({ where: { review_scores: { $contains: 9 } }, }); // Returns paper-1 and paper-3 ``` Go Client Array metadata `$contains` in the Go client is pending. See the [chroma-go repository](https://github.com/amikos-tech/chroma-go) for updates. ```rust // Find papers that received a review score of 9 (Search API - Chroma Cloud) let results = collection.search(vec![ SearchPayload::default() .r#where(Key::field("review_scores").contains(9)) .limit(Some(10), 0), ]).await?; // Returns paper-1 and paper-3 ``` #### Not Contains (`$not_contains`) Returns records where an array metadata field does not include a specific value. ```python # Find papers not tagged with "nlp" results = collection.get( where={"topics": {"$not_contains": "nlp"}} ) # Returns paper-2 and paper-3 ``` ```typescript // Find papers not tagged with "nlp" const results = await collection.get({ where: { topics: { $not_contains: "nlp" } }, }); // Returns paper-2 and paper-3 ``` Go Client Array metadata `$not_contains` in the Go client is pending. See the [chroma-go repository](https://github.com/amikos-tech/chroma-go) for updates. ```rust // Find papers not tagged with "nlp" (Search API - Chroma Cloud) let results = collection.search(vec![ SearchPayload::default() .r#where(Key::field("topics").not_contains("nlp")) .limit(Some(10), 0), ]).await?; // Returns paper-2 and paper-3 ``` #### Combining Array Filters Array filters work with `$and`/`$or` logical operators and can be mixed with scalar filters: ```python # Papers by "Chen" published in 2024 that cover "energy" results = collection.query( query_texts=["renewable energy optimization"], where={ "$and": [ {"authors": {"$contains": "Chen"}}, {"topics": {"$contains": "energy"}}, {"year": {"$eq": 2024}}, ] }, ) # Returns paper-3 ``` ```typescript // Papers by "Chen" published in 2024 that cover "energy" const results = await collection.query({ queryTexts: ["renewable energy optimization"], where: { $and: [ { authors: { $contains: "Chen" } }, { topics: { $contains: "energy" } }, { year: { $eq: 2024 } }, ], }, }); // Returns paper-3 ``` Go Client Array metadata `$contains` in the Go client is pending. See the [chroma-go repository](https://github.com/amikos-tech/chroma-go) for updates. ```rust // Papers by "Chen" published in 2024 that cover "energy" (Search API - Chroma Cloud) let results = collection.search(vec![ SearchPayload::default() .r#where( (Key::field("authors").contains("Chen")) & (Key::field("topics").contains("energy")) & (Key::field("year").eq(2024)), ) .rank(knn_query.clone()) .limit(Some(10), 0), ]).await?; // Returns paper-3 ``` ### Logical Operator: And (`$and`) The `$and` logical operator joins two or more simple (`$eq`, `$ne`, `$gt` etc.) filters together and matches records for which all of the conditions in the list are satisfied. ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"$and": [{"metadata_field1": "value1"}, {"metadata_field2": "value2"}]} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { $and: [{ metadata_field1: "value1" }, { metadata_field2: "value2" }] }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.And( chroma.EqString("metadata_field1", "value1"), chroma.EqString("metadata_field2", "value2"), )), ) ``` ```rust let results = collection.get( None, Some(Where::Composite(CompositeExpression { operator: BooleanOperator::And, children: vec![ Where::Metadata(MetadataExpression { key: "metadata_field1".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::Equal, MetadataValue::Str("value1".to_string()), ), }), Where::Metadata(MetadataExpression { key: "metadata_field2".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::Equal, MetadataValue::Str("value2".to_string()), ), }), ], })), None, None, None, ).await?; ``` ### Logical Operator: Or (`$or`) The `$or` logical operator that joins two or more simple (`$eq`, `$ne`, `$gt` etc.) filters together and matches records for which at least one of the conditions in the list is satisfied. ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={"$or": [{"metadata_field1": "value1"}, {"metadata_field2": "value2"}]} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { $or: [{ metadata_field1: "value1" }, { metadata_field2: "value2" }] }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.Or( chroma.EqString("metadata_field1", "value1"), chroma.EqString("metadata_field2", "value2"), )), ) ``` ```rust let results = collection.get( None, Some(Where::Composite(CompositeExpression { operator: BooleanOperator::Or, children: vec![ Where::Metadata(MetadataExpression { key: "metadata_field1".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::Equal, MetadataValue::Str("value1".to_string()), ), }), Where::Metadata(MetadataExpression { key: "metadata_field2".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::Equal, MetadataValue::Str("value2".to_string()), ), }), ], })), None, None, None, ).await?; ``` ### Logical Operator Nesting Logical Operators can be nested. ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where={ "$and": [ {"metadata_field1": "value1"}, {"$and": [{"metadata_field2": "value2"}, {"metadata_field3": "value3"}]}, ] }, ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, where: { $and: [ { metadata_field1: "value1" }, { $and: [{ metadata_field2: "value2" }, { metadata_field3: "value3" }] }, ], }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhere(chroma.And( chroma.EqString("metadata_field1", "value1"), chroma.And( chroma.EqString("metadata_field2", "value2"), chroma.EqString("metadata_field3", "value3"), ), )), ) ``` ```rust let results = collection.get( None, Some(Where::Composite(CompositeExpression { operator: BooleanOperator::And, children: vec![ Where::Metadata(MetadataExpression { key: "metadata_field1".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::Equal, MetadataValue::Str("value1".to_string()), ), }), Where::Composite(CompositeExpression { operator: BooleanOperator::And, children: vec![ Where::Metadata(MetadataExpression { key: "metadata_field2".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::Equal, MetadataValue::Str("value2".to_string()), ), }), Where::Metadata(MetadataExpression { key: "metadata_field3".to_string(), comparison: MetadataComparison::Primitive( PrimitiveOperator::Equal, MetadataValue::Str("value3".to_string()), ), }), ], }), ], })), None, None, None, ).await?; ``` ## Document Filters Rust: Search API Required In the Rust client, document filters use the Search API (`collection.search()`) with the `Key::Document` builder. The Search API requires Chroma Cloud. ### Schema You can use the following JSON schema to validate `where_document` expressions: ```json { "$schema": "https://json-schema.org/draft/2020-12/schema#", "title": "Chroma Document Filter Schema", "description": "Schema for Chroma document filters used in where_document clauses", "type": "object", "oneOf": [ { "properties": { "$contains": { "type": "string" } }, "required": ["$contains"], "additionalProperties": false }, { "properties": { "$not_contains": { "type": "string" } }, "required": ["$not_contains"], "additionalProperties": false }, { "properties": { "$regex": { "type": "string" } }, "required": ["$regex"], "additionalProperties": false }, { "properties": { "$not_regex": { "type": "string" } }, "required": ["$not_regex"], "additionalProperties": false }, { "properties": { "$and": { "type": "array", "items": {"$ref": "#"}, "minItems": 2 } }, "required": ["$and"], "additionalProperties": false }, { "properties": { "$or": { "type": "array", "items": {"$ref": "#"}, "minItems": 2 } }, "required": ["$or"], "additionalProperties": false } ] } ``` ### Contains (`$contains`) Case-Sensitive The `$contains` document filter performs a case-sensitive full-text search. For example, `{"$contains": "Hello"}` will not match a document containing only `"hello"`. ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where_document={"$contains": "search_string"} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, whereDocument: { $contains: "search_string" }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhereDocument(chroma.Contains("search_string")), ) ``` ```rust let results = collection.search(vec![ SearchPayload::default() .r#where(Key::Document.contains("search_string")) .rank(knn_query.clone()) .limit(Some(2), 0), ]).await?; ``` ### Not Contains (`$not_contains`) ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where_document={"$not_contains": "search_string"} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, whereDocument: { $not_contains: "search_string" }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhereDocument(chroma.NotContains("search_string")), ) ``` ```rust let results = collection.search(vec![ SearchPayload::default() .r#where(Key::Document.not_contains("search_string")) .rank(knn_query.clone()) .limit(Some(2), 0), ]).await?; ``` ### Regex (`$regex`) Matches documents whose content matches the given regular expression pattern. ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where_document={"$regex": "search.*pattern"} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, whereDocument: { $regex: "search.*pattern" }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhereDocument(chroma.Regex("search.*pattern")), ) ``` ```rust let results = collection.search(vec![ SearchPayload::default() .r#where(Key::Document.regex("search.*pattern")) .rank(knn_query.clone()) .limit(Some(2), 0), ]).await?; ``` ### Not Regex (`$not_regex`) Matches documents whose content does not match the given regular expression pattern. ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where_document={"$not_regex": "exclude.*pattern"} ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, whereDocument: { $not_regex: "exclude.*pattern" }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhereDocument(chroma.NotRegex("exclude.*pattern")), ) ``` ```rust let results = collection.search(vec![ SearchPayload::default() .r#where(Key::Document.not_regex("exclude.*pattern")) .rank(knn_query.clone()) .limit(Some(2), 0), ]).await?; ``` ### Logical Operator: And (`$and`) ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where_document={ "$and": [ {"$contains": "search_string1"}, {"$contains": "search_string2"}, ] }, ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, whereDocument: { $and: [{ $contains: "search_string1" }, { $contains: "search_string2" }], }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhereDocument(chroma.AndDocument( chroma.Contains("search_string1"), chroma.Contains("search_string2"), )), ) ``` ```rust let results = collection.search(vec![ SearchPayload::default() .r#where( (Key::Document.contains("search_string1")) & (Key::Document.contains("search_string2")), ) .rank(knn_query.clone()) .limit(Some(2), 0), ]).await?; ``` Logical Operators can be nested. ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where_document={ "$and": [ {"$contains": "search_string1"}, { "$or": [ {"$not_contains": "search_string2"}, {"$not_contains": "search_string3"}, ] }, ] }, ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, whereDocument: { $and: [ { $contains: "search_string1" }, { $or: [ { $not_contains: "search_string2" }, { $not_contains: "search_string3" }, ], }, ], }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhereDocument(chroma.AndDocument( chroma.Contains("search_string1"), chroma.OrDocument( chroma.NotContains("search_string2"), chroma.NotContains("search_string3"), ), )), ) ``` ```rust let results = collection.search(vec![ SearchPayload::default() .r#where( (Key::Document.contains("search_string1")) & ((Key::Document.not_contains("search_string2")) | (Key::Document.not_contains("search_string3"))), ) .rank(knn_query.clone()) .limit(Some(2), 0), ]).await?; ``` ### Logical Operator: Or (`$or`) ```python results = collection.query( query_texts=["This is a query document"], n_results=2, where_document={ "$or": [ {"$not_contains": "search_string1"}, {"$not_contains": "search_string2"}, ] }, ) ``` ```typescript const results = await collection.query({ queryTexts: ["This is a query document"], nResults: 2, whereDocument: { $or: [ { $not_contains: "search_string1" }, { $not_contains: "search_string2" }, ], }, }); ``` ```go results, _ := collection.Query(ctx, chroma.WithQueryTexts("This is a query document"), chroma.WithNResults(2), chroma.WithWhereDocument(chroma.OrDocument( chroma.NotContains("search_string1"), chroma.NotContains("search_string2"), )), ) ``` ```rust let results = collection.search(vec![ SearchPayload::default() .r#where( (Key::Document.not_contains("search_string1")) | (Key::Document.not_contains("search_string2")), ) .rank(knn_query.clone()) .limit(Some(2), 0), ]).await?; ``` ## Pagination `Collection.get()` allows users to specify page details `limit` and `offset`. ```python results = collection.get(limit=10, offset=20) ``` ```typescript const results = await collection.get({ limit: 10, offset: 20 }); ``` ```go results, _ := collection.Get(ctx, chroma.WithLimit(10), chroma.WithOffset(20), ) ``` ```rust let results = collection.get( None, // ids None, // where Some(10), // limit Some(20), // offset None, // include ).await?; ``` ## Interactive Filter Playground (Cloud + Local) Use this interactive sandbox to sketch a filter payload before running Chroma. Switch between **Cloud** and **Local** tabs to see how the client code changes while the filter shape remains consistent. For nested logic and full schema control, switch either section to **JSON mode**. Filter Playground Build metadata (`where`) or document-text (`where_document`) filters and preview generated code. Cloud Local Reset Cloud advanced options Expand Ranking Vector KNN Hybrid RRF No rank expression Dense query KNN limit Hybrid sparse query Sparse key RRF (k, weights) Pagination Limit Offset Field selection #document #score #metadata #embedding Metadata keys (csv) Grouping / aggregation Group keys (csv) min_k max_k Batch operations Batch size Local query options Expand Call query() get() Limit Offset (get) Include fields documents metadatas distances embeddings Metadata filters (`where`) Join $and $or JSON mode Add condition Document filters (`where_document`) Join $and $or JSON mode Add condition Filter JSON Python TypeScript Filter JSON Copy Playground scope: this is a learning aid for composing filter payloads and starter client code; it is not a full schema validator. # Installation Chroma provides packages for Python, JavaScript/TypeScript, Go, and Rust. ## Quick Start Get a Chroma server running quickly with the CLI or Docker: ```bash pip install chromadb chroma run --path ./getting-started ``` ```bash curl -sSL https://raw.githubusercontent.com/chroma-core/chroma/main/rust/cli/install/install.sh | bash chroma run --path ./getting-started ``` ```bash docker pull chromadb/chroma:1.5.3 && docker run -p 8000:8000 chromadb/chroma:1.5.3 ``` Version pinning Avoid relying on `latest` for production or repeatable environments. Pin to a specific image tag (for example `chromadb/chroma:1.5.3`) and upgrade intentionally after validation. ## Chroma CLI (Standalone Installer) If you prefer not to install Chroma through Python or JavaScript package managers, use the standalone CLI installer: ```bash curl -sSL https://raw.githubusercontent.com/chroma-core/chroma/main/rust/cli/install/install.sh | bash ``` ```powershell iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/chroma-core/chroma/main/rust/cli/install/install.ps1')) ``` The standalone installer installs the `chroma` CLI binary so you can run: ```bash chroma run --path ./getting-started ``` For the full CLI installation matrix (`pip`, `pipx`, `uv`, `npm`, `pnpm`, `bun`, `yarn`, and standalone installers), see [Running Chroma](https://cookbook.chromadb.dev/running/running-chroma/#chroma-cli) and the [official CLI installation docs](https://docs.trychroma.com/docs/cli/install). ## Python The `chromadb` package includes everything needed for both local (embedded) usage and connecting to a remote Chroma server. Here's a [minimal working example](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/install/python/verify.py) to confirm your installation. Backward compatibility While Chroma strives to be as compatible with older versions as possible, certain releases introduce breaking changes and most importantly database migrations. All database migrations are irreversible and once upgraded to a new version of Chroma, you cannot downgrade to an older version. Releases You can find Chroma releases on PyPI [here](https://pypi.org/project/chromadb/#history). ```bash pip install chromadb ``` Directly from GitHub: ```bash pip install git+https://github.com/chroma-core/chroma.git@main ``` From test PyPI: ```bash pip install --index-url https://test.pypi.org/simple/ chromadb ``` Installing a specific version of Chroma is useful when you want to ensure that your code works with a specific version of Chroma. To install a specific version of Chroma, run: From PyPI: ```bash pip install chromadb== ``` Directly from GitHub (replace `x.y.z` with the [tag](https://github.com/chroma-core/chroma/tags) of the version you want to install): ```bash pip install git+https://github.com/chroma-core/chroma.git@x.y.z ``` It is sometimes useful to install a version of Chroma that has still some unreleased functionality. Like a PR that either fixes a bug or brings in a new functionality you may need. To test such unreleased code it is possible to install directly from a GH PR branch. ```bash pip install git+https://github.com/chroma-core/chroma.git@ ``` ## JavaScript/TypeScript To install the Chroma JS/TS client package, use the following command depending on your package manager. Here's a [minimal working example](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/install/js/verify.mjs) to confirm your installation. ```bash yarn add chromadb @chroma-core/default-embed ``` ```bash npm install --save chromadb @chroma-core/default-embed ``` ```bash pnpm add chromadb @chroma-core/default-embed ``` Embedding Function Packages All embedding function packages for JS/TS use the `@chroma-core/*` namespace. For example: - `@chroma-core/default-embed` - Default embedding function - `@chroma-core/openai` - OpenAI embeddings - `@chroma-core/cohere` - Cohere embeddings ## Go ```bash go get github.com/amikos-tech/chroma-go ``` Here's a [minimal working example](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/install/go/main.go) to confirm your installation. ## Rust ```bash cargo add chroma ``` Here's a [minimal working example](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/install/rust/src/main.rs) to confirm your installation. # Resource Requirements Chroma makes use of the following compute resources: - RAM - In single-node/self-hosted deployments, Chroma keeps vector search structures in memory for low-latency search. - Disk - Chroma persists data to disk, including the vector index files, metadata index, system DB, and write-ahead log (WAL). - CPU - Chroma uses CPU for ingest, indexing, filtering, and search execution. Use the formulas and heuristics below to estimate Chroma resource needs. Scope This page is primarily for single-node/self-hosted sizing (HNSW-based indexing). Distributed/Cloud deployments use different index internals (SPANN) and different resource behavior. See [Core Concepts](https://cookbook.chromadb.dev/core/concepts/#how-data-flows-through-chroma-cloud-distributed-chroma) and [SPANN Index Configuration](https://cookbook.chromadb.dev/core/configuration/#spann-index-configuration) for the distributed model. ## RAM Once you select your embedding model, start with this lower-bound estimate for vector payload memory: # payload_bytes vectors Γ— dimensions Γ— 4 # payload_gib payload_bytes 1024 3 Example (`10,000,000` vectors, `1536` dimensions): # payload_bytes # 10,000,000 Γ— 1536 Γ— 4 61,440,000,000 # payload_gib 61,440,000,000 1024 3 β‰ˆ 57.2 Β GiB - `number of vectors` - This is the number of vectors you plan to index. These are the documents in your Chroma collection (or chunks if you use LlamaIndex or LangChain terminology). - `dimensionality of vectors` - This is the dimensionality of the vectors output by your embedding model. For example, if you use the `sentence-transformers/paraphrase-MiniLM-L6-v2` model, the dimensionality of the vectors is 384. - `4 bytes` - This is the size of each component of a vector. Chroma relies on HNSW lib implementation that uses 32bit floats. Treat this as a lower-bound estimate. Real memory usage is higher because of: - HNSW graph/link overhead (affected by HNSW settings such as `hnsw:M` and `hnsw:construction_ef`) - In-memory bruteforce buffer (`hnsw:batch_size`) before vectors are merged into HNSW - Query-time and filter working memory, plus normal process overhead For production sizing, add headroom and validate under realistic load. ## Quick Resource Calculator Use this lightweight estimator to get a practical starting point. Dataset size (number of vectors) 10k 100k 1M 10M Selected: **-** Embedding model (dimension) MiniLM (384) Cohere (1024) OAI 3-small (1536) OAI 3-large (3072) Memory profile Balanced (+30%) Lean prototype (+15%) Heavy query load (+50%) Storage profile Typical mixed (~3x) Vector-heavy (~2x) Metadata-heavy (~4x) Vector payload - Estimated RAM - Estimated Disk - CPU hint - This calculator is for single-node/self-hosted sizing. For distributed/cloud (SPANN), see [Core Concepts](https://cookbook.chromadb.dev/core/concepts/#how-data-flows-through-chroma-cloud-distributed-chroma) and [SPANN Index Configuration](https://cookbook.chromadb.dev/core/configuration/#spann-index-configuration). Important: vector-first estimate This calculator is **vector-first** and does not fully model document or index growth. If you store large documents (near model context limits), **document and metadata storage can exceed vector storage**. Chroma also maintains a SQLite full-text index (`embedding_fulltext_search`) used by `where_document` queries, which adds disk overhead. See [Storage Layout](https://cookbook.chromadb.dev/core/storage-layout/#metadata-segment) and [Filters](https://cookbook.chromadb.dev/core/filters/index.md) for details. ## Disk Disk storage requirements depend on vectors, documents, metadata, and WAL behavior. Use these heuristics: - Start with at least the raw vector footprint plus several GB for metadata/documents/system data - In many workloads, `2-4x` the vector payload estimate is a reasonable planning range - If you store large documents/metadata blobs, disk needs can exceed that range - If you rely on document filtering (`where_document`), plan additional space for SQLite FTS index structures WAL Cleanup Recent Chroma versions support automatic WAL pruning. If your persistent data directory was created on older versions, or if auto-pruning is disabled, run WAL cleanup once and verify WAL config. See [WAL Pruning](https://cookbook.chromadb.dev/core/advanced/wal-pruning/index.md) and [Maintenance](https://cookbook.chromadb.dev/running/maintenance/#wal-configuration). ### Temporary Disk Space Chroma uses temporary storage for its SQLite3 related operations - sorting and buffering large queries. By default, SQLite3 uses `/tmp` for temporary storage. There are two guidelines to follow: - Have enough space if your application intends to make large queries or has multiple concurrent queries. - Ensure temporary storage is on a fast disk to avoid performance bottlenecks. You can configure the location of sqlite temp files with the `SQLITE_TMPDIR` environment variable. SQLite3 Temporary Storage You can read more about SQLite3 temporary storage in the [SQLite3 documentation](https://www.sqlite.org/tempfiles.html). ## CPU There are no hard requirements for the CPU, but it is recommended to use as much CPU as you can spare as it directly relates to index and search speeds. # Storage Layout When configured as `PersistentClient` or running as a server, Chroma persists its data under the provided `persist_directory`. For `PersistentClient` the persistent directory is usually passed as `path` parameter when creating the client, if not passed the default is `./chroma/` (relative path to where the client is started from). For the server, the persistent directory can be passed as environment variable `PERSIST_DIRECTORY` or as a command line argument `--path`. If not passed, the default is `./chroma/` (relative path to where the server is started). Once the client or the server is started a basic directory structure is created under the persistent directory containing the `chroma.sqlite3` file. Once collections are created and data is added, subdirectories are created for each collection. The subdirectories are UUID-named and refer to the vector segment. Chroma Ops - Maintenance CLI If you are looking maintenance CLI that can help you inspect, configure and improve the performance of your Chroma, try [Chroma Ops](https://github.com/amikos-tech/chromadb-ops?tab=readme-ov-file#chroma-maintenance-cli). ## Directory Structure The following diagram represents a typical Chroma persistent directory structure: ### `chroma.sqlite3` Note about the tables While we try to make it as accurate as possible chroma data layout inside the `slite3` database is subject to change. The following description is valid as of version `0.5.0`. The tables are also not representative of the distributed architecture of Chroma. The `chroma.sqlite3` is typical for Chroma single-node. The file contains the following four types of data: - Sysdb - Chroma system database, responsible for storing tenant, database, collection and segment information. - WAL - the write-ahead log, which is used to ensure durability of the data. - Metadata Segment - all metadata and documents stored in Chroma. - Migrations - the database schema migration scripts. - Collections - each collection has its own subdirectory, a UUIDv4-named diretory which stores HNSW index and its metadata. #### Sysdb The system database comprises the following tables: - `tenants` - contains all the tenants in the system. Usually gets initialized with a single tenant - `default_tenant`. - `databases` - contains all the databases per tenant. Usually gets initialized with a single database - `default_database` related to the `default_tenant`. - `collections` - contains all the collections per database. - `collection_metadata` - contains all the metadata associated with each collection. The metadata for a collection consists of any user-specified key-value pairs and the [`hnsw:*` keys](https://cookbook.chromadb.dev/core/configuration/#hnsw-configuration-legacy) that store the HNSW index parameters. - `segments` - contains all the segments per collection. Each collection gets two segments - `metadata` and `vector`. - `segment_metadata` - contains all the metadata associated with each segment. This table contains [`hnsw:*` keys](https://cookbook.chromadb.dev/core/configuration/#hnsw-configuration-legacy) that store the HNSW index parameters for the vector segment. #### WAL The write-ahead log is a table that stores all the changes made to the database. It is used to ensure that the data is durable and can be recovered in case of a crash. The WAL is composed of the following tables: - `embeddings_queue` - contains all data ingested into Chroma. Each row of the table represents an operation upon a collection (add, update, delete, upsert). The row contains all the necessary information (embedding, document, metadata and associated relationship to a collection) to replay the operation and ensure data consistency. - `embeddings_queue_config` - contains the configuration for the embedding queue. As of version `0.6.3` the configuration only pertains to automatic embedding queue purging. - `max_seq_id` - maintains the maximum sequence ID of the metadata segment that is used as a WAL replay starting point for the metadata segment. Example Queue Configuration ```json { "automatically_purge": true, "_type": "EmbeddingsQueueConfigurationInternal" } ``` #### Metadata Segment The metadata segment is a table that stores all the metadata and documents stored in Chroma. The metadata segment is composed of the following tables: - `embeddings` - contains embedding listings for all collections. - `embedding_metadata` - contains all the metadata associated with each document and its embedding. - `embedding_fulltext_search` - document full-text search index. This is a virtual table and upon inspection of the sqlite will appear as a series of tables starting with `embedding_fulltext_search_`. This is an FTS5 table and is used for full-text search queries on documents stored in Chroma (via `where_document` filter in `query` and `get` methods). #### Migrations The migrations table contains all schema migrations applied to the `chroma.sqlite3` database. The table is used for tracking applied migrations. ### Collection Subdirectories Each collection has its own subdirectory, a UUIDv4-named diretory which stores HNSW index and its metadata. - `header.bin` - Holds metadata about the index, such as its parameters and structure details. - `length.bin` - Records the number of links each node has, aiding in efficient traversal during searches. - `link_lists.bin` - Stores the adjacency lists for nodes, detailing their connections within the graph. - `data_level0.bin` - Contains the base layer of the hierarchical graph, storing the actual vectors and their connections. - `index_metadata.pickle` - Chroma specific metadata about mapping between ids in `embeddings` table and labels in the HNSW index. # Chroma System Constraints This section contains common constraints of Chroma. - Chroma is thread-safe - Chroma is not process-safe for concurrent writers sharing the same local persistence path - Multiple Chroma clients can be created from one or more threads within the same process (`PersistentClient`, `EphemeralClient`/`Client`, `HttpClient`, `AsyncHttpClient`, `CloudClient`) - A collection's name is unique within a database (and therefore unique for a given Tenant + DB pair) - A collection's dimension is fixed once embeddings are first written; subsequent writes and query embeddings must match that dimension - Treat embedding function + distance metric as part of a collection's retrieval contract. Even where compatible embedding-function configuration updates are accepted, existing vectors are not re-embedded; switching models for existing data requires creating a new collection and re-indexing. - Chroma is commonly used in two deployment patterns in this cookbook: embedded (local `PersistentClient`/`EphemeralClient`) and client/server (`HttpClient`/`AsyncHttpClient` with a running Chroma server, including managed cloud deployments) - The distance function cannot be changed after collection creation. - In embedded/single-node deployments, vector index segments are loaded into process memory on use. Memory growth follows the active working set unless you configure segment-cache controls (for example LRU with a memory limit). - For single-node deployments, scaling is primarily vertical. For horizontal scale across larger workloads, use distributed Chroma/Cloud (or application-managed sharding across multiple single-node instances). ## Operational Modes In this cookbook, Chroma is shown in two deployment patterns: - Embedded (local process) - Chroma runs in your application process with local persistence. - Client/Server - Chroma runs as a separate server process and clients connect over HTTP. This includes both self-hosted servers and managed cloud deployments. Depending on the mode you choose, you will need to consider the following component responsibilities: - Embedded: - Local clients (`PersistentClient`, `EphemeralClient`, `Client`) - Responsible for persistence, embedding (if configured), and querying in the same process - Client/Server: - HTTP clients (`HttpClient`, `AsyncHttpClient`, JS/TS `ChromaClient`, `CloudClient`) - Responsible for embedding calls (or providing precomputed embeddings) and communication with the Chroma server APIs - Server - Responsible for persistence, indexing, and query execution ### Deployment Comparison | Deployment Pattern | Client Responsibilities | Server Responsibilities | Scaling Pattern | Best Fit | | ---------------------------------- | ----------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------- | | Embedded (local process) | Embed (if configured), write/read/query locally | N/A (in-process) | Primarily vertical | Local dev, small deployments, lowest latency | | Client/Server (single-node server) | Embed or provide embeddings, call API | Persistence, indexing, query execution | Primarily vertical for DB node; app tier can scale out | Shared server across apps/processes | | Distributed / Cloud | Embed or provide embeddings, call API | Distributed persistence/indexing/query services | Horizontal | Large scale, high concurrency, operational isolation | # Tenants and Databases Tenants and Databases are two grouping abstractions that provides means to organize and manage data in Chroma. ## Tenants A tenant is a logical grouping of databases. ## Databases A database is a logical grouping of collections. # Chroma Queries This page explains what happens after you call `get()`, `query()`, or `search()`. We reuse the same running example from [Search Concepts](https://cookbook.chromadb.dev/core/concepts/#search-concepts): - Query intent: "Find troubleshooting docs about SSO login failures." - Constraints: only `status=published`, `year >= 2024` - Output goal: top 20 results (`title` + `score`), without one product area dominating. ## Core Concepts Modern Rust Chroma has two query execution paths: - **Local single-node** (`query()/get()` path): local executor over `Sqlite` metadata + local HNSW segments. - **Distributed / Cloud** (`search()` / distributed query workers): worker orchestrators over distributed segments (`BlockfileMetadata`, `BlockfileRecord`, `HnswDistributed`, `Spann`, `QuantizedSpann`) plus WAL/log materialization. When a query runs, Chroma pulls from three storage/index families: | Need | Local single-node | Distributed / Cloud | | -------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------- | | Metadata + documents | SQLite tables + FTS5 (`where_document`) in `chroma.sqlite3` | Blockfile-backed record/metadata segments + recent log materialization | | Vector ANN | Local HNSW segments (persisted or memory) | `HnswDistributed`, `Spann`, or `QuantizedSpann` (by collection config) | | Ranking primitives | KNN (`query`) | KNN + sparse paths + rank expressions/fusion (`search`) | For deeper Rust internals (segment families and Arrow-backed blockfiles), see [Concepts](https://cookbook.chromadb.dev/core/concepts/#main-primitives-and-index-families) and [Blockfile Format and Update Model](https://cookbook.chromadb.dev/core/concepts/#blockfile-format-and-update-model). ## Query Pipeline Quick flow: 1. **Validation**: reject malformed requests early. 1. **Candidate selection**: apply filters/IDs to decide which records can compete. 1. **KNN/rank evaluation**: score and order eligible records. 1. **Field loading (projection/select)**: fetch the fields you asked for. 1. **Result aggregation**: return final rows in the requested shape. The same idea in one line: **filter -> score -> load fields -> return page**. Query Pipeline? Why is it called a pipeline? Because each step in the query process depends on its predecessor's output. ## Advanced Search Semantics (Cloud + Local) This section explains how query composition works beyond filter syntax. ### Stage Model The same query intent is typically expressed as: 1. Candidate selection (filters) 1. Relevance ranking 1. Optional ranking fusion (hybrid) 1. Optional grouping/aggregation 1. Response shaping (pagination + field selection) | Stage | What it controls | Local `query()/get()` | Cloud Search API | | -------------------- | ------------------------------ | --------------------------------------- | ------------------------------------------ | | Candidate selection | Which records are eligible | `where`, `where_document` | `where(...)` | | Relevance ranking | Order of eligible records | KNN over query embeddings | `rank(Knn(...))` or other rank expressions | | Hybrid fusion | Merge multiple ranking signals | Not available as native query primitive | `rank(Rrf(...))` | | Grouping/aggregation | Per-group diversity and caps | Not available as native query primitive | `groupBy(...)` (`MinK`/`MaxK`) | | Response shaping | Size and returned fields | `limit`, `offset`, `include` | `limit`, pagination, `select` | What each stage means: - **Filters** decide what gets into the eligible pool. - **Ranking** decides what goes first. - **Fusion/grouping** decide how to balance quality and diversity. - **Response shaping** decides what the client actually gets back. ### Boundary Rules - Filters (`where`, `where_document`) define eligibility, not final order. - Ranking defines order among eligible candidates. - RRF is a rank-fusion step for combining multiple ranked lists. - Grouping/aggregation reshapes already-ranked results. - Pagination and field selection shape the final payload. ### Practical Tradeoffs - **Filter selectivity affects latency**: tighter filters reduce the eligible pool before similarity search. - **Ranking depth affects recall and latency**: deeper rank windows can improve recall but increase cost. - **Hybrid helps lexical plus semantic intent**: use RRF when exact token matches and semantic matches both matter. - **Grouping improves diversity**: use group caps when one category can dominate top results. Example: - Without grouping, top results might all come from `product_area=auth`. - With `groupBy(product_area)` and a per-group cap, results are more balanced across areas. ### Where to go next - [Filters](https://cookbook.chromadb.dev/core/filters/index.md) for exact operator syntax. - [Search Concepts](https://cookbook.chromadb.dev/core/concepts/#search-concepts) for the conceptual pipeline. - [Search API Overview](https://docs.trychroma.com/cloud/search-api/overview) for request composition. - [Ranking and Scoring](https://docs.trychroma.com/cloud/search-api/ranking), [Hybrid Search with RRF](https://docs.trychroma.com/cloud/search-api/hybrid-search), and [Group By & Aggregation](https://docs.trychroma.com/cloud/search-api/group-by) for Cloud behavior details. ### Validation The following validations are performed: - Validate `where` if present - Validate `where_document` if present - Ensure collection exists - Validate query embeddings dimensions match that of the collection Common failure patterns: - typo in filter operator - querying a collection with incompatible embedding dimensions - requesting unsupported fields/options for the chosen API path ### Metadata Pre-Filter Metadata pre-filter is the first narrowing step for filtered queries. - In local/single-node Chroma, this stage evaluates `where` and `where_document` against the SQLite metadata segment. - `where_document` is backed by SQLite FTS5 (`embedding_fulltext_search`) as documented in [Storage Layout](https://cookbook.chromadb.dev/core/storage-layout/#metadata-segment). - The output is an eligible ID set passed to the ANN stage; if no filters are provided, this stage is skipped. Why this matters: - Executing predicates before ANN search reduces unnecessary distance computations. - Highly selective filters can reduce latency, but may also reduce the number of final hits if few records satisfy the predicate. In our running example: - `where: {"status":"published","year":{"$gte":2024}}` - `where_document: {"$contains":"SSO"}` - Only matching IDs move on to KNN/rank. Research context: - Filtered ANN is known to be harder than unfiltered ANN because vector proximity and predicate selectivity can be weakly correlated. - See [ACORN (Patel et al., 2024)](https://arxiv.org/abs/2403.04871) and [Filtered-DiskANN (Gollapudi et al., WWW 2023)](https://doi.org/10.1145/3543507.3583552) for predicate-aware ANN design tradeoffs. ### KNN Search in HNSW Index This stage runs approximate nearest-neighbor search over the collection's HNSW vector index. - Chroma local stores one HNSW index per collection (see [Storage Layout](https://cookbook.chromadb.dev/core/storage-layout/#collection-subdirectories)). - If metadata pre-filter produced eligible IDs, the KNN stage searches within that constrained set. - If `include` requests embeddings, vectors can be returned as part of this stage. In our running example: - Chroma scores the query embedding against only the allowed IDs. - Lower distance means closer semantic match. Tuning and tradeoffs: - `ef_search` controls how many neighbors are explored at query time; increasing it typically improves recall at higher latency/memory cost. - Construction knobs such as `max_neighbors` (HNSW `M`) and `ef_construction` influence graph quality and memory/ingest cost. - Use current index configuration surfaces (`configuration.hnsw` / `configuration.spann`) rather than legacy `metadata` keys (see [Configuration](https://cookbook.chromadb.dev/core/configuration/#hnsw-index-configuration)). - For Cloud's schema-based configuration model (`VectorIndexConfig`, `HnswConfig`, `SpannConfig`), see [Index Configuration Reference](https://docs.trychroma.com/cloud/schema/index-reference) and [Schema Basics](https://docs.trychroma.com/cloud/schema/schema-basics). Research context: - HNSW's core idea is hierarchical small-world graph traversal with strong empirical recall/latency tradeoffs ([Malkov & Yashunin, 2016/2018](https://arxiv.org/abs/1603.09320)). - For Cloud Search API ranking semantics (distance-style ordering, candidate limits, and expression behavior), see [Ranking and Scoring](https://docs.trychroma.com/cloud/search-api/ranking). ### Post-Search Field Loading (Projection / Hydration) After KNN returns ranked records (`offset_id` + score), Chroma loads the requested fields to build the final payload. - **Local single-node path**: the local executor performs an explicit follow-up `Get` over returned IDs to load documents/metadata when requested. - **Distributed/Cloud query path**: worker execution uses `ProjectionOrchestrator`/`KnnProjection` (for `query/knn`) or `Select` in `RankOrchestrator` (for `search`) to load fields from record segments plus recent logs. - In distributed execution, this is not a SQLite post-query; it is an operator-stage merge of compacted segment data and WAL/log materialization. What this means: - Ranking can stay fast because it moves IDs/scores first. - Field loading happens afterward, only for the rows you keep. Why this stage exists: - ANN structures are optimized for vector neighborhood traversal, not full document/metadata row retrieval. - Decoupling ANN from payload hydration is a common system pattern in vector retrieval pipelines. Operational guidance: - Requested fields directly affect this stage cost: keep payload small (`include` in local APIs, `select` in Cloud Search API) when low latency is important. - For Cloud query composition and output shaping, see [Search API Overview](https://docs.trychroma.com/cloud/search-api/overview) and [Pagination & Selection](https://docs.trychroma.com/cloud/search-api/pagination-selection). - For single-node metadata/FTS storage details, see [Storage Layout](https://cookbook.chromadb.dev/core/storage-layout/#metadata-segment). ### Result Aggregation Result aggregation fuses ranked neighbors with hydrated payloads and requested fields into the final response shape. Example: if your request asks for `id`, `document`, and `score`, this is the stage that combines them into final rows (plus pagination/grouping effects, if configured). # Write-ahead Log (WAL) Pruning Chroma Write-Ahead Log is unbounded by default and grows indefinitely. This can lead to high disk usage and slow performance. To prevent this, it is recommended to prune/cleanup the WAL periodically. Below we offer a couple of tools, including an official and recommended CLI tool, to help you prune your WAL. ## Tooling There are two ways to prune your WAL: - Chroma CLI - this is the official tooling provided by Chroma and is the recommended way to prune your WAL. This functionality is available either from `main` branch or Chroma release `>0.5.5`. - [chroma-ops](https://github.com/amikos-tech/chromadb-ops) ### Chroma CLI To prune your WAL you need to install Chroma CLI (it comes as part of the core Chroma package): ```shell pip install chromadb chroma utils vacuum --path /path/to/persist_dir ``` Auto-pruning Running the above command will enable auto WAL pruning. This means that Chroma will periodically prune the WAL during its normal operations. ### Chroma Ops To prune your WAL you can run the following command: ```shell pip install chroma-ops chops cleanup-wal /path/to/persist_dir ``` > ⚠️ IMPORTANT: It is always a good thing to backup your data before you prune the WAL. ## Manual Steps: Stop Chroma It is vitally important that you stop Chroma before you prune the WAL. If you don't stop Chroma you risk corrupting - ⚠️ Stop Chroma - πŸ’Ύ Create a backup of your `chroma.sqlite3` file in your persistent dir - πŸ‘€ Check your current `chroma.sqlite3` size (e.g. `ls -lh /path/to/persist/dir/chroma.sqlite3`) - πŸ–₯️ Run the script below - πŸ”­ Check your current `chroma.sqlite3` size again to verify that the WAL has been pruned - πŸš€ Start Chroma Script (store it in a file like `compact-wal.sql`) wal_clean.py ```py #!/usr/bin/env python3 # Call the script: python wal_clean.py ./chroma-test-compact import os import sqlite3 from typing import cast, Optional, Dict import argparse import pickle class PersistentData: """Stores the data and metadata needed for a PersistentLocalHnswSegment""" dimensionality: Optional[int] total_elements_added: int max_seq_id: int id_to_label: Dict[str, int] label_to_id: Dict[int, str] id_to_seq_id: Dict[str, int] def load_from_file(filename: str) -> "PersistentData": """Load persistent data from a file""" with open(filename, "rb") as f: ret = cast(PersistentData, pickle.load(f)) return ret def clean_wal(chroma_persist_dir: str): if not os.path.exists(chroma_persist_dir): raise Exception(f"Persist {chroma_persist_dir} dir does not exist") if not os.path.exists(f'{chroma_persist_dir}/chroma.sqlite3'): raise Exception( f"SQL file not found int persist dir {chroma_persist_dir}/chroma.sqlite3") # Connect to SQLite database conn = sqlite3.connect(f'{chroma_persist_dir}/chroma.sqlite3') # Create a cursor object cursor = conn.cursor() # SQL query query = "SELECT id,topic FROM segments where scope='VECTOR'" # Replace with your query # Execute the query cursor.execute(query) # Fetch the results (if needed) results = cursor.fetchall() wal_cleanup_queries = [] for row in results: # print(row) metadata = load_from_file( f'{chroma_persist_dir}/{row[0]}/index_metadata.pickle') wal_cleanup_queries.append( f"DELETE FROM embeddings_queue WHERE seq_id < {metadata.max_seq_id} AND topic='{row[1]}';") cursor.executescript('\n'.join(wal_cleanup_queries)) # Close the cursor and connection cursor.close() conn.close() if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('persist_dir', type=str) arg = parser.parse_args() print(arg.persist_dir) clean_wal(arg.persist_dir) ``` Run the script ```shell # Let's create a backup tar -czvf /path/to/persist/dir/chroma.sqlite3.backup.tar.gz /path/to/persist/dir/chroma.sqlite3 lsof /path/to/persist/dir/chroma.sqlite3 # make sure that no process is using the file python wal_clean.py /path/to/persist/dir/ # start chroma ``` # Write-ahead Log (WAL) Chroma uses WAL to ensure data durability, even if things go wrong (e.g. server crashes). To achieve the latter Chroma uses what is known in the DB-industry as WAL or Write-Ahead Log. The purpose of the WAL is to ensure that each user request (aka transaction) is safely stored before acknowledging back to the user. Subsequently, in fact immediately after writing to the WAL, the data is also written to the index. This enables Chroma to serve as real-time search engine, where the data is available for querying immediately after it is written to the WAL. Below is a diagram that illustrates the WAL in ChromaDB (ca. 0.6.3): ## Vector Indices Overview The diagram below illustrates how data gets transferred from the WAL to the binary vector indices (Bruteforce and HNSW): For each collection Chroma maintains two binary indices - Bruteforce (in-memory, fast) and HNSW lib (persisted to disk, slow when adding new vectors and persisting). As you can imagine, the BF index serves the role of a buffer that holds the uncommitted to HNWS persisted index portion of the WAL. The HNSW index itself has a max sequence id counter, stored in a metadata file, that indicates from which position in the WAL the buffering to the BF index should begin. The latter buffering usually happens when the collection is first accessed. There are two transfer points (in the diagram, sync threshold) for BF to HNSW: - `hnsw:batch_size` - forces the BF vectors to be added to HNSW in-memory (this is a slow operation) - `hnsw:sync_threshold` - forces Chroma to dump the HNSW in-memory index to disk (this is a slow operation) - Both of the above sync points are controlled via Collection-level metadata with respective named params. It is customary `hnsw:sync_threshold` > `hnsw:batch_size` ## Metadata Indices Overview The following diagram illustrates how data gets transferred from the WAL to the metadata index: ## Further Reading For the DevOps minded folks we have a few more resources: - [WAL Pruning](https://cookbook.chromadb.dev/core/advanced/wal-pruning/index.md) - Clean up your WAL # Running Chroma # Deployment Patterns If you are building with Chroma, you usually start with one of two setups: 1. **Embedded mode** - Chroma runs inside your Python process (`PersistentClient`) 1. **Server mode** - Chroma runs as a standalone service and your app connects via HTTP (`HttpClient`) You do not need to pick forever. Many teams start embedded, then move to server mode when they need shared access across apps or machines. Runnable Examples Complete, standalone files for this page are in [examples/deployment-patterns](https://github.com/amikos-tech/chroma-cookbook/tree/main/examples/deployment-patterns): - [Embedded Python app (`app_embedded.py`)](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/deployment-patterns/embedded/python/app_embedded.py) - [Server Python app (`app_http.py`)](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/deployment-patterns/server/python/app_http.py) - [Docker Compose (`docker-compose.yml`)](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/deployment-patterns/server/docker-compose.yml) - [Python dependencies (`requirements.txt`)](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/deployment-patterns/requirements.txt) ## Embedded in your application This is the easiest way to get moving: your app and Chroma run in the same Python process, and data is stored in a local folder. Pick this when you want: - the shortest path from idea to working prototype - simple local development (no separate DB service to run) - low-latency reads/writes in one app process One important gotcha Chroma is thread-safe but not process-safe. Avoid multiple processes writing to the same local `path`. ### Example 1: Embedded `PersistentClient` in a Python service 1. Install Chroma: ```bash pip install chromadb ``` If you are running the repo examples directly, use pinned dependencies: ```bash pip install -r examples/deployment-patterns/requirements.txt ``` 1. Create `app_embedded.py`. A complete runnable version is available at [`examples/deployment-patterns/embedded/python/app_embedded.py`](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/deployment-patterns/embedded/python/app_embedded.py). ```python import chromadb class EmbeddedKnowledgeBase: def __init__(self, path: str = "./chroma_data") -> None: self.client = chromadb.PersistentClient(path=path) self.collection = self.client.get_or_create_collection( name="support_kb", embedding_function=None, # using explicit embeddings below ) def add_article(self, article_id: str, text: str, embedding: list[float], product: str) -> None: self.collection.upsert( ids=[article_id], documents=[text], embeddings=[embedding], metadatas=[{"product": product}], ) def search(self, query_embedding: list[float], product: str, n_results: int = 2) -> dict: return self.collection.query( query_embeddings=[query_embedding], where={"product": product}, n_results=n_results, include=["documents", "metadatas", "distances"], ) if __name__ == "__main__": kb = EmbeddedKnowledgeBase(path="./chroma_data") kb.add_article( article_id="a1", text="Refunds are available within 30 days for annual plans.", embedding=[0.11, 0.20, 0.31], product="billing", ) kb.add_article( article_id="a2", text="You can rotate API keys from the admin settings page.", embedding=[0.12, 0.18, 0.30], product="platform", ) result = kb.search(query_embedding=[0.10, 0.19, 0.32], product="billing") print("Top match:", result["documents"][0]) ``` 1. Run it: ```bash python app_embedded.py ``` That is it. Your vectors are now persisted under `./chroma_data` and travel with your app lifecycle. ## Standalone server In this pattern, Chroma runs as its own service. Your app talks to it over HTTP using `HttpClient`. Pick this when you want: - multiple app instances using one shared Chroma deployment - a clean boundary between app code and database service - the option to scale app and database separately ### Example 2: Typical server deployment + Python `HttpClient` 1. Start Chroma server with Docker Compose. A complete runnable version is available at [`examples/deployment-patterns/server/docker-compose.yml`](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/deployment-patterns/server/docker-compose.yml). docker-compose.yml ```yaml services: chroma: image: chromadb/chroma:1.5.3 ports: - "8000:8000" volumes: - ./chroma-data:/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/api/v2/heartbeat"] interval: 30s timeout: 10s retries: 3 ``` ```bash docker compose up -d ``` 1. Create `app_http.py` and connect with `HttpClient`. A complete runnable version is available at [`examples/deployment-patterns/server/python/app_http.py`](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/deployment-patterns/server/python/app_http.py). ```python import os import chromadb def get_client() -> chromadb.HttpClient: return chromadb.HttpClient( host=os.getenv("CHROMA_HOST", "localhost"), port=int(os.getenv("CHROMA_PORT", "8000")), ssl=os.getenv("CHROMA_SSL", "false").lower() == "true", ) if __name__ == "__main__": client = get_client() collection = client.get_or_create_collection( name="support_kb", embedding_function=None, ) collection.upsert( ids=["a3"], documents=["Password reset links expire after 15 minutes."], embeddings=[[0.09, 0.22, 0.28]], metadatas=[{"product": "platform"}], ) result = collection.query( query_embeddings=[[0.10, 0.21, 0.29]], n_results=1, where={"product": "platform"}, include=["documents", "distances"], ) print("Top match:", result["documents"][0]) ``` 1. Set host/port when needed (for example in containers): ```bash export CHROMA_HOST=chroma export CHROMA_PORT=8000 python app_http.py ``` ### Usual production shape - put Chroma behind a reverse proxy or load balancer for HTTPS and controlled access - mount durable storage for Chroma data (`/data` in container deployments) - use `/api/v2/heartbeat` for health checks - scale your app replicas and Chroma deployment independently For next steps, see: - [Running Chroma](https://cookbook.chromadb.dev/running/running-chroma/index.md) - [Health Checks](https://cookbook.chromadb.dev/running/health-checks/index.md) - [Systemd service](https://cookbook.chromadb.dev/running/systemd-service/index.md) - [Road To Production](https://cookbook.chromadb.dev/running/road-to-prod/index.md) # Health Checks Health checks tell your orchestrator whether Chroma is running and ready to serve requests. They are used to keep traffic away from unready instances, trigger automated restarts for unhealthy ones, and coordinate startup order for dependent services. ## Docker Compose The simplest form of health check is to use the `healthcheck` directive in the `docker-compose.yml` file. This is useful if you are deploying Chroma alongside other services that may depend on it. ```yaml services: chromadb: image: chromadb/chroma:1.5.3 volumes: - ./chroma-data:/data ports: - "8000:8000" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/api/v2/heartbeat"] interval: 30s timeout: 10s retries: 3 ``` ## Kubernetes In kubernetes you can use the `livenessProbe` and `readinessProbe` to check the health of the server. This is useful if you are deploying Chroma in a kubernetes cluster. ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: chroma labels: app: chroma spec: replicas: 1 selector: matchLabels: app: chroma template: metadata: labels: app: chroma spec: containers: - name: chroma image: ports: - containerPort: 8000 livenessProbe: httpGet: path: /api/v2/heartbeat port: 8000 initialDelaySeconds: 5 periodSeconds: 5 readinessProbe: httpGet: path: /api/v2/heartbeat port: 8000 initialDelaySeconds: 5 periodSeconds: 5 startupProbe: httpGet: path: /api/v2/heartbeat port: 8000 failureThreshold: 3 periodSeconds: 60 initialDelaySeconds: 60 ``` Alternative to the `httpGet` you can also use `tcpSocket`: ```yaml readinessProbe: tcpSocket: port: 8000 failureThreshold: 3 timeoutSeconds: 30 periodSeconds: 60 livenessProbe: tcpSocket: port: 8000 failureThreshold: 3 timeoutSeconds: 30 periodSeconds: 60 startupProbe: tcpSocket: port: 8000 failureThreshold: 3 periodSeconds: 60 initialDelaySeconds: 60 ``` # Maintenance This section describes maintenance tooling and procedures for running your Chroma database. ## Chroma Ops (Tooling) [Chroma Ops](https://github.com/amikos-tech/chromadb-ops) is a maintenance CLI for Chroma. It provides a set of commands for inspecting, configuring and improving the performance of your Chroma database. ### Use Cases Chroma Ops is designed to help you maintain a healthy Chroma database. It can also be used for inspecting the state of your database. The following use cases are supported: - πŸ“¦ Database Maintenance - [`db info`](#database-info) - gathers general information about your Chroma persistent database - [`db clean`](#database-clean) - cleans up the database from unused files (for now only orphanated HNSW segment directories) - πŸ“ Write-Ahead Log (WAL) Maintenance - [`wal info`](#wal-info) - gathers information about the Write-Ahead Log (WAL) - [`wal commit`](#wal-commit) - commits the WAL to all collections with outstanding changes - [`wal export`](#wal-export) - exports the WAL to a `jsonl` file. This can be used for debugging and for auditing. - [`wal config`](#wal-configuration) - allows you to configure the WAL for your Chroma database. - [`wal clean`](#wal-clean) - cleans up the WAL from old, committed transactions. - πŸ” Full Text Search (FTS) Maintenance - [`fts rebuild`](#fts-rebuild) - rebuilds the FTS index for all collections or change the tokenizer. - 🧬 Vector Index (HNSW) Maintenance - [`hnsw info`](#hnsw-info) - gathers information about the HNSW index for a given collection - [`hnsw rebuild`](#hnsw-rebuild) - rebuilds the HNSW index for a given collection and allows the modification of otherwise immutable (construction-only) parameters. Useful command to keep your HNSW index healthy and prevent fragmentation. - [`hnsw config`](#hnsw-configuration) - allows you to configure the HNSW index for your Chroma database. - πŸ“Έ Collection Maintenance - [`collection snapshot`](#collection-snapshot) - creates a snapshot of a collection. The snapshots are self-contained and are meant to be used for backup and restore. Need help/Need more? If you need help or need more features, please join the [Discord server](https://discord.gg/MMeYNTmh3x) and let us know. Or just do a pull request on [GitHub](https://github.com/amikos-tech/chromadb-ops/pulls). ### Installation Chroma Ops can be installed using pip: ```bash pip install --upgrade chromadb-ops ``` ### Usage #### Database Maintenance ##### Database Info What it does: Gathers general information about your Chroma persistent database (works only for local persistent databases). Why it's useful: Run this command to better understand the current state of your database. It can provide you with invaluable information about any potential issues and also helps us help you in debugging issues. ```bash chops db info /path/to/persist_dir ``` Options: - `--skip-collection-names` (`-s`) - to skip specific collections - `--privacy-mode` (`-p`) - privacy mode hides paths and collection names so that the output can be shared without exposing sensitive information When sharing larger outputs consider storing the output in a file: ```bash chops db info /path/to/persist_dir -p > chroma_info.txt ``` Example output: ```console chops db info smallc General Info ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┑━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ β”‚ Chroma Version β”‚ 0.5.5 β”‚ β”‚ Number of Collection β”‚ 1 β”‚ β”‚ Persist Directory β”‚ /tmp/tmp9l3ceuvp β”‚ β”‚ Persist Directory Size β”‚ 142.2MiB β”‚ β”‚ SystemDB size: β”‚ 81.6MiB (/tmp/tmp9l3ceuvp/chroma.sqlite3) β”‚ β”‚ Orphan HNSW Directories β”‚ [] β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ───────────────────────────────── Collections ────────────────────────────────── ───────────────────────────────────── test ───────────────────────────────────── 'test' Collection Data ┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Table Data ┃ Value ┃ ┑━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ β”‚ ID β”‚ 9e80e4fd-fd4b-47b8-810c-e8ffa57c1912 β”‚ β”‚ Name β”‚ test β”‚ β”‚ Metadata β”‚ None β”‚ β”‚ Dimension β”‚ 1536 β”‚ β”‚ Tenant β”‚ default_tenant β”‚ β”‚ Database β”‚ default_database β”‚ β”‚ Records β”‚ 10,000 β”‚ β”‚ WAL Entries β”‚ 10,000 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ─────────────────────────────────── Segments ─────────────────────────────────── Metadata Segment (test) ┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ β”‚ Segment ID β”‚ 832fa2cd-6c40-4eee-ad7d-35f260acaaaa β”‚ β”‚ Type β”‚ urn:chroma:segment/metadata/sqlite β”‚ β”‚ Scope β”‚ METADATA β”‚ β”‚ SysDB Max Seq ID β”‚ 10,000 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ HNSW Segment (test) ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Property ┃ Value ┃ ┑━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ β”‚ Segment ID β”‚ 13609103-d317-4556-a744-008c96229b72 β”‚ β”‚ Type β”‚ urn:chroma:segment/vector/hnsw-local-persist… β”‚ β”‚ Scope β”‚ VECTOR β”‚ β”‚ Path β”‚ /tmp/tmp9l3ceuvp/13609103-d317-4556-a744-008… β”‚ β”‚ SysDB Max Seq ID β”‚ 0 β”‚ β”‚ HNSW Dir Size β”‚ 60.6MiB β”‚ β”‚ HNSW Metadata Max Seq ID β”‚ 10,000 β”‚ β”‚ HNSW Metadata Total Labels β”‚ 10,000 β”‚ β”‚ WAL Gap β”‚ 0 β”‚ β”‚ HNSW Raw Total Active Labels β”‚ 10,000 β”‚ β”‚ HNSW Raw Allocated Labels β”‚ 10,000 β”‚ β”‚ HNSW Orphan Labels β”‚ set() β”‚ β”‚ Fragmentation Level β”‚ 0.0 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ⚠️ Interesting things to look for: - Fragmentation Level - the higher the value the more unnecessary memory and performance hits your HNSW index suffers. It needs to be rebuilt. - Orphan HNSW Directories - these are directories that are not associated with any collection. They can be safely deleted. - WAL Entries - high values usually means that you need prune your WAL. Use either this tool or the [official Chroma CLI](https://cookbook.chromadb.dev/core/advanced/wal-pruning/#chroma-cli). - HNSW Orphan Labels - this must always be empty set, if you see anything else report it in [Discord @taz](https://discord.gg/MMeYNTmh3x). **How to Read the output** ***General Info*** This section presents general Chroma persistent dir info. - Chroma Version - the currently installed Chroma version. - Number of Collection - the number of collections in the persistent dir. - Persist Directory - the path to the persistent dir (if privacy mode is off). - Persist Directory Size - the size of the persistent dir. - SystemDB size - the size of the system database (if privacy mode is off the full path to the sqlite3 file is shown). - Orphan HNSW Directories - a list of orphan HNSW directories. These directories are present in the persistent dir but are not associated with any collection. ***Collections*** - ID - the collection ID. - Name - the collection name. - Metadata - the metadata associated with the collection. - Dimension - the dimension of the embeddings in the collection. (this can be None in case no vectors are present and the collection is newly created). - Tenant - the tenant of the collection. - Database - the database of the collection. - Records - the number of records in the collection. - WAL Entries - the number of WAL entries in the collection (as of 0.5.5 for new instances Chroma will clean WAL for each collection periodically). ***Metadata Segment*** - Segment ID - the segment ID. - Type - the segment type. - Scope - the segment scope. - SysDB Max Seq ID - the maximum sequence ID in the system database. ***HNSW Segment*** - Segment ID - the segment ID. - Type - the segment type. - Scope - the segment scope. - Path - the path to the HNSW directory. - SysDB Max Seq ID - the maximum sequence ID in the system database. - HNSW Dir Size - the size of the HNSW directory. - HNSW Metadata Max Seq ID - the maximum sequence ID in the HNSW metadata. - HNSW Metadata Total Labels - the total number of labels in the HNSW metadata. - WAL Gap - the difference between the maximum sequence ID in the system database and the maximum sequence ID in the HNSW metadata. The gap usually represents the number of WAL entries that are not committed to the HNSW index. - HNSW Raw Total Active Labels - the total number of active labels in the HNSW index. - HNSW Raw Allocated Labels - the total number of allocated labels in the HNSW index. - HNSW Orphan Labels - a set of orphan labels in the HNSW index. These are labels in the HNSW index that are not visible to Chroma as they are not part of the metadata. This set should always be empty, if not please report it!!! - Fragmentation Level - the fragmentation level of the HNSW index. ##### Database Clean *What it does*: Cleans up the database from unused files. It will remove all orphanated HNSW segment directories. *Why it's useful*: Orphanated HNSW segment directories sometimes are the byproduct of a filesystem failure to remove the HNSW segment directory, most commonly encountered on Windows systems, but any type of file loocking or disk operation failure can cause Chroma to leave behind these directories. ```bash chops db clean /path/to/persist_dir ``` Supported options are: - `--dry-run` (`-d`) - to see what would be deleted without actually deleting anything. Example output: ```console chops db clean smallc ChromaDB version: 0.6.2 Cleaning up orphanated segment dirs... Orphanated HNSW segment dirs ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Segment ID ┃ Path ┃ ┑━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ β”‚ 2E9021A8-A767-4339-B2C2-2F4B22C05F1D β”‚ smallc/2E9021A8-A767-4339-B2C2-2F4B22C05F1D β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Are you sure you want to delete these segment dirs? [y/N]: ``` #### WAL Maintenance ##### WAL Info *What it does*: Gathers information about the Write-Ahead Log (WAL). *Why it's useful*: Run this command to better understand the current state of the Write-Ahead Log (WAL). It can provide you with invaluable information about any potential issues and also helps us help you in debugging issues. ```bash chops wal info /path/to/persist_dir ``` Example output: ```console chops wal info smallc ChromaDB version: 0.6.2 WAL config is set to: auto purge. WAL Info ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓ ┃ Collection ┃ Topic ┃ Count ┃ ┑━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩ β”‚ test β”‚ persistent://default/default/97f5234e-d02a-43b8-9909-99447950c949 β”‚ 20 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ##### WAL Export *What it does*: Exports the Write-Ahead Log (WAL) to a `jsonl` file. This can be used for debugging and for auditing. *Why it's useful*: This command is useful for exporting the Write-Ahead Log (WAL) to a `jsonl` file. This can be used for debugging and for auditing. ```bash chops wal export /path/to/persist_dir ``` Example output: ```console chops wal export smallc --out wal.jsonl ChromaDB version: 0.6.2 Exporting WAL ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━┓ ┃ Collection ┃ WAL Entries ┃ ┑━━━━━━━━━━━━╇━━━━━━━━━━━━━┩ β”‚ test β”‚ 20 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Are you sure you want to export the WAL? [y/N]: y Exported 20 rows ``` ##### WAL Commit *What it does*: Commits the Write-Ahead Log (WAL) to all collections with outstanding changes. *Why it's useful*: This command is useful for committing the Write-Ahead Log (WAL) to all collections with outstanding changes. ```bash chops wal commit /path/to/persist_dir ``` Options: - `--skip` (`-s`) - skip certain collections by running `chops wal commit /path/to/persist_dir --skip ` - `--yes` (`-y`) - skip confirmation prompt (default: `False`, prompt will be shown) Example output: ```console chops wal commit smallc ChromaDB version: 0.6.2 WAL Commit Summary ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━┓ ┃ Collection ┃ WAL Entries ┃ ┑━━━━━━━━━━━━╇━━━━━━━━━━━━━┩ β”‚ test β”‚ 20 β”‚ β”‚ test1 β”‚ 0 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Skipped Collections ┏━━━━━━━━━━━━┓ ┃ Collection ┃ ┑━━━━━━━━━━━━┩ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Are you sure you want to commit the WAL in smallc? As part of the WAL commit action your database will be migrated to currently installed version 0.6.2. [y/N]: y Processing index for collection test (0137d64b-8d71-42f5-b0d9-28716647b068) - total vectors in index 20 WAL commit completed. ``` ##### WAL Clean *What it does*: Cleans up the Write-Ahead Log (WAL) from committed transactions. Recent Chroma version automatically prune the WAL so this is not needed unless you have older version of Chroma or disabled automatic WAL pruning. *Why it's useful*: Keep your WAL in check so it doesn't grow too large (in case automatic WAL pruning is disabled). ```bash chops wal clean /path/to/persist_dir ``` Options: - `--yes` (`-y`) - skip confirmation prompt (default: `False`, prompt will be shown) Example output: ```console chops wal clean smallc 11:33:36 ☁ main β˜‚ ⚑ ✭ ChromaDB version: 0.6.2 Size before: 429596 Are you sure you want to clean up the WAL in smallc? This action will delete all WAL entries that are not committed to the HNSW index. [y/N]: y Cleaning up WAL WAL cleaned up. Size after: 388636 ``` ##### WAL Configuration *What it does*: Configures the Write-Ahead Log (WAL) for your Chroma database. *Why it's useful*: This command is useful for configuring the Write-Ahead Log (WAL) for your Chroma database. ```bash chops wal config /path/to/persist_dir --purge off ``` Options: - `--purge` option can be set to `auto` (automatically purge the WAL when the number of records in the collection exceeds the number of records in the WAL) or `off` (disable automatic purge of the WAL). Automatic WAL purge is enabled by default. The automatic purge keeps your slite3 file smaller and faster, but it makes it hard or impossible to restore Chroma. - `--yes` option can be set to `true` (skip confirmation prompt) or `false` (show confirmation prompt). The default is `false`. Example output: ```console chops wal config smallc --purge off ChromaDB version: 0.6.2 Current WAL config ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Config key ┃ Config Change ┃ ┑━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ β”‚ Automatically purge (automatically_purge) β”‚ True (old) -> False (new) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Are you sure you want to update the WAL config? [y/N]: y WAL config updated successfully! ``` #### Full Text Search (FTS) Maintenance ##### FTS Rebuild *What it does*: Rebuilds the Full Text Search (FTS) index for all collections. *Why it's useful*: This command is useful for rebuilding the Full Text Search (FTS) index for all collections. ```bash chops fts rebuild /path/to/persist_dir ``` Additional options: - `--yes` (`-y`) - skip confirmation prompt (default: `False`, prompt will be shown) - `--tokenizer` (`-t`) - the tokenizer to use for the index. Example output: ```console chops fts rebuild --tokenizer unicode61 smallc ChromaDB version: 0.6.2 Are you sure you want to rebuild the FTS index in smallc? This action will drop the existing FTS index and create a new one. [y/N]: y Rebuilt FTS. Will try to start your Chroma now. NOTE: Depending on the size of your documents in Chroma it may take a while for Chroma to start up again. Chroma started successfully. FTS rebuilt. ``` #### HNSW Maintenance ##### HNSW Info *What it does*: Gathers information about the HNSW index for a given collection. *Why it's useful*: This command is useful for gathering information about the HNSW index for a given collection. ```bash chops hnsw info /path/to/persist_dir ``` Additional options: - `--collection` (`-c`) - the collection name - `--verbose` (`-v`) - If specified, the HNSW index will be loaded for more accurate fragmentation level reporting. Example output: ```console chops hnsw info smallc -c test ChromaDB version: 0.6.2 HNSW details for collection test in default_database database ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Metric ┃ Value ┃ ┑━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ β”‚ Space β”‚ cosine β”‚ β”‚ Dimensions β”‚ 384 β”‚ β”‚ EF Construction β”‚ 200 β”‚ β”‚ EF Search β”‚ 100 β”‚ β”‚ M β”‚ 64 β”‚ β”‚ Number of threads β”‚ 16 β”‚ β”‚ Resize factor β”‚ 1.2 β”‚ β”‚ Batch size β”‚ 100 β”‚ β”‚ Sync threshold β”‚ 1000 β”‚ β”‚ Segment ID β”‚ 0137d64b-8d71-42f5-b0d9-28716647b068 β”‚ β”‚ Path β”‚ smallc/0137d64b-8d71-42f5-b0d9-28716647b068 β”‚ β”‚ Has metadata β”‚ True β”‚ β”‚ Number of elements β”‚ 20 β”‚ β”‚ Collection ID β”‚ 97f5234e-d02a-43b8-9909-99447950c949 β”‚ β”‚ Index size β”‚ 41.6KiB β”‚ β”‚ Fragmentation level β”‚ 0.00% (estimated) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ##### HNSW Rebuild *What it does*: Rebuilds the HNSW index for a given collection and allows the modification of otherwise immutable (construction-only) parameters. *Why it's useful*: This command is useful for rebuilding the HNSW index for a given collection and allows the modification of otherwise immutable (construction-only) parameters. ```bash chops hnsw rebuild /path/to/persist_dir ``` Options: - `--backup` (`-b`) - backup the old index. At the end of the rebuild process the location of the backed up index will be printed out. (default: `True`) - `--database` (`-d`) - the database name (default: `default_database`) - `--yes` (`-y`) - skip confirmation prompt (default: `False`, prompt will be shown) - `--space` (`-s`) - the distance metric to use for the index. - `--construction-ef` (`-c`) - the construction ef to use for the index. - `--search-ef` (`-e`) - the search ef to use for the index. - `--m` (`-m`) - the m to use for the index. - `--num-threads` (`-t`) - the number of threads to use for the index. - `--resize-factor` (`-r`) - the resize factor to use for the index. - `--batch-size` (`-b`) - the batch size to use for the index. - `--sync-threshold` (`-s`) - the sync threshold to use for the index. Unchanged options will be skipped All the HNSW index options default to `None` which means no changes will be made if the parameter is not specified. Additionally, any options provided that are identical to the current index configuration will be skipped. Example output: ```console chops hnsw rebuild smallc -c test --m 64 --construction-ef 200 ChromaDB version: 0.6.2 HNSW details for collection test in default_database database ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Metric ┃ Value ┃ ┑━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ β”‚ Space β”‚ cosine β”‚ β”‚ Dimensions β”‚ 384 β”‚ β”‚ EF Construction β”‚ 200 β”‚ β”‚ EF Search β”‚ 100 β”‚ β”‚ M β”‚ 64 β”‚ β”‚ Number of threads β”‚ 16 β”‚ β”‚ Resize factor β”‚ 1.2 β”‚ β”‚ Batch size β”‚ 100 β”‚ β”‚ Sync threshold β”‚ 1000 β”‚ β”‚ Segment ID β”‚ 0137d64b-8d71-42f5-b0d9-28716647b068 β”‚ β”‚ Path β”‚ smallc/0137d64b-8d71-42f5-b0d9-28716647b068 β”‚ β”‚ Has metadata β”‚ True β”‚ β”‚ Number of elements β”‚ 20 β”‚ β”‚ Collection ID β”‚ 97f5234e-d02a-43b8-9909-99447950c949 β”‚ β”‚ Index size β”‚ 47.6KiB β”‚ β”‚ Fragmentation level β”‚ 0.00% (estimated) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ HNSW segment config changes ┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━┓ ┃ Config Key ┃ Old ┃ New ┃ ┑━━━━━━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━┩ β”‚ hnsw:construction_ef β”‚ 100 β”‚ 200 β”‚ β”‚ hnsw:M β”‚ 102 β”‚ 64 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”˜ Are you sure you want to rebuild this index? [y/N]: y Backup of old index created at smallc/0137d64b-8d71-42f5-b0d9-28716647b068_backup_20250208100514 HNSW details for collection test in default_database database ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Metric ┃ Value ┃ ┑━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ β”‚ Space β”‚ cosine β”‚ β”‚ Dimensions β”‚ 384 β”‚ β”‚ EF Construction β”‚ 200 β”‚ β”‚ EF Search β”‚ 100 β”‚ β”‚ M β”‚ 64 β”‚ β”‚ Number of threads β”‚ 16 β”‚ β”‚ Resize factor β”‚ 1.2 β”‚ β”‚ Batch size β”‚ 100 β”‚ β”‚ Sync threshold β”‚ 1000 β”‚ β”‚ Segment ID β”‚ 0137d64b-8d71-42f5-b0d9-28716647b068 β”‚ β”‚ Path β”‚ smallc/0137d64b-8d71-42f5-b0d9-28716647b068 β”‚ β”‚ Has metadata β”‚ True β”‚ β”‚ Number of elements β”‚ 20 β”‚ β”‚ Collection ID β”‚ 97f5234e-d02a-43b8-9909-99447950c949 β”‚ β”‚ Index size β”‚ 41.6KiB β”‚ β”‚ Fragmentation level β”‚ 0.00% β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ##### HNSW Configuration *What it does*: Configures the HNSW index for your Chroma database. *Why it's useful*: This command is useful for configuring the HNSW index for your Chroma database. ```bash chops hnsw config /path/to/persist_dir --collection ``` Options: - `--search-ef` (`-e`) - the search ef to use for the index. - `--num-threads` (`-t`) - the number of threads to use for the index. - `--resize-factor` (`-r`) - the resize factor to use for the index. - `--batch-size` (`-b`) - the batch size to use for the index. - `--sync-threshold` (`-s`) - the sync threshold to use for the index. Example output: ```console chops hnsw config smallc -c test --search-ef 100 ChromaDB version: 0.6.2 HNSW segment config changes ┏━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━┓ ┃ Config Key ┃ Old ┃ New ┃ ┑━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━┩ β”‚ hnsw:search_ef β”‚ 110 β”‚ 100 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”˜ Are you sure you want to apply these changes? [y/N]: y HNSW index configuration modified successfully ``` #### Collection Maintenance ##### Collection Snapshot *What it does*: Creates a snapshot of a collection. The snapshots are self-contained sqlite3 files. *Why it's useful*: The command is useful if you want to create a backup or a point-in-time copy of a collection in its entirety. The snapshot files are self-contained and use sqlite3 as a storage engine. You can use `sqlite3` commands to inspect the snapshot files. ```bash chops collection snapshot /path/to/persist_dir --collection -o /path/to/snapshot.sqlite3 ``` Additional options: - `--yes` (`-y`) - skip confirmation prompt (default: `False`, prompt will be shown) - `--collection` (`-c`) - the collection name - `--output` (`-o`) - the path to the output snapshot file Example output: ```console chops collection snapshot ./smallc --collection test -o snapshot.sqlite3 ChromaDB version: 0.6.2 Are you sure you want to overwrite /Users/tazarov/experiments/chroma/chromadb-ops/snapshot.sqlite3 file? [y/N]: y Bootstrapping snapshot database... Snapshot database bootstrapped in /Users/tazarov/experiments/chroma/chromadb-ops/snapshot.sqlite3 Copying collection test to snapshot database... Copying collection to snapshot database... ┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓ ┃ Table ┃ Count ┃ ┑━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩ β”‚ Embeddings Queue β”‚ 20 β”‚ β”‚ Max Seq ID β”‚ 1 β”‚ β”‚ Embeddings β”‚ 20 β”‚ β”‚ Embedding Metadata β”‚ 20 β”‚ β”‚ Segments β”‚ 2 β”‚ β”‚ Segment Metadata β”‚ 3 β”‚ β”‚ Collections β”‚ 1 β”‚ β”‚ Collection Metadata β”‚ 0 β”‚ β”‚ HNSW Segment Data Files β”‚ 5 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜ Are you sure you want to copy this collection to the snapshot database? [y/N]: y Collection test copied to snapshot database in /Users/tazarov/experiments/chroma/chromadb-ops/snapshot.sqlite3 ``` # Performance Tips This section covers tips and tricks of how to improve your Chroma performance. ## Rebuild HNSW for your architecutre Single node chroma [core package](https://pypi.org/project/chromadb/) and [server](https://hub.docker.com/r/chromadb/chroma) ship with a default HNSW build which is optimized for maximum compatibility. The default HNSW does not make use of available optimization for your CPU architecture such as SIMD/AVX. You can rebuild the HNSW index for the core package or the server as follows. To rebuild the HNSW index locally you may need to install build tooling such as `gcc` depending on your operating system. ```bash pip install --no-binary :all: chroma-hnswlib ``` In the following snippet, we clone the Chroma repository (you'll need git and docker installed), and then build a new docker image with the HNSW rebuild flag set to `true`. ```bash git clone https://github.com/chroma-core/chroma.git && cd chroma docker build --build-arg REBUILD_HNSWLIB=true -t my-chroma-image:latest . ``` Need help? If you need help with the above steps, please reach out to us on [Discord](https://discord.gg/MMeYNTmh3x) (look for `@taz`) ## Reducing (shortening) the dimensionality of your embeddings Some embeddings models (or APIs) offer the ability to reduce the dimensionality of the resulting embeddings. This is a great way to reduce the storage and memory requirements of your Chroma. Currently the following embedding functions support this feature: - OpenAI with 3rd generation models (i.e. `text-embedding-3-small` and `text-embedding-3-large`) ### OpenAI Example For more information on shortening embeddings see the official [OpenAI Blog post](https://openai.com/index/new-embedding-models-and-api-updates/). ```python from chromadb.utils.embedding_functions.openai_embedding_function import ( OpenAIEmbeddingFunction, ) import os ef = OpenAIEmbeddingFunction(api_key=os.environ["OPENAI_API_KEY"], model_name="text-embedding-3-small", dimensions=64) embeddings = ef(["hello world"]) ``` ```javascript import {OpenAIEmbeddingFunction} from "chromadb" const embedder = new OpenAIEmbeddingFunction({ openai_api_key: process.env.OPENAI_API_KEY, openai_embedding_dimensions: 64, openai_model: "text-embedding-3-small", }); const embeddings = embedder.generate(["hello world"]); ``` ## Defragment your HNSW indices If you have many updates (other than `add`) on your collections, overtime the HNSW indices become fragmented which has the following consequences: - Increased memory usage - Increased disk usage - Increased query times - Reduced accuracy To mitigate the above side-effects, you can periodically defragment/compact your HNSW indices. To do that use the `chops hnsw rebuild` [command](https://cookbook.chromadb.dev/running/maintenance/#hnsw-rebuild). # Road To Production In this section we will cover considerations for operating Chroma ina production environment. To operate Chroma in production your deployment must follow your organization's best practices and guidelines around business continuity, security, and compliance. Here we will list the core concepts and offer some guidance on how to achieve them. Core system abilities: - High Availability - The deployment should be able to handle failures while continuing to serve requests. - Scalability - The deployment should be able to handle increased load by adding more resources (aka scale horizontally). - Privacy and Security - The deployment should protect data from unauthorized access and ensure data integrity. - Observability - The deployment should provide metrics and logs to help operators understand the system's health. - Backup and Restore - The deployment should have a backup and restore strategy to protect against data loss. - Disaster Recovery - The deployment should have a disaster recovery plan to recover from catastrophic failures. - Maintenance - The deployment should be easy to maintain and upgrade. While our guidance is most likely incomplete it can be taken as a compliment to your own organizational processes. For those deploying Chroma in a smaller enterprise without such processes, we advise common sense and caution. ## High Availability ## Scalability ## Privacy and Security ### Data Security #### In Transit The bare minimum for securing data in transit is to use HTTPS when performing Chroma API calls. This ensures that data is encrypted when it is sent over the network. There are several ways to achieve this: - Use a reverse proxy like Envoy or Nginx to terminate SSL/TLS connections. - Use a load balancer like AWS ELB or Google Cloud Load Balancer to terminate SSL/TLS connections (technically a Envoy and Nginx are also LBs). - Use a service mesh like Istio or Linkerd to manage SSL/TLS connections between services. - Enable SSL/TLS in your Chroma server. Depending on your requirements you may choose one or more of these options. **Reverse Proxy:** **Load Balancer:** **Service Mesh:** **Chroma Server:** #### At Rest ### Access Control #### Authentication #### Authorization ## Observability ## Backup and Restore ## Disaster Recovery ## Maintenance # Running Chroma ## Local Server Article Link This article is also available on Medium [Running ChromaDB β€” Part 1: Local Server](https://medium.com/@amikostech/running-chromadb-part-1-local-server-2c61cb1c9f2c). ### Chroma CLI The simplest way to run Chroma locally is via the Chroma `cli`. Prerequisites: - Python 3.9+ (for `pip`, `pipx`, or `uv`) - [Download Python | Python.org](https://www.python.org/downloads/) - Node.js (for `npm`, `pnpm`, `bun`, or `yarn`) - [Download Node.js | nodejs.org](https://nodejs.org/en/download) - `curl` (or Windows PowerShell) for standalone CLI install script Install Chroma CLI with any of the following: #### Python ```shell pip install chromadb ``` ```shell uv venv .venv source .venv/bin/activate # macOS/Linux # Windows (PowerShell): .venv\Scripts\Activate.ps1 uv pip install chromadb ``` ```shell pipx install chromadb ``` ```shell uv tool install chromadb ``` #### JavaScript (Global) ```shell npm install -g chromadb ``` ```shell pnpm add -g chromadb ``` ```shell bun add -g chromadb ``` ```shell yarn global add chromadb ``` #### Standalone Installer ```shell curl -sSL https://raw.githubusercontent.com/chroma-core/chroma/main/rust/cli/install/install.sh | bash ``` ```powershell iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/chroma-core/chroma/main/rust/cli/install/install.ps1')) ``` ```shell chroma run --host localhost --port 8000 --path ./my_chroma_data ``` `--host` The host to bind to, by default `localhost`. Use `0.0.0.0` to expose it on your local network. `--port` The port on which to listen to, by default this is `8000`. `--path` The path where to persist your Chroma data locally. Target Path Install It is possible to install Chroma in a specific directory by running `pip install chromadb -t /path/to/dir`. To run Chroma CLI from that install location, execute: `/path/to/dir/bin/chroma run --path ./my_chroma_data` For advanced 1.x server settings (YAML config and `CHROMA_` overrides), see [Chroma Configuration](https://cookbook.chromadb.dev/core/configuration/index.md). Optional: CLI with YAML config (collapsed) chroma.local.yaml ```yaml port: 8000 listen_address: "127.0.0.1" persist_path: "./my_chroma_data" allow_reset: false sqlitedb: hash_type: "md5" migration_mode: "apply" ``` ```shell CONFIG_PATH=./chroma.local.yaml chroma run ``` ### Docker Running Chroma server locally can be achieved via a simple docker command as shown below. Prerequisites: - Docker - [Overview of Docker Desktop | Docker Docs](https://docs.docker.com/desktop/) ```shell docker run -d --rm --name chromadb \ -p 8000:8000 \ -v ./chroma-data:/data \ chromadb/chroma:1.5.3 ``` Options: - `-p 8000:8000` specifies the port on which the Chroma server will be exposed. - `-v` specifies a local dir which is where Chroma will store its data so when the container is destroyed the data remains. For current Chroma server images, mount `/data` to persist DB files. - `chromadb/chroma:1.5.3` indicates the Chroma release version. Current v1.x Images Legacy environment variables such as `IS_PERSISTENT`, `PERSIST_DIRECTORY`, and `ANONYMIZED_TELEMETRY` are from older server configuration flows and should not be used in the default v1.x Docker run setup. Optional: Docker with YAML config file (collapsed) chroma.docker.yaml ```yaml port: 8000 listen_address: "0.0.0.0" persist_path: "/data" allow_reset: false sqlitedb: hash_type: "md5" migration_mode: "apply" ``` ```shell docker run -d --rm --name chromadb \ -p 8000:8000 \ -v ./chroma-data:/data \ -v ./chroma.docker.yaml:/chroma/config.yaml:ro \ -e CONFIG_PATH=/chroma/config.yaml \ chromadb/chroma:1.5.3 ``` ### Docker Compose Chroma server can also be run with Docker Compose by creating a `docker-compose.yaml`. Prerequisites: - Docker - [Overview of Docker Desktop | Docker Docs](https://docs.docker.com/desktop/) ```yaml services: chromadb: image: chromadb/chroma:1.5.3 volumes: - ./chroma-data:/data ports: - "8000:8000" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/api/v2/heartbeat"] interval: 30s timeout: 10s retries: 3 ``` The above will create a container with Chroma `1.5.3`, expose it on local port `8000`, and persist data in `./chroma-data` relative to where `docker-compose.yaml` is run. Optional: Docker Compose with YAML config file (collapsed) ```yaml services: chromadb: image: chromadb/chroma:1.5.3 volumes: - ./chroma-data:/data - ./chroma.docker.yaml:/chroma/config.yaml:ro environment: - CONFIG_PATH=/chroma/config.yaml ports: - "8000:8000" ``` Versioning When running Chroma with docker compose try to pin the version to a specific release. This will ensure intentional upgrades and avoid potential issues (usually with clients). ### Minikube With Helm Chart KinD Alternative This deployment can also be done with `KinD`, depending on your preference. A more advanced approach to running Chroma locally (but also on a remote cluster) is to deploy it using a Helm chart. Community-Maintained Chart The chart used here is not a first-party Chroma chart and is maintained by a core contributor. Prerequisites: - Docker - [Overview of Docker Desktop | Docker Docs](https://docs.docker.com/desktop/) - Install minikube - [minikube start | minikube (k8s.io)](https://minikube.sigs.k8s.io/docs/start/) - kubectl - [Install Tools | Kubernetes](https://kubernetes.io/docs/tasks/tools/#kubectl) - Helm - [Helm | Installing Helm](https://helm.sh/docs/intro/install/) Once you have all of the above, running Chroma in a local `minikube` cluster is quite simple. Create a `minikube` cluster: ```bash minikube start --addons=ingress -p chroma minikube profile chroma ``` Get and install the chart: ```bash helm repo add chroma https://amikos-tech.github.io/chromadb-chart/ helm repo update helm install chroma chroma/chromadb \ --set image.tag="1.5.3" ``` Auth values for Chroma `>= 1.0.0` Chart values under `chromadb.auth.*` are legacy and ignored. Use network-level controls (private networking, ingress auth, API gateway, mTLS) when needed. The first step to connect and start using Chroma is to forward your port: ```bash minikube service chroma-chromadb --url ``` The command returns a local URL such as `http://127.0.0.1:61892`. Driver-specific behavior On some setups (for example Docker driver on macOS), this command runs a local tunnel in the foreground. Keep that terminal open while you use the URL. Test it out (`pip install chromadb`): ```python import chromadb client = chromadb.HttpClient(host="http://127.0.0.1:61892") client.heartbeat() # public endpoint client.get_version() # public endpoint client.list_collections() # expected to work in this local chart setup ``` For more information about the Helm chart, see [amikos-tech/chromadb-chart](https://github.com/amikos-tech/chromadb-chart) or [Artifact Hub](https://artifacthub.io/packages/helm/chromadb-helm/chromadb). # Systemd service You can run Chroma as a systemd service which wil allow you to automatically start Chroma on boot and restart it if it crashes. ## Docker Compose The following is an examples systemd service for running Chroma using Docker Compose. Create a file `/etc/systemd/system/chroma.service` with the following content: Example assumptions The below example assumes Debian-based system with docker-ce installed. ```ini [Unit] Description = Chroma Service After = network.target docker.service Requires = docker.service [Service] Type = forking User = root Group = root WorkingDirectory = /home/admin/chroma ExecStart = /usr/bin/docker compose up -d ExecStop = /usr/bin/docker compose down RemainAfterExit = true [Install] WantedBy = multi-user.target ``` Replace `WorkingDirectory` with the path to your docker compose is. You may also need to replace `/usr/bin/docker` with the path to your docker binary. Alternatively you can install directly from a gist: ```bash wget https://gist.githubusercontent.com/tazarov/9c46966de0b32a4962dcc79dce8b2646/raw/7cf8c471f33fba8a51d6f808f9b1af6ca1b0923c/chroma-docker.service \ -O /etc/systemd/system/chroma.service ``` Loading, enabling and starting the service: ```bash sudo systemctl daemon-reload sudo systemctl enable chroma sudo systemctl start chroma ``` Type=forking In the above example, we use `Type=forking` because Docker Compose runs in the background (`-d`). If you are using a different command that runs in the foreground, you may need to use `Type=simple` instead. ## Chroma CLI The following is an examples systemd service for running Chroma using the Chroma CLI. Create a file `/etc/systemd/system/chroma.service` with the following content: Example assumptions The below example assumes that Chroma is installed in Python `site-packages` package. ```ini [Unit] Description = Chroma Service After = network.target [Service] Type = simple User = root Group = root WorkingDirectory = /chroma ExecStart=/usr/local/bin/chroma run --host 127.0.0.1 --port 8000 --path /chroma/data --log-path /var/log/chroma.log [Install] WantedBy = multi-user.target ``` Replace the `WorkingDirectory`, `/chroma/data` and `/var/log/chroma.log` with the appropriate paths. Safe Config The above example service listens and `localhost` which may not work if you are looking to expose Chroma to outside world. Adjust the `--host` and `--port` flags as needed. Alternatively you can install from a gist: ```bash wget https://gist.githubusercontent.com/tazarov/5e10ce892c06757d8188a8a34cd6d26d/raw/327a9d0b07afeb0b0cb77453aa9171fdd190984f/chroma-cli.service \ -O /etc/systemd/system/chroma.service ``` Loading, enabling and starting the service: ```bash sudo systemctl daemon-reload sudo systemctl enable chroma sudo systemctl start chroma ``` Type=simple In the above example, we use `Type=simple` because the Chroma CLI runs in the foreground. If you are using a different command that runs in the background, you may need to use `Type=forking` instead. # Integrations # Chroma Integrations With LangChain Last updated: **February 25, 2026** ## What's New In This Refresh - Updated embeddings guidance for Chroma `1.5.3`. - Replaced legacy vector store imports with `from langchain_chroma import Chroma`. - Replaced deprecated adapter usage with `ChromaLangchainEmbeddingFunction`. - Added a full runnable example: [examples/langchain/python/embeddings_example.py](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/langchain/python/embeddings_example.py) - Added example dependencies: [examples/langchain/requirements.txt](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/langchain/requirements.txt) - Documented a Chroma `1.5.3` query workaround for wrapped LangChain embeddings. ## Guides - [Embeddings](https://cookbook.chromadb.dev/integrations/langchain/embeddings/index.md) - use LangChain embedding models with Chroma collections and `langchain_chroma`. - [Retrievers](https://cookbook.chromadb.dev/integrations/langchain/retrievers/index.md) - use LangChain retrievers with Chroma. # LangChain Embeddings This page shows the current Chroma (`1.5.3`) and LangChain embedding integration patterns. ## Use LangChain Embeddings With Chroma Collections In Chroma `1.5.3`, wrap a LangChain `Embeddings` implementation with `ChromaLangchainEmbeddingFunction`. Query workaround in 1.5.3 If `collection.query(query_texts=[...])` raises an error with wrapped LangChain embeddings, use `query_embeddings=[lc_embeddings.embed_query("...")]` instead. ```python # pip install chromadb==1.5.3 langchain-core langchain-huggingface sentence-transformers import chromadb from chromadb.utils.embedding_functions import ChromaLangchainEmbeddingFunction from langchain_huggingface import HuggingFaceEmbeddings lc_embeddings = HuggingFaceEmbeddings( model_name="sentence-transformers/all-MiniLM-L6-v2" ) chroma_ef = ChromaLangchainEmbeddingFunction(embedding_function=lc_embeddings) client = chromadb.PersistentClient(path="./chroma-langchain-data") collection = client.get_or_create_collection( name="my_collection", embedding_function=chroma_ef, ) collection.add(ids=["1"], documents=["test document goes here"]) ``` ```python # pip install chromadb==1.5.3 langchain-core langchain-openai import chromadb from chromadb.utils.embedding_functions import ChromaLangchainEmbeddingFunction from langchain_openai import OpenAIEmbeddings lc_embeddings = OpenAIEmbeddings(model="text-embedding-3-small") chroma_ef = ChromaLangchainEmbeddingFunction(embedding_function=lc_embeddings) client = chromadb.PersistentClient(path="./chroma-langchain-data") collection = client.get_or_create_collection( name="my_collection", embedding_function=chroma_ef, ) collection.add(ids=["1"], documents=["test document goes here"]) ``` ## Use Chroma As A LangChain Vector Store For LangChain vector stores, use the `langchain-chroma` package: ```python # pip install chromadb==1.5.3 langchain-core langchain-chroma langchain-openai from langchain_chroma import Chroma from langchain_openai import OpenAIEmbeddings embeddings = OpenAIEmbeddings(model="text-embedding-3-small") vector_store = Chroma( collection_name="docs_store", persist_directory="./chroma-langchain-store", embedding_function=embeddings, ) vector_store.add_texts( texts=["foo", "bar", "baz"], metadatas=[{"topic": "a"}, {"topic": "b"}, {"topic": "c"}], ids=["1", "2", "3"], ) results = vector_store.similarity_search("foo", k=2, filter={"topic": "a"}) ``` ## Optional: Manual Adapter (Chroma -> LangChain) If you already have a Chroma embedding function and want to use it in LangChain, you can adapt it to LangChain's `Embeddings` interface: ```python from chromadb.api.types import EmbeddingFunction from langchain_core.embeddings import Embeddings class ChromaEmbeddingsAdapter(Embeddings): def __init__(self, ef: EmbeddingFunction) -> None: self.ef = ef def embed_documents(self, texts: list[str]) -> list[list[float]]: return self.ef(texts) def embed_query(self, query: str) -> list[float]: return self.ef([query])[0] ``` ## Full Working Example See [examples/langchain/python/embeddings_example.py](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/langchain/python/embeddings_example.py) for a complete runnable script and dependency list. The example uses `DeterministicFakeEmbedding` so it runs without API keys. # πŸ¦œβ›“οΈ Langchain Retriever TBD: describe what retrievers are in LC and how they work. ## Vector Store Retriever In the below example we demonstrate how to use Chroma as a vector store retriever with a filter query. Note that the filter is supplied whenever we create the retriever object so the filter applies to all queries (`get_relevant_documents`). ```py from langchain.document_loaders import OnlinePDFLoader from langchain.chains import RetrievalQA from langchain.llms import OpenAI from langchain.vectorstores import Chroma from typing import Dict, Any import chromadb from langchain_core.embeddings import Embeddings client = chromadb.PersistentClient(path="./chroma") col = client.get_or_create_collection("test") col.upsert([f"{i}" for i in range(10)],documents=[f"This is document #{i}" for i in range(10)],metadatas=[{"id":f"{i}"} for i in range(10)]) ef = chromadb.utils.embedding_functions.DefaultEmbeddingFunction() class DefChromaEF(Embeddings): def __init__(self,ef): self.ef = ef def embed_documents(self,texts): return self.ef(texts) def embed_query(self, query): return self.ef([query])[0] db = Chroma(client=client, collection_name="test",embedding_function=DefChromaEF(ef)) retriever = db.as_retriever(search_kwargs={"filter":{"id":"1"}}) docs = retriever.get_relevant_documents("document") assert len(docs)==1 ``` Ref: https://colab.research.google.com/drive/1L0RwQVVBtvTTd6Le523P4uzz3m3fm0pH#scrollTo=xROOfxLohE5j # Chroma Integrations With LlamaIndex - [Embeddings](https://cookbook.chromadb.dev/integrations/llamaindex/embeddings/index.md) - learn how to use LlamaIndex embeddings functions with Chroma and vice versa # LlamaIndex Embeddings ## Embedding Functions Chroma and LlamaIndex both offer embedding functions which are wrappers on top of popular embedding models. Unfortunately Chroma and LI's embedding functions are not compatible with each other. Below we offer an adapters to convert LI embedding function to Chroma one. ```python from llama_index.core.schema import TextNode from llama_index.core.base.embeddings.base import BaseEmbedding from chromadb import EmbeddingFunction, Documents, Embeddings class LlamaIndexEmbeddingAdapter(EmbeddingFunction): def __init__(self, ef: BaseEmbedding): self.ef = ef def __call__(self, input: Documents) -> Embeddings: return [node.embedding for node in self.ef([TextNode(text=doc) for doc in input])] ``` Text modality The above adapter assumes that the input documents are text. If you are using a different modality, you will need to modify the adapter accordingly. An example of how to use the above with LlamaIndex: Prerequisites for example Run `pip install llama-index chromadb llama-index-embeddings-fastembed fastembed` ```python import chromadb from llama_index.embeddings.fastembed import FastEmbedEmbedding # make sure to include the above adapter and imports embed_model = FastEmbedEmbedding(model_name="BAAI/bge-small-en-v1.5") client = chromadb.Client() col = client.get_or_create_collection("test_collection", embedding_function=LlamaIndexEmbeddingAdapter(embed_model)) col.add(ids=["1"], documents=["this is a test document"]) ``` # Chroma Integrations With Ollama - [Embeddings](https://cookbook.chromadb.dev/integrations/ollama/embeddings/index.md) - learn how to use Ollama as embedder for Chroma documents - ✨`Coming soon` RAG with Ollama - a primer on how to build a simple RAG app with Ollama and Chroma # Ollama Ollama offers out-of-the-box embedding API which allows you to generate embeddings for your documents. Chroma provides a convenient wrapper around Ollama's embedding API. ## Ollama Embedding Models While you can use any of the ollama models including LLMs to generate embeddings. We generally recommend using specialized models like `nomic-embed-text` for text embeddings. The latter models are specifically trained for embeddings and are more efficient for this purpose (e.g. the dimensions of the output embeddings are much smaller than those from LLMs e.g. 1024 - nomic-embed-text vs 4096 - llama3) **Models:** | Model | Pull | Ollama Registry Link | | ------------------------ | ----------------------------------------- | --------------------------------------------------------------------------- | | `nomic-embed-text` | `ollama pull nomic-embed-text` | [nomic-embed-text](https://ollama.com/library/nomic-embed-text) | | `mxbai-embed-large` | `ollama pull mxbai-embed-large` | [mxbai-embed-large](https://ollama.com/library/mxbai-embed-large) | | `snowflake-arctic-embed` | `ollama pull snowflake-arctic-embed` | [snowflake-arctic-embed](https://ollama.com/library/snowflake-arctic-embed) | | `all-minilm-l6-v2` | `ollama pull chroma/all-minilm-l6-v2-f32` | [all-minilm-l6-v2-f32](https://ollama.com/chroma/all-minilm-l6-v2-f32) | ## Basic Usage First let's run a local docker container with Ollama. We'll pull `nomic-embed-text` model: ```bash docker run -d --rm -v ./ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama docker exec -it ollama ollama run nomic-embed-text # press Ctrl+D to exit after model downloads successfully # test it curl http://localhost:11434/api/embeddings -d '{"model": "nomic-embed-text","prompt": "Here is an article about llamas..."}' ``` Ollama Docs For more information on Ollama, visit the [Ollama GitHub repository](https://github.com/ollama/ollama). Using the CLI If you have or prefer to use the Ollama CLI, you can use the following command to get a model: ```bash ollama pull nomic-embed-text ``` Now let's configure our OllamaEmbeddingFunction Embedding (python) function with the default Ollama endpoint: ### Python ```python import chromadb from chromadb.utils.embedding_functions import OllamaEmbeddingFunction client = chromadb.PersistentClient(path="ollama") # create EF with custom endpoint ef = OllamaEmbeddingFunction( model_name="nomic-embed-text", url="http://localhost:11434/api/embeddings", ) print(ef(["Here is an article about llamas..."])) ``` ### JavaScript For JS users, you can use the `OllamaEmbeddingFunction` class to create embeddings: ```javascript const {OllamaEmbeddingFunction} = require('chromadb'); const embedder = new OllamaEmbeddingFunction({ url: "http://localhost:11434/api/embeddings", model: "nomic-embed-text" }) // use directly const embeddings = embedder.generate(["Here is an article about llamas..."]) ``` ### Golang For Golang you can use the `chroma-go` client's `OllamaEmbeddingFunction` embedding function to generate embeddings for your documents: ```go package main import ( "context" "fmt" ollama "github.com/amikos-tech/chroma-go/ollama" ) func main() { documents := []string{ "Document 1 content here", "Document 2 content here", } // the `/api/embeddings` endpoint is automatically appended to the base URL ef, err := ollama.NewOllamaEmbeddingFunction(ollama.WithBaseURL("http://127.0.0.1:11434"), ollama.WithModel("nomic-embed-text")) if err != nil { fmt.Printf("Error creating Ollama embedding function: %s \n", err) } resp, err := ef.EmbedDocuments(context.Background(), documents) if err != nil { fmt.Printf("Error embedding documents: %s \n", err) } fmt.Printf("Embedding response: %v \n", resp) } ``` Golang Client You can install the Golang client by running the following command: ```bash go get github.com/amikos-tech/chroma-go ``` For more information visit # Embeddings # Creating your own embedding function ```python from chromadb.api.types import ( Documents, EmbeddingFunction, Embeddings ) class MyCustomEmbeddingFunction(EmbeddingFunction[Documents]): def __init__( self, my_ef_param: str ): """Initialize the embedding function.""" def __call__(self, input: Documents) -> Embeddings: """Embed the input documents.""" return self._my_ef(input) ``` Now let's break the above down. First you create a class that inherits from `EmbeddingFunction[Documents]`. The `Documents` type is a list of `Document` objects. Each `Document` object has a `text` attribute that contains the text of the document. Chroma also supports multi-modal ## Example Implementation Below is an implementation of an embedding function that works with `transformers` models. Note This example requires the `transformers` and `torch` python packages. You can install them with `pip install transformers torch`. By default, all `transformers` models on HF are supported are also supported by the `sentence-transformers` package. For which Chroma provides [out of the box support](https://docs.trychroma.com/embeddings#sentence-transformers). ```python import importlib from typing import Optional, cast import numpy as np import numpy.typing as npt from chromadb.api.types import EmbeddingFunction, Documents, Embeddings class TransformerEmbeddingFunction(EmbeddingFunction[Documents]): def __init__( self, model_name: str = "dbmdz/bert-base-turkish-cased", cache_dir: Optional[str] = None, ): try: from transformers import AutoModel, AutoTokenizer self._torch = importlib.import_module("torch") self._tokenizer = AutoTokenizer.from_pretrained(model_name) self._model = AutoModel.from_pretrained(model_name, cache_dir=cache_dir) except ImportError: raise ValueError( "The transformers and/or pytorch python package is not installed. Please install it with " "`pip install transformers` or `pip install torch`" ) @staticmethod def _normalize(vector: npt.NDArray) -> npt.NDArray: """Normalizes a vector to unit length using L2 norm.""" norm = np.linalg.norm(vector) if norm == 0: return vector return vector / norm def __call__(self, input: Documents) -> Embeddings: inputs = self._tokenizer( input, padding=True, truncation=True, return_tensors="pt" ) with self._torch.no_grad(): outputs = self._model(**inputs) embeddings = outputs.last_hidden_state.mean(dim=1) # mean pooling return [e.tolist() for e in self._normalize(embeddings)] ``` # Cross-Encoders Reranking Work in Progress This page is a work in progress and may not be complete. For now this is just a tiny snippet how to use a cross-encoder to rerank results returned from Chroma. Soon we will provide a more detailed guide to the usefulness of cross-encoders/rerankers. ## Hugging Face Cross Encoders ```python from sentence_transformers import CrossEncoder import numpy as np import chromadb client = chromadb.Client() collection = client.get_or_create_collection("my_collection") # add some documents collection.add(ids=["doc1", "doc2", "doc3"], documents=["Hello, world!", "Hello, Chroma!", "Hello, Universe!"]) # query the collection query = "Hello, world!" results = collection.query(query_texts=[query], n_results=3) model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2', max_length=512) # rerank the results with original query and documents returned from Chroma scores = model.predict([(query, doc) for doc in results["documents"][0]]) # get the highest scoring document print(results["documents"][0][np.argmax(scores)]) ``` # Embedding Models Work in Progress This page is a work in progress. Embedding Models are your best friends in the world of Chroma, and vector databases in general. They take something you understand in the form of text, images, audio etc. and turn it into a list of numbers (embeddings), which a machine learning model can understand. This process makes documents interpretable by a machine learning model. The goal of this page is to arm you with enough knowledge to make an informed decision about which embedding model to choose for your use case. The importance of a model GenAI moves pretty fast therefore we recommend not to over-rely on models too much. When creating your solution create the necessary abstractions and tests to be able to quickly experiment and change things up (don't overdo it on the abstraction though). ## Characteristics of an Embedding Model - Modality - the type of data each model is designed to work with. For example, text, images, audio, video. Note: Some models can work with multiple modalities (e.g. [OpenAI's CLIP](https://github.com/openai/CLIP)). - Context - The maximum number of tokens the model can process at once. - Tokenization - The model's tokenizer or the way a model turns text into tokens to process. - Dimensionality - The number of dimensions in the output embeddings/vectors. - Training Data - The data the model was trained on. - Execution Environment - How the model is run (e.g. local, cloud, API). - Loss Function - The function used to train the model e.g. how well the model is doing in predicting the embeddings, compared to the actual embeddings. ## Model Categories There are several ways to categorize embedding models other than the above characteristics: - Execution environment e.g. API vs local - Licensing e.g. open-source vs proprietary - Privacy e.g. on-premises vs cloud ## Execution Environment The execution environment is probably the first choice you should consider when creating your GenAI solution. Can I afford my data to leave the confines of my computer, cluster, organization? If the answer is yes and you are still in the experimentation phase of your GenAI journey we recommend using API-based embedding models. # Embedding Functions GPU Support By default, Chroma does not require GPU support for embedding functions. However, if you want to use GPU support, some of the functions, especially those running locally provide GPU support. ## Default Embedding Functions (Onnxruntime) To use the default embedding functions with GPU support, you need to install `onnxruntime-gpu` package. You can install it with the following command: ```bash pip install onnxruntime-gpu ``` > Note: To ensure no conflicts, you can uninstall `onnxruntime` (e.g. `pip uninstall onnxruntime`) in a separate environment. List available providers: ```python import onnxruntime print(onnxruntime.get_available_providers()) ``` Select the desired provider and set it as preferred before using the embedding functions (in the below example, we use `CUDAExecutionProvider`): ```python import time from chromadb.utils.embedding_functions import ONNXMiniLM_L6_V2 ef = ONNXMiniLM_L6_V2(preferred_providers=['CUDAExecutionProvider']) docs = [] for i in range(1000): docs.append(f"this is a document with id {i}") start_time = time.perf_counter() embeddings = ef(docs) end_time = time.perf_counter() print(f"Elapsed time: {end_time - start_time} seconds") ``` > **IMPORTANT OBSERVATION**: Our observations are that for GPU support using sentence transformers with model `all-MiniLM-L6-v2` outperforms onnxruntime with GPU support. In practical terms on a Colab T4 GPU, the onnxruntime example above runs for about 100s whereas the equivalent sentence transformers example runs for about 1.8s. ## Sentence Transformers ```python import time from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction # This will download the model to your machine and set it up for GPU support ef = SentenceTransformerEmbeddingFunction(model_name="thenlper/gte-small", device="cuda") # Test with 10k documents docs = [] for i in range(10000): docs.append(f"this is a document with id {i}") start_time = time.perf_counter() embeddings = ef(docs) end_time = time.perf_counter() print(f"Elapsed time: {end_time - start_time} seconds") ``` > Note: You can run the above example in google Colab - see the [notebook](https://cookbook.chromadb.dev/recipes/embeddings/google-colab-hf-sentence-transformers-gpu.ipynb) ## OpenCLIP Prior to [PR #1806](https://github.com/chroma-core/chroma/pull/1806), we simply used the `torch` package to load the model and run it on the GPU. ```python import chromadb from chromadb.utils.embedding_functions import OpenCLIPEmbeddingFunction from chromadb.utils.data_loaders import ImageLoader import torch import os IMAGE_FOLDER = "images" torch.device("cuda") embedding_function = OpenCLIPEmbeddingFunction() image_loader = ImageLoader() client = chromadb.PersistentClient(path="my_local_data") collection = client.create_collection( name='multimodal_collection', embedding_function=embedding_function, data_loader=image_loader) image_uris = sorted([os.path.join(IMAGE_FOLDER, image_name) for image_name in os.listdir(IMAGE_FOLDER)]) ids = [str(i) for i in range(len(image_uris))] collection.add(ids=ids, uris=image_uris) ``` After [PR #1806](https://github.com/chroma-core/chroma/pull/1806): ```python from chromadb.utils.embedding_functions import OpenCLIPEmbeddingFunction embedding_function = OpenCLIPEmbeddingFunction(device="cuda") ``` For a full local walkthrough (index image URIs + text-to-image and image-to-image queries), see [Image Search](https://cookbook.chromadb.dev/strategies/image-search/index.md). # Security # Security Security is an important topic and this section is devoted to it. There are many ways to secure a service, such as Chroma and this section attempts to encompass the most common use cases. Way to secure Chroma include: - In-transit encryption using SSL/TLS certificates - Access control - At-rest encryption - Adding authentication and authorization ## SSL/TLS Certificates Securing your Chroma with a proxy is one of the most common ways to secure your Chroma. Ensuring that all traffic between your client and Chroma server is encrypted is a good practice. There are multiple ways to secure your Chroma instance using SSL/TLS certificates and here we'll explore a few. - [SSL/TLS certificate in Chroma server](https://cookbook.chromadb.dev/security/chroma-ssl-cert/index.md) - configure and use SSL/TLS certificates directly in Chroma. - [Proxy with SSL/TLS termination](https://cookbook.chromadb.dev/security/ssl-proxies/index.md) - use a proxy to terminate SSL/TLS and forward traffic to Chroma. - (Coming soon) Cloud Provider API Gateway with SSL/TLS termination - use a cloud provider's API Gateway to terminate SSL/TLS and forward traffic to Chroma. ## Authentication and Authorization Version prior to 1.0.x support [legacy authentication and authorization](https://cookbook.chromadb.dev/security/legacy-auth/index.md) - Configure Chroma built-in authentication and authorization. Versions 1.0.0-1.0.10 do not support Authentication or Authorization natively so you will need to adjust your deployment with a [proxy-based authentcation](https://cookbook.chromadb.dev/security/auth-1.0.x/index.md). # Authentication in Chroma v1.0.x ## Envoy You can secure your Chroma instance with a token-based auth using Envoy proxy. Create a `envoy.yaml` configuration file with the following content (adjust as needed or combined with [SSL](https://cookbook.chromadb.dev/security/ssl-proxies/index.md)): ```yaml static_resources: listeners: - name: listener_0 address: socket_address: address: 0.0.0.0 port_value: 8000 filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http route_config: name: chroma_route virtual_hosts: - name: local_chromadb domains: [ "*" ] routes: - match: prefix: "/" route: cluster: chromadb_service prefix_rewrite: "/" http_filters: - name: envoy.filters.http.rbac typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC rules: action: ALLOW policies: "static-token-policy": permissions: - header: name: %CHROMA_AUTH_TOKEN_TRANSPORT_HEADER% string_match: exact: %CHROMA_SERVER_AUTHN_CREDENTIALS% principals: - any: true - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: chromadb_service connect_timeout: 0.25s type: LOGICAL_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: chromadb_service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: chromadb port_value: 8000 ``` Then create a`entrypoint.sh` startup script to interpolate the values in the `envoy.yaml` configuration. ```bash #!/bin/sh sed 's/%CHROMA_AUTH_TOKEN_TRANSPORT_HEADER%/'$CHROMA_AUTH_TOKEN_TRANSPORT_HEADER'/g' /opt/bitnami/envoy/conf/envoy.yaml > /tmp/envoy_temp.yaml if [ $CHROMA_AUTH_TOKEN_TRANSPORT_HEADER = "Authorization" ]; then sed -i 's/%CHROMA_SERVER_AUTHN_CREDENTIALS%/Bearer '$CHROMA_SERVER_AUTHN_CREDENTIALS'/g' /tmp/envoy_temp.yaml else sed -i 's/%CHROMA_SERVER_AUTHN_CREDENTIALS%/'$CHROMA_SERVER_AUTHN_CREDENTIALS'/g' /tmp/envoy_temp.yaml fi cat /tmp/envoy_temp.yaml /opt/bitnami/envoy/bin/envoy -c /tmp/envoy_temp.yaml ``` Last but not least your `docker-compose.yaml`: ```yaml networks: net: driver: bridge services: envoy: image: bitnami/envoy volumes: - ./envoy.yaml:/opt/bitnami/envoy/conf/envoy.yaml - ./certs:/etc/envoy/certs - ./entrypoint.sh:/entrypoint.sh ports: - "8000:8000" environment: CHROMA_SERVER_AUTHN_CREDENTIALS: ${CHROMA_SERVER_AUTHN_CREDENTIALS:-chr0m4t0k3n} CHROMA_AUTH_TOKEN_TRANSPORT_HEADER: ${CHROMA_AUTH_TOKEN_TRANSPORT_HEADER:-Authorization} networks: - net entrypoint: | sh -c " chmod +x /entrypoint.sh && \ /entrypoint.sh " chromadb: image: chromadb/chroma:1.0.10 volumes: - ./chroma-data:/data networks: - net healthcheck: # Adjust below to match your container port test: ["CMD", "bash", "-c", "echo -n '' > /dev/tcp/127.0.0.1/8000"] interval: 30s timeout: 10s retries: 3 ``` To get going configure your preferred auth type: - Bearer `Authorization` header - `X-Chroma-Token` header ### `Authorization` header: ```bash export CHROMA_AUTH_TOKEN_TRANSPORT_HEADER=Authorization export CHROMA_SERVER_AUTHN_CREDENTIALS=myT0k3n123 docker compose up -d ``` Verify: ```bash curl -v http://localhost:8000/api/v2/tenants/default_tenant/databases/default_database/collections -H "Authorization: Bearer myT0k3n123" ``` Header format Observe the presence of `Bearer` in the authorization header ```python import chromadb from chromadb.config import Settings client = chromadb.HttpClient( settings=Settings( chroma_client_auth_provider="chromadb.auth.token_authn.TokenAuthClientProvider", chroma_client_auth_credentials="myT0k3n123", chroma_auth_token_transport_header="Authorization" ) ) # if everything is correctly configured the below should list all collections client.list_collections() ``` ### `X-Chroma-Token` header: ```bash export CHROMA_AUTH_TOKEN_TRANSPORT_HEADER=X-Chroma-Token export CHROMA_SERVER_AUTHN_CREDENTIALS=myT0k3n123 docker compose up -d ``` Verify: ```bash curl -v http://localhost:8000/api/v2/tenants/default_tenant/databases/default_database/collections -H "X-Chroma-Token: myT0k3n123" ``` ```python import chromadb from chromadb.config import Settings client = chromadb.HttpClient( settings=Settings( chroma_client_auth_provider="chromadb.auth.token_authn.TokenAuthClientProvider", chroma_client_auth_credentials="myT0k3n123", chroma_auth_token_transport_header="X-Chroma-Token" ) ) # if everything is correctly configured the below should list all collections client.list_collections() ``` # SSL/TLS Certificates in Chroma Chroma uses uvicorn as an ASGI server, which can be configured to use SSL/TLS certificates. CLI not supported Using certificates with Chroma CLI is not yet supported. Performance Impact Using certificates within Chroma will have a performance impact as `uvicorn` will need to hnadle the encryption and decryption of the data. If performance is of concern, consider using a reverse proxy like `nginx` or `envoy` to handle the SSL/TLS termination. ## Self-Signed Certificates ### Creating a self-signed certificate Important The `SAN` (Subject Alternative Name) is required for the certificate to work as modern security standards require the certificate to match the domain name. You will also need to create a `openssl.cnf` file in the same directory with the following content: ````text ```ini [req] distinguished_name = req_distinguished_name x509_extensions = usr_cert [req_distinguished_name] CN = $ENV::CHROMA_DOMAIN [usr_cert] subjectAltName = DNS:$ENV::CHROMA_DOMAIN ```` ```` Certificate Domain - CHROMA_DOMAIN You can set the `CHROMA_DOMAIN` environment variable to the domain you want to use for the certificate. To run the following you will need to have `openssl` installed on your system. ```bash export CHROMA_DOMAIN=${CHROMA_DOMAIN:-"localhost"} openssl req -new -newkey rsa:2048 -sha256 -days 365 -nodes -x509 \ -keyout certs/serverkey.pem \ -subj '/O=Chroma/C=US' \ -out certs/servercert.pem \ -config openssl.cnf ```` This will create a self-signed certificate and key in the `certs` directory. If you are using Docker, you can use the following command to generate the certificates: ```bash docker run --rm -v $(pwd)/certs:/certs \ -v $(pwd)/openssl.cnf:/etc/ssl/openssl.cnf \ -e CHROMA_DOMAIN=localhost \ openquantumsafe/openssl3 \ openssl req -new -newkey rsa:2048 -sha256 -days 365 -nodes -x509 \ -keyout /certs/serverkey.pem \ -subj '/O=Chroma/C=US' \ -out /certs/servercert.pem \ -config /etc/ssl/openssl.cnf ``` Security Warning Self-signed certificates are not recommended for production use. They are only suitable for testing and development purposes. Additionally in the above example the keyfile is not password protected, which is also not recommended for production use. ### Configuring and running Chroma You can run Chroma with the SSL/TLS certificate generate above or any other certificate you have. To run Chroma with the self-signed certificate, you can use the following command: ```bash docker run --rm -it -p 8000:8000 \ -v $(pwd)/certs:/chroma/certs \ chromadb/chroma:0.5.0 \ --workers 1 \ --host 0.0.0.0 \ --port 8000 \ --proxy-headers \ --log-config chromadb/log_config.yml \ --timeout-keep-alive 30 \ --ssl-keyfile /chroma/certs/serverkey.pem \ --ssl-certfile /chroma/certs/servercert.pem ``` To run Chroma with the self-signed certificate using Docker Compose, you can use the following `docker-compose.yml` file: ```yaml version: '3.9' networks: net: driver: bridge services: server: image: chromadb/chroma:0.6.3 volumes: # Be aware that indexed data are located in "/chroma/chroma/" # Default configuration for persist_directory in chromadb/config.py # Read more about deployments: https://docs.trychroma.com/deployment - chroma-data:/chroma/chroma command: "--workers 1 --host 0.0.0.0 --port 8000 --proxy-headers --log-config chromadb/log_config.yml --timeout-keep-alive 30 --ssl-keyfile /chroma/certs/serverkey.pem --ssl-certfile /chroma/certs/servercert.pem" environment: - IS_PERSISTENT=TRUE - CHROMA_SERVER_AUTHN_PROVIDER=${CHROMA_SERVER_AUTHN_PROVIDER} - CHROMA_SERVER_AUTHN_CREDENTIALS_FILE=${CHROMA_SERVER_AUTHN_CREDENTIALS_FILE} - CHROMA_SERVER_AUTHN_CREDENTIALS=${CHROMA_SERVER_AUTHN_CREDENTIALS} - CHROMA_AUTH_TOKEN_TRANSPORT_HEADER=${CHROMA_AUTH_TOKEN_TRANSPORT_HEADER} - PERSIST_DIRECTORY=${PERSIST_DIRECTORY:-/chroma/chroma} - CHROMA_OTEL_EXPORTER_ENDPOINT=${CHROMA_OTEL_EXPORTER_ENDPOINT} - CHROMA_OTEL_EXPORTER_HEADERS=${CHROMA_OTEL_EXPORTER_HEADERS} - CHROMA_OTEL_SERVICE_NAME=${CHROMA_OTEL_SERVICE_NAME} - CHROMA_OTEL_GRANULARITY=${CHROMA_OTEL_GRANULARITY} - CHROMA_SERVER_NOFILE=${CHROMA_SERVER_NOFILE} restart: unless-stopped ports: - "8000:8000" healthcheck: # Adjust below to match your container port test: [ "CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat" ] interval: 30s timeout: 10s retries: 3 networks: - net volumes: chroma-data: driver: local ``` ## Using a Certificate Authority Examples below will demonstrate how to use `certbot` to generate a certificate with a given certificate authority. ### Let's Encrypt Coming soon! ### AWS Certificate Manager Coming soon! # Chroma-native Auth (Legacy) Chroma-native Auth is not supported in v1.0.x Chroma native-auth described in this article is not supported in Chroma versions 1.0.0-1.0.10 (latest as of time of writing). **DO NOT USE** the below if you are on any of the affected version as it will not secure your instance. Chroma offers built in authentication and authorization mechanisms to secure your Chroma instance. Auth Disabled by Default By default, Chroma does not require authentication. You must enable it manually. If you are deploying Chroma in a public-facing environment, it is **highly** recommended to enable authentication. Auth needs the company of SSL/TLS Authentication without encryption is insecure. If you are deploying Chroma in a public-facing environment, it is **highly** recommended that you add [SSL/TLS](https://cookbook.chromadb.dev/security/ssl-proxies/index.md). ## Authentication Chroma supports two types of authentication: - Basic Auth - RFC 7617 compliant pre-emptive authentication with username and password credentials in Authorization header. - Token Auth - Standard token-based auth with `Authorization` or `X-Chroma-Token` headers. For each authentication method there are configurations in both client and server. ### Basic Authentication **Server** Generate a password file with bcrypt hashed password: ```bash docker run --rm --entrypoint htpasswd httpd:2 -Bbn admin password123 >> server.htpasswd ``` Verify the password file: ```bash docker run --rm -v ./server.htpasswd:/server.htpasswd --entrypoint htpasswd httpd:2 -vb /server.htpasswd admin password123 ``` Multiple users Chroma supports multiple users in the htpasswd file. You can add multiple users by running the command multiple times WITHOUT `-c` flag. Frequently encountered Chroma errors If you see the following error: ```bash UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte ``` It is likely that you have not used the `-B` (bcrypt) flag when creating the password file. Environment variables: ```bash export CHROMA_SERVER_AUTHN_CREDENTIALS_FILE="server.htpasswd" export CHROMA_SERVER_AUTHN_PROVIDER="chromadb.auth.basic_authn.BasicAuthenticationServerProvider" ``` Running the server: ```bash export CHROMA_SERVER_AUTHN_CREDENTIALS_FILE="server.htpasswd" export CHROMA_SERVER_AUTHN_PROVIDER="chromadb.auth.basic_authn.BasicAuthenticationServerProvider" chroma run --path /chroma-data ``` ```bash docker run --rm -v ./server.htpasswd:/chroma/server.htpasswd \ -e CHROMA_SERVER_AUTHN_CREDENTIALS_FILE="server.htpasswd" \ -e CHROMA_SERVER_AUTHN_PROVIDER="chromadb.auth.basic_authn.BasicAuthenticationServerProvider" \ -p 8000:8000 \ chromadb/chroma:latest ``` Create a `docker-compose.yaml` with the following content: ```yaml networks: net: driver: bridge services: chromadb: image: chromadb/chroma:latest volumes: - ./chromadb:/chroma/chroma - ./server.htpasswd:/chroma/server.htpasswd environment: - IS_PERSISTENT=TRUE - PERSIST_DIRECTORY=/chroma/chroma # this is the default path, change it as needed - ANONYMIZED_TELEMETRY=${ANONYMIZED_TELEMETRY:-TRUE} - CHROMA_SERVER_AUTHN_CREDENTIALS_FILE=server.htpasswd - CHROMA_SERVER_AUTHN_PROVIDER=chromadb.auth.basic_authn.BasicAuthenticationServerProvider ports: - 8000:8000 networks: - net ``` Run the following command to start the Chroma server: ```bash docker compose -f docker-compose.yaml up -d ``` Is my config right? If you have correctly configured the server you should see the following line in the server logs: ```bash Starting component BasicAuthenticationServerProvider ``` **Client** ```python import chromadb from chromadb.config import Settings client = chromadb.HttpClient( settings=Settings( chroma_client_auth_provider="chromadb.auth.basic_authn.BasicAuthClientProvider", chroma_client_auth_credentials="admin:admin") ) # if everything is correctly configured the below should list all collections client.list_collections() ``` ```python import chromadb import base64 base64_credentials = base64.b64encode(b"admin:admin").decode("utf-8") client = await chromadb.AsyncHttpClient(headers={"Authorization": f"Basic {base64_credentials}"}) ``` ```javascript // const {ChromaClient} = require("chromadb"); // CommonJS import { ChromaClient } from "chromadb"; // ES Modules const client = new ChromaClient({ url: "http://localhost:8000", auth: { provider: "basic", credentials: "admin:admin", } }); ``` ```go package main import ( "context" "log" chroma "github.com/amikos-tech/chroma-go" "github.com/amikos-tech/chroma-go/types" ) func main() { client, err := chroma.NewClient( chroma.WithBasePath("http://localhost:8000"), chroma.WithAuth(types.NewBasicAuthCredentialsProvider("admin", "admin")), ) if err != nil { log.Fatalf("Error creating client: %s \n", err) } _, err = client.ListCollections(context.TODO()) if err != nil { log.Fatalf("Error calling ListCollections: %s \n", err) } } ``` The below example shows auth with just headers. A more robust authentication mechanism is being implemented. ```java package tech.amikos; import tech.amikos.chromadb.*; import tech.amikos.chromadb.Collection; import java.util.*; public class Main { public static void main(String[] args) { try { Client client = new Client(System.getenv("http://localhost:8000")); client.setDefaultHeaders(new HashMap<>() {{ put("Authorization", "Basic " + Base64.getEncoder().encodeToString("admin:admin".getBytes())); }}); // your code here } catch (Exception e) { System.out.println(e); } } } ``` Testing with cURL ```bash curl -v http://localhost:8000/api/v1/collections -u user1:change_this_password ``` ### Token Authentication **Server** Environment variables: ```bash export CHROMA_SERVER_AUTHN_CREDENTIALS="chr0ma-t0k3n" export CHROMA_SERVER_AUTHN_PROVIDER="chromadb.auth.token_authn.TokenAuthenticationServerProvider" export CHROMA_AUTH_TOKEN_TRANSPORT_HEADER="Authorization" # or X-Chroma-Token ``` *Auth Headers* Chroma supports two token transport headers: - `Authorization` (default) - the clients are expected to pass `Authorization: Bearer ` header - `X-Chroma-Token` - the clients are expected to pass `X-Chroma-Token: ` header The header can be configured via `CHROMA_AUTH_TOKEN_TRANSPORT_HEADER` environment variable. Running the server: ```bash export CHROMA_SERVER_AUTHN_CREDENTIALS="chr0ma-t0k3n" export CHROMA_SERVER_AUTHN_PROVIDER="chromadb.auth.token_authn.TokenAuthenticationServerProvider" export CHROMA_AUTH_TOKEN_TRANSPORT_HEADER="Authorization" chroma run --path /chroma-data ``` ```bash docker run --rm -e CHROMA_SERVER_AUTHN_CREDENTIALS="chr0ma-t0k3n" \ -e CHROMA_SERVER_AUTHN_PROVIDER="chromadb.auth.token_authn.TokenAuthenticationServerProvider" \ -e CHROMA_AUTH_TOKEN_TRANSPORT_HEADER="Authorization" \ -p 8000:8000 \ chromadb/chroma:latest ``` Create a `docker-compose.yaml` with the following content: ```yaml networks: net: driver: bridge services: chromadb: image: chromadb/chroma:latest volumes: - ./chromadb:/chroma/chroma environment: - IS_PERSISTENT=TRUE - PERSIST_DIRECTORY=/chroma/chroma # this is the default path, change it as needed - ANONYMIZED_TELEMETRY=${ANONYMIZED_TELEMETRY:-TRUE} - CHROMA_SERVER_AUTHN_CREDENTIALS="chr0ma-t0k3n" - CHROMA_AUTH_TOKEN_TRANSPORT_HEADER="Authorization" - CHROMA_SERVER_AUTHN_PROVIDER=chromadb.auth.token_authn.TokenAuthenticationServerProvider ports: - 8000:8000 networks: - net ``` Run the following command to start the Chroma server: ```bash docker compose -f docker-compose.yaml up -d ``` Is my config right? If you have correctly configured the server you should see the following line in the server logs: ```bash Starting component TokenAuthenticationServerProvider ``` **Client** ```python import chromadb from chromadb.config import Settings client = chromadb.HttpClient( settings=Settings( chroma_client_auth_provider="chromadb.auth.token_authn.TokenAuthClientProvider", chroma_client_auth_credentials="chr0ma-t0k3n", chroma_auth_token_transport_header="Authorization" ) ) # if everything is correctly configured the below should list all collections client.list_collections() ``` ```python import chromadb # for Authorization header client = await chromadb.AsyncHttpClient(headers={"Authorization": "Bearer chr0ma-t0k3n"}) # for X-Chroma-Token header client = await chromadb.AsyncHttpClient(headers={"X-Chroma-Token": "chr0ma-t0k3n"}) # if everything is correctly configured the below should list all collections await client.list_collections() ``` ```javascript // const {ChromaClient} = require("chromadb"); // CommonJS import { ChromaClient } from "chromadb"; // ES Modules const client = new ChromaClient({ url: "http://localhost:8000", auth: { provider: "token", credentials: "chr0ma-t0k3n", } }); ``` ```go package main import ( "context" "log" chroma "github.com/amikos-tech/chroma-go" "github.com/amikos-tech/chroma-go/types" ) func main() { client, err := chroma.NewClient( chroma.WithBasePath("http://localhost:8000"), chroma.WithAuth(types.NewTokenAuthCredentialsProvider("chr0ma-t0k3n", types.AuthorizationTokenHeader)), ) if err != nil { log.Fatalf("Error creating client: %s \n", err) } _, err = client.ListCollections(context.TODO()) if err != nil { log.Fatalf("Error calling ListCollections: %s \n", err) } } ``` The example below shows authorization with just headers. A more robust auth mechanism is under implementation. ```java package tech.amikos; import tech.amikos.chromadb.*; import tech.amikos.chromadb.Collection; import java.util.*; public class Main { public static void main(String[] args) { try { Client client = new Client(System.getenv("http://localhost:8000")); client.setDefaultHeaders(new HashMap<>() {{ put("Authorization", "Bearer chr0ma-t0k3n"); }}); // your code here } catch (Exception e) { System.out.println(e); } } } ``` Testing with cURL ```bash curl -v http://localhost:8000/api/v1/collections -H "Authorization: Bearer chr0ma-t0k3n" ``` ## Authorization Coming soon! # SSL/TLS Proxy In this section we'll explore how to secure Chroma with a TLS-terminated HTTPS proxy. Below we'll give two examples of how to do this using Envoy and Nginx. The certificates are self-signed and generated using OpenSSL, but in the future we'll also provide examples of how to achieve this with Let's Encrypt and certbot. ## Getting The cert To manually generate a certificate follow the steps [here](https://cookbook.chromadb.dev/security/chroma-ssl-cert/#creating-a-self-signed-certificate). ## Envoy The following envoy configuration will create a listener on port 443 that will forward all requests to the `chromadb`. ```yaml static_resources: listeners: - name: listener_0 address: socket_address: address: 0.0.0.0 port_value: 443 filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http route_config: name: chroma_route virtual_hosts: - name: local_chromadb domains: [ "*" ] routes: - match: prefix: "/" route: cluster: chromadb_service prefix_rewrite: "/" http_filters: - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext common_tls_context: tls_certificates: - certificate_chain: filename: "/etc/envoy/certs/servercert.pem" private_key: filename: "/etc/envoy/certs/serverkey.pem" clusters: - name: chromadb_service connect_timeout: 0.25s type: LOGICAL_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: chromadb_service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: chromadb port_value: 8000 ``` Finally the docker compose to tie things up where we have added a `cert-gen` step to automatically generate the certificates, prior to starting the `envoy` and `chromadb` services. ```yaml version: '3' networks: net: driver: bridge services: cert-gen: image: openquantumsafe/openssl3 volumes: - ./certs:/certs - ./openssl.cnf:/etc/ssl/openssl.cnf command: | sh -c "[ -f /certs/servercert.pem ] || \ openssl req -new -newkey rsa:2048 -sha256 -days 365 -nodes -x509 -keyout /certs/serverkey.pem -out /certs/servercert.pem -subj '/O=Chroma/C=US' -config /etc/ssl/openssl.cnf" environment: - CHROMA_DOMAIN=${CHROMA_DOMAIN:-localhost} envoy: image: bitnami/envoy volumes: - ./envoy.yaml:/opt/bitnami/envoy/conf/envoy.yaml - ./certs:/etc/envoy/certs - ./wait-for-certs.sh:/usr/local/bin/wait-for-certs.sh ports: - "443:443" networks: - net depends_on: cert-gen: condition: service_completed_successfully chromadb: condition: service_healthy entrypoint: | sh -c "/usr/local/bin/wait-for-certs.sh && \ /opt/bitnami/envoy/bin/envoy -c /opt/bitnami/envoy/conf/envoy.yaml" chromadb: image: chromadb/chroma:0.6.3 volumes: - ./chromadb:/chroma/chroma environment: - IS_PERSISTENT=TRUE - ANONYMIZED_TELEMETRY=${ANONYMIZED_TELEMETRY:-TRUE} networks: - net healthcheck: # Adjust below to match your container port test: [ "CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat" ] interval: 30s timeout: 10s retries: 3 ``` ## Nginx Use the following Nginx config (`nginx.conf`) as a starting point and build from there: ```text server { listen 443 ssl; server_name localhost; ssl_certificate /etc/nginx/certs/servercert.pem; ssl_certificate_key /etc/nginx/certs/serverkey.pem; location / { proxy_pass http://chromadb:8000; proxy_set_header Host $host; proxy_http_version 1.1; # Use HTTP/1.1 proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` Create a `docker-compose.yml` file with the following content: Config Files For the following `docker-compose.yaml` to operate successfully `openssl.cnf` and `nginx.conf` files need to be present in the same directory. ```yaml version: '3' networks: net: driver: bridge services: cert-gen: image: openquantumsafe/openssl3 volumes: - ./certs:/certs - ./openssl.cnf:/etc/ssl/openssl.cnf command: | sh -c "[ -f /certs/servercert.pem ] || \ openssl req -new -newkey rsa:2048 -sha256 -days 365 -nodes -x509 -keyout /certs/serverkey.pem -out /certs/servercert.pem -subj '/O=Chroma/C=US' -config /etc/ssl/openssl.cnf" environment: - CHROMA_DOMAIN=${CHROMA_DOMAIN:-localhost} chromadb: image: chromadb/chroma:0.6.3 volumes: - ./chromadb:/chroma/chroma environment: - IS_PERSISTENT=TRUE - ANONYMIZED_TELEMETRY=${ANONYMIZED_TELEMETRY:-TRUE} ports: - "8000:8000" healthcheck: # Adjust below to match your container port test: [ "CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat" ] interval: 30s timeout: 10s retries: 3 networks: - net nginx: image: nginx:latest depends_on: - cert-gen - chromadb ports: - "443:443" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf - ./certs:/etc/nginx/certs networks: - net depends_on: chromadb: condition: service_healthy ``` # Strategies # ChromaDB Backups Depending on your use case there are a few different ways to back up your ChromaDB data. - API export - this approach is relatively simple, slow for large datasets and may result in a backup that is missing some updates, should your data change frequently. - Disk snapshot - this approach is fast, but is highly dependent on the underlying storage. Should your cloud provider and underlying volume support snapshots, this is a good option. - Filesystem backup - this approach is also fast, but requires stopping your Chroma container to avoid data corruption. This is a good option if you can afford to stop your Chroma container for a few minutes. Other Options Have another option in mind, feel free to [add](https://github.com/amikos-tech/chroma-cookbook) it to the above list. ## API Export ### With Chroma Datapipes One way to export via the API is to use Tooling like Chroma Data Pipes. Chroma Data Pipes is a command-line tool that provides a simple way import/export/transform ChromaDB data. Exporting from local filesystem: ```bash cdp export "file:///absolute/path/to/chroma-data/my-collection-name" > my_chroma_data.jsonl ``` Exporting from remote server: ```bash cdp export "http://remote-chroma-server:8000/my-collection-name" > my_chroma_data.jsonl ``` Get Help Read more about Chroma Data Pipes [here](https://datapipes.chromadb.dev) ## Disk Snapshot TBD ## Filesystem Backup ### From Docker Container Sometimes you have been running Chroma in a Docker container without a host mount, intentionally or unintentionally. So all your data is now stored in the container's filesystem. Here's how you can back up your data: 1. Stop the container: ```bash docker stop ``` 1. Create a backup of the container's filesystem: ```bash docker cp :/chroma/chroma /path/to/backup ``` `/path/to/backup` is the directory where you want to store the backup on your host machine. # Batching It is often that you may need to ingest a large number of documents into Chroma. The problem you may face is related to the underlying SQLite version of the machine running Chroma which imposes a maximum number of statements and parameters which Chroma translates into a batchable record size, exposed via the `max_batch_size` parameter of the `ChromaClient` class. ```python import chromadb client = chromadb.PersistentClient(path="test") print("Number of documents that can be inserted at once: ",client.max_batch_size) ``` ## Creating Batches Due to consistency and data integrity reasons, Chroma does not offer, yet, out-of-the-box batching support. The below code snippet shows how to create batches of documents and ingest them into Chroma. ```python import chromadb from chromadb.utils.batch_utils import create_batches import uuid client = chromadb.PersistentClient(path="test-large-batch") large_batch = [(f"{uuid.uuid4()}", f"document {i}", [0.1] * 1536) for i in range(100000)] ids, documents, embeddings = zip(*large_batch) batches = create_batches(api=client,ids=list(ids), documents=list(documents), embeddings=list(embeddings)) collection = client.get_or_create_collection("test") for batch in batches: print(f"Adding batch of size {len(batch[0])}") collection.add(ids=batch[0], documents=batch[3], embeddings=batch[1], metadatas=batch[2]) ``` # CORS Configuration for Browser-Based Access Chroma [JS package](https://www.npmjs.com/package/chromadb) allows you to use Chroma in your browser-based SPA application. This is great, but that means that you'll need to configure Chroma to work with your browser to avoid CORS issues. ## Setting up Chroma for Browser-Based Access ### Chroma 1.0 or later To allow browsers to directly access your Chroma instance you'll need to configure the `CHROMA_CORS_ALLOW_ORIGINS`. The `CHROMA_CORS_ALLOW_ORIGINS` environment variable controls the hosts which are allowed to access your Chroma instance. Note The `CHROMA_CORS_ALLOW_ORIGINS` environment variable is a list of strings. Each string is a URL that is allowed to access your Chroma instance. If you want to allow all hosts to access your Chroma instance, you can set `CHROMA_CORS_ALLOW_ORIGINS` to `["*"]`. This is **not recommended** for production environments. ```bash export CHROMA_CORS_ALLOW_ORIGINS='["http://localhost:3000"]' chroma run --path /path/to/chroma-data ``` Verify with `curl -i -X GET http://localhost:8000/api/v2/version -H "Origin: http://localhost:3000"` in the response you should see `access-control-allow-origin: http://localhost:3000` being returned if all works fine ```bash docker run -e CHROMA_CORS_ALLOW_ORIGINS='["http://localhost:3000"]' -p 8000:8000 chromadb/chroma:1.5.0 ``` Verify with `curl -i -X GET http://localhost:8000/api/v2/version -H "Origin: http://localhost:3000"` in the response you should see `access-control-allow-origin: http://localhost:3000` being returned if all works fine ```yaml version: '3.9' networks: net: driver: bridge services: server: image: chromadb/chroma:1.5.0 volumes: # Be aware that indexed data are located in "/data/" - chroma-data:/data environment: - CHROMA_CORS_ALLOW_ORIGINS=["http://localhost:3000"] restart: unless-stopped # possible values are: "no", always", "on-failure", "unless-stopped" ports: - "8000:8000" healthcheck: # Adjust below to match your container port test: [ "CMD", "curl", "-f", "http://localhost:8000/api/v2/heartbeat" ] interval: 30s timeout: 10s retries: 3 networks: - net volumes: chroma-data: driver: local ``` Run `docker compose up` to start your Chroma instance. Verify with `curl -i -X GET http://localhost:8000/api/v2/version -H "Origin: http://localhost:3000"` in the response you should see `access-control-allow-origin: http://localhost:3000` being returned if all works fine ### Chroma Pre-1.0 (Legacy) To allow browsers to directly access your Chroma instance you'll need to configure the `CHROMA_SERVER_CORS_ALLOW_ORIGINS`. The `CHROMA_SERVER_CORS_ALLOW_ORIGINS` environment variable controls the hosts which are allowed to access your Chroma instance. Note The `CHROMA_SERVER_CORS_ALLOW_ORIGINS` environment variable is a list of strings. Each string is a URL that is allowed to access your Chroma instance. If you want to allow all hosts to access your Chroma instance, you can set `CHROMA_SERVER_CORS_ALLOW_ORIGINS` to `["*"]`. This is not recommended for production environments. The below examples assume that your web app is running on `http://localhost:3000`. You can find an example of NextJS and Langchain [here](https://github.com/amikos-tech/chroma-langchain-nextjs). ```bash export CHROMA_SERVER_CORS_ALLOW_ORIGINS='["http://localhost:3000"]' chroma run --path /path/to/chroma-data ``` ```bash docker run -e CHROMA_SERVER_CORS_ALLOW_ORIGINS='["http://localhost:3000"]' -v /path/to/chroma-data:/chroma/chroma -p 8000:8000 chromadb/chroma:0.6.3 ``` ```yaml version: '3.9' networks: net: driver: bridge services: server: image: chromadb/chroma:0.6.3 volumes: # Be aware that indexed data are located in "/chroma/chroma/" # Default configuration for persist_directory in chromadb/config.py # Read more about deployments: https://docs.trychroma.com/deployment - chroma-data:/chroma/chroma command: "--workers 1 --host 0.0.0.0 --port 8000 --proxy-headers --log-config chromadb/log_config.yml --timeout-keep-alive 30" environment: - IS_PERSISTENT=TRUE - CHROMA_SERVER_AUTH_PROVIDER=${CHROMA_SERVER_AUTH_PROVIDER} - CHROMA_SERVER_AUTHN_CREDENTIALS_FILE=${CHROMA_SERVER_AUTHN_CREDENTIALS_FILE} - CHROMA_SERVER_AUTHN_CREDENTIALS=${CHROMA_SERVER_AUTHN_CREDENTIALS} - CHROMA_AUTH_TOKEN_TRANSPORT_HEADER=${CHROMA_AUTH_TOKEN_TRANSPORT_HEADER} - PERSIST_DIRECTORY=${PERSIST_DIRECTORY:-/chroma/chroma} - CHROMA_OTEL_EXPORTER_ENDPOINT=${CHROMA_OTEL_EXPORTER_ENDPOINT} - CHROMA_OTEL_EXPORTER_HEADERS=${CHROMA_OTEL_EXPORTER_HEADERS} - CHROMA_OTEL_SERVICE_NAME=${CHROMA_OTEL_SERVICE_NAME} - CHROMA_OTEL_GRANULARITY=${CHROMA_OTEL_GRANULARITY} - CHROMA_SERVER_NOFILE=${CHROMA_SERVER_NOFILE} - CHROMA_SERVER_CORS_ALLOW_ORIGINS=["http://localhost:3000"] restart: unless-stopped # possible values are: "no", always", "on-failure", "unless-stopped" ports: - "8000:8000" healthcheck: # Adjust below to match your container port test: [ "CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat" ] interval: 30s timeout: 10s retries: 3 networks: - net volumes: chroma-data: driver: local ``` Run `docker compose up` to start your Chroma instance. # Go Local Markdown CLI with `PersistentClient` This walkthrough highlights a new `chroma-go` capability: a **local in-process `PersistentClient`** that enables portable Go CLIs/tools without running a separate Chroma server or using Chroma Cloud. The example is intentionally small, but shows a complete local retrieval loop: 1. `index `: read markdown files, chunk text, and upsert into local Chroma. 1. `search `: query semantically, with optional metadata filters in a compact DSL (`key:value` or ``). ## Why this example matters - No external dependencies for vector storage/search. - No API keys required. - Great fit for local tools, dev assistants, and single-binary internal utilities. Requires `chroma-go` v0.4.0+ The `PersistentClient` API used in this example was introduced in [`chroma-go` v0.4.0](https://github.com/amikos-tech/chroma-go/releases/tag/v0.4.0). ## Implementation highlights - CLI framework: [`cobra`](https://github.com/spf13/cobra) - Markdown processing: [`goldmark`](https://github.com/yuin/goldmark) - Chroma runtime: `NewPersistentClient(...)` - Embeddings: `ort.NewDefaultEmbeddingFunction()` (`default_ef`, local) ## Run it ```bash cd examples/persistent-cli/go go mod tidy go run . index ../sample-docs --reset go run . search "incident response" go run . search "incident response env:prod" ``` ## Metadata filter DSL Any `key:value` token in the query is interpreted as metadata filter and removed from semantic query text. Examples: - `incident response env:prod` - `rollback checklist ` Under the hood, each filter maps to metadata `where` with equality (combined via `AND`). ## Going further The word-window chunker in this example is intentionally simple. For production use cases consider more advanced strategies such as recursive or semantic chunking. [LangChainGo](https://tmc.github.io/langchaingo/docs/) provides ready-made text splitters that pair well with `chroma-go`. ## Source - [Example README](https://github.com/amikos-tech/chroma-cookbook/tree/main/examples/persistent-cli) - [Go source](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/persistent-cli/go/main.go) # Image Search (Multimodal Retrieval) This page covers local/OSS image search with Chroma using OpenCLIP embeddings. You can index image URIs and query by: - text (`query_texts`) for text-to-image retrieval - image URI (`query_uris`) for image-to-image retrieval This is useful when you need one collection to support both semantic text prompts and visual similarity search. Language Scope This strategy currently ships with a Python runnable example. The OpenCLIP + `ImageLoader` workflow demonstrated here is Python-centric in this cookbook. ## Quick Start Pattern Use this default flow: 1. Create a collection with `OpenCLIPEmbeddingFunction` and `ImageLoader`. 1. Add local image URIs via `collection.add(..., uris=[...])`. 1. Query by text for discovery (`query_texts=["a photo of a dog"]`). 1. Query by image URI for visual nearest-neighbor lookup (`query_uris=[...]`). 1. Add metadata filters (`where`) when you need constrained retrieval by source, category, or tenant. ## Short Snippet Concept Snippet This snippet is intentionally small and may omit setup. Use the runnable example for an end-to-end script. ```python from chromadb.utils.data_loaders import ImageLoader from chromadb.utils.embedding_functions import OpenCLIPEmbeddingFunction collection = client.create_collection( name="multimodal_collection", embedding_function=OpenCLIPEmbeddingFunction(), data_loader=ImageLoader(), ) collection.add( ids=["cat", "dog"], uris=["/path/to/cat.png", "/path/to/dog.jpg"], metadatas=[{"label": "cat"}, {"label": "dog"}], ) text_results = collection.query( query_texts=["a photo of a dog"], n_results=2, include=["uris", "metadatas", "distances"], ) image_results = collection.query( query_uris=["/path/to/dog.jpg"], n_results=2, include=["uris", "metadatas", "distances"], ) ``` ## End-to-End Python Walkthrough The runnable example uses: - local persistence (`PersistentClient`) - bundled sample images (`cat`, `dog`, `dog-with-person`) - OpenCLIP embeddings generated by Chroma Run: ```bash cd examples/image-search/python python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt python image_search.py ``` Expected output includes: - ranked text-to-image results for `"a photo of a dog"` - ranked text-to-image results for `"a photo of a cat"` - ranked image-to-image results using `dog-with-person.jpg` as query ## Operational Notes - First run downloads model weights; startup is slower initially. - CPU-only execution works but is slower than GPU. - Keep image files accessible on disk if you store local URIs. - Use metadata (`where`) to scope search in multi-tenant or multi-source pipelines. - Example data is written under `examples/image-search/python/chroma_data/image_search_example`. ## Core References - [Collections API (`add`, `query`, result shapes)](https://cookbook.chromadb.dev/core/collections/index.md) - [Embedding Models (overview of supported models)](https://cookbook.chromadb.dev/embeddings/embedding-models/index.md) - [Embedding Functions GPU Support (OpenCLIP)](https://cookbook.chromadb.dev/embeddings/gpu-support/index.md) ## Full Runnable Example - [Overview and run commands](https://github.com/amikos-tech/chroma-cookbook/tree/main/examples/image-search) - [Python](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/image-search/python/image_search.py) - [Requirements](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/image-search/python/requirements.txt) # Keyword Search This page covers local/OSS keyword filtering for collection `get()` and `query()` calls. Use document filters (`where_document`) with `$contains` / `$not_contains` / `$regex` / `$not_regex`, optionally combined with metadata `where`, when you need semantic retrieval plus lexical constraints. In Rust, the same behavior is expressed through the unified `Where` AST (for example `Where::Document(...)`). Full-text matching is case-sensitive. For Cloud Search API hybrid ranking/fusion workflows, use the Cloud Search docs. ## Short Snippets Concept Snippets The snippets below are intentionally small and may omit surrounding setup. Use the runnable examples for end-to-end scripts. ```python results = collection.query( query_embeddings=[[0.15, 0.85, 0.25]], n_results=3, where_document={ "$or": [ {"$contains": "technology"}, {"$contains": "freak"}, ] }, ) ``` ```typescript const results = await collection.query({ queryEmbeddings: [[0.15, 0.85, 0.25]], nResults: 3, whereDocument: { $or: [{ $contains: "technology" }, { $contains: "freak" }], }, }); ``` ```go results, err := collection.Query(ctx, chroma.WithQueryEmbeddings( embeddings.NewEmbeddingFromFloat32([]float32{0.15, 0.85, 0.25}), ), chroma.WithNResults(3), chroma.WithWhereDocument(chroma.OrDocument( chroma.Contains("technology"), chroma.Contains("freak"), )), ) ``` Rust uses a unified `Where` AST for both metadata and document filters. ```rust let where_clause = Where::Composite(CompositeExpression { operator: BooleanOperator::Or, children: vec![ Where::Document(DocumentExpression { operator: DocumentOperator::Contains, pattern: "technology".to_string(), }), Where::Document(DocumentExpression { operator: DocumentOperator::Contains, pattern: "freak".to_string(), }), ], }); let results = collection .query( vec![vec![0.15, 0.85, 0.25]], Some(3), Some(where_clause), None, None, ) .await?; ``` ## Regex Filters Use regex operators when substring matching is not expressive enough. ```python regex_results = collection.query( query_embeddings=[[0.15, 0.85, 0.25]], n_results=3, where_document={"$regex": "technology.*pace"}, ) not_regex_results = collection.query( query_embeddings=[[0.15, 0.85, 0.25]], n_results=3, where_document={"$not_regex": "Innovation.*topic"}, ) ``` ```typescript const regexResults = await collection.query({ queryEmbeddings: [[0.15, 0.85, 0.25]], nResults: 3, whereDocument: { $regex: "technology.*pace" }, }); const notRegexResults = await collection.query({ queryEmbeddings: [[0.15, 0.85, 0.25]], nResults: 3, whereDocument: { $not_regex: "Innovation.*topic" }, }); ``` ```go regexResults, err := collection.Query(ctx, chroma.WithQueryEmbeddings( embeddings.NewEmbeddingFromFloat32([]float32{0.15, 0.85, 0.25}), ), chroma.WithNResults(3), chroma.WithWhereDocument(chroma.Regex("technology.*pace")), ) notRegexResults, err := collection.Query(ctx, chroma.WithQueryEmbeddings( embeddings.NewEmbeddingFromFloat32([]float32{0.15, 0.85, 0.25}), ), chroma.WithNResults(3), chroma.WithWhereDocument(chroma.NotRegex("Innovation.*topic")), ) ``` ```rust let regex_results = collection .query( vec![vec![0.15, 0.85, 0.25]], Some(3), Some(Where::Document(DocumentExpression { operator: DocumentOperator::Regex, pattern: "technology.*pace".to_string(), })), None, None, ) .await?; let not_regex_results = collection .query( vec![vec![0.15, 0.85, 0.25]], Some(3), Some(Where::Document(DocumentExpression { operator: DocumentOperator::NotRegex, pattern: "Innovation.*topic".to_string(), })), None, None, ) .await?; ``` ## Composing Complex Queries Use this canonical filter shape when you need semantic retrieval with richer document-text constraints: ```json { "where_document": { "$and": [ { "$or": [ { "$contains": "technology" }, { "$regex": "\\bAI\\b" } ] }, { "$or": [ { "$contains": "LLM" }, { "$regex": "(GPU|CUDA)" } ] }, { "$not_contains": "deprecated" }, { "$not_regex": "(draft|obsolete)" } ] } } ``` This expresses: - `technology` substring OR exact-word `AI` regex. - `LLM` substring OR `GPU`/`CUDA` regex. - Excludes documents containing `deprecated`. - Excludes documents matching `draft` or `obsolete` patterns. Client mapping: - Python: pass this object as `where_document=...`. - TypeScript: pass as `whereDocument`. - Go: map to `chroma.WithWhereDocument(...)`. - Rust: express this with document nodes in the unified `Where` tree. Hints: - If you also pass metadata `where`, it is combined with `where_document` using an implicit `AND`. - Use nested `$and`/`$or` in either clause to model richer logic. - For debugging candidate selection, run `get(where=..., where_document=..., include=[])` first to inspect matching IDs before ranking. - Keep `include` narrow to reduce payload size and response time. - Full-text operators include contains/not-contains and regex/not-regex. In Rust these map to `DocumentOperator::{Contains, NotContains, Regex, NotRegex}`. - Prefer anchored and specific regex patterns to avoid broad scans. ## Core References - [Filters (`where` and `where_document` operators)](https://cookbook.chromadb.dev/core/filters/index.md) - [Collections (`query` result shape, `include`, ID-constrained query)](https://cookbook.chromadb.dev/core/collections/index.md) - [Concepts (search stages and query flow)](https://cookbook.chromadb.dev/core/concepts/index.md) - [Advanced Queries (query-stage semantics and tradeoffs)](https://cookbook.chromadb.dev/core/advanced/queries/index.md) - [Official Full Text Search docs](https://docs.trychroma.com/docs/querying-collections/full-text-search) ## Full Runnable Examples All runnable examples assume a local Chroma server: ```bash docker run --rm -p 8000:8000 chromadb/chroma:1.5.3 ``` - [Overview and run commands](https://github.com/amikos-tech/chroma-cookbook/tree/main/examples/keyword-search) - [Python](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/keyword-search/python/keyword_search.py) - [TypeScript](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/keyword-search/typescript/keyword_search.ts) - [Go](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/keyword-search/go/main.go) - [Rust](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/keyword-search/rust/src/main.rs) # Memory Management This section provided additional info and strategies how to manage memory in Chroma. ## LRU Cache Strategy Out of the box Chroma offers an LRU cache strategy which unloads segments (collections) that are not used while trying to abide to the configured memory usage limits. To enable the LRU cache the following two settings parameters or environment variables need to be set: ```python from chromadb.config import Settings settings = Settings( chroma_segment_cache_policy="LRU", chroma_memory_limit_bytes=10000000000 # ~10GB ) ``` ```bash export CHROMA_SEGMENT_CACHE_POLICY=LRU export CHROMA_MEMORY_LIMIT_BYTES=10000000000 # ~10GB ``` ## Manual/Custom Collection Unloading Local Clients The below code snippets assume you are working with a `PersistentClient` or an `EphemeralClient` instance. At the time of writing (Chroma v0.6.3), Chroma does not allow you to manually unloading of collections from memory. Here we provide a simple utility function to help users unload collections from memory. Internal APIs The below code relies on internal APIs and may change in future versions of Chroma. The function relies on Chroma internal APIs which may change. The below snippet has been tested with Chroma `0.4.24+`. ```python import gc import os import chromadb import psutil from chromadb.types import SegmentScope def bytes_to_gb(bytes_value): return bytes_value / (1024 ** 3) def get_process_info(): pid = os.getpid() p = psutil.Process(pid) with p.oneshot(): mem_info = p.memory_info() # disk_io = p.io_counters() return { "memory_usage": bytes_to_gb(mem_info.rss), } def unload_index(collection_name: str, chroma_client: chromadb.PersistentClient): """ Unloads binary hnsw index from memory and removes both segments (binary and metadata) from the segment cache. """ collection = chroma_client.get_collection(collection_name) collection_id = collection.id segment_manager = chroma_client._server._manager for scope in [SegmentScope.VECTOR, SegmentScope.METADATA]: if scope in segment_manager.segment_cache: cache = segment_manager.segment_cache[scope].cache if collection_id in cache: segment_manager.callback_cache_evict(cache[collection_id]) gc.collect() ``` Example Contributed The above example was enhanced and contributed by `Amir` (amdeilami) from our Discord comminity. We appreciate and encourage his work and contributions to the Chroma community. Usage Example ```python import chromadb client = chromadb.PersistentClient(path="testds-1M/chroma-data") col=client.get_collection("test") print(col.count()) col.get(limit=1,include=["embeddings"]) # force load the collection into memory unload_index("test", client) ``` # Metadata Schema Validation (Application Layer) Chroma validates metadata value types, but it does not enforce a per-collection metadata contract. If your app needs stable metadata shape (for example required keys, enums, ranges), enforce that before calling `add()` or `upsert()`. ## Shared Contract Used In Examples The examples below all enforce the same metadata contract: | Field | Type | Rule | | --------------- | ------- | --------------------------------- | | `tenant_id` | string | required, non-empty | | `doc_type` | string | one of `policy`, `faq`, `runbook` | | `published` | boolean | required | | `priority` | integer | between `1` and `5` | | `quality_score` | float | between `0.0` and `1.0` | ## Integration Blueprint Use this as the default workflow when integrating schema enforcement into an app: 1. Define one metadata schema type per logical record family. 1. Centralize write/read helpers (`insert_validated_record`, `read_validated_record`) in your repository/service layer. 1. Route all write paths (`add`, `upsert`, ETL/backfill jobs) through the write helper. 1. Re-parse metadata from `get()` and `query()` responses into typed objects before downstream use. 1. Choose an explicit invalid-data policy (fail-fast or quarantine). Where To Place This Put these helpers where Chroma calls are centralized (repository/DAO/service). Avoid validating ad-hoc in controllers/handlers; that drifts quickly. ## Roundtrip Helper Pattern (Recommended) In production code, wrap validation + persistence into helpers so all write paths are consistent. ```python def insert_validated_record(collection, record): validated = RecordMetadata.model_validate(record["metadata"]) collection.add( ids=[record["id"]], documents=[record["document"]], embeddings=[record["embedding"]], metadatas=[validated.model_dump()], ) def read_validated_record(collection, record_id): res = collection.get(ids=[record_id], include=["documents", "metadatas"]) return RecordMetadata.model_validate(res["metadatas"][0]) query_res = collection.query( query_embeddings=[[0.1, 0.2, 0.3]], n_results=3, where={"tenant_id": "acme"}, include=["documents", "metadatas", "distances"], ) ``` ```typescript const insertValidatedRecord = async (record: RawRecord) => { const validated = RecordMetadata.parse(record.metadata); await collection.add({ ids: [record.id], documents: [record.document], embeddings: [record.embedding], metadatas: [validated], }); }; const readValidatedRecord = async (id: string) => { const res = await collection.get({ ids: [id], include: ["documents", "metadatas"] }); return RecordMetadata.parse(res.metadatas?.[0]); }; const queryRes = await collection.query({ queryEmbeddings: [[0.1, 0.2, 0.3]], nResults: 3, where: { tenant_id: "acme" }, include: ["documents", "metadatas", "distances"], }); ``` ```go insertValidatedRecord := func(record RawRecord) error { meta, _ := decodeMetadata(record.Metadata) if err := validate.Struct(meta); err != nil { return err } return collection.Add(ctx, chroma.WithIDs(chroma.DocumentID(record.ID)), chroma.WithTexts(record.Document), chroma.WithEmbeddings(embeddings.NewEmbeddingFromFloat32(record.Embedding)), chroma.WithMetadatas(toChromaMetadata(meta)), ) } readValidatedRecord := func(id chroma.DocumentID) (RecordMetadata, error) { res, _ := collection.Get(ctx, chroma.WithIDs(id), chroma.WithInclude(chroma.IncludeDocuments, chroma.IncludeMetadatas)) return parseMetadataFromChroma(res.GetMetadatas()[0], validate) } ``` Parsing note: When reading metadata back, treat numeric fields defensively (`GetInt` first, then optional `GetFloat` fallback for whole-number values) before re-validating. ```rust async fn insert_validated_record(collection: &chroma::ChromaCollection, record: &RawRecord) { let metadata: RecordMetadata = serde_json::from_value(record.metadata.clone())?; metadata.validate()?; collection.add(/* id + embedding + doc + validated metadata */).await?; } async fn read_validated_record(collection: &chroma::ChromaCollection, id: &str) { let response = collection.get(Some(vec![id.to_string()]), None, Some(1), Some(0), None).await?; let metadata = response.metadatas.unwrap()[0].as_ref().unwrap(); let typed = parse_metadata_from_chroma(metadata)?; } ``` ## Failure Policy Templates Use one of these policies explicitly. ### Fail-Fast Reject invalid records immediately and return an error to caller. ```python try: validated = RecordMetadata.model_validate(raw_metadata) except ValidationError as exc: raise ValueError(f"invalid metadata: {exc}") ``` ### Quarantine Store invalid payloads separately (for review/replay) and continue pipeline. ```python try: validated = RecordMetadata.model_validate(raw_metadata) except ValidationError as exc: quarantine_store.write({"raw": raw_metadata, "errors": exc.errors()}) return # skip write to Chroma ``` ## Short Snippets (Concept-Only) Concept Snippets The snippets below are intentionally minimal and may omit surrounding setup. Use the runnable examples for copy/paste-ready flows. ```python from typing import Literal from pydantic import BaseModel, Field class RecordMetadata(BaseModel): tenant_id: str = Field(min_length=1) doc_type: Literal["policy", "faq", "runbook"] published: bool priority: int = Field(ge=1, le=5) quality_score: float = Field(ge=0.0, le=1.0) validated = RecordMetadata.model_validate(raw_metadata).model_dump() collection.add(ids=["doc-1"], embeddings=[[0.1, 0.2, 0.3]], metadatas=[validated]) ``` ```typescript import { z } from "zod"; const RecordMetadata = z.object({ tenant_id: z.string().min(1), doc_type: z.enum(["policy", "faq", "runbook"]), published: z.boolean(), priority: z.number().int().min(1).max(5), quality_score: z.number().min(0).max(1), }).strict(); const validated = RecordMetadata.parse(rawMetadata); await collection.add({ ids: ["doc-1"], embeddings: [[0.1, 0.2, 0.3]], metadatas: [validated], }); ``` Runtime note: If you use the JS client with server-side default embedding-function metadata, include `@chroma-core/default-embed` in dependencies. ```go type RecordMetadata struct { TenantID string `validate:"required,min=1"` DocType string `validate:"required,oneof=policy faq runbook"` Published bool Priority int64 `validate:"gte=1,lte=5"` QualityScore float64 `validate:"gte=0,lte=1"` } validate := validator.New() if err := validate.Struct(meta); err != nil { return err } ``` ```rust #[derive(Deserialize, Validate)] struct RecordMetadata { #[validate(length(min = 1))] tenant_id: String, doc_type: DocType, published: bool, #[validate(range(min = 1, max = 5))] priority: i64, #[validate(range(min = 0.0, max = 1.0))] quality_score: f64, } let metadata: RecordMetadata = serde_json::from_value(raw)?; metadata.validate()?; ``` ## Full Runnable Examples All runnable examples assume a local Chroma server: ```bash docker run --rm -p 8000:8000 chromadb/chroma:1.5.3 ``` - [Overview and run commands](https://github.com/amikos-tech/chroma-cookbook/tree/main/examples/metadata-schema) - [Python + Pydantic](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/metadata-schema/python/schema_validation.py) - [TypeScript + Zod](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/metadata-schema/typescript/schema_validation.ts) - [Go + validator](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/metadata-schema/go/main.go) - [Rust + serde/validator](https://github.com/amikos-tech/chroma-cookbook/blob/main/examples/metadata-schema/rust/src/main.rs) ## Schema Evolution Both validation strategies are valid: 1. Typed-model source of truth (`pydantic`/`zod`/Go validators/Rust validators). 1. JSON-Schema source of truth (portable and reusable across languages). Schema evolution is the versioning/governance layer and works with either strategy. If metadata contracts change over time, add a `schema_version` field and evolve intentionally. 1. Write new records with latest `schema_version`. 1. Read old records by version and normalize before business logic. 1. Backfill old records when practical. ### Versioning In Collection Metadata You can also keep the active schema version in **collection metadata** so every writer/reader can discover it. ```python import json SCHEMA_V2 = { "type": "object", "properties": { "tenant_id": {"type": "string", "minLength": 1}, "doc_type": {"enum": ["policy", "faq", "runbook"]}, "published": {"type": "boolean"}, "priority": {"type": "integer", "minimum": 1, "maximum": 5}, "quality_score": {"type": "number", "minimum": 0, "maximum": 1}, }, "required": ["tenant_id", "doc_type", "published", "priority", "quality_score"], } collection = client.get_or_create_collection( "support_docs", metadata={ "metadata_schema_name": "support_docs", "metadata_schema_version": 2, "metadata_schema_json": json.dumps(SCHEMA_V2), # optional }, ) ``` Collection Metadata Update Behavior `collection.modify(metadata=...)` overwrites collection metadata. Read + merge existing keys before writing updates. ### Loading Schema From JSON Stored As Metadata String Yes. This is supported because collection metadata values can be strings. ```python import json from jsonschema import Draft202012Validator raw = collection.metadata.get("metadata_schema_json") if raw is not None: validator = Draft202012Validator(json.loads(raw)) validator.validate(candidate_metadata) ``` ### Complexity Tradeoff - For small teams/single service: storing schema JSON in collection metadata can be fine. - As systems grow (multiple services, bigger schemas, stricter governance): prefer storing `schema_name` + `schema_version` + optional `schema_hash` in collection metadata, and keep full schemas in a code repo or schema registry. - If JSON Schema is your source of truth, consider generating typed models from it to keep runtime portability plus language-level ergonomics. ## Practical Guidance - Validate on every write path (`add`, `upsert`, update jobs). - Decide policy explicitly: fail-fast or quarantine. - Pick one primary source of truth for the contract (typed models or JSON Schema) and avoid unmanaged dual-authoring drift. - Keep filter-critical fields (e.g., `tenant_id`, `doc_type`, flags) strongly typed to avoid inconsistent query behavior. - Re-parse and re-validate metadata after `get()`/`query()` if your app relies on strict typed contracts. # Multi-Category/Tag Filters Sometimes you may want to filter documents in Chroma based on multiple categories or tags e.g. `games` and `movies`. ## Adding Categories Store categories directly as an array metadata field: ```python collection.add( ids=[f"{uuid.uuid4()}"], documents=["This is a document"], metadatas=[{"categories": ["games", "movies"]}], ) ``` On older Chroma versions that don't support array metadata, add each category as a separate boolean field: No Empty Categories/Tags Only add categories an item belongs to with flags set to `True`. Do not add categories an item does not belong to and set the flag to `False`. ```python collection.add( ids=[f"{uuid.uuid4()}"], documents=["This is a document"], metadatas=[{"games": True, "movies": True}], ) ``` ## Querying by Category Use `$contains` to match documents with a specific category: ```python results = collection.query( query_texts=["This is a query document"], where={"categories": {"$contains": "games"}}, ) ``` Match documents in any of several categories with `$or`: ```python results = collection.query( query_texts=["This is a query document"], where={ "$or": [ {"categories": {"$contains": "games"}}, {"categories": {"$contains": "movies"}}, ] }, ) ``` Exclude a category with `$not_contains`: ```python results = collection.query( query_texts=["This is a query document"], where={"categories": {"$not_contains": "sports"}}, ) ``` Filter by a single category: ```python results = collection.query( query_texts=["This is a query document"], where={"games": True}, ) ``` Filter by multiple categories with `$or`: ```python results = collection.query( query_texts=["This is a query document"], where={"$or": [{"games": True}, {"movies": True}]}, ) ``` # Privacy Strategies ## Overview TBD ## Encryption ### Document Encryption #### Client-side Document Encryption Client-side document encryption notebook coming soon. # Rebuilding Chroma DB ## Rebuilding a Collection Here are several reasons you might want to rebuild a collection: - Your metadata or binary index is corrupted or even deleted - Optimize performance of HNSW index after a large number of updates WAL Consistency and Backups Before you proceed, make sure to backup your data. Secondly make sure that your WAL contains all the data to allow the proper rebuilding of the collection. For instance, after v0.4.22 you should not have run optimizations or WAL cleanup. IMPORTANT Only do this on a stopped Chroma instance. Find the UUID of the target binary index directory to remove. Typically, the binary index directory is located in the persistent directory and is named after the collection vector segment (in `segments` table). You can find the UUID by running the following SQL query: ```bash sqlite3 /path/to/db/chroma.sqlite3 "select s.id, c.name from segments s join collections c on s.collection=c.id where s.scope='VECTOR';" ``` The above should print UUID dir and collection names. Once you remove/rename the UUID dir, restart Chroma and query your collection like so: ```python import chromadb client = chromadb.HttpClient() # Adjust as per your client res = client.get_collection("my_collection").get(limit=1,include=['embeddings']) ``` Chroma will recreate your collection from the WAL. Rebuilding the collection Depending on how large your collection is, this process can take a while. # Time-based Queries ## Filtering Documents By Timestamps In the example below, we create a collection with 100 documents, each with a random timestamp in the last two weeks. We then query the collection for documents that were created in the last week. The example demonstrates how Chroma metadata can be leveraged to filter documents based on how recently they were added or updated. ```python import uuid import chromadb import datetime import random now = datetime.datetime.now() two_weeks_ago = now - datetime.timedelta(days=14) dates = [ two_weeks_ago + datetime.timedelta(days=random.randint(0, 14)) for _ in range(100) ] dates = [int(date.timestamp()) for date in dates] # convert epoch seconds to iso format def iso_date(epoch_seconds): return datetime.datetime.fromtimestamp( epoch_seconds).isoformat() client = chromadb.EphemeralClient() col = client.get_or_create_collection("test") col.add(ids=[f"{uuid.uuid4()}" for _ in range(100)], documents=[ f"document {i}" for i in range(100)], metadatas=[{"date": date} for date in dates]) res = col.get(where={"date": {"$gt": (now - datetime.timedelta(days=7)).timestamp()}}) for i in res['metadatas']: print(iso_date(i['date'])) ``` Ref: https://gist.github.com/tazarov/3c9301d22ab863dca0b6fb1e5e3511b1 # Multi-Tenancy Strategies ## Introduction Some deployment settings of Chroma may require multi-tenancy support. This document outlines the strategies for multi-tenancy approaches in Chroma. ## Approaches - [Naive approach](https://cookbook.chromadb.dev/strategies/multi-tenancy/naive-multi-tenancy/index.md) - This is a simple approach puts the onus of enforcing multi-tenancy on the application. It is the simplest approach to implement, but is not very well suited for production environments. - [Multi-User Basic Auth](https://cookbook.chromadb.dev/strategies/multi-tenancy/multi-user-basic-auth/index.md) - This article provides a stepping stone to more advanced multi-tenancy where the Chroma authentication allows for multiple users to access the same Chroma instance with their own credentials. - [Authorization Model with OpenFGA](https://cookbook.chromadb.dev/strategies/multi-tenancy/authorization-model-with-openfga/index.md) - Implement an advanced authorization model with OpenFGA. - [Implementing OpenFGA Authorization Model In Chroma](https://cookbook.chromadb.dev/strategies/multi-tenancy/authorization-model-impl-with-openfga/index.md) - Learn how to implement OpenFGA authorization model in Chroma with full code example. # Implementing OpenFGA Authorization Model In Chroma Source Code The source code for this article can be found [here](https://github.com/amikos-tech/chromadb-auth). ## Preparation To make things useful we also introduce an initial tuple set with permissions which will allows us to test the authorization model. We define three users: - `admin` part of `chroma` team as `owner` - `user1` part of `chroma` team as `reader` - `admin-ext` part of `external` team as `owner` We will give enough permissions to these three users and their respective teams so that they can perform collection creation, deletion, add records, remove records, get records and query records in the context of their role within the team - `owner` has access to all API actions while `reader` can only read, list get, query. Abbreviate Example We have removed some of the data from the above example for brevity. The full tuple set can be found under data/data/initial-data.json ```json [ { "object": "team:chroma", "relation": "owner", "user": "user:admin" }, { "object": "team:chroma", "relation": "reader", "user": "user:user1" }, { "object": "team:external", "relation": "owner", "user": "user:admin-ext" }, { "object": "server:localhost", "relation": "can_get_tenant", "user": "team:chroma#owner" }, { "object": "tenant:default_tenant-default_database", "relation": "can_get_database", "user": "team:chroma#owner" }, { "object": "database:default_tenant-default_database", "relation": "can_create_collection", "user": "team:chroma#owner" }, { "object": "database:default_tenant-default_database", "relation": "can_list_collections", "user": "team:chroma#owner" }, { "object": "database:default_tenant-default_database", "relation": "can_get_or_create_collection", "user": "team:chroma#owner" }, { "object": "database:default_tenant-default_database", "relation": "can_count_collections", "user": "team:chroma#owner" } ] ``` ## Testing the model Let’s spin up a quick docker compose to test our setup. In the repo we have provided `openfga/docker-compose.openfga-standalone.yaml` ```bash docker compose -f openfga/docker-compose.openfga-standalone.yaml up ``` For this next part ensure you have FGA CLI installed. Once the containers are up and running let’s create a store and import the model: ```bash export FGA_API_URL=http://localhost:8082 # our OpenFGA binds to 8082 on localhost fga store create --model data/models/model-article-p4.fga --name chromadb-auth ``` You should see a response like this: ```json { "store": { "created_at": "2024-04-09T18:37:26.367747Z", "id": "01HV3VB347NPY3NMX6VQ5N2E23", "name": "chromadb-auth", "updated_at": "2024-04-09T18:37:26.367747Z" }, "model": { "authorization_model_id": "01HV3VB34JAXWF0F3C00DFBZV4" } } ``` Let’s import our initial tuple set. Before that make sure to export `FGA_STORE_ID` and `FGA_MODEL_ID` as per the output of the previous command: ```bash export FGA_STORE_ID=01HV3VB347NPY3NMX6VQ5N2E23 export FGA_MODEL_ID=01HV3VB34JAXWF0F3C00DFBZV4 fga tuple write --file data/data/initial-data.json ``` Let’s test our imported model and tuples: ```bash fga query check user:admin can_get_preflight server:localhost ``` If everything is working you should see this: ```json { "allowed": true, "resolution": "" } ``` ## Implementing Authorization Plumbing in Chroma First we will start with making a few small changes to the authorization plugin we’ve made. Why you ask? We need to introduce teams (aka groups). For that we’ll resort to standard Apache `groupfile` as follows: ```json chroma: admin, user1 external: admin-ext ``` The `groupfile` will be mounted to our Chroma container and read by the multi-user basic auth plugin. The changes to the authentication plugin are as follows: ```python # imports as before @register_provider("multi_user_htpasswd_file") class MultiUserHtpasswdFileServerAuthCredentialsProvider(ServerAuthCredentialsProvider): _creds: Dict[str, SecretStr] # contains user:password-hash def __init__(self, system: System) -> None: super().__init__(system) try: self.bc = importlib.import_module("bcrypt") except ImportError: raise ValueError(aa "The bcrypt python package is not installed. " "Please install it with `pip install bcrypt`" ) system.settings.require("chroma_server_auth_credentials_file") _file = str(system.settings.chroma_server_auth_credentials_file) ... # as before _basepath = path.dirname(_file) self._user_group_map = dict() if path.exists(path.join(_basepath, "groupfile")): _groups = dict() with open(path.join(_basepath, "groupfile"), "r") as f: for line in f: _raw_group = [v for v in line.strip().split(":")] if len(_raw_group) < 2: raise ValueError( "Invalid Htpasswd group file found in " f"[{path.join(_basepath, 'groupfile')}]. " "Must be :,,...,." ) _groups[_raw_group[0]] = [u.strip() for u in _raw_group[1].split(",")] for _group, _users in _groups.items(): for _user in _users: if _user not in self._user_group_map: self._user_group_map[_user] = _group @trace_method( # type: ignore "MultiUserHtpasswdFileServerAuthCredentialsProvider.validate_credentials", OpenTelemetryGranularity.ALL, ) @override def validate_credentials(self, credentials: AbstractCredentials[T]) -> bool: ... # as before @override def get_user_identity( self, credentials: AbstractCredentials[T] ) -> Optional[SimpleUserIdentity]: _creds = cast(Dict[str, SecretStr], credentials.get_credentials()) if _creds["username"].get_secret_value() in self._user_group_map.keys(): return SimpleUserIdentity( _creds["username"].get_secret_value(), attributes={ "team": self._user_group_map[_creds["username"].get_secret_value()] }, ) return SimpleUserIdentity(_creds["username"].get_secret_value(), attributes={"team": "public"}) ``` Full code The code can be found under `chroma_auth/authn/basic/__**init__**.py` We read the group file and for each user create a key in `self._user_group_map` to specify the group or team of that user. The information is returned as user identity attributes that is further used by the authz plugin. Now let’s turn our attention to the authorization plugin. First let’s start with that we’re trying to achieve with it: - Handle OpenFGA configuration from the import of the model as per the snippet above. This will help us to wire all necessary parts of the code with correct authorization model configuration. - Map all existing Chroma authorization actions to our authorization model - Adapt any shortcomings or quirks in Chroma authorization to the way OpenFGA works - Implement the Enforcement Point (EP) logic - Implement OpenFGA Permissions API wrapper - this is a utility class that will help us update and keep updating the OpenFGA tuples throughout collections’ lifecycle. We’ve split the implementation in two files: - `chroma_auth/authz/openfga/__init__.py` - Storing our OpenFGA authorization configuration reader and our authorization plugin that adapts to Chroma authz model and enforces authorization decisions - `chroma_auth/authz/openfga/openfga_permissions.py` - Holds our OpenFGA permissions update logic. - `chroma_auth/instr/**__init__**.py` - holds our adapted FastAPI server from Chroma `0.4.24`. While the authz plugin system in Chroma makes it easy to write the enforcement of authorization decisions, the update of permissions does require us to into this rabbit hole. Don’t worry the actual changes are minimal Let’s cover things in a little more detail. **Reading the configuration.** ```python @register_provider("openfga_config_provider") class OpenFGAAuthorizationConfigurationProvider( ServerAuthorizationConfigurationProvider[ClientConfiguration] ): _config_file: str _config: ClientConfiguration def __init__(self, system: System) -> None: super().__init__(system) self._settings = system.settings if "FGA_API_URL" not in os.environ: raise ValueError("FGA_API_URL not set") self._config = self._try_load_from_file() # TODO in the future we can also add credentials (preshared) or OIDC def _try_load_from_file(self) -> ClientConfiguration: store_id = None model_id = None if "FGA_STORE_ID" in os.environ and "FGA_MODEL_ID" in os.environ: return ClientConfiguration( api_url=os.environ.get("FGA_API_URL"), store_id=os.environ["FGA_STORE_ID"], authorization_model_id=os.environ["FGA_MODEL_ID"], ) if "FGA_CONFIG_FILE" not in os.environ and not store_id and not model_id: raise ValueError("FGA_CONFIG_FILE or FGA_STORE_ID/FGA_MODEL_ID env vars not set") with open(os.environ["FGA_CONFIG_FILE"], "r") as f: config = json.load(f) return ClientConfiguration( api_url=os.environ.get("FGA_API_URL"), store_id=config["store"]["id"], authorization_model_id=config["model"]["authorization_model_id"], ) @override def get_configuration(self) -> ClientConfiguration: return self._config ``` This is a pretty simple and straightforward implementation that will either take env variables for the FGA Server URL, Store and Model or it will only take the server ULR + json configuration (the same as above). Next let’s have a look at our `OpenFGAAuthorizationProvider` implementation. We’ll start with the constructor where we adapt existing Chroma authorization actions to our model: ```python def __init__(self, system: System) -> None: # more code here, but we're skipping for brevity self._authz_to_model_action_map = { AuthzResourceActions.CREATE_DATABASE.value: "can_create_database", AuthzResourceActions.GET_DATABASE.value: "can_get_database", AuthzResourceActions.CREATE_TENANT.value: "can_create_tenant", AuthzResourceActions.GET_TENANT.value: "can_get_tenant", AuthzResourceActions.LIST_COLLECTIONS.value: "can_list_collections", AuthzResourceActions.COUNT_COLLECTIONS.value: "can_count_collections", AuthzResourceActions.GET_COLLECTION.value: "can_get_collection", AuthzResourceActions.CREATE_COLLECTION.value: "can_create_collection", AuthzResourceActions.GET_OR_CREATE_COLLECTION.value: "can_get_or_create_collection", AuthzResourceActions.DELETE_COLLECTION.value: "can_delete_collection", AuthzResourceActions.UPDATE_COLLECTION.value: "can_update_collection", AuthzResourceActions.ADD.value: "can_add_records", AuthzResourceActions.DELETE.value: "can_delete_records", AuthzResourceActions.GET.value: "can_get_records", AuthzResourceActions.QUERY.value: "can_query_records", AuthzResourceActions.COUNT.value: "can_count_records", AuthzResourceActions.UPDATE.value: "can_update_records", AuthzResourceActions.UPSERT.value: "can_upsert_records", AuthzResourceActions.RESET.value: "can_reset", } self._authz_to_model_object_map = { AuthzResourceTypes.DB.value: "database", AuthzResourceTypes.TENANT.value: "tenant", AuthzResourceTypes.COLLECTION.value: "collection", } ``` > The above is located in `chroma_auth/authz/openfga/__init__.py` The above is fairly straightforward mapping between `AuthzResourceActions` part of Chroma’s auth framework and the relations (aka actions) we’ve defined in our model above. Next we map also the `AuthzResourceTypes` to OpenFGA objects. This seem pretty simple right? Wrong, things are not so perfect and nothing exhibits this more than our next portion that takes the action and resource and returns object and relation to be checked: ```python def resolve_resource_action(self, resource: AuthzResource, action: AuthzAction) -> tuple: attrs = "" tenant = None, database = None if "tenant" in resource.attributes: attrs += f"{resource.attributes['tenant']}" tenant = resource.attributes['tenant'] if "database" in resource.attributes: attrs += f"-{resource.attributes['database']}" database = resource.attributes['database'] if action.id == AuthzResourceActions.GET_TENANT.value or action.id == AuthzResourceActions.CREATE_TENANT.value: return "server:localhost", self._authz_to_model_action_map[action.id] if action.id == AuthzResourceActions.GET_DATABASE.value or action.id == AuthzResourceActions.CREATE_DATABASE.value: return f"tenant:{attrs}", self._authz_to_model_action_map[action.id] if action.id == AuthzResourceActions.CREATE_COLLECTION.value: try: cole_exists = self._api.get_collection( resource.id, tenant=tenant, database=database ) return f"collection:{attrs}-{cole_exists.name}", self._authz_to_model_action_map[ AuthzResourceActions.GET_COLLECTION.value] except Exception as e: return f"{self._authz_to_model_object_map[resource.type]}:{attrs}", self._authz_to_model_action_map[ action.id] if resource.id == "*": return f"{self._authz_to_model_object_map[resource.type]}:{attrs}", self._authz_to_model_action_map[action.id] else: return f"{self._authz_to_model_object_map[resource.type]}:{attrs}-{resource.id}", self._authz_to_model_action_map[action.id] ``` Full code The above is located in `chroma_auth/authz/openfga/__init__.py` The `resolve_resource_action` function demonstrates the idiosyncrasies of Chroma’s auth. I have only myself to blame. The key takeaway is that there is room for improvement. The actual authorization enforcement is then dead simple: ```python def authorize(self, context: AuthorizationContext) -> bool: with OpenFgaClient(self._authz_config_provider.get_configuration()) as fga_client: try: obj, act = self.resolve_resource_action(resource=context.resource, action=context.action) resp = fga_client.check(body=ClientCheckRequest( user=f"user:{context.user.id}", relation=act, object=obj, )) # openfga_sdk.models.check_response.CheckResponse return resp.allowed except Exception as e: logger.error(f"Error while authorizing: {str(e)}") return False ``` At the end we’ll look at the our permissions API wrapper. While a full-blown solution will implement all possible object lifecycle hooks, we’re content with collections. Therefore we’ll add lifecycle callbacks for creating and deleting collection (we’re not considering, sharing of the collection with other users and change of ownership). So how does our create collection hook might look like you ask? ```python def create_collection_permissions(self, collection: Collection, request: Request) -> None: if not hasattr(request.state, "user_identity"): return identity = request.state.user_identity # AuthzUser tenant = request.query_params.get("tenant") database = request.query_params.get("database") _object = f"collection:{tenant}-{database}-{collection.id}" _object_for_get_collection = f"collection:{tenant}-{database}-{collection.name}" # this is a bug in the Chroma Authz that feeds in the name of the collection instead of ID _user = f"team:{identity.get_user_attributes()['team']}#owner" if identity.get_user_attributes() and "team" in identity.get_user_attributes() else f"user:{identity.get_user_id()}" _user_writer = f"team:{identity.get_user_attributes()['team']}#writer" if identity.get_user_attributes() and "team" in identity.get_user_attributes() else None _user_reader = f"team:{identity.get_user_attributes()['team']}#reader" if identity.get_user_attributes() and "team" in identity.get_user_attributes() else None with OpenFgaClient(self._fga_configuration) as fga_client: fga_client.write_tuples( body=[ ClientTuple(_user, "can_add_records", _object), ClientTuple(_user, "can_delete_records", _object), ClientTuple(_user, "can_update_records", _object), ClientTuple(_user, "can_get_records", _object), ClientTuple(_user, "can_upsert_records", _object), ClientTuple(_user, "can_count_records", _object), ClientTuple(_user, "can_query_records", _object), ClientTuple(_user, "can_get_collection", _object_for_get_collection), ClientTuple(_user, "can_delete_collection", _object_for_get_collection), ClientTuple(_user, "can_update_collection", _object), ] ) if _user_writer: fga_client.write_tuples( body=[ ClientTuple(_user_writer, "can_add_records", _object), ClientTuple(_user_writer, "can_delete_records", _object), ClientTuple(_user_writer, "can_update_records", _object), ClientTuple(_user_writer, "can_get_records", _object), ClientTuple(_user_writer, "can_upsert_records", _object), ClientTuple(_user_writer, "can_count_records", _object), ClientTuple(_user_writer, "can_query_records", _object), ClientTuple(_user_writer, "can_get_collection", _object_for_get_collection), ClientTuple(_user_writer, "can_delete_collection", _object_for_get_collection), ClientTuple(_user_writer, "can_update_collection", _object), ] ) if _user_reader: fga_client.write_tuples( body=[ ClientTuple(_user_reader, "can_get_records", _object), ClientTuple(_user_reader, "can_query_records", _object), ClientTuple(_user_reader, "can_count_records", _object), ClientTuple(_user_reader, "can_get_collection", _object_for_get_collection), ] ) ``` Full code You can find the full code in `chroma_auth/authz/openfga/openfga_permissions.py` Looks pretty straight, but hold on I hear a thought creeping in your mind. β€œWhy are you adding roles manually?” You are right, it lacks that DRY-je-ne-sais-quoi, and I’m happy to keep it simple an explicit. A more mature implementation can read the model figure out what type we’re adding permissions for and then for each relation add the requisite users, but premature optimization is difficult to put in an article that won’t turn into a book. With the above code we make the assumption that the collection doesn’t exist ergo its permissions tuples don’t exist. ( OpenFGA will fail to add tuples that already exist and there is not way around it other than deleting them first). Remember permission tuple lifecycle is your responsibility when adding authz to your application. The delete is oddly similar (that’s why we’ve skipped the bulk of it): ```python def delete_collection_permissions(self, collection: Collection, request: Request) -> None: if not hasattr(request.state, "user_identity"): return identity = request.state.user_identity _object = f"collection:{collection.tenant}-{collection.database}-{collection.id}" _object_for_get_collection = f"collection:{collection.tenant}-{collection.database}-{collection.name}" # this is a bug in the Chroma Authz that feeds in the name of the collection instead of ID _user = f"team:{identity.get_user_attributes()['team']}#owner" if identity.get_user_attributes() and "team" in identity.get_user_attributes() else f"user:{identity.get_user_id()}" _user_writer = f"team:{identity.get_user_attributes()['team']}#writer" if identity.get_user_attributes() and "team" in identity.get_user_attributes() else None _user_reader = f"team:{identity.get_user_attributes()['team']}#reader" if identity.get_user_attributes() and "team" in identity.get_user_attributes() else None with OpenFgaClient(self._fga_configuration) as fga_client: fga_client.delete_tuples( body=[ ClientTuple(_user, "can_add_records", _object), ClientTuple(_user, "can_delete_records", _object), ClientTuple(_user, "can_update_records", _object), ClientTuple(_user, "can_get_records", _object), ClientTuple(_user, "can_upsert_records", _object), ClientTuple(_user, "can_count_records", _object), ClientTuple(_user, "can_query_records", _object), ClientTuple(_user, "can_get_collection", _object_for_get_collection), ClientTuple(_user, "can_delete_collection", _object_for_get_collection), ClientTuple(_user, "can_update_collection", _object), ] ) # more code in the repo ``` Full code You can find the full code in `chroma_auth/authz/openfga/openfga_permissions.py` Let’s turn our attention at the last piece of code - the necessary evil of updating the FastAPI in Chroma to add our Permissions API hooks. We start simple by injecting our component using Chroma’s DI (dependency injection). ```python from chroma_auth.authz.openfga.openfga_permissions import OpenFGAPermissionsAPI self._permissionsApi: OpenFGAPermissionsAPI = self._system.instance(OpenFGAPermissionsAPI) ``` The we add a hook for collection creation: ```python def create_collection( self, request: Request, collection: CreateCollection, tenant: str = DEFAULT_TENANT, database: str = DEFAULT_DATABASE, ) -> Collection: existing = None try: existing = self._api.get_collection(collection.name, tenant=tenant, database=database) except ValueError as e: if "does not exist" not in str(e): raise e collection = self._api.create_collection( name=collection.name, metadata=collection.metadata, get_or_create=collection.get_or_create, tenant=tenant, database=database, ) if not existing: self._permissionsApi.create_collection_permissions(collection=collection, request=request) return collection ``` Full code You can find the full code in `chroma_auth/instr/__init__.py` And one for collection removal: ```python def delete_collection( self, request: Request, collection_name: str, tenant: str = DEFAULT_TENANT, database: str = DEFAULT_DATABASE, ) -> None: collection = self._api.get_collection(collection_name, tenant=tenant, database=database) resp = self._api.delete_collection( collection_name, tenant=tenant, database=database ) self._permissionsApi.delete_collection_permissions(collection=collection, request=request) return resp ``` Full code You can find the full code in `chroma_auth/instr/__init__.py` The key thing to observe about the above snippets is that we invoke permissions API when we’re sure things have been persisted in the DB. I know, I know, atomicity here is also important, but that is for another article. Just keep in mind that it is easier to fix broken permission than broken data. I promise this was the last bit of python code you’ll see in this article. ## The Infra Infrastructure!!! Finally, a sigh of relieve. Let’s draw a diagrams: [Link](https://excalidraw.com/#json=4wcUMJU5pNYEzzcmEj1BZ,a7z_MZUGf9m5t6OPu3RuiA) We have our Chroma server, that relies on OpenFGA which persists data in PostgreSQL. β€œOk, but …”, I can see you scratch your head, β€œβ€¦ how do I bring this magnificent architecture to live?”. I thought you’d never ask. We’ll rely on our trusty docker compose skills with the following sequence in mind: β€œWhere is the `docker-compose.yaml`!”. VoilΓ , my impatient friends: ```yaml version: '3.9' networks: net: driver: bridge services: server: depends_on: openfga: condition: service_healthy import: condition: service_completed_successfully image: chroma-server build: dockerfile: Dockerfile volumes: - ./chroma-data:/chroma/chroma - ./server.htpasswd:/chroma/server.htpasswd - ./groupfile:/chroma/groupfile - ./data/:/data command: "--workers 1 --host 0.0.0.0 --port 8000 --proxy-headers --log-config chromadb/log_config.yml --timeout-keep-alive 30" environment: - IS_PERSISTENT=TRUE - CHROMA_SERVER_AUTH_PROVIDER=${CHROMA_SERVER_AUTH_PROVIDER} - CHROMA_SERVER_AUTH_CREDENTIALS_FILE=${CHROMA_SERVER_AUTH_CREDENTIALS_FILE} - CHROMA_SERVER_AUTH_CREDENTIALS=${CHROMA_SERVER_AUTH_CREDENTIALS} - CHROMA_SERVER_AUTH_CREDENTIALS_PROVIDER=${CHROMA_SERVER_AUTH_CREDENTIALS_PROVIDER} - CHROMA_SERVER_AUTH_TOKEN_TRANSPORT_HEADER=${CHROMA_SERVER_AUTH_TOKEN_TRANSPORT_HEADER} - PERSIST_DIRECTORY=${PERSIST_DIRECTORY:-/chroma/chroma} - CHROMA_OTEL_EXPORTER_ENDPOINT=${CHROMA_OTEL_EXPORTER_ENDPOINT} - CHROMA_OTEL_EXPORTER_HEADERS=${CHROMA_OTEL_EXPORTER_HEADERS} - CHROMA_OTEL_SERVICE_NAME=${CHROMA_OTEL_SERVICE_NAME} - CHROMA_OTEL_GRANULARITY=${CHROMA_OTEL_GRANULARITY} - CHROMA_SERVER_NOFILE=${CHROMA_SERVER_NOFILE} - CHROMA_SERVER_AUTHZ_PROVIDER=${CHROMA_SERVER_AUTHZ_PROVIDER} - CHROMA_SERVER_AUTHZ_CONFIG_PROVIDER=${CHROMA_SERVER_AUTHZ_CONFIG_PROVIDER} - FGA_API_URL=http://openfga:8080 - FGA_CONFIG_FILE=/data/store.json # we expect that the import job will create this file restart: unless-stopped # possible values are: "no", always", "on-failure", "unless-stopped" ports: - "8000:8000" healthcheck: # Adjust below to match your container port test: [ "CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat" ] interval: 30s timeout: 10s retries: 3 networks: - net postgres: image: postgres:14 container_name: postgres networks: - net ports: - "5432:5432" environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=password healthcheck: test: [ "CMD-SHELL", "pg_isready -U postgres" ] interval: 5s timeout: 5s retries: 5 volumes: - postgres_data_openfga:/var/lib/postgresql/data migrate: depends_on: postgres: condition: service_healthy image: openfga/openfga:latest container_name: migrate command: migrate environment: - OPENFGA_DATASTORE_ENGINE=postgres - OPENFGA_DATASTORE_URI=postgres://postgres:password@postgres:5432/postgres?sslmode=disable networks: - net openfga: depends_on: migrate: condition: service_completed_successfully image: openfga/openfga:latest container_name: openfga environment: - OPENFGA_DATASTORE_ENGINE=postgres - OPENFGA_DATASTORE_URI=postgres://postgres:password@postgres:5432/postgres?sslmode=disable - OPENFGA_LOG_FORMAT=json command: run networks: - net ports: # Needed for the http server - "8082:8080" # Needed for the grpc server (if used) - "8083:8081" # Needed for the playground (Do not enable in prod!) - "3003:3000" healthcheck: test: [ "CMD", "/usr/local/bin/grpc_health_probe", "-addr=openfga:8081" ] interval: 5s timeout: 30s retries: 3 import: depends_on: openfga: condition: service_healthy image: fga-cli build: context: . dockerfile: Dockerfile-fgacli container_name: import volumes: - ./data/:/data command: | /bin/sh -c "/data/create_store_and_import.sh" environment: - FGA_SERVER_URL=http://openfga:8080 networks: - net volumes: postgres_data_openfga: driver: local ``` Don’t forget to create an `.env` file: ```python CHROMA_SERVER_AUTH_PROVIDER = "chromadb.auth.basic.BasicAuthServerProvider" CHROMA_SERVER_AUTH_CREDENTIALS_FILE = "server.htpasswd" CHROMA_SERVER_AUTH_CREDENTIALS_PROVIDER = "chroma_auth.authn.basic.MultiUserHtpasswdFileServerAuthCredentialsProvider" CHROMA_SERVER_AUTHZ_PROVIDER = "chroma_auth.authz.openfga.OpenFGAAuthorizationProvider" CHROMA_SERVER_AUTHZ_CONFIG_PROVIDER = "chroma_auth.authz.openfga.OpenFGAAuthorizationConfigurationProvider" ``` Update your `server.htpasswd` to include the new user: ```python admin:$2 y$05$vkBK4b1Vk5O98jNHgr.uduTJsTOfM395sKEKe48EkJCVPH / MBIeHK user1:$2 y$05$UQ0kC2x3T2XgeN4WU12BdekUwCJmLjJNhMaMtFNolYdj83OqiEpVu admin - ext:$2 y$05$9. L13wKQTHeXz9IH2UO2RurWEK. / Z24qapzyi6ywQGJds2DaC36C2 ``` And the `groupfile` from before. And don’t forget to take a look at the import script under - `data/create_store_and_import.sh` Run the following command at the root of the repo and let things fail and burn down (or in the event this works - awe you, disclaimer - it worked on my machine): ```python docker compose up - -build ``` ## Tests, who needs test when you have stable infra! Authorization is serious stuff, which is why we’ve created a bare minimum set of tests to prove we’re not totally wrong about it! Real Serious Note Serious Note: Take these things seriously and write a copious amounts of tests before rolling out things to prod. Don’t become [OWASP Top10 β€œHero”](https://owasp.org/Top10/). [Broken access](https://owasp.org/Top10/A01_2021-Broken_Access_Control/) controls is a thing that WILL keep you up at night. We’ll focus on three areas: - Testing admin (owner) access - Testing team access for owner and reader roles - Testing cross team permissions **Admin Access** Simple check to ensure that whoever created the collection (aka the owner) is allowed all actions. ```python import uuid import chromadb from chromadb.config import Settings client = chromadb.HttpClient( settings=Settings(chroma_client_auth_provider="chromadb.auth.basic.BasicAuthClientProvider", chroma_client_auth_credentials="admin:password123")) client.heartbeat() # this should work with or without authentication - it is a public endpoint client.list_collections() # this is a protected endpoint and requires authentication col = client.get_or_create_collection(f"test_collection-{str(uuid.uuid4())}") col.add(ids=["1"], documents=["test doc"]) col.get() col.update(ids=["1"], documents=["test doc 2"]) col.count() col.upsert(ids=["1"], documents=["test doc 3"]) col.delete(ids=["1"]) client.delete_collection(col.name) ``` Delete return value on newer core builds This example keeps `col.delete(ids=["1"])` simple for compatibility. On core-backed clients that include [#6573](https://github.com/chroma-core/chroma/pull/6573) and [#6582](https://github.com/chroma-core/chroma/pull/6582), the same call may return a payload with a `deleted` count. Full code You can find the full code in `test_auth.ipynb` **Team Access** Team access tests whether roles and permissions associated with those roles are correctly enforced. ```python import uuid import chromadb from chromadb.config import Settings client = chromadb.HttpClient( settings=Settings(chroma_client_auth_provider="chromadb.auth.basic.BasicAuthClientProvider", chroma_client_auth_credentials="admin:password123")) client.heartbeat() # this should work with or without authentication - it is a public endpoint client.list_collections() # this is a protected endpoint and requires authentication col_name = f"test_collection-{str(uuid.uuid4())}" col = client.get_or_create_collection(col_name) print(f"Creating collection {col.id}") col.add(ids=["1"], documents=["test doc"]) client.get_collection(col_name) client = chromadb.HttpClient( settings=Settings(chroma_client_auth_provider="chromadb.auth.basic.BasicAuthClientProvider", chroma_client_auth_credentials="user1:password123")) client.heartbeat() # this should work with or without authentication - it is a public endpoint client.list_collections() # this is a protected endpoint and requires authentication client.count_collections() print("Getting collection " + col_name) col = client.get_collection(col_name) col.get() col.count() try: client.delete_collection(col_name) except Exception as e: print(e) #expect unauthorized error client = chromadb.HttpClient( settings=Settings(chroma_client_auth_provider="chromadb.auth.basic.BasicAuthClientProvider", chroma_client_auth_credentials="admin:password123")) client.delete_collection(col_name) ``` Full code You can find the full code in `test_auth.ipynb` **Cross-team access** In the cross team access scenario we’ll create a collection with one team owner (`admin`) and will try to access it (aka delete it) with another team’s owner in a very mano-a-mano (owner-to-owner way). It is important to observe that all these collections are created within the same database (`default_database`) ```python import uuid import chromadb from chromadb.config import Settings col_name = f"test_collection-{str(uuid.uuid4())}" client = chromadb.HttpClient( settings=Settings(chroma_client_auth_provider="chromadb.auth.basic.BasicAuthClientProvider", chroma_client_auth_credentials="admin:password123")) client.get_or_create_collection(col_name) client = chromadb.HttpClient( settings=Settings(chroma_client_auth_provider="chromadb.auth.basic.BasicAuthClientProvider", chroma_client_auth_credentials="admin-ext:password123")) client.get_or_create_collection("external-collection") try: client.delete_collection(col_name) except Exception as e: print("Expected error for admin-ext: ", str(e)) #expect unauthorized error client = chromadb.HttpClient( settings=Settings(chroma_client_auth_provider="chromadb.auth.basic.BasicAuthClientProvider", chroma_client_auth_credentials="admin:password123")) client.delete_collection(col_name) try: client.delete_collection("external-collection") except Exception as e: print("Expected error for admin: ", str(e)) #expect unauthorized error ``` Full code You can find the full code in `test_auth.ipynb` # Chroma Authorization Model with OpenFGA Source Code The source code for this article can be found [here](https://github.com/amikos-tech/chromadb-auth). This article will not provide any code that you can use immediately but will set the stage for our next article, which will introduce the actual Chroma-OpenFGA integration. With that in mind, let’s get started. Who is this article for? The intended audience is DevSecOps, but engineers and architects could also use this to learn about Chroma and the authorization models. ## Authorization Model Authorization models are an excellent way to abstract the way you wish your users to access your application form the actual implementation. There are many ways to do authz, ranging from commercial Auth0 FGA to OSS options like Ory Keto/Kratos, CASBIN, Permify, and Kubescape, but for this article, we’ve decided to use OpenFGA (which technically is Auth0’s open-source framework for FGA). Why OpenFGA, I hear you ask? Here are a few reasons: - Apache-2 licensed - CNCF Incubating project - Zanzibar alignment in that it is a ReBAC (Relation-based access control) system - DSL for modeling and testing permissions (as well as JSON-base version for those with masochistic tendencies) OpenFGA has done a great job explaining the steps to building an Authorization model, which you can read [here](https://openfga.dev/docs/modeling/getting-started). We will go over those while keeping our goal of creating an authorization model for Chroma. It is worth noting that the resulting authorization model that we will create here will be suitable for many GenAI applications, such as general-purpose RAG systems. Still, it is not a one-size-fits-all solution to all problems. For instance, if you want to implement authz in Chroma within your organization, OpenFGA might not be the right tool for the job, and you should consult with your IT/Security department for guidance on integrating with existing systems. ## The Goal Our goal is to achieve the following: - Allow fine-grained access to the following resources - collection, database, tenant, and Chroma server. - AlGrouping of users for improved permission management. - Individual user access to resources - Roles - owner, writer, reader Document-Level Access Although granting access to individual documents in a collection can be beneficial in some contexts, we have left that part out of our goals to keep things as simple and short as possible. If you are interested in this topic, reach out, and we will help you. This article will not cover user management, commonly called Identity Access Management (IAM). We’ll cover that in a subsequent article. ## Modeling Fundamentals Let’s start with the fundamentals: **`Why could user U perform an action A on an object O?`** We will attempt to answer the question in the context of Chroma by following OpenFGA approach to refining the model. The steps are: 1. Pick the most important features. 1. List of object types 1. List of relations for the types 1. Test the model 1. Iterate Given that OpenFGA is Zanzibar inspired, the basic primitive for it is a tuple of the following format: ```bash (User,Relation,Object) ``` With the above we can express any relation between a user (or a team or even another object) the action the user performs (captured by object relations) and the object (aka API resource). ### Pick the features In the context of Chroma, the features are the actions the user can perform on Chroma API (as of this writing v0.4.24). Let’s explore what are the actions that users can perform: - **Create** a tenant - **Get** a tenant - **Create** a database for a tenant - **Get** a database for a tenant - **Create** a collection in a database - **Delete** a collection from a database - **Update** collection name and metadata - **List** collections in a database - **Count** collections in a database - **Add** records to a collection - **Delete** records from a collection - **Update** records in a collection - **Upsert** records in a collection - **Count** records in a collection - **Get** records from a collection - **Query** records in a collection - **Get** pre-flight-checks Open Endpoints Note we will omit get `hearbeat` and get `version`actions as this is generally a good idea to be open so that orchestrators (docker/k8s) can get the health status of chroma. To make it easy to reason about relations in our authorization model we will rephrase the above to the following format: ```bash A user {user} can perform action {action} to/on/in {object types} ... IF {conditions} ``` - A user can perform action create tenant on Chroma server if they are owner of the server - A user can perform action get tenant on Chroma server if they are a reader or writer or owner of the server - A user can perform action create database on a tenant if they are an owner of the tenant - A user can perform action get database on a tenant if they are reader, writer or owner of the tenant - A user can perform action create collection on a database if they are a writer or an owner of the database - A user can perform action delete collection on a database if they are a writer or an owner of the database - A user can perform action update collection name or metadata on a database if they are a writer or an owner of the database - A user can perform action list collections in a database if they are a writer or an owner of the database - A user can perform action count collections in a database if they are a writer or an owner of the database - A user can perform action add records on a collection if they are writer or owner of the collection - A user can perform action delete records on a collection if they are writer or owner of the collection - A user can perform action update records on a collection if they are writer or owner of the collection - A user can perform action upsert records on a collection if they are writer or owner of the collection - A user can perform action get records on a collection if they are writer or owner or reader of the collection - A user can perform action count records on a collection if they are writer or owner or reader of the collection - A user can perform action query records on a collection if they are writer or owner or reader of the collection - A user can perform action get pre-flight-checks on a Chroma server if they are writer or owner or reader of the server We don’t have to get it all right in the first iteration, but the above is a good starting point that can be adapted further. The above statements alone are already a great introspection as to what we can do within Chroma and who is supposed to be able to do what. Please note that your mileage may vary, as per your authz requirements, but in our experience the variations are generally around the who. As an astute reader you have already noted that we’re generally outlined some RBAC stuff in the form of owner, writer and reader. ### List the objects!!! Now that we know what our users can do, let’s figure solidify our understanding of on what our users will be performing these actions, aka the object types. Let’s call them out: - User - this is basic and pretty obvious object type that we want to model our users after - Chroma server - this is our top level object in the access relations - Tenant - for most Chroma developers this will equate to a team or a group - Database - Collection We can also examine all of the `of the ` in the above statements to ensure we haven’t missed any objects. So far seems we’re all good. Now that we have our objects let’s create a first iteration of our authorization model using [OpenFGA DSL](https://openfga.dev/docs/concepts#what-is-an-authorization-model): ```graphql model schema 1.1 type server type user type tenant type database type collection ``` OpenFGA CLI You will need to install openfga CLI - https://openfga.dev/docs/getting-started/install-sdk. Also check the [VSCode extension](https://marketplace.visualstudio.com/items?itemName=openfga.openfga-vscode) for OpenFGA. Let’s validate our work: ```bash fga model validate --file model-article-p1.fga ``` You should see the following output: ```bash { "is_valid":true } ``` ### Relations Now that we have the actions and the objects, let us figure out the relationships we want to build into our model. To come up with our relations we can follow these two rules: - Any noun of the type `{noun} of a/an/the {type}` expression (e.g. `of the collection`) - Any verb or action described with `can {action} on/in {type}` So now let’s work on our model to expand it with relationships: ```bash model schema 1.1 type user type server relations define owner: [user] define reader: [user] define writer: [user] define can_get_preflight: reader or owner or writer define can_create_tenant: owner or writer type tenant relations define owner: [user] define reader: [user] define writer: [user] define belongsTo: [server] define can_create_database: owner from belongsTo or writer from belongsTo or owner or writer define can_get_database: reader or owner or writer or owner from belongsTo or reader from belongsTo or writer from belongsTo type database relations define owner: [user] define reader: [user] define writer: [user] define belongsTo: [tenant] define can_create_collection: owner from belongsTo or writer from belongsTo or owner or writer define can_delete_collection: owner from belongsTo or writer from belongsTo or owner or writer define can_list_collections: owner or writer or owner from belongsTo or writer from belongsTo define can_get_collection: owner or writer or owner from belongsTo or writer from belongsTo define can_get_or_create_collection: owner or writer or owner from belongsTo or writer from belongsTo define can_count_collections: owner or writer or owner from belongsTo or writer from belongsTo type collection relations define owner: [user] define reader: [user] define writer: [user] define belongsTo: [database] define can_add_records: writer or reader or owner from belongsTo or writer from belongsTo define can_delete_records: writer or owner from belongsTo or writer from belongsTo define can_update_records: writer or owner from belongsTo or writer from belongsTo define can_get_records: reader or owner or writer or owner from belongsTo or reader from belongsTo or writer from belongsTo define can_upsert_records: writer or owner from belongsTo or writer from belongsTo define can_count_records: reader or owner or writer or owner from belongsTo or reader from belongsTo or writer from belongsTo define can_query_records: reader or owner or writer or owner from belongsTo or reader from belongsTo or writer from belongsTo ``` Let’s validated: ```bash fga model validate --file model-article-p2.fga ``` This seems mostly accurate and should do ok as Authorization model. But let us see if we can make it better. If we are to implement the above we will end up with lots of permissions in OpenFGA, not that it can’t handle them, but as we go into the implementation details it will become cumbersome to update and maintain all these permissions. So let’s look for opportunity to simplify things a little. Can we make the model a little simpler and the first question we ask is do we really need owner, reader, writer on every object or can we make a decision about our model and simplify this. As it turns out we can. The way that most multi-user systems work is that they tend to gravitate to grouping things as a way to reduce the need to maintain a large number of permissions. In our case we can group our users into `team` and in each team we’ll have owner, writer, reader Let’s see the results: ```bash model schema 1.1 type user type team relations define owner: [user] define writer: [user] define reader: [user] type server relations define can_get_preflight: [user, team#owner, team#writer, team#reader] define can_create_tenant: [user, team#owner, team#writer] define can_get_tenant: [user, team#owner, team#writer, team#reader] type tenant relations define can_create_database: [user, team#owner, team#writer] define can_get_database: [user, team#owner, team#writer, team#reader] type database relations define can_create_collection: [user, team#owner, team#writer] define can_list_collections: [user, team#owner, team#writer, team#reader] define can_get_or_create_collection: [user, team#owner, team#writer] define can_count_collections: [user, team#owner, team#writer, team#reader] type collection relations define can_delete_collection: [user, team#owner, team#writer] define can_get_collection: [user, team#owner, team#writer, team#reader] define can_update_collection: [user, team#owner, team#writer] define can_add_records: [user, team#owner, team#writer] define can_delete_records: [user, team#owner, team#writer] define can_update_records: [user, team#owner, team#writer] define can_get_records: [user, team#owner, team#writer, team#reader] define can_upsert_records: [user, team#owner, team#writer] define can_count_records: [user, team#owner, team#writer, team#reader] define can_query_records: [user, team#owner, team#writer, team#reader] ``` That is arguably more readable. As you will observe we have also added `[user]` in the permissions of each object, why is that you may ask. The reason is that we want to build a fine-grained authorization, which means while a collection can be belong to a team, we can also grant individual permissions to users. This gives us a great way to play around with permissions at the cost of a more complex implementation of how permissions are managed, but we will get to that in the next post. We have also removed the `belongsTo` relationship as we no longer need it. Reason: OpenFGA does not allow access of relations more than a single layer into the hierarchy thus a collection cannot use the owner of its team for permissions (there are other ways to implement that outside of the scope of this article). Let’s recap what is our model capable of doing: - Fine-grained access control to objects is possible via relations - Users can be grouped into teams (a single user per team is also acceptable for cases where you need a user to be the sole owner of a collection or a database) - Access to resources can be granted to individual users via object relations - Define roles within a team (this can be extended to allow roles per resource, but is outside of the scope of this article) In short we have achieved the goals we have initially set, with a relatively simple and understandable model. However, does our model work? Let’s find out in the next section. ## Testing the model Luckily OpenFGA folks have provided a great developer experience by making it easy to [write and run tests](https://openfga.dev/docs/modeling/testing). This is a massive W and time-saver. - An individual user can be given access to specific resources via relations - Users can be part of any of the team roles - An object can access by a team ```yaml name: Chroma Authorization Model Tests # optional model_file: ./model-article-p4.fga # you can specify an external .fga file, or include it inline # tuple_file: ./tuples.yaml # you can specify an external file, or include it inline tuples: - user: user:jane relation: owner object: team:chroma - user: user:john relation: writer object: team:chroma - user: user:jill relation: reader object: team:chroma - user: user:sam relation: can_create_tenant object: server:server1 - user: user:sam relation: can_get_tenant object: server:server1 - user: user:sam relation: can_get_preflight object: server:server1 - user: user:michelle relation: can_create_tenant object: server:server1 - user: team:chroma#owner relation: can_get_preflight object: server:server1 - user: team:chroma#owner relation: can_create_tenant object: server:server1 - user: team:chroma#owner relation: can_get_tenant object: server:server1 - user: team:chroma#writer relation: can_get_preflight object: server:server1 - user: team:chroma#writer relation: can_create_tenant object: server:server1 - user: team:chroma#writer relation: can_get_tenant object: server:server1 - user: team:chroma#reader relation: can_get_preflight object: server:server1 - user: team:chroma#reader relation: can_get_tenant object: server:server1 tests: - name: Users should have team roles check: - user: user:jane object: team:chroma assertions: owner: true writer: false reader: false - user: user:john object: team:chroma assertions: writer: true owner: false reader: false - user: user:jill object: team:chroma assertions: writer: false owner: false reader: true - user: user:unknown object: team:chroma assertions: writer: false owner: false reader: false - user: user:jane object: team:unknown assertions: writer: false owner: false reader: false - user: user:unknown object: team:unknown assertions: writer: false owner: false reader: false - name: Users should have direct access to server check: - user: user:sam object: server:server1 assertions: can_get_preflight: true can_create_tenant: true can_get_tenant: true - user: user:michelle object: server:server1 assertions: can_get_preflight: false can_create_tenant: true can_get_tenant: false - user: user:unknown object: server:server1 assertions: can_get_preflight: false can_create_tenant: false can_get_tenant: false - user: user:jill object: server:serverX assertions: can_get_preflight: false can_create_tenant: false can_get_tenant: false - name: Users of a team should have access to server check: - user: user:jane object: server:server1 assertions: can_create_tenant: true can_get_tenant: true can_get_preflight: true - user: user:john object: server:server1 assertions: can_create_tenant: true can_get_tenant: true can_get_preflight: true - user: user:jill object: server:server1 assertions: can_create_tenant: false can_get_tenant: true can_get_preflight: true - user: user:unknown object: server:server1 assertions: can_create_tenant: false can_get_tenant: false can_get_preflight: false ``` Let’s run the tests: ```yaml fga model test --tests test.model-article-p4.fga.yaml ``` This will result in the following output: ```yaml # Test Summary # Tests 3/3 passing Checks 42/42 passing ``` That is all folks. We try to keep things as concise as possible and this article has already our levels of comfort in that area. The bottom line is that authorization is no joke and it should take as long of a time as needed. Writing out all tests will not be concise (maybe we’ll add that to the repo). ## Conclusion In this article we’ve have built an authorization model for Chroma from scratch using OpenFGA. Admittedly it is a simple model, it still gives is a lot of flexibility to control access to Chroma resources. ## Resources - https://github.com/amikos-tech/chromadb-auth - the companion repo for this article (files are stored under `openfga/basic/`) - https://openfga.dev/docs - Read it, understand it, code it! - https://marketplace.visualstudio.com/items?itemName=openfga.openfga-vscode - It makes your life easier # Multi-User Basic Auth ## Why Multi-user Auth? Multi-user authentication can be crucial for several reasons. Let's delve into this topic. **Security**β€”The primary concern is the security of your deployments. You need to control who can access your data and ensure they are authorized to do so. You may wonder, since Chroma offers basic and token-based authentication, why is multi-user authentication necessary? You should never share your Chroma access credentials with your users or any app that depends on Chroma. The answer to this concern is a categorical NO. Another reason to consider multi-user authentication is to differentiate access to your data. However, the solution presented here doesn't provide this. It's a stepping stone towards our upcoming article on multi-tenancy and securing Chroma data. Last but not least is auditing. While we acknowledge this is not for everybody, there is ~~an~~ increasing pressure to provide visibility into your app via auditable events. Multi-user experiences - Not all GenAI apps are intended to be private or individual. This is another reason to consider and implement multi-user authentication and authorization. ## Dive right in. Let's get straight to the point and build a multi-user authorization with basic authentication. Here's our goal: - Develop a server-side authorization provider that can read multiple users from a `.htpasswd` file - Generate a multi-user `.htpasswd` file with several test users - Package our plugin with the Chroma base image and execute it using Docker Compose Auth CIP Chroma has [detailed info](https://github.com/chroma-core/chroma/blob/main/docs/CIP_2_Auth_Providers_Proposal.md) about how its authentication and authorization are implemented. Should you want to learn more go read the CIP (Chroma Improvement Proposal doc). ### The Plugin ```python import importlib import logging from typing import Dict, cast, TypeVar, Optional from chromadb.auth import ( ServerAuthCredentialsProvider, AbstractCredentials, SimpleUserIdentity, ) from chromadb.auth.registry import register_provider from chromadb.config import System from chromadb.telemetry.opentelemetry import ( OpenTelemetryGranularity, trace_method, add_attributes_to_current_span, ) from pydantic import SecretStr from overrides import override T = TypeVar("T") logger = logging.getLogger(__name__) @register_provider("multi_user_htpasswd_file") class MultiUserHtpasswdFileServerAuthCredentialsProvider(ServerAuthCredentialsProvider): _creds: Dict[str, SecretStr] # contains user:password-hash def __init__(self, system: System) -> None: super().__init__(system) try: self.bc = importlib.import_module("bcrypt") except ImportError: raise ValueError( "The bcrypt python package is not installed. " "Please install it with `pip install bcrypt`" ) system.settings.require("chroma_server_auth_credentials_file") _file = str(system.settings.chroma_server_auth_credentials_file) self._creds = dict() with open(_file, "r") as f: for line in f: _raw_creds = [v for v in line.strip().split(":")] if len(_raw_creds) != 2: raise ValueError( "Invalid Htpasswd credentials found in " f"[{str(system.settings.chroma_server_auth_credentials_file)}]. " "Must be :." ) self._creds[_raw_creds[0]] = SecretStr(_raw_creds[1]) @trace_method( # type: ignore "MultiUserHtpasswdFileServerAuthCredentialsProvider.validate_credentials", OpenTelemetryGranularity.ALL, ) @override def validate_credentials(self, credentials: AbstractCredentials[T]) -> bool: _creds = cast(Dict[str, SecretStr], credentials.get_credentials()) if len(_creds) != 2 or "username" not in _creds or "password" not in _creds: logger.error( "Returned credentials did match expected format: " "dict[username:SecretStr, password: SecretStr]" ) add_attributes_to_current_span( { "auth_succeeded": False, "auth_error": "Returned credentials did match expected format: " "dict[username:SecretStr, password: SecretStr]", } ) return False # early exit on wrong format _user_pwd_hash = ( self._creds[_creds["username"].get_secret_value()] if _creds["username"].get_secret_value() in self._creds else None ) validation_response = _user_pwd_hash is not None and self.bc.checkpw( _creds["password"].get_secret_value().encode("utf-8"), _user_pwd_hash.get_secret_value().encode("utf-8"), ) add_attributes_to_current_span( { "auth_succeeded": validation_response, "auth_error": f"Failed to validate credentials for user {_creds['username'].get_secret_value()}" if not validation_response else "", } ) return validation_response @override def get_user_identity( self, credentials: AbstractCredentials[T] ) -> Optional[SimpleUserIdentity]: _creds = cast(Dict[str, SecretStr], credentials.get_credentials()) return SimpleUserIdentity(_creds["username"].get_secret_value()) ``` In less than 80 lines of code, we have our plugin. Let's delve into and explain some of the key points of the code above: - `__init__` - Here, we dynamically import bcrypt, which we'll use to check user credentials. We also read the configured credentials file - `server.htpasswd` line by line, to retrieve each user (we assume each line contains a new user with its bcrypt hash). - `validate_credentials` - This is where the magic happens. We initially perform some lightweight validations on the credentials parsed by Chroma and passed to the plugin. Then, we attempt to retrieve the user and its hash from the `_creds` dictionary. The final step is to verify the hash. We've also added some attributes to monitor our authentication process in our observability layer (we have an upcoming article about this). - `get_user_identity` - Constructs a simple user identity, which the authorization plugin uses to verify permissions. Although not needed for now, each authentication plugin must implement this, as user identities are crucial for authorization. We'll store our plugin in `__init__.py` within the following directory structure - `chroma_auth/authn/basic/__init__.py` (refer to the repository for details). ### Password file Now that we have our plugin let’s create a password file with a few users: Initial user: ```bash echo "password123" | htpasswd -iBc server.htpasswd admin ``` The above will create (`-c` flag) a new server.htpasswd file with initial user `admin` and the password will be read from stdin (`-i` flag) and saved as bcrypt hash (`-B` flag) Let’s add another user: ```bash echo "password123" | htpasswd -iB server.htpasswd user1 ``` Now our `server.htpasswd` file will look like this: ```bash admin:$2y$05$vkBK4b1Vk5O98jNHgr.uduTJsTOfM395sKEKe48EkJCVPH/MBIeHK user1:$2y$05$UQ0kC2x3T2XgeN4WU12BdekUwCJmLjJNhMaMtFNolYdj83OqiEpVu ``` Moving on to docker setup. ### Docker compose setup Let’s create a `Dockerfile` to bundle our plugin with the official Chroma image: ```docker ARG CHROMA_VERSION=0.4.24 FROM ghcr.io/chroma-core/chroma:${CHROMA_VERSION} as base COPY chroma_auth/ /chroma/chroma_auth ``` This will pick up the official docker image for Chroma and will add our plugin directory structure so that we can use it. Now let’s create an `.env` file to load our plugin: ```bash CHROMA_SERVER_AUTH_PROVIDER="chromadb.auth.basic.BasicAuthServerProvider" CHROMA_SERVER_AUTH_CREDENTIALS_FILE="server.htpasswd" CHROMA_SERVER_AUTH_CREDENTIALS_PROVIDER="chroma_auth.authn.basic.MultiUserHtpasswdFileServerAuthCredentialsProvider" ``` And finally our `docker-compose.yaml`: ```yaml version: '3.9' networks: net: driver: bridge services: server: image: chroma-server build: dockerfile: Dockerfile volumes: - ./chroma-data:/chroma/chroma - ./server.htpasswd:/chroma/server.htpasswd command: "--workers 1 --host 0.0.0.0 --port 8000 --proxy-headers --log-config chromadb/log_config.yml --timeout-keep-alive 30" environment: - IS_PERSISTENT=TRUE - CHROMA_SERVER_AUTH_PROVIDER=${CHROMA_SERVER_AUTH_PROVIDER} - CHROMA_SERVER_AUTH_CREDENTIALS_FILE=${CHROMA_SERVER_AUTH_CREDENTIALS_FILE} - CHROMA_SERVER_AUTH_CREDENTIALS=${CHROMA_SERVER_AUTH_CREDENTIALS} - CHROMA_SERVER_AUTH_CREDENTIALS_PROVIDER=${CHROMA_SERVER_AUTH_CREDENTIALS_PROVIDER} - CHROMA_SERVER_AUTH_TOKEN_TRANSPORT_HEADER=${CHROMA_SERVER_AUTH_TOKEN_TRANSPORT_HEADER} - PERSIST_DIRECTORY=${PERSIST_DIRECTORY:-/chroma/chroma} - CHROMA_OTEL_EXPORTER_ENDPOINT=${CHROMA_OTEL_EXPORTER_ENDPOINT} - CHROMA_OTEL_EXPORTER_HEADERS=${CHROMA_OTEL_EXPORTER_HEADERS} - CHROMA_OTEL_SERVICE_NAME=${CHROMA_OTEL_SERVICE_NAME} - CHROMA_OTEL_GRANULARITY=${CHROMA_OTEL_GRANULARITY} - CHROMA_SERVER_NOFILE=${CHROMA_SERVER_NOFILE} restart: unless-stopped # possible values are: "no", always", "on-failure", "unless-stopped" ports: - "8000:8000" healthcheck: # Adjust below to match your container port test: [ "CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat" ] interval: 30s timeout: 10s retries: 3 networks: - net ``` ### The test Let’s run our docker compose setup: ```yaml docker compose --env-file ./.env up --build ``` You *should* see the following log message if the plugin was successfully loaded: ```bash server-1 | DEBUG: [01-04-2024 14:10:13] Starting component MultiUserHtpasswdFileServerAuthCredentialsProvider server-1 | DEBUG: [01-04-2024 14:10:13] Starting component BasicAuthServerProvider server-1 | DEBUG: [01-04-2024 14:10:13] Starting component FastAPIChromaAuthMiddleware ``` Once our container is up and running, let’s see if our multi-user auth works: ```bash import chromadb from chromadb.config import Settings client = chromadb.HttpClient( settings=Settings(chroma_client_auth_provider="chromadb.auth.basic.BasicAuthClientProvider",chroma_client_auth_credentials="admin:password123")) client.heartbeat() # this should work with or without authentication - it is a public endpoint client.get_or_create_collection("test_collection") # this is a protected endpoint and requires authentication client.list_collections() # this is a protected endpoint and requires authentication ``` The above code should return the list of collections, a single collection `test_collection` that we created. ```bash (chromadb-multi-user-basic-auth-py3.11) [chromadb-multi-user-basic-auth]python 19:51:38 ☁ main β˜‚ ⚑ ✚ Python 3.11.7 (main, Dec 30 2023, 14:03:09) [Clang 15.0.0 (clang-1500.1.0.2.5)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import chromadb >>> from chromadb.config import Settings >>> >>> client = chromadb.HttpClient( ... settings=Settings(chroma_client_auth_provider="chromadb.auth.basic.BasicAuthClientProvider",chroma_client_auth_credentials="admin:password123")) >>> client.heartbeat() # this should work with or without authentication - it is a public endpoint 1711990302270211007 >>> >>> client.list_collections() # this is a protected endpoint and requires authentication [] ``` Great, now let’s test for our other user: ```bash client = chromadb.HttpClient( settings=Settings(chroma_client_auth_provider="chromadb.auth.basic.BasicAuthClientProvider",chroma_client_auth_credentials="user1:password123")) ``` Works just as well (logs omitted for brevity). To ensure that our plugin works as expected let’s also test with an user that is not in our `server.htpasswd` file: ```bash client = chromadb.HttpClient( settings=Settings(chroma_client_auth_provider="chromadb.auth.basic.BasicAuthClientProvider",chroma_client_auth_credentials="invalid_user:password123")) ``` ```bash Traceback (most recent call last): File "", line 1, in File "/Users/tazarov/Library/Caches/pypoetry/virtualenvs/chromadb-multi-user-basic-auth-vIZuPNTE-py3.11/lib/python3.11/site-packages/chromadb/__init__.py", line 197, in HttpClient return ClientCreator(tenant=tenant, database=database, settings=settings) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/tazarov/Library/Caches/pypoetry/virtualenvs/chromadb-multi-user-basic-auth-vIZuPNTE-py3.11/lib/python3.11/site-packages/chromadb/api/client.py", line 144, in __init__ self._validate_tenant_database(tenant=tenant, database=database) File "/Users/tazarov/Library/Caches/pypoetry/virtualenvs/chromadb-multi-user-basic-auth-vIZuPNTE-py3.11/lib/python3.11/site-packages/chromadb/api/client.py", line 445, in _validate_tenant_database raise e File "/Users/tazarov/Library/Caches/pypoetry/virtualenvs/chromadb-multi-user-basic-auth-vIZuPNTE-py3.11/lib/python3.11/site-packages/chromadb/api/client.py", line 438, in _validate_tenant_database self._admin_client.get_tenant(name=tenant) File "/Users/tazarov/Library/Caches/pypoetry/virtualenvs/chromadb-multi-user-basic-auth-vIZuPNTE-py3.11/lib/python3.11/site-packages/chromadb/api/client.py", line 486, in get_tenant return self._server.get_tenant(name=name) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/tazarov/Library/Caches/pypoetry/virtualenvs/chromadb-multi-user-basic-auth-vIZuPNTE-py3.11/lib/python3.11/site-packages/chromadb/telemetry/opentelemetry/__init__.py", line 127, in wrapper return f(*args, **kwargs) ^^^^^^^^^^^^^^^^^^ File "/Users/tazarov/Library/Caches/pypoetry/virtualenvs/chromadb-multi-user-basic-auth-vIZuPNTE-py3.11/lib/python3.11/site-packages/chromadb/api/fastapi.py", line 200, in get_tenant raise_chroma_error(resp) File "/Users/tazarov/Library/Caches/pypoetry/virtualenvs/chromadb-multi-user-basic-auth-vIZuPNTE-py3.11/lib/python3.11/site-packages/chromadb/api/fastapi.py", line 649, in raise_chroma_error raise chroma_error chromadb.errors.AuthorizationError: Unauthorized ``` As expected, we get auth error when trying to connect to Chroma (the client initialization validates the tenant and DB which are both protected endpoints which raises the exception above). # Naive Multi-tenancy Strategies Single-note Chroma The below strategies are applicable to single-node Chroma only. The strategies require your app to act as both PEP (Policy Enforcement Point) and PDP (Policy Decision Point) for authorization. This is a naive approach to multi-tenancy and is probably not suited for production environments, however it is a good and simple way to get started with multi-tenancy in Chroma. Authorization We are in the process of creating a list of articles on how to implement proper authorization in Chroma, leveraging the an external service and Chroma's auth plugins. The first article of the series is available in [Medium](https://medium.com/@amikostech/implementing-multi-tenancy-in-chroma-part-1-multi-user-basic-auth-a4e790f1254d) and will also be made available here soon. ## Introduction There are several multi-tenancy strategies available to users of Chroma. The actual strategy will depend on the needs of the user and the application. The strategies below apply to multi-user environments, but do no factor in partly-shared resources like groups or teams. - **User-Per-Doc**: In this scenario, the app maintains multiple collections and each collection document is associated with a single user. - **User-Per-Collection**: In this scenario, the app maintains multiple collections and each collection is associated with a single user. - **User-Per-Database**: In this scenario, the app maintains multiple databases with a single tenant and each database is associated with a single user. - **User-Per-Tenant**: In this scenario, the app maintains multiple tenants and each tenant is associated with a single user. ## User-Per-Doc The goal of this strategy is to grant user permissions to access individual documents. To implement this strategy you need to add some sort of user identification to each document that belongs to a user. For this example we will assume it is `user_id`. ```python import chromadb client = chromadb.PersistentClient() collection = client.get_or_create_collection("my-collection") collection.add( documents=["This is document1", "This is document2"], metadatas=[{"user_id": "user1"}, {"user_id": "user2"}], ids=["doc1", "doc2"], ) ``` At query time you will have to provide the `user_id` as a filter to your query like so: ```python results = collection.query( query_texts=["This is a query document"], where=[{"user_id": "user1"}], ) ``` To successfully implement this strategy your code needs to consistently add and filter on the `user_id` metadata to ensure separation of data. **Drawbacks**: - Error-prone: Messing up the filtering can lead to data being leaked across users. - Scalability: As the number of users and documents grow, doing filtering on metadata can become slow. ## User-Per-Collection The goal of this strategy is to grant a user access to all documents in a collection. To implement this strategy you need to create a collection for each user. For this example we will assume it is `user_id`. ```python import chromadb client = chromadb.PersistentClient() user_id = "user1" collection = client.get_or_create_collection(f"user-collection:{user_id}") collection.add( documents=["This is document1", "This is document2"], ids=["doc1", "doc2"], ) ``` At query time you will have to provide the `user_id` as a filter to your query like so: ```python user_id = "user1" user_collection = client.get_collection(f"user-collection:{user_id}") results = user_collection.query( query_texts=["This is a query document"], ) ``` To successfully implement this strategy your code needs to consistently create and query the correct collection for the user. **Drawbacks**: - Error-prone: Messing up the collection name can lead to data being leaked across users. - Shared document search: If you want to maintain some documents shared then you will have to create a separate collection for those documents and allow users to query the shared collection as well. ## User-Per-Database The goal of this strategy is to associate a user with a single database thus granting them access to all collections and documents within the database. ```python import chromadb from chromadb import DEFAULT_TENANT from chromadb import Settings adminClient = chromadb.AdminClient(Settings( is_persistent=True, persist_directory="multitenant", )) # For Remote Chroma server: # # adminClient= chromadb.AdminClient(Settings( # chroma_api_impl="chromadb.api.fastapi.FastAPI", # chroma_server_host="localhost", # chroma_server_http_port="8000", # )) def get_or_create_db_for_user(user_id): database = f"db:{user_id}" try: adminClient.get_database(database) except Exception as e: adminClient.create_database(database, DEFAULT_TENANT) return DEFAULT_TENANT, database user_id = "user_John" tenant, database = get_or_create_db_for_user(user_id) # replace with chromadb.HttpClient for remote Chroma server client = chromadb.PersistentClient(path="multitenant", tenant=tenant, database=database) collection = client.get_or_create_collection("user_collection") collection.add( documents=["This is document1", "This is document2"], ids=["doc1", "doc2"], ) ``` In the above code we do the following: - We create or get a database for each user in the `DEFAULT_TENANT` using the `chromadb.AdminClient`. - We then create a `PersistentClient` for each user with the `tenant` and `database` we got from the `AdminClient`. - We then create or get collection and add data to it. **Drawbacks**: - This strategy requires consistent management of tenants and databases and their use in the client application. ## User-Per-Tenant The goal of this strategy is to associate a user with a single tenant thus granting them access to all databases, collections, and documents within the tenant. ```python import chromadb from chromadb import DEFAULT_DATABASE from chromadb import Settings adminClient = chromadb.AdminClient(Settings( chroma_api_impl="chromadb.api.segment.SegmentAPI", is_persistent=True, persist_directory="multitenant", )) # For Remote Chroma server: # # adminClient= chromadb.AdminClient(Settings( # chroma_api_impl="chromadb.api.fastapi.FastAPI", # chroma_server_host="localhost", # chroma_server_http_port="8000", # )) def get_or_create_tenant_for_user(user_id): tenant_id = f"tenant_user:{user_id}" try: adminClient.get_tenant(tenant_id) except Exception as e: adminClient.create_tenant(tenant_id) adminClient.create_database(DEFAULT_DATABASE, tenant_id) return tenant_id, DEFAULT_DATABASE user_id = "user1" tenant, database = get_or_create_tenant_for_user(user_id) # replace with chromadb.HttpClient for remote Chroma server client = chromadb.PersistentClient(path="multitenant", tenant=tenant, database=database) collection = client.get_or_create_collection("user_collection") collection.add( documents=["This is document1", "This is document2"], ids=["doc1", "doc2"], ) ``` In the above code we do the following: - We create or get a tenant for each user with `DEFAULT_DATABASE` using the `chromadb.AdminClient`. - We then create a `PersistentClient` for each user with the `tenant` and `database` we got from the `AdminClient`. - We then create or get collection and add data to it. **Drawbacks**: - This strategy requires consistent management of tenants and databases and their use in the client application. # FAQ # Frequently Asked Questions and Commonly Encountered Issues This section provides answers to frequently asked questions and information on commonly encountered problem when working with Chroma. These information below is based on interactions with the Chroma community. 404 Answer Not Found If you have a question that is not answered here, please reach out to us on our [Discord @taz](https://discord.gg/MMeYNTmh3x) or [GitHub Issues](https://github.com/chroma-core/chroma/issues) ## Frequently Asked Questions ### Distances and Similarity Chroma uses distance metrics to measure how dissimilar a result is from a query. A distance of 0 indicates that the two items are identical, while larger distances indicate greater dissimilarity. This approach starts at 0 and increases upward, aligning with the intuitive notion of distance. In contrast, similarity metrics measure how similar two items are, often on a scale where higher values represent greater similarity. For example: - Cosine Similarity ranges from -1 to 1, where: - 1 indicates identical orientation (maximum similarity), - 0 indicates orthogonality (no similarity), - -1 indicates opposite orientation (maximum dissimilarity). - Dot Product can range from negative to positive infinity, depending on the vectors' magnitudes and directions. When vectors are normalized and non-negative, the dot product ranges from 0 to 1. #### Why Does Chroma Use Distance Metrics? Chroma uses distance metrics because they provide a straightforward way to quantify dissimilarity between vectors. Consistency is another key aspect - irrespecitve of the distance metric used, distance results follow the same conceptual framework. Finally, distance is is an intuitive metric which makes Chroma more accessible to a wider audience. ### What does Chroma use to index embedding vectors? Chroma uses its own [fork](https://github.com/chroma-core/hnswlib) of HNSW lib for indexing and searching embeddings. In addition to HNSW, Chroma also uses a Brute Force index, which acts as a buffer (prior to updating the HNSW graph) and performs exhaustive search using the same distance metric as the HNSW index. **Alternative Questions:** - What library does Chroma use for vector index and search? - What algorithm does Chroma use for vector search? ### How to set dimensionality of my collections? When creating a collection, its dimensionality is determined by the dimensionality of the first embedding added to it. Once the dimensionality is set, it cannot be changed. Therefore, it is important to consistently use embeddings of the same dimensionality when adding or querying a collection. **Example:** ```python import chromadb client = chromadb.Client() collection = client.create_collection("name") # dimensionality is not set yet # add an embedding to the collection collection.add(ids=["id1"], embeddings=[[1, 2, 3]]) # dimensionality is set to 3 ``` **Alternative Questions:** - Can I change the dimensionality of a collection? ### Can I use `transformers` models with Chroma? Generally, yes you can use `transformers` models with Chroma. Although Chroma does not provide a wrapper for this, you can use `SentenceTransformerEmbeddingFunction` to achieve the same result. The sentence-transformer library will implicitly do mean-pooling on the last hidden layer, and you'll get a warning about it - `No sentence-transformers model found with name [model name]. Creating a new one with MEAN pooling.` **Example:** ```python from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction ef = SentenceTransformerEmbeddingFunction(model_name="FacebookAI/xlm-roberta-large-finetuned-conll03-english") print(ef(["test"])) ``` Warning Not all models will work with the above method. Also mean pooling may not be the best strategy for the model. Read the model card and try to understand what if any pooling the creators recommend. You may also want to normalize the embeddings before adding them to Chroma (pass `normalize_embeddings=True` to the `SentenceTransformerEmbeddingFunction` EF constructor). ### Should I store my documents in Chroma? > Note: This applies to Chroma single-node and local embedded clients. (Chroma version ca. 0.5.x) Chroma allows users to store both embeddings and documents, alongside metadata, in collections. Documents and metadata are both optional and depending on your use case you may choose to store them in Chroma or externally, or not at all. Here are some pros/cons to help you decide whether to store your documents in Chroma: **Pros:** - Keeps all the data in the same place. You don't have to manage a separate DB for the documents - Allows you to do keyword searches on the documents **Cons:** - The database can grow substantially in size because documents are effectively duplicated - once for storing them as metadata for queries and another for the FTS5 index. - Queries performance hit ### "Dude, where's my data?" If you are new to Chroma, you might be asking yourself: "Where is my data been stored?". As, per usual, the answer is: "It depends". Generally Chroma uses `PERSIST_DIRECTORY` to store the data, but when running in CLI mode, this is overridden by the CLI itself. - Running in CLI mode (`--path` is not specified) data is stored in the `./chroma_data` directory. - Running in Jupyter notebook, Colab or directly using `PersistentClient` (unless `path` is specified or env var `PERSIST_DIRECTORY` is set), data is stored in the `./chroma` directory. - Running with docker compose (from source repo), the data is stored in docker volume named `chroma-data` (unless an explicit volume binding is specified) - Running with `docker run` (no volume binding with `-v`) the data is stored in the container and is lost ☠️ when the container is removed. In all other cases where env var, parameter or binding is specified, the data is stored in your specified directory. ## Commonly Encountered Problems ### Collection Dimensionality Mismatch **Symptoms:** This error usually exhibits in the following error message: `chromadb.errors.InvalidDimensionException: Embedding dimension XXX does not match collection dimensionality YYY` **Context:** When adding/upserting or querying Chroma collection. This error is more visible/pronounced when using the Python APIs, but will also show up in also surface in other clients. **Cause:** You are trying to add or query a collection with vectors of a different dimensionality than the collection was created with. **Explanation/Solution:** When you first create a collection `client.create_collection("name")`, the collection will not have knowledge of its dimensionality so that allows you to add vectors of any dimensionality to it. However, once your first batch of embeddings is added to the collection, the collection will be locked to that dimensionality. Any subsequent query or add operation must use embeddings of the same dimensionality. The dimensionality of the embeddings is a characteristic of the embedding model (EmbeddingFunction) used to generate the embeddings, therefore it is important to consistently use the same EmbeddingFunction when adding or querying a collection. Tip If you do not specify an `embedding_function` when creating (`client.create_collection`) or getting (`client.get_or_create_collection`) a collection, Chroma wil use its default [embedding function](https://docs.trychroma.com/embeddings#default-all-minilm-l6-v2). ### Large Distances in Search Results **Symptoms:** When querying a collection, you get results that are in the 10s or 100s. **Context:** Frequently when using you own embedding function. **Cause:** The embeddings are not normalized. **Explanation/Solution:** `L2` (Euclidean distance) and `IP` (inner product) distance metrics are sensitive to the magnitude of the vectors. Chroma uses `L2` by default. Therefore, it is recommended to normalize the embeddings before adding them to Chroma. Here is an example how to normalize embeddings using L2 norm: ```python import numpy as np def normalize_L2(vector): """Normalizes a vector to unit length using L2 norm.""" norm = np.linalg.norm(vector) if norm == 0: return vector return vector / norm ``` ### `OperationalError: no such column: collections.topic` **Symptoms:** The error `OperationalError: no such column: collections.topic` is raised when trying to access Chroma locally or remotely. **Context:** After upgrading to Chroma `0.5.0` or accessing your Chroma persistent data with Chroma client version `0.5.0`. **Cause:** In version `0.5.x` Chroma has made some SQLite3 schema changes that are not backwards compatible with the previous versions. Once you access your persistent data on the server or locally with the new Chroma version it will automatically migrate to the new schema. This operation is not reversible. **Explanation/Solution:** To resolve this issue you will need to upgrade all your clients accessing the Chroma data to version `0.5.x`. Here's a link to the migration performed by Chroma - https://github.com/chroma-core/chroma/blob/main/chromadb/migrations/sysdb/00005-remove-topic.sqlite.sql ### `sqlite3.OperationalError: database or disk is full` **Symptoms:** The error `sqlite3.OperationalError: database or disk is full` is raised when trying to access Chroma locally or remotely. The error can occur in any of the Chroma API calls. **Context:** There are two contexts in which this error can occur: - When the persistent disk space is full or the disk quota is reached - This is where your `PERSIST_DIRECTORY` points to. - When there is not enough space in the temporary director - frequently `/tmp` on your system or container. **Cause:** When inserting new data and your Chroma persistent disk space is full or the disk quota is reached, the database will not be able to write metadata to SQLite3 db thus raising the error. When performing large queries or multiple concurrent queries, the temporary disk space may be exhausted. **Explanation/Solution:** To work around the first issue, you can increase the disk space or clean up the disk space. To work around the second issue, you can increase the temporary disk space (works fine for containers but might be a problem for VMs) or point SQLite3 to a different temporary directory by using `SQLITE_TMPDIR` environment variable. SQLite Temp File More information on how sqlite3 uses temp files can be found [here](https://www.sqlite.org/tempfiles.html). ### `RuntimeError: Chroma is running in http-only client mode, and can only be run with 'chromadb.api.fastapi.FastAPI'` **Symptoms and Context:** The following error is raised when trying to create a new `PersistentClient`, `EphemeralClient`, or `Client`: ```text RuntimeError: Chroma is running in http-only client mode, and can only be run with 'chromadb.api.fastapi.FastAPI' as the chroma_api_impl. see https://docs.trychroma.com/usage-guide?lang=py#using-the-python-http-only-client for more information. ``` **Cause:** There are two possible causes for this error: - `chromadb-client` is installed and you are trying to work with a local client. - Dependency conflict with `chromadb-client` and `chromadb` packages. **Explanation/Solution:** Chroma (python) comes in two packages - `chromadb` and `chromadb-client`. The `chromadb-client` package is used to interact with a remote Chroma server. If you are trying to work with a local client, you should use the `chromadb` package. If you are planning to interact with remote server only it is recommended to use the `chromadb-client` package. If you intend to work locally with Chroma (e.g. embed in your app) then we suggest that you uninstall the `chromadb-client` package and install the `chromadb` package. To check which package you have installed: ```bash pip list | grep chromadb ``` To uninstall the `chromadb-client` package: ```bash pip uninstall chromadb-client ``` Working with virtual environments It is recommended to work with virtual environments to avoid dependency conflicts. To create a virtual environment you can use the following snippet: ```bash pip install virtualenv python -m venv myenv source myenv/bin/activate pip install chromadb # and other packages you need ``` Alternatively you can use `conda` or `poetry` to manage your environments. Default Embedding Function Default embedding function - `chromadb.utils.embedding_functions.DefaultEmbeddingFunction` - can only be used with `chromadb` package. ### `ValueError: You must provide an embedding function to compute embeddings` **Symptoms and Context:** The error `ValueError: You must provide an embedding function to compute embeddings.https://docs.trychroma.com/embeddings"` is frequently raised when trying to add embeddings to a collection using Python thin client (`chromadb-client` package). **Cause:** To reduce the size of the `chromadb-client` package the default embedding function which requires `onnxruntime` package is not included and is instead aliased to `None`. **Explanation/Solution:** To resolve this issue you must always provide an embedding function when you call `get_collection` or `get_or_create_collection` methods to provide the Http client with the necessary information to compute embeddings. ### Adding documents is slow **Symptoms:** Adding documents to Chroma appears slow. **Context:** You've tried adding documents to a collection using the `add()` or `upsert()` methods. **Cause:** There are several reasons why the addition may be slow: - Very large batches - Slow embeddings - Slow network Let's break down each of the factors. **Very large batches** If you are trying to add 1000s or even 10,000s of documents at once and depending on how much data is already in your collection Chroma (specifically the HNSW graph updates) can become a bottleneck. To debug if this is the case you can reduce the size of the batch and see if the operation is faster. You can also check how many records are in the collection with `count()` method. **Slow embeddings** This is the most common reason for slow addition. Some embedding functions are slower than others. To debug this you can try the following example by adjusting the embeding function to your own. What the code tests is how much it takes to compute the embedings and then to add them to the collection in separate steps such that each can be measured independenty. ```python from chromadb.utils import embedding_functions import chromadb import uuid list_of_sentences = ["Hello world!", "How are you?"] # this should be your list of documents to add # change the below EF definition to match your embedding function default_ef = embedding_functions.DefaultEmbeddingFunction() start_time = time.perf_counter() embeddings=default_ef(list_of_sentences) end_time = time.perf_counter() print(f"Embedding time: {end_time - start_time}") client = chromadb.PersistentClient(path="my_chroma_data") collection = client.get_or_create_collection("my_collection") start_time = time.perf_counter() # this will add your documents and the generated embeddings without Chroma doing the embedding for you internally collection.add(ids=[f"{uuid.uuid4()}" for _ in range(len(list_of_sentences))],documents=list_of_sentences, embeddings=embeddings) end_time = time.perf_counter() print(f"Chroma add time: {end_time - start_time}") ``` **Slow network** If you are adding documents to a remote Chroma the network speed can become a bottleneck. To debug this you can with a local `PersistentClient` and see if the operation is faster. # Ecosystem # Chroma Ecosystem Clients ## Python | | | | ------------- | --------------------------------------------------------------------- | | Maintainer | Chroma Core team | | Repo | | | Status | βœ… Stable | | Version | `0.5.5.dev0` ([PyPi Link](https://pypi.org/project/chromadb-client/)) | | Docs | | | Compatibility | Python: `3.8+`, Chroma API Version: `0.5.x` | Feature Support: | Feature | Supported | | ----------------- | --------- | | Create Tenant | βœ… | | Get Tenant | βœ… | | Create DB | βœ… | | Get DB | βœ… | | Create Collection | βœ… | | Get Collection | βœ… | | List Collection | βœ… | | Count Collection | βœ… | | Delete Collection | βœ… | | Add Documents | βœ… | | Delete Documents | βœ… | | Update Documents | βœ… | | Query Documents | βœ… | | Get Document | βœ… | | Count Documents | βœ… | | Auth - Basic | βœ… | | Auth - Token | βœ… | | Reset | βœ… | Embedding Function Support: | Embedding Function | Supported | | ----------------------------- | --------- | | OpenAI | βœ… | | Sentence Transformers | βœ… | | HuggingFace Inference API | βœ… | | Cohere | βœ… | | Google Vertex AI | βœ… | | Google Generative AI (Gemini) | βœ… | | OpenCLIP (Multi-modal) | βœ… | Embedding Functions The list above is not exhaustive. Check [official docs](https://docs.trychroma.com/integrations#%F0%9F%A7%AC-embedding-integrations) for up-to-date information. ## JavaScript | | | | ------------- | ------------------------------------------------------------ | | Maintainer | Chroma Core team | | Repo | | | Status | βœ… Stable | | Version | `1.8.1` ([NPM Link](https://www.npmjs.com/package/chromadb)) | | Docs | | | Compatibility | Python: `3.7+`, Chroma API Version: `TBD` | Feature Support: | Feature | Supported | | ----------------- | --------- | | Create Tenant | βœ… | | Get Tenant | βœ… | | Create DB | βœ… | | Get DB | βœ… | | Create Collection | βœ… | | Get Collection | βœ… | | List Collection | βœ… | | Count Collection | βœ… | | Delete Collection | βœ… | | Add Documents | βœ… | | Delete Documents | βœ… | | Update Documents | βœ… | | Query Documents | βœ… | | Get Document | βœ… | | Count Documents | βœ… | | Auth - Basic | βœ… | | Auth - Token | βœ… | | Reset | βœ… | Embedding Function Support: | Embedding Function | Supported | | ----------------------------- | --------- | | OpenAI | βœ… | | Sentence Transformers | βœ… | | HuggingFace Inference API | βœ… | | Cohere | βœ… | | Google Vertex AI | βœ… | | Google Generative AI (Gemini) | βœ… | | OpenCLIP (Multi-modal) | βœ… | Embedding Functions The list above is not exhaustive. Check [official docs](https://docs.trychroma.com/integrations#%F0%9F%A7%AC-embedding-integrations) for up-to-date information. ## Ruby Client https://github.com/mariochavez/chroma ## Java Client https://github.com/amikos-tech/chromadb-java-client ## Go Client | | | | ------------- | ----------------------------------------------------------------------------- | | Maintainer | Amikos Tech (Chroma Core contributor) | | Repo | | | Status | βœ… Stable | | Version | `0.1.4` ([Go Pkg Link](https://pkg.go.dev/github.com/amikos-tech/chroma-go/)) | | Docs | | | Compatibility | Go: `1.21+`, Chroma API Version: `0.5.x` | Feature Support: | Feature | Supported | | ----------------- | --------- | | Create Tenant | βœ… | | Get Tenant | βœ… | | Create DB | βœ… | | Get DB | βœ… | | Create Collection | βœ… | | Get Collection | βœ… | | List Collection | βœ… | | Count Collection | βœ… | | Delete Collection | βœ… | | Add Documents | βœ… | | Delete Documents | βœ… | | Update Documents | βœ… | | Query Documents | βœ… | | Get Document | βœ… | | Count Documents | βœ… | | Auth - Basic | βœ… | | Auth - Token | βœ… | | Reset | βœ… | Embedding Function Support: | Embedding Function | Supported | | ---------------------------------------------------------------------------------------------------------------------------- | --------- | | [OpenAI](https://go-client.chromadb.dev/embeddings/#openai) | βœ… | | [HuggingFace Inference API](https://go-client.chromadb.dev/embeddings/#huggingface-inference-api) | βœ… | | [Cohere](https://go-client.chromadb.dev/embeddings/#cohere) | βœ… | | [Google Generative AI (Gemini)](https://go-client.chromadb.dev/embeddings/#google-gemini-ai) | βœ… | | [Mistral AI](https://go-client.chromadb.dev/embeddings/#mistral-ai) | βœ… | | [Cloudflare Workers AI](https://go-client.chromadb.dev/embeddings/#cloudflare-workers-ai)) | βœ… | | [Together AI](https://go-client.chromadb.dev/embeddings/#together-ai) | βœ… | | [Ollama](https://go-client.chromadb.dev/embeddings/#ollama) | βœ… | | [Nomic AI](https://go-client.chromadb.dev/embeddings/#nomic-ai) | βœ… | | [Hugging Face Embedding Inference Server](https://go-client.chromadb.dev/embeddings/#huggingface-embedding-inference-server) | βœ… | ## C# Client https://github.com/microsoft/semantic-kernel/tree/main/dotnet/src/Connectors/Connectors.Memory.Chroma ## Rust Client https://crates.io/crates/chromadb ## Elixir Client https://hex.pm/packages/chroma/ ## Dart Client https://pub.dev/packages/chromadb ## PHP Client https://github.com/CodeWithKyrian/chromadb-php ## PHP (Laravel) Client https://github.com/helgeSverre/chromadb