chroma-client

    A Chroma vector database HTTP client for MoonBit: collections, records, queries and embeddings.

    chroma
    vector-database
    embedding
    rag
    http-client
    Download zip
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    5 days ago
    Downloads
    44

    #SundayT1998/chroma-client

    A Chroma vector-database client for MoonBit.

    Chroma is the embedding database commonly used behind retrieval-augmented generation (RAG) pipelines. chroma-client speaks Chroma's v2 HTTP API so MoonBit programs can create collections, add and query documents, and manage records without leaving the language.

    #Docs

    The README is the entry point; everything longer lives in docs/:

    • docs/CONTRIBUTING.md — setup, the checks a change has to pass, the two test layers, code style, commit and PR conventions.
    • docs/chroma-api-coverage.md — which Chroma v2 endpoints the client covers, which it deliberately does not, and why.
    • docs/protocol-constraints.md — the rules Chroma imposes that a thin endpoint wrapper gets wrong (columnar records, batch limits, destructive filters, where spelling, short query rows), with the input/output of each and where it is enforced.
    • docs/serialization-perf.md — what a large record batch costs to serialize, and why the write path is not streamed.

    #Design

    The library builds requests and decodes responses, and delegates the actual sockets to a trait. It ships the transport real applications use — AsyncHttpTransport, backed by moonbitlang/async/http — and leaves the trait open so an application with its own HTTP stack can plug it in. There are two traits:

    • AsyncTransport — asynchronous, with async fn send_async. This is the one the bundled AsyncHttpTransport implements, because moonbitlang/async/http only offers async functions and an async call cannot be made from a synchronous one.

    • Transport — synchronous. MemoryTransport implements it, so scripted responses cover the whole request/response cycle in ordinary moon test runs.

    Every public method exists in both flavours: client.add(...) is synchronous, client.add_async(...) is asynchronous. Choosing between them is choosing which trait your transport implements and whether your call site is async.

    The library ships its own transport, AsyncHttpTransport, backed by moonbitlang/async/http: a Chroma client that cannot reach a server is not useful, and native + wasm both run on the async stack the ecosystem already has. Callers get a working client out of the box, and the transport is:

    • portable — the same code runs on native, wasm and js. The transport limits the library to the backends moonbitlang/async supports; wasm-gc has no async runtime yet, so it is not a supported target;
    • testable — MemoryTransport implements the synchronous Transport and replays scripted responses, so the whole request/response cycle is covered by ordinary moon test runs without a server;
    • replaceable — AsyncTransport is a two-method-wide seam, so an application that already owns an HTTP stack can plug it in instead of using the bundled one.

    #Install

    moon add SundayT1998/chroma-client

    #Quick start

    ///|
    async fn quick_start() -> Unit raise ChromaError {
    // AsyncHttpTransport is bundled; point the config at your Chroma server.
    let client = ChromaClient::new(
    AsyncHttpTransport::new(),
    config=Config::new(endpoint="http://localhost:8000"),
    )
    let collection = client.get_or_create_collection_async("notes")
    // Chroma 1.x stores vectors, not text: every record needs coordinates.
    client.add_async(collection.id, [
    Record::of_embedding("r1", [1.0, 0.0, 0.0]).with_document(
    "MoonBit is a language for the web",
    ),
    Record::of_embedding("r2", [0.0, 1.0, 0.0]).with_document(
    "Chroma stores embeddings",
    ),
    ])
    let result = client.query_embeddings_async(
    collection.id,
    [[1.0, 0.0, 0.0]],
    n_results=2,
    )
    for id in result.ids {
    println(id)
    }
    }

    AsyncHttpTransport ships with the library (see Native transport), so most callers never write a transport. If your application already owns an HTTP stack, implement AsyncTransport instead — see Adding a real transport.

    Chroma 1.x has no server-side embedding. An add whose records carry documents but no vectors is rejected by the server, so the client refuses it locally and points at Record::with_embedding. Produce the vectors with whatever embedding model you already use; Record::of_document(...).with_embedding(vec) is the usual shape.

    #Choosing what a query returns

    QueryResult mirrors Chroma's include field:

    ///|
    async fn query_with_everything(
    client : ChromaClient[AsyncHttpTransport],
    collection_id : String,
    ) -> Unit raise ChromaError {
    ignore(
    client.query_embeddings_async(
    collection_id,
    [[0.1, 0.9, 0.0]],
    n_results=5,
    include_fields=Include::all(),
    ),
    )
    }

    Include::default() returns documents, metadata and distances — the query default, and the same one chroma-go documents. Include::get_default() is the get default (documents and metadata; a get has no distances to report), Include::all() adds embeddings, and Include::ids_only() returns just ids.

    The two defaults are not interchangeable: get, get_async and peek all post to POST /get, which Chroma rejects when the body asks for distances (distances are not supported in get). They therefore default to Include::get_default(); only query_embeddings defaults to Include::default().

    #Filtering

    Filter carries Chroma's where / where_document clauses plus the record ids, limit and offset an operation may take — the same five pieces chroma-go spells as WithWhere, WithWhereDocument, WithIDs, WithLimit and WithOffset:

    ///|
    async fn filtered_get(
    client : ChromaClient[AsyncHttpTransport],
    collection_id : String,
    ) -> Unit raise ChromaError {
    let filter = Filter::new()
    .with_metadata("source", MString("handbook"))
    .with_limit(20)
    ignore(client.get_async(collection_id, filter~))
    }

    with_metadata is a one-key equality test. Repeated calls accumulate the way chroma-go's WithWhere(EqString(..)) options do — the clause above is {"source": {"$eq": "handbook"}}, and adding a second key narrows the result rather than replacing the first. For anything richer, the where_* helpers build the JSON chroma-go's typed clauses build:

    ///|
    let clause = where_and([
    where_eq("topic", MString("ai")),
    where_gt("year", MInt(2020)),
    ])

    ///|
    let text_clause = where_document_or([
    where_document_contains("moon"),
    where_document_regex("chroma.*client"),
    ])

    chroma-gohere
    EqString(k, v) / EqInt / EqFloat / EqBoolwhere_eq(k, MString(v)) / where_eq(k, MInt(v)) …
    NotEq*where_ne
    GtInt, GtFloat, Lt*, Gte*, Lte*where_gt, where_lt, where_gte, where_lte
    InString, InInt, …where_in(k, [MString(..), ..])
    And(...), Or(...)where_and([..]), where_or([..])
    Contains(t), NotContains(t), Regex(t)where_document_contains / _not_contains / _regex
    AndDocument, OrDocumentwhere_document_and, where_document_or

    The helpers are plain Json builders, so a clause the table does not cover can be written by hand and passed to Filter::new(where_clause=Some(...)).

    #Metadata types

    Chroma accepts only scalar metadata. MetadataValue models that directly:

    ///|
    let greeting : MetadataValue = MString("hello")

    ///|
    let answer : MetadataValue = MInt(42)

    ///|
    let ratio : MetadataValue = MFloat(0.5)

    ///|
    let pinned : MetadataValue = MBool(true)

    ///|
    let nothing : MetadataValue = MNull

    Collections and records take a Metadata value, which is an ordered list of (key, MetadataValue) pairs.

    #Adding a real transport

    Implement AsyncTransport against whatever HTTP stack you use. With moonbitlang/async/http, the whole transport is a dozen lines:

    ///|
    impl AsyncTransport for MyTransport with fn send_async(self, request) {
    let headers : @ah.Headers = Map([])
    for entry in request.headers {
    let (key, value) = entry
    headers[key] = value
    }
    let payload : &@aio.Data = match request.body {
    Some(body) => body
    None => ""
    }
    let (response, response_body) = @ah.request(
    request.url,
    my_method(request.verb),
    headers,
    payload,
    )
    let out_headers : Map[String, String] = Map([])
    for entry in response.headers {
    let (key, value) = entry
    out_headers[key.to_string().to_lower()] = value
    }
    { status: response.code, headers: out_headers, body: response_body.text(), }
    }

    Request carries the verb, URL, headers and optional JSON body; the transport only has to return a Response.

    That is exactly what the bundled AsyncHttpTransport does; its source is async_http_transport.mbt. With moonbitlang/async/http the impl alone is not enough: MoonBit also needs an extend declaration to make the method callable:

    ///|
    pub extend MyTransport with AsyncTransport::{send_async}

    The extend line has to sit next to the impl in the same file; see async_http_transport.mbt for the authoritative version of both.

    #Native transport

    AsyncHttpTransport (in the library) is wired to moonbitlang/async/http, which is the only HTTP client in the ecosystem that runs on native:

    moon run cmd/main --target native -- http://localhost:8000 my-collection

    It creates a collection, adds vectors, counts, queries with distances, applies a metadata filter, and deletes the collection — a full round-trip against a real server, and the fastest way to check that a local Chroma is reachable.

    For a long-lived application, hold one @ah.Client open and reuse it rather than connecting per request: @ah.Client keeps the connection alive, while the example's @ah.request helper does not.

    #Authentication

    Config carries the headers every request shares. The common schemes have shorthands so they do not have to be spelled out by hand:

    ///|
    let authed_client = ChromaClient::new(
    AsyncHttpTransport::new(),
    config=Config::new()
    .with_token("my-api-token")
    .with_basic_auth("user", "password"),
    )

    with_headers merges a whole map, and with_header sets a single value. All four return a copy, so the original configuration is never mutated.

    #Timeouts

    The client builds and decodes; the transport owns the socket. So the client cannot cancel a read by itself — that is the HTTP stack's job. The deadline travels as x-chroma-timeout-ms, and the bundled AsyncHttpTransport honours it with a real @async.with_timeout around the whole exchange (connect, send, read). A timeout surfaces as ChromaError::Transport.

    The simplest way to set one is on the config; the default is 30 seconds, and a timeout_ms of 0 or less means "no deadline":

    ///|
    let timed_client = ChromaClient::new(
    AsyncHttpTransport::new(),
    config=Config::new(endpoint="http://localhost:8000", timeout_ms=500),
    )

    Config::timeout_ms is stamped on every request by the client itself, as x-chroma-timeout-ms, so the value reaches the transport whether or not you wrap anything — a transport that ignores the header just has no deadline. To change it after construction, Config::with_timeout_ms(ms) returns a copy with a different deadline (<= 0 turns it off); to build a transport straight from a config, AsyncHttpTransport::from_config(config) returns a TimeoutAsyncTransport wrapping it.

    TimeoutTransport is the synchronous counterpart — it wraps Transport, so it composes with MemoryTransport or your own stack. It does not fit AsyncHttpTransport, which needs an AsyncTransport: the type checker refuses that mix rather than silently ignoring the deadline, which is why the example above passes the config instead.

    ///|
    let timed_sync_client = ChromaClient::new(
    TimeoutTransport::new(MemoryTransport::new(), 5000),
    )

    For the async side there is TimeoutAsyncTransport, which is what AsyncHttpTransport::from_config returns:

    ///|
    let timed_async_client = ChromaClient::new(
    TimeoutAsyncTransport::new(AsyncHttpTransport::new(), 5000),
    )

    #Counting and previewing

    ///|
    fn count_and_preview(
    client : ChromaClient[MemoryTransport],
    collection_id : String,
    ) -> Unit raise ChromaError {
    let total = client.count(collection_id)
    let sample = client.peek(collection_id, limit=5)
    println(
    total.to_string() + " records, showing " + sample.ids.length().to_string(),
    )
    }

    peek is a get with one page and no filter: it posts to POST /get with limit and include and nothing else, so it cannot take a where clause — use get_async(collection_id, filter=...) for that. limit defaults to 10 and is clamped to PEEK_LIMIT (1..=100) so a preview can never turn into a full scan. include_fields defaults to Include::get_default() — documents and metadata, the same default as get, because peek posts to POST /get and Chroma rejects that endpoint when it carries distances. Pass include_fields=Include::all() when you want the vectors too.

    #Renaming and re-tagging a collection

    modify_collection updates the name, the metadata, or both:

    ///|
    fn rename_collection(
    client : ChromaClient[MemoryTransport],
    ) -> Unit raise ChromaError {
    client.modify_collection(
    "notes",
    new_name=Some("notes-v2"),
    metadata=Some([("topic", MString("docs"))]),
    )
    }

    The signature is modify_collection(name, new_name~ : String? = None, metadata~: Metadata? = None). A field left as None is left alone — Some([]) is how metadata is cleared — and name may be the collection's name or its id; a name is looked up first, because the server's PUT /collections/{id} accepts only an id (400 Collection ID is not a valid UUIDv4 otherwise). The async twins are modify_collection_async and get_collection_async.

    #Pagination

    Filter carries limit and offset, and they are independent — the same way chroma-go's WithLimit(100), WithOffset(200) pages a result set. Only the signs are checked locally, so a mistake surfaces as InvalidArgument instead of a round-trip and an opaque 400:

    ///|
    async fn page(
    client : ChromaClient[AsyncHttpTransport],
    collection_id : String,
    ) -> Unit raise ChromaError {
    ignore(
    client.get_async(
    collection_id,
    filter=Filter::new(limit=Some(100), offset=Some(200)),
    include_fields=Include::get_default(),
    ),
    )
    }

    get takes include_fields; it defaults to documents and metadata (Include::get_default()), while Include::default() — documents, metadata and distances — is the query default, matching chroma-go. A get never sends distances unless you pass Include::default() or Include::all() yourself, and Chroma will then answer with an error.

    query_embeddings accepts n_results and rejects values <= 0 the same way. A query carries where, where_document and ids (Filter::new(ids=[...]) restricts the search to known records, like chroma-go's WithIDs); limit and offset have no meaning on a query and are not forwarded.

    #Records and vectors

    Chroma stores a vector for every record; the document, when present, is the text that vector stands for. The client models that directly:

    ///|
    // Vector first, text attached afterwards.
    let with_text = Record::of_embedding("r1", [1.0, 0.0]).with_document("hello")

    ///|
    // Text first, vector attached afterwards.
    let with_vector = Record::of_document("r2", "world").with_embedding([0.0, 1.0])

    ///|
    // Metadata, either way.
    let with_metadata = Record::of_embedding("r3", [1.0, 1.0]).with_metadata([
    ("source", MString("handbook")),
    ])

    add and upsert require the vector; update does not, because an update patches a record by id and a missing field means "leave this one alone". A batch that add would have to reject is refused before a request is built, so the failure names the record instead of arriving as a server-side 422.

    #Errors

    Every fallible call raises ChromaError:

    • Transport(String) — the request could not be sent;
    • Api(ApiError) — the server answered non-2xx with Chroma's error body;
    • Status(Int, String) — the server answered non-2xx with a body that is not a Chroma error envelope (a proxy page, say);
    • Decode(String) — the response body was not the expected JSON;
    • InvalidArgument(String) — the arguments would be rejected by Chroma, so nothing was sent.

    ApiError splits the response so no caller has to parse it again: status, error_code (Chroma's short label), message (the sentence), details (any extra structure) and the raw body. A couple of helpers cover the usual branches:

    ///|
    fn report(result : Result[QueryResult, ChromaError]) -> Unit {
    match result {
    Err(Transport(message)) => println("could not send: " + message)
    Err(api_error) =>
    if api_error.status_code() == Some(404) {
    println("nothing there")
    } else if api_error.is_retryable() {
    println("429 or 5xx, worth trying again: " + api_error.message())
    } else {
    println(api_error.message())
    }
    Ok(value) => println(value.ids.length().to_string() + " rows")
    }
    }

    Note the split: status_code, is_retryable and message live on ChromaError. ApiError is the parsed body (status, error_code, message, details, body) and has none of those helpers, so match on the error variant before reaching for them.

    #Retrying

    Retrying is opt-in: a plain client sends one request and reports what came back. Wrapping the transport with a RetryPolicy turns on retries for 429 and 5xx only — a 4xx is a deterministic answer, so repeating it just burns a round-trip.

    ///|
    let retrying_client = ChromaClient::new(
    RetryTransport::new(
    AsyncHttpTransport::new(),
    policy=RetryPolicy::new(
    enabled=true,
    max_attempts=3,
    initial_delay_ms=200,
    max_delay_ms=5000,
    backoff_factor=2.0,
    ),
    ),
    )

    RetryTransport wraps the synchronous Transport trait, so it composes with MemoryTransport or your own stack — put it around whatever transport you write. It never sleeps itself. Each retry is announced through on_retry(attempt, delay_ms, error), and the delay is stamped on the request as x-chroma-retry-delay-ms; the bundled AsyncHttpTransport is what actually waits, before it sends. A transport that ignores the header just re-sends back to back:

    ///|
    fn retry_with_logging(
    transport : MemoryTransport,
    ) -> RetryTransport[MemoryTransport] {
    RetryTransport::new(transport, policy=RetryPolicy::new(enabled=true)).on_retry((
    attempt,
    delay_ms,
    error,
    ) => {
    println(
    "retry " +
    attempt.to_string() +
    " in " +
    delay_ms.to_string() +
    "ms: " +
    error.to_string(),
    )
    },
    )
    }

    For example, in a js transport the callback is where setTimeout belongs.

    #Response size

    A response body is read into one String, so the client caps it at MAX_RESPONSE_BYTES (32 MiB) and raises Decode past it. Truncating would turn a memory problem into silent data loss, so the client fails loudly instead; the fix on the caller's side is to narrow include, lower n_results, or page with limit. Error bodies are read before the cap is applied, so a long error message still reaches the caller.

    #Local validation

    Arguments Chroma would reject outright are caught before a request is built, so a bug in your code surfaces as a InvalidArgument with a sentence rather than a 400 and a round-trip:

    • an empty records batch on add / upsert / update;
    • a record with an empty id, or with neither a document nor an embedding (and, on add / upsert, a document without a vector);
    • an empty embeddings list on query_embeddings;
    • n_results <= 0, a negative limit or offset, and a delete whose filter names nothing, or whose limit comes without a where clause;
    • a write batch that names the same id twice (both positions are reported — upsert is exempt, because writing an id twice is defined there), or whose vectors do not all share one width.

    The checks are exposed as validate_records, validate_batch, validate_query_inputs, validate_n_results, validate_limit_offset (and its validate_paging spelling) if you build requests yourself.

    Batches are also bounded by the server: WriteOptions::from_preflight turns the max_batch_size a server advertises into a chunk size, and add / upsert then send that many records per request, reporting progress through on_chunk. A chunk that fails after an earlier one succeeded raises ChromaError::PartialFailure: partial.sent is how many records are stored and partial.cause is the error that stopped the write, so the tail records[partial.sent:] is what a retry re-sends. See docs/protocol-constraints.md for the whole story.

    #Decoding rules

    The client distinguishes fields Chroma always returns from fields that depend on the request:

    • required and missing ⇒ Decode. Collection.id / Collection.name and the ids of a get result are never silently defaulted, because an empty list and a broken response are different answers;
    • optional and missing ⇒ empty default. metadata, configuration, log_position, version, and everything include controls (documents, embeddings, metadatas, distances) may legitimately be absent.

    #Examples

    cmd/main is a single runnable example: it talks to a real Chroma server through the library's AsyncHttpTransport — see Native transport above.

    moon run cmd/main --target native -- http://localhost:8000 my-collection

    #Testing

    Two layers, both run by CI. docs/CONTRIBUTING.md has the full version, including when a change needs a live test. The gates a change has to clear are listed there too — in short, everything green via sh scripts/ci.sh, including a core-path coverage floor of 80%.

    The unit suites need nothing but moon: the synchronous half is driven through MemoryTransport, and the _async half through the package-private ScriptedAsyncTransport (see scripted_transport_wbtest.mbt), because an async method cannot be driven from the synchronous double.

    sh scripts/ci.sh

    It runs, in order: formatting, moon check, generated-interface freshness, tests with coverage, coverage reports, a core-path coverage gate (>= 80%), and debug + release builds. A single step can be run on its own:

    sh scripts/ci.sh fmt check # just formatting and type-checking sh scripts/ci.sh test coverage # just tests and coverage

    The live suite (chroma_wbtest.mbt, the white-box tests) runs against a real Chroma server and is what caught the protocol bugs the unit tests could not: scripted responses only prove the client agrees with itself. It shares the root package with the client, and the transport it drives (AsyncHttpTransport, backed by moonbitlang/async/http) is exported by the library, so the code under test is exactly what an application runs. Those tests skip when CHROMA_ENDPOINT is unset, so scripts/ci.sh stays runnable without Docker. To run them, start a server and point the suite at it:

    sh scripts/integration.sh # starts chromadb/chroma, runs, tears down

    or, against a server you already have:

    CHROMA_ENDPOINT=http://localhost:8000 \ moon test --target native -p SundayT1998/chroma-client

    Both CI platforms run them the same way: through scripts/integration.sh, which starts the container itself, waits for /api/v2/heartbeat, exports CHROMA_ENDPOINT, runs the suite and removes the container again. CNB reaches integration.sh from the push-integration / pull-request-integration jobs in .cnb.yml — the services: [docker] there is only a Docker daemon (dind) for the script to talk to, not a Chroma container — and GitHub Actions reaches it from the integration job in .github/workflows/ci.yml, on ubuntu-latest only, because macOS and Windows runners cannot start a Linux container. Neither path declares Chroma as a services: sidecar: that setup was dropped because GitHub reports "Service container chroma failed" without the server's own logs, and a moving latest tag can invalidate the image's health check.

    Set CHROMA_TEST_RUN_ID when several jobs share one server: each run namespaces its collection names with it, so parallel runs cannot collide.

    #Alignment with chroma-go

    The shapes here follow the official Go client, chroma-go pkg/api/v2, so a reader who knows one can find their way in the other. The mapping, method by method:

    chroma-gohere
    Client.Heartbeatheartbeat / heartbeat_async
    Client.GetVersionversion / version_async
    Client.PreFlightpreflight / preflight_async → PreflightChecks
    Client.GetIdentityidentity / identity_async → Identity
    Client.CreateCollectioncreate_collection (get_or_create: false)
    Client.GetOrCreateCollectionget_or_create_collection (get_or_create: true)
    Client.GetCollectionget_collection (None on 404)
    Client.DeleteCollectiondelete_collection
    Client.ListCollections + ListWithLimit/Offsetlist_collections(limit~, offset~)
    Client.CountCollectionscount_collections
    Collection.Add + WithIDs/WithTexts/WithEmbeddings/WithMetadatasadd(collection_id, [Record])
    Collection.Upsertupsert
    Collection.Updateupdate (fields left unset are left alone)
    Collection.Delete + WithIDs/WithWhere/WithWhereDocumentdelete(filter); a filter is required
    Collection.Countcount
    Collection.Get + WithInclude/WithLimit/WithOffsetget(filter, include_fields~)
    Collection.Query + WithQueryEmbeddings/WithNResults/WithWhere/WithIDsquery_embeddings(.., n_results~, include_fields~, filter~)
    Collection.ModifyName / ModifyMetadatamodify_collection(new_name~, metadata~)

    Behaviour that was aligned deliberately:

    • delete needs a filter. chroma-go's Collection.Delete refuses a call with no ids / where / where_document; this client raises InvalidArgument instead of emptying the collection.
    • limit and offset are independent. The offset < limit rule this client used to enforce is gone: chroma-go pages with WithLimit(100), WithOffset(200).
    • include is per call. get, get_async and peek send Include::get_default() (documents and metadata) unless told otherwise, and a query sends Include::default() (documents, metadata, distances) — the defaults chroma-go documents. POST /get cannot carry distances (distances are not supported in get), so the query default would be a bug there.
    • A query takes ids. Filter::new(ids=[...]) restricts a query to known records, like WithIDs; limit / offset are still dropped there, because a query has no paging in Chroma.
    • preflight and identity exist. chroma-go calls PreFlight before a batch and exposes GetIdentity; both are now part of this client, so the limits a server advertises are readable from MoonBit.

    What is still not covered, and why, is listed in docs/chroma-api-coverage.md: tenant and database administration, fork, search (the newer ranking API) and server-side embedding.

    #Status

    Early but functional, and verified against a live server rather than only against scripted responses. The v2 collection, record and query endpoints are implemented, including count, peek, preflight and identity; token and basic authentication have shorthands on Config, and tenant/database selection is part of the config. Server errors are parsed into ApiError, transient failures can be retried explicitly, and request arguments are validated locally before they are sent.

    Both a synchronous and an asynchronous API exist, and the wire format matches Chroma 1.x: columnar record bodies, get_or_create, bare-integer counts, and no text-to-vector conversion on the server. The supported API surface is the one chroma-go's Collection exposes, mapped in Alignment with chroma-go.

    #License

    Apache-2.0.

    AsyncChunkSender

    type AsyncChunkSender = async (Array[Record]) -> Unit raise ChromaError

    The async flavour of ChunkSender. Each chunk is awaited before the next is built, so a failure stops the sequence.

    ChunkProgress

    type ChunkProgress = (Int, Int) -> Unit

    Reports progress of a chunked write, once per request that succeeded.

    A caller that writes 5,000 records in chunks of 500 has no way to tell how far a failure got unless the client says so: on_chunk is called after each slice is accepted by the server, with the number of records written so far and the batch's total. When a later chunk fails, the count is what has already landed and the ids of the failed chunk are the ones to retry.

    ChunkSender

    type ChunkSender = (Array[Record]) -> Unit raise ChromaError

    Send one chunk of a write batch: the seam write_records hands each slice to, so the sync and async paths share the chunking and differ only here.

    Embedding

    type Embedding = Array[Double]

    A vector embedding as returned or requested by Chroma.

    Metadata

    type Metadata = Array[(String, MetadataValue)]

    Ordered list of (key, value) metadata pairs.

    AsyncTransport

    pub(open) trait AsyncTransport {
    async fn send_async(Self, Request) -> Response
    }

    The asynchronous counterpart of Transport.

    Transport is synchronous, which is enough for MemoryTransport but not for the real HTTP stack: moonbitlang/async/http only exposes async functions, and an async call cannot be made from a synchronous one. A native or wasm build therefore implements this trait instead, and the client methods gain an async suffix.

    The two traits carry the same request and response types, so switching between a scripted test and a live server changes only which send is implemented.

    Transport

    pub(open) trait Transport {
    fn send(Self, Request) -> Response raise
    }

    Minimal transport abstraction.

    chroma-client is deliberately transport-agnostic: the library builds requests and decodes responses while the host application supplies the actual I/O. MemoryTransport covers tests.

    ChromaError

    pub(all) suberror ChromaError {
    Transport(String)
    Status(Int, String)
    Api(ApiError)
    Decode(String)
    InvalidArgument(String)
    PartialFailure(PartialFailure)
    }

    Errors surfaced by the client.

    ChromaError::equal

    fn ChromaError::equal(self : ChromaError, other : ChromaError) -> Bool

    ChromaError::is_retryable

    fn ChromaError::is_retryable(self : ChromaError) -> Bool

    Whether the error is worth retrying: 429 and 5xx, i.e. the server was reachable but too busy or broken. Client errors (4xx except 429) are deterministic and repeating them only wastes a round-trip.

    The verdict comes from status_code() for every variant that carries a numeric status, so a Status (an unparsed body, a proxy page) keeps the retry semantics its code implies instead of degrading to "not retryable" just because the body was not Chroma's. Only Transport, Decode and InvalidArgument have no status and answer false.

    ChromaError::message

    fn ChromaError::message(self : ChromaError) -> String

    The message a caller can show a user, without the transport wrapper.

    ChromaError::not_equal

    fn ChromaError::not_equal(x : ChromaError, y : ChromaError) -> Bool

    ChromaError::output

    fn ChromaError::output(self : ChromaError, logger : &Logger) -> Unit

    ChromaError::status_code

    fn ChromaError::status_code(self : ChromaError) -> Int?

    Extracts the numeric status of any server-side response error.

    None means the error never reached the server, so retrying it is the caller's decision rather than a given.

    ChromaError::to_string

    fn ChromaError::to_string(self : ChromaError) -> String

    ApiError

    pub(all) struct ApiError {
    status : Int
    error_code : String
    message : String
    body : String
    details : Json
    } derive(Eq,
    Debug
    )

    A non-2xx response, split into the fields Chroma documents.

    Callers get the message without writing a parser or a regular expression: error is the short machine-readable label, message the human sentence, and details whatever extra structure the server attached.

    ApiError::equal

    fn ApiError::equal(ApiError, ApiError) -> Bool

    ApiError::from_json

    fn ApiError::from_json(status : Int, body : String) -> ApiError

    Decodes a non-2xx body into an ApiError.

    Chroma answers with {"error": "...", "message": "..."} on most paths. Both fields are optional in practice: a proxy or an older server may send only one, or neither, so nothing here is treated as a hard decode failure and the raw body is always preserved.

    ApiError::not_equal

    fn ApiError::not_equal(x : ApiError, y : ApiError) -> Bool

    ApiError::output

    fn ApiError::output(self : ApiError, logger : &Logger) -> Unit

    ApiError::to_repr

    ApiError::to_string

    fn ApiError::to_string(self : ApiError) -> String

    AsyncHttpTransport

    pub struct AsyncHttpTransport {
    }

    A ChromaClient transport backed by moonbitlang/async/http.

    This is the piece that turns the client's request objects into real sockets. It implements AsyncTransport, so it plugs into the *_async methods, and it is deliberately thin: it maps the client's method/header vocabulary onto @ah, sends one request, and reads the whole body back.

    It lives in the library rather than in a test or example package because a Chroma client that cannot reach a server is not useful: preferred_target= "wasm" describes where the client runs, and this transport is how it talks to Chroma from native and wasm alike.

    One connection per request is enough for a demo and for a test suite. A long-lived application that cares about latency should keep an @ah.Client open and reuse it, because @ah.Client keeps the connection alive across requests; see the "Native transport" section of the README.

    The transport holds no state: the client already puts the full URL and the deadline in every request, so there is nothing to join and nothing to configure here — see AsyncHttpTransport::from_config if you would rather build one from a Config.

    Timeouts and retry waits

    A Chroma server does not implement x-chroma-timeout-ms or x-chroma-retry-delay-ms: they are a contract between this client and a transport, and the one that ships here honours them itself, because it is the only place that owns a socket and can cancel or sleep.

    • x-chroma-timeout-ms: n wraps the whole exchange (connect, send, read) in @async.with_timeout(n). Exceeding it raises ChromaError::Transport, which is what a caller can catch.
    • x-chroma-retry-delay-ms: n makes the transport sleep n milliseconds before sending, which is what RetryTransport asks of it. A caller that does not want to wait should not enable retries: the sleep happens here rather than inside RetryTransport precisely so that a wrapped transport without a runtime (a scripted double, say) never blocks.

    Both headers are consumed locally and stripped before the request goes on the wire, so a Chroma server never sees client-internal state as a header it might reject.

    Redirects

    @ah.request does not follow 3xx, and ChromaClient maps every non-2xx status to Api / Status — so a redirect (an http→https proxy in front of Chroma, say) surfaces as an error rather than as a silently re-sent request. Point Config::endpoint at the final URL instead.

    AsyncHttpTransport::from_config

    Builds a transport from a Config, applying config.timeout_ms as a deadline on the socket.

    This is the wiring Config::timeout_ms was meant to have: the header TimeoutTransport stamps is honoured by AsyncHttpTransport above, and a client built with this transport also stamps the header itself (see ChromaClient::send_async), so both routes end up at the same value.

    A non-positive timeout_ms means "no deadline".

    AsyncHttpTransport::new

    Builds a transport.

    AsyncHttpTransport::send_async

    async fn AsyncHttpTransport::send_async(self : AsyncHttpTransport, request : Request) -> Response

    ChromaClient

    pub(all) struct ChromaClient[T] {
    transport : T
    config : Config
    }

    A Chroma client bound to one transport and one server configuration.

    The client is generic over its transport so applications can plug in any HTTP implementation, and tests can use MemoryTransport. It is otherwise stateless: every call is a plain HTTP request.

    ChromaClient::add

    fn[T : Transport] ChromaClient::add(self : ChromaClient[T], collection_id : String, records : Array[Record], options? : WriteOptions, on_chunk? : (Int, Int) -> Unit) -> Unit raise ChromaError

    Adds records to a collection.

    Every record must carry either a document or an embedding, otherwise Chroma rejects the request. options.max_batch_size splits the batch into several requests; a batch that fails validate_batch is not sent at all. on_chunk reports how many records have landed after each request, so a failure in chunk n tells the caller exactly which prefix of the batch is already stored.

    ChromaClient::add_async

    async fn[T : AsyncTransport] ChromaClient::add_async(self : ChromaClient[T], collection_id : String, records : Array[Record], options? : WriteOptions, on_chunk? : (Int, Int) -> Unit) -> Unit raise ChromaError

    Adds records to a collection.

    ChromaClient::config

    fn[T] ChromaClient::config(self : ChromaClient[T]) -> Config

    Returns the configuration this client was built with.

    ChromaClient::count

    fn[T : Transport] ChromaClient::count(self : ChromaClient[T], collection_id : String) -> Int raise ChromaError

    Counts the records in a collection.

    ChromaClient::count_async

    async fn[T : AsyncTransport] ChromaClient::count_async(self : ChromaClient[T], collection_id : String) -> Int raise ChromaError

    Counts the records in a collection.

    ChromaClient::count_collections

    fn[T : Transport] ChromaClient::count_collections(self : ChromaClient[T]) -> Int raise ChromaError

    Counts the collections in the configured tenant and database.

    ChromaClient::count_collections_async

    async fn[T : AsyncTransport] ChromaClient::count_collections_async(self : ChromaClient[T]) -> Int raise ChromaError

    Counts the collections in the configured tenant and database.

    ChromaClient::create_collection

    fn[T : Transport] ChromaClient::create_collection(self : ChromaClient[T], name : String, config? : CollectionConfig) -> Collection raise ChromaError

    Creates a collection, failing when name is already taken.

    Sends get_or_create: false, so the server answers 409 rather than silently returning a collection that may have a different configuration. Use get_or_create_collection when either outcome is fine.

    ChromaClient::create_collection_async

    async fn[T : AsyncTransport] ChromaClient::create_collection_async(self : ChromaClient[T], name : String, config? : CollectionConfig) -> Collection raise ChromaError

    Creates a collection, failing when name is already taken.

    ChromaClient::create_database

    fn[T : Transport] ChromaClient::create_database(self : ChromaClient[T]) -> Unit raise ChromaError

    Creates the database this client is configured for.

    Chroma keeps tenants and databases as ordinary resources: a tenant is created on demand but a database is not, so writing into a database that does not exist answers 400. This is the one call a caller needs to prepare a namespace; a 409 means it already exists.

    The async twin is create_database_async.

    ChromaClient::create_database_async

    async fn[T : AsyncTransport] ChromaClient::create_database_async(self : ChromaClient[T]) -> Unit raise ChromaError

    The async counterpart of create_database.

    ChromaClient::delete

    fn[T : Transport] ChromaClient::delete(self : ChromaClient[T], collection_id : String, filter? : Filter) -> Unit raise ChromaError

    Deletes the records a filter names.

    Like chroma-go's Collection.Delete, at least one of ids, where or where_document is required: an empty filter is refused locally instead of silently emptying the collection. limit is forwarded, and only together with a where / where_document clause, which is the one case Chroma accepts it in.

    ChromaClient::delete_async

    async fn[T : AsyncTransport] ChromaClient::delete_async(self : ChromaClient[T], collection_id : String, filter? : Filter) -> Unit raise ChromaError

    Deletes the records a filter names. Same rules as delete: at least one of ids, where or where_document is required.

    ChromaClient::delete_collection

    fn[T : Transport] ChromaClient::delete_collection(self : ChromaClient[T], name : String) -> Unit raise ChromaError

    Deletes a collection by name.

    ChromaClient::delete_collection_async

    async fn[T : AsyncTransport] ChromaClient::delete_collection_async(self : ChromaClient[T], name : String) -> Unit raise ChromaError

    Deletes a collection by name.

    ChromaClient::get

    fn[T : Transport] ChromaClient::get(self : ChromaClient[T], collection_id : String, filter? : Filter, include_fields? : Include) -> QueryResult raise ChromaError

    Fetches records by filter, or the whole collection when the filter is empty.

    Mirrors chroma-go's Collection.Get: include_fields decides which columns come back and defaults to Include::get_default() (documents and metadata). It is deliberately not Include::default(), the query default: that one asks for distances, and Chroma rejects POST /get carrying them (distances are not supported in get). limit / offset page a result set. The two are independent — chroma-go's own example pages with WithLimit(100), WithOffset(200), an offset past the page size — so neither is clamped against the other.

    ChromaClient::get_async

    async fn[T : AsyncTransport] ChromaClient::get_async(self : ChromaClient[T], collection_id : String, filter? : Filter, include_fields? : Include) -> QueryResult raise ChromaError

    Fetches records by filter, or the whole collection when the filter is empty.

    The async counterpart of get: include_fields defaults to Include::get_default() (documents and metadata), not to the query default, because Chroma rejects a POST /get that carries distances.

    ChromaClient::get_collection

    fn[T : Transport] ChromaClient::get_collection(self : ChromaClient[T], name : String) -> Collection? raise ChromaError

    Fetches an existing collection by name, returning None on 404.

    ChromaClient::get_collection_async

    async fn[T : AsyncTransport] ChromaClient::get_collection_async(self : ChromaClient[T], name : String) -> Collection? raise ChromaError

    Fetches an existing collection by name, returning None on 404.

    ChromaClient::get_or_create_collection

    fn[T : Transport] ChromaClient::get_or_create_collection(self : ChromaClient[T], name : String, config? : CollectionConfig) -> Collection raise ChromaError

    Returns a collection handle for name, creating it when it is missing.

    Sends get_or_create: true, so calling it twice finds the collection instead of colliding on the second call.

    ChromaClient::get_or_create_collection_async

    async fn[T : AsyncTransport] ChromaClient::get_or_create_collection_async(self : ChromaClient[T], name : String, config? : CollectionConfig) -> Collection raise ChromaError

    Returns a collection handle for name, creating it when it is missing.

    ChromaClient::heartbeat

    fn[T : Transport] ChromaClient::heartbeat(self : ChromaClient[T]) -> Int64 raise ChromaError

    Checks that the server is reachable and returns its heartbeat timestamp.

    Chroma answers {"nanosecond heartbeat": <ns since the epoch>}. The nanoseconds do not fit an Int32, so the value is returned as an Int64; a server that answers with a bare number (or with an unexpected shape) is accepted too, since the point of the call is reachability rather than the timestamp itself.

    ChromaClient::heartbeat_async

    async fn[T : AsyncTransport] ChromaClient::heartbeat_async(self : ChromaClient[T]) -> Int64 raise ChromaError

    Checks that the server is reachable and returns its heartbeat timestamp.

    ChromaClient::identity

    fn[T : Transport] ChromaClient::identity(self : ChromaClient[T]) -> Identity raise ChromaError

    Returns the identity the server reports.

    GET /auth/identity only answers with a user when the server runs with authentication; otherwise it returns an empty object, which decodes to an Identity with blank fields.

    ChromaClient::identity_async

    async fn[T : AsyncTransport] ChromaClient::identity_async(self : ChromaClient[T]) -> Identity raise ChromaError

    The asynchronous counterpart of identity.

    ChromaClient::list_collections

    fn[T : Transport] ChromaClient::list_collections(self : ChromaClient[T], limit? : Int, offset? : Int) -> Array[Collection] raise ChromaError

    Lists the collections in the configured tenant and database.

    limit and offset page the listing, the same way chroma-go's ListWithLimit / ListWithOffset do. Both default to "not sent", which returns everything.

    ChromaClient::list_collections_async

    async fn[T : AsyncTransport] ChromaClient::list_collections_async(self : ChromaClient[T], limit? : Int, offset? : Int) -> Array[Collection] raise ChromaError

    Lists the collections in the configured tenant and database, with the same optional paging as list_collections.

    ChromaClient::modify_collection

    fn[T : Transport] ChromaClient::modify_collection(self : ChromaClient[T], name : String, new_name? : String?, metadata? : Array[(String, MetadataValue)]?) -> Unit raise ChromaError

    Updates a collection's name and/or metadata.

    new_name of None keeps the current name, and metadata of None keeps the current metadata; Some([]) is how a caller clears it. name may be the collection's name or its id; a name is looked up first, because the server's PUT endpoint only accepts an id (see resolve_collection_id).

    ChromaClient::modify_collection_async

    async fn[T : AsyncTransport] ChromaClient::modify_collection_async(self : ChromaClient[T], name : String, new_name? : String?, metadata? : Array[(String, MetadataValue)]?) -> Unit raise ChromaError

    Updates a collection's name and/or metadata.

    The async counterpart of modify_collection: new_name of None keeps the current name, metadata of None keeps the current metadata, and name may be the collection's id or its name, which is looked up first because the server's PUT endpoint only accepts an id.

    ChromaClient::new

    fn[T] ChromaClient::new(transport : T, config? : Config) -> ChromaClient[T]

    Binds a transport and configuration into a client.

    ChromaClient::peek

    fn[T : Transport] ChromaClient::peek(self : ChromaClient[T], collection_id : String, limit? : Int, include_fields? : Include) -> QueryResult raise ChromaError

    Returns a small sample of the records in a collection.

    limit defaults to 10 and is clamped to PEEK_LIMIT (1..=100) on the way out, so a stray large value cannot turn a preview into a full scan of the collection. include_fields defaults to Include::get_default() (documents and metadata), the same default as get: peek posts to POST /get, and Chroma rejects that endpoint when it carries distances.

    It is a get with one page and no filter: POST /get gets a limit and an include, nothing else, so there is no where clause to pass. Use get(collection_id, filter~) when the sample has to be filtered.

    ChromaClient::peek_async

    async fn[T : AsyncTransport] ChromaClient::peek_async(self : ChromaClient[T], collection_id : String, limit? : Int, include_fields? : Include) -> QueryResult raise ChromaError

    Returns a small sample of the records in a collection.

    The async counterpart of peek: same body, same clamp, same defaults — limit is clamped to PEEK_LIMIT (1..=100) and include_fields defaults to Include::get_default() (documents and metadata). See peek for why there is no where clause.

    ChromaClient::preflight

    fn[T : Transport] ChromaClient::preflight(self : ChromaClient[T]) -> PreflightChecks raise ChromaError

    Asks the server what limits it applies.

    GET /pre-flight-checks is served from the API root (it is not scoped to a tenant or a database), which is why it does not go through database_prefix. The answer is advisory: a server that does not implement the endpoint answers 404, and a caller that does not care can ignore it.

    ChromaClient::preflight_async

    async fn[T : AsyncTransport] ChromaClient::preflight_async(self : ChromaClient[T]) -> PreflightChecks raise ChromaError

    The asynchronous counterpart of preflight.

    ChromaClient::query_embeddings

    fn[T : Transport] ChromaClient::query_embeddings(self : ChromaClient[T], collection_id : String, embeddings : Array[Array[Double]], n_results? : Int, include_fields? : Include, filter? : Filter) -> QueryResult raise ChromaError

    Searches a collection with caller-supplied query vectors.

    Mirrors chroma-go's Collection.Query: the where and where_document clauses of filter narrow what is searched, ids restricts the search to a known set of records, and include_fields decides which columns come back. filter.limit / filter.offset are not query parameters in Chroma and are therefore not forwarded — use n_results for the result count and get for paging.

    ChromaClient::query_embeddings_async

    async fn[T : AsyncTransport] ChromaClient::query_embeddings_async(self : ChromaClient[T], collection_id : String, embeddings : Array[Array[Double]], n_results? : Int, include_fields? : Include, filter? : Filter) -> QueryResult raise ChromaError

    Searches a collection with caller-supplied query vectors.

    ChromaClient::update

    fn[T : Transport] ChromaClient::update(self : ChromaClient[T], collection_id : String, records : Array[Record], filter? : Filter) -> Unit raise ChromaError

    Updates the document, metadata or embedding of existing records.

    Records are matched by id; ids that do not exist are ignored by Chroma.

    ChromaClient::update_async

    async fn[T : AsyncTransport] ChromaClient::update_async(self : ChromaClient[T], collection_id : String, records : Array[Record], filter? : Filter) -> Unit raise ChromaError

    Updates the document, metadata or embedding of existing records.

    Records are matched by id; ids that do not exist are ignored by Chroma.

    ChromaClient::upsert

    fn[T : Transport] ChromaClient::upsert(self : ChromaClient[T], collection_id : String, records : Array[Record], options? : WriteOptions, on_chunk? : (Int, Int) -> Unit) -> Unit raise ChromaError

    Inserts or replaces records by id. An upsert may name the same id twice: the second write is defined by the id already existing, so only the vector-width rule of validate_batch applies.

    ChromaClient::upsert_async

    async fn[T : AsyncTransport] ChromaClient::upsert_async(self : ChromaClient[T], collection_id : String, records : Array[Record], options? : WriteOptions, on_chunk? : (Int, Int) -> Unit) -> Unit raise ChromaError

    Inserts or replaces records by id.

    ChromaClient::version

    fn[T : Transport] ChromaClient::version(self : ChromaClient[T]) -> String raise ChromaError

    Returns the server version string.

    ChromaClient::version_async

    async fn[T : AsyncTransport] ChromaClient::version_async(self : ChromaClient[T]) -> String raise ChromaError

    Returns the server version string.

    Collection

    pub(all) struct Collection {
    id : String
    name : String
    metadata : Array[(String, MetadataValue)]
    configuration : Json
    log_position : Int
    version : Int
    } derive(Eq,
    Debug
    )

    A Chroma collection as returned by the server.

    Collection::equal

    fn Collection::equal(Collection, Collection) -> Bool

    Collection::from_json

    fn Collection::from_json(json : Json) -> Collection raise ChromaError

    Decodes a collection from a server response object.

    id and name are required: a response without them is not a collection, and silently defaulting a name to "" would only push the failure further down the line. The remaining fields are optional and default to empty so that responses from older servers still decode.

    Collection::not_equal

    fn Collection::not_equal(x : Collection, y : Collection) -> Bool

    CollectionConfig

    pub(all) struct CollectionConfig {
    metadata : Array[(String, MetadataValue)]
    configuration : Json?
    } derive(Eq,
    Debug
    )

    Parameters for get_or_create_collection and create_collection.

    configuration is sent verbatim, so index options the server knows about (for example {"hnsw": {"space": "cosine"}}) can be set without waiting for this client to model them. When it is unset a default L2 HNSW configuration is sent.

    CollectionConfig::equal

    CollectionConfig::new

    fn CollectionConfig::new(metadata? : Array[(String, MetadataValue)], configuration? : Json?) -> CollectionConfig

    Builds a CollectionConfig with all options unset.

    CollectionConfig::not_equal

    fn CollectionConfig::not_equal(x : CollectionConfig, y : CollectionConfig) -> Bool

    CollectionConfig::to_json

    fn CollectionConfig::to_json(config : CollectionConfig) -> Json

    Serializes the collection configuration for a create request.

    This is the body create_collection sends, with get_or_create set to false.

    Config

    pub(all) struct Config {
    endpoint : String
    tenant : String
    database : String
    headers : Map[String, String]
    timeout_ms : Int
    } derive(Eq,
    Debug
    )

    Configuration for talking to a Chroma server.

    The client speaks the v2 API, so endpoint is normally http://localhost:8000 and the tenant/database are separate fields.

    Config::equal

    fn Config::equal(Config, Config) -> Bool

    Config::new

    fn Config::new(endpoint? : String, tenant? : String, database? : String, headers? : Map[String, String], timeout_ms? : Int) -> Config

    Builds a Config with the Chroma defaults: localhost:8000, tenant default_tenant, database default_database.

    Config::not_equal

    fn Config::not_equal(x : Config, y : Config) -> Bool

    Config::to_repr

    Config::with_basic_auth

    fn Config::with_basic_auth(self : Config, username : String, password : String) -> Config

    Returns a copy of the configuration with HTTP Basic authentication.

    Config::with_header

    fn Config::with_header(self : Config, name : String, value : String) -> Config

    Returns a copy of the configuration with one extra header set.

    Config::with_headers

    fn Config::with_headers(self : Config, headers : Map[String, String]) -> Config

    Returns a copy of the configuration with a merged header map, leaving self untouched.

    Config::with_timeout_ms

    fn Config::with_timeout_ms(self : Config, timeout_ms : Int) -> Config

    Returns a copy of the configuration with a different request deadline.

    timeout_ms <= 0 turns the deadline off. The client stamps the value on every request as x-chroma-timeout-ms, and it is the transport that turns that header into a real @async.with_timeout — the bundled AsyncHttpTransport does, because only a transport owns a socket. A transport that ignores the header still works; it just has no deadline.

    Config::with_token

    fn Config::with_token(self : Config, token : String) -> Config

    Returns a copy of the configuration with a Bearer token in Authorization.

    Filter

    pub(all) struct Filter {
    where_clause : Json?
    where_document_clause : Json?
    ids : Array[String]
    limit : Int?
    offset : Int?
    } derive(Eq,
    Debug
    )

    Where-clause filters for get, update and delete.

    These mirror Chroma's where / where_document JSON filters.

    Filter::equal

    fn Filter::equal(Filter, Filter) -> Bool

    Filter::new

    fn Filter::new(where_clause? : Json?, where_document_clause? : Json?, ids? : Array[String], limit? : Int?, offset? : Int?) -> Filter

    Builds an empty filter that matches everything.

    Filter::not_equal

    fn Filter::not_equal(x : Filter, y : Filter) -> Bool

    Filter::to_repr

    Filter::with_limit

    fn Filter::with_limit(self : Filter, limit : Int) -> Filter

    Adds a limit to the filter.

    Filter::with_metadata

    fn Filter::with_metadata(self : Filter, key : String, value : MetadataValue) -> Filter

    Adds a metadata equality condition to the filter.

    Repeated calls accumulate instead of replacing each other, the way chroma-go's WithWhere(EqString(k, v)) options append to the same where clause: with_metadata("a", ..).with_metadata("b", ..) matches records where both hold, not just the last one.

    The second call wraps both tests in $and, because Chroma reads a flat object's keys as alternatives and rejects more than one of them outright ("Invalid where clause") rather than intersecting them. Calling it twice with the same key works too: the two tests end up as separate conjuncts and nothing is overwritten.

    Identity

    pub(all) struct Identity {
    user_id : String
    tenant : String
    databases : Array[String]
    } derive(Eq,
    Debug
    )

    The identity the server is serving.

    chroma-go returns this from GetIdentity, and only a server with authentication enabled fills in the tenant and database lists.

    Identity::equal

    fn Identity::equal(Identity, Identity) -> Bool

    Identity::from_json

    fn Identity::from_json(json : Json) -> Identity

    Reads an identity response, tolerating the fields an older server omits.

    Identity::not_equal

    fn Identity::not_equal(x : Identity, y : Identity) -> Bool

    Identity::to_repr

    Include

    pub(all) struct Include {
    documents : Bool
    embeddings : Bool
    metadatas : Bool
    distances : Bool
    } derive(Eq,
    Debug
    )

    How a query should return embeddings, documents and metadata.

    Chroma only honours include on query/get endpoints, and uploading embeddings is usually what you want to skip on large collections.

    Include::all

    fn Include::all() -> Include

    Every field, including embeddings.

    Include::default

    fn Include::default() -> Include

    Default include set for a query: documents, metadata and distances, no vectors.

    This is chroma-go's documented default for Collection.Query. get takes Include::get_default() instead: POST /get is not a ranked search, so a distances column there is only something for the server to reject.

    Include::equal

    fn Include::equal(Include, Include) -> Bool

    Include::get_default

    fn Include::get_default() -> Include

    Default include set for a get: documents and metadata, no distances.

    A get has no distances to report, so asking for them only adds a column the server has nothing to fill — Chroma answers POST /get carrying distances with an error (distances are not supported in get), which is why get, get_async and peek all default to this set and not to Include::default().

    Include::ids_only

    fn Include::ids_only() -> Include

    Only ids.

    Include::not_equal

    fn Include::not_equal(x : Include, y : Include) -> Bool

    Include::to_repr

    MemoryTransport

    pub(all) struct MemoryTransport {
    responses : Array[Response]
    observed : Array[Request]
    cursor :
    Ref
    [Int]
    } derive(
    Debug
    )

    A scripted transport for tests: records every request and replays the queued responses in order.

    MemoryTransport::last_request

    fn MemoryTransport::last_request(self : MemoryTransport) -> Request?

    Returns the last observed request, if any.

    MemoryTransport::new

    fn MemoryTransport::new(responses? : Array[Response]) -> MemoryTransport

    Creates a MemoryTransport that returns the given responses in order.

    MemoryTransport::observed

    fn MemoryTransport::observed(self : MemoryTransport) -> Array[Request]

    Returns the requests this transport has observed, in send order.

    MemoryTransport::send

    fn MemoryTransport::send(self : MemoryTransport, request : Request) -> Response raise

    MetadataValue

    pub(all) enum MetadataValue {
    MString(String)
    MInt(Int)
    MFloat(Double)
    MBool(Bool)
    MNull
    } derive(Eq,
    Debug
    )

    Metadata attached to a collection or a record.

    Chroma only accepts scalar metadata values, which MetadataValue models directly.

    MetadataValue::equal

    MetadataValue::from_json

    fn MetadataValue::from_json(value : Json) -> MetadataValue?

    Converts a JSON scalar into a MetadataValue, returning None for arrays and objects since Chroma rejects them as metadata.

    MetadataValue::not_equal

    fn MetadataValue::not_equal(x : MetadataValue, y : MetadataValue) -> Bool

    MetadataValue::to_json

    fn MetadataValue::to_json(self : MetadataValue) -> Json

    Converts a MetadataValue into the JSON representation Chroma expects.

    Method

    pub(all) enum Method {
    Get
    Post
    Put
    Delete
    Patch
    } derive(Eq,
    Debug
    )

    HTTP verb used by the Chroma v2 API.

    Method::equal

    fn Method::equal(Method, Method) -> Bool

    Method::not_equal

    fn Method::not_equal(x : Method, y : Method) -> Bool

    Method::output

    fn Method::output(self : Method, logger : &Logger) -> Unit

    Method::to_repr

    Method::to_string

    fn Method::to_string(self : Method) -> String

    PartialFailure

    pub(all) struct PartialFailure {
    sent : Int
    cause : ChromaError
    }

    How far a write got before it was interrupted.

    A chunked write can stop in the middle: chunk 3 of 5 is refused and chunks 4 and 5 are never sent, so the caller is left with a partially written collection. This is the value that says so: it carries the error and the count of records the server accepted, which is what makes a retry possible — sent is a prefix of the batch, so replaying the batch from sent under upsert is safe.

    sent counts accepted chunks only: it is advanced after a chunk's response comes back, never when the chunk is handed to the transport, so a chunk that fails on the way out is not counted. Nothing is raised when the whole batch lands, or when it fails before any request is built (validation errors are the caller's arguments, not a partial write).

    PreflightChecks

    pub(all) struct PreflightChecks {
    max_batch_size : Int?
    raw : Json
    } derive(Eq,
    Debug
    )

    The limits a server advertises through GET /pre-flight-checks.

    chroma-go calls this PreFlight and uses it to learn the server's max_batch_size before sending a batch; the field is optional because older servers answer with an empty object.

    PreflightChecks::equal

    PreflightChecks::from_json

    fn PreflightChecks::from_json(json : Json) -> PreflightChecks

    Reads a preflight response.

    PreflightChecks::not_equal

    fn PreflightChecks::not_equal(x : PreflightChecks, y : PreflightChecks) -> Bool

    QueryResult

    pub(all) struct QueryResult {
    ids : Array[String]
    documents : Array[String?]
    embeddings : Array[Array[Double]?]
    metadatas : Array[Array[(String, MetadataValue)]]
    distances : Array[Double?]
    } derive(Eq,
    Debug
    )

    A row returned by get or query.

    QueryResult::equal

    fn QueryResult::equal(QueryResult, QueryResult) -> Bool

    QueryResult::from_json

    fn QueryResult::from_json(json : Json) -> QueryResult raise ChromaError

    Reads a QueryResult from a get response.

    ids is the one field every result must carry, so its absence is a decode error rather than an empty result: an empty ids is a valid answer, a missing one means the response is not a result set. The remaining fields depend on include, so they stay optional.

    QueryResult::from_query_json

    fn QueryResult::from_query_json(json : Json) -> QueryResult

    Reads a query response, where each field is an array with one entry per query embedding. Returns the result for the first embedding.

    Unlike from_query_json, the nested ids may legitimately be missing when the query matched nothing, so an absent field still means "no results" here.

    QueryResult::not_equal

    fn QueryResult::not_equal(x : QueryResult, y : QueryResult) -> Bool

    Record

    pub(all) struct Record {
    id : String
    document : String?
    embedding : Array[Double]?
    metadata : Array[(String, MetadataValue)]
    } derive(Eq,
    Debug
    )

    A record stored in a collection.

    embedding is None for a record that carries only a document. Chroma needs a vector for every record, so a batch with a missing embedding is rejected locally instead of by the server (see validate_records).

    Record::equal

    fn Record::equal(Record, Record) -> Bool

    Record::not_equal

    fn Record::not_equal(x : Record, y : Record) -> Bool

    Record::of_document

    fn Record::of_document(id : String, document : String, metadata? : Array[(String, MetadataValue)]) -> Record

    Builds a record that carries a document and no embedding.

    Chroma 1.x has no server-side embedding, so a record built this way can only be stored once an embedding is attached via Record::with_embedding.

    Record::of_embedding

    fn Record::of_embedding(id : String, embedding : Array[Double], metadata? : Array[(String, MetadataValue)]) -> Record

    Builds a record with a caller-supplied embedding.

    Record::to_repr

    Record::with_document

    fn Record::with_document(self : Record, document : String) -> Record

    Returns a copy of the record carrying document as well.

    Pairs with Record::of_embedding when the vector is known first and the text it stands for second.

    Record::with_embedding

    fn Record::with_embedding(self : Record, embedding : Array[Double]) -> Record

    Returns a copy of the record with embedding attached.

    Use it to add a vector to a record built by Record::of_document; a record with explicit coordinates is built with Record::of_embedding.

    Record::with_metadata

    fn Record::with_metadata(self : Record, metadata : Array[(String, MetadataValue)]) -> Record

    Returns a copy of the record carrying metadata instead of its own.

    Request

    pub(all) struct Request {
    verb : Method
    url : String
    headers : Map[String, String]
    body : String?
    } derive(Eq,
    Debug
    )

    A fully-built HTTP request, ready to hand to a transport.

    Request::equal

    fn Request::equal(Request, Request) -> Bool

    Request::not_equal

    fn Request::not_equal(x : Request, y : Request) -> Bool

    Request::to_repr

    Response

    pub(all) struct Response {
    status : Int
    headers : Map[String, String]
    body : String
    } derive(Eq,
    Debug
    )

    The response a transport produces.

    Response::equal

    fn Response::equal(Response, Response) -> Bool

    Response::json

    fn Response::json(status : Int, body : String, headers? : Map[String, String]) -> Response

    Convenience constructor for a scripted JSON response.

    Response::not_equal

    fn Response::not_equal(x : Response, y : Response) -> Bool

    Response::to_repr

    RetryPolicy

    pub(all) struct RetryPolicy {
    enabled : Bool
    max_attempts : Int
    initial_delay_ms : Int
    max_delay_ms : Int
    backoff_factor : Double
    } derive(Eq,
    Debug
    )

    Retry policy for RetryTransport.

    Retrying is never implicit: nothing is retried until a RetryTransport is built with enabled: true, so a plain client keeps the "one call, one request" behaviour.

    RetryPolicy::delay_ms

    fn RetryPolicy::delay_ms(self : RetryPolicy, attempt : Int) -> Int

    The delay to wait before attempt attempt (0-based) of the backoff sequence, capped at max_delay_ms.

    RetryPolicy::equal

    fn RetryPolicy::equal(RetryPolicy, RetryPolicy) -> Bool

    RetryPolicy::new

    fn RetryPolicy::new(enabled? : Bool, max_attempts? : Int, initial_delay_ms? : Int, max_delay_ms? : Int, backoff_factor? : Double) -> RetryPolicy

    Builds a retry policy. enabled defaults to false, so the caller has to opt in to the retry behaviour.

    RetryPolicy::not_equal

    fn RetryPolicy::not_equal(x : RetryPolicy, y : RetryPolicy) -> Bool

    RetryTransport

    pub(all) struct RetryTransport[T] {
    inner : T
    policy : RetryPolicy
    on_retry : (Int, Int, ChromaError) -> Unit?
    }

    A transport that re-sends requests the server answered with 429 or 5xx.

    It waits between attempts only on the caller's behalf: the library never sleeps, because blocking inside a transport is exactly the implicit behaviour the retry policy is meant to avoid. Instead each retry is announced as on_retry(attempt, delay_ms, error) and the delay is stamped on the request as x-chroma-retry-delay-ms, the same contract TimeoutTransport uses for deadlines. A transport that cannot wait and has no hook still sees the retried requests; it just sends them back to back.

    RetryTransport::new

    fn[T] RetryTransport::new(inner : T, policy? : RetryPolicy) -> RetryTransport[T]

    Wraps inner with policy.

    RetryTransport::on_retry

    fn[T] RetryTransport::on_retry(self : RetryTransport[T], handler : (Int, Int, ChromaError) -> Unit) -> RetryTransport[T]

    Returns a copy that reports every retry before it is attempted.

    The callback is where a real transport blocks, logs or reports metrics; a browser build could sleep with setTimeout there.

    RetryTransport::policy

    fn[T] RetryTransport::policy(self : RetryTransport[T]) -> RetryPolicy

    Errors raised by ChromaClient::send keep their original shape, so the retry loop only has to decide on HTTP statuses. A failure to send at all is not retried: under a transport that raises instead of returning a status there is no way to tell a dropped connection from a bad URL.

    RetryTransport::send

    fn[T : Transport] RetryTransport::send(self : RetryTransport[T], request : Request) -> Response raise

    RetryTransport::with_policy

    fn[T] RetryTransport::with_policy(self : RetryTransport[T], policy : RetryPolicy) -> RetryTransport[T]

    Returns a copy with a different policy.

    TimeoutAsyncTransport

    pub(all) struct TimeoutAsyncTransport[T] {
    inner : T
    timeout_ms : Int
    }

    The async counterpart of TimeoutTransport: stamps a deadline that an AsyncTransport honours.

    It exists because Config::timeout_ms has to reach the socket through the async half too. TimeoutTransport wraps the synchronous Transport trait, which none of the *_async methods use, so a client built as ChromaClient::new(TimeoutTransport::new(AsyncHttpTransport::new(), 5000)) would not even compile: the async methods need an AsyncTransport.

    AsyncHttpTransport turns the header into a real @async.with_timeout, so wrapping it is what makes the deadline effective; wrapping a double that ignores the header is harmless.

    TimeoutAsyncTransport::from_config

    fn[T] TimeoutAsyncTransport::from_config(inner : T, config : Config) -> TimeoutAsyncTransport[T]

    Builds a deadline wrapper from a client configuration.

    TimeoutAsyncTransport::new

    fn[T] TimeoutAsyncTransport::new(inner : T, timeout_ms : Int) -> TimeoutAsyncTransport[T]

    Wraps inner, asking for a timeout_ms deadline on every request. A non-positive value passes requests through untouched, so the wrapper can be driven straight from Config::timeout_ms.

    TimeoutAsyncTransport::send_async

    async fn[T : AsyncTransport] TimeoutAsyncTransport::send_async(self : TimeoutAsyncTransport[T], request : Request) -> Response

    TimeoutAsyncTransport::timeout_ms

    fn[T] TimeoutAsyncTransport::timeout_ms(self : TimeoutAsyncTransport[T]) -> Int

    Returns the deadline this transport asks for.

    TimeoutTransport

    pub(all) struct TimeoutTransport[T] {
    inner : T
    timeout_ms : Int
    }

    Wraps a transport so that every request carries a deadline.

    The library never performs I/O itself, so it cannot enforce a timeout on its own: cancelling a socket read is a property of the HTTP stack, not of the request. This wrapper makes the promise explicit instead. It stamps x-chroma-timeout-ms on each request and hands it to the inner transport, so a real transport can honour the value; a transport that ignores the header still works, it just loses the guarantee. The bundled async transport does honour it (see AsyncHttpTransport).

    let client = ChromaClient::new(TimeoutTransport::new(http_stack, 5000))

    TimeoutTransport::from_config

    fn[T] TimeoutTransport::from_config(inner : T, config : Config) -> TimeoutTransport[T]

    Builds a timeout transport from a client's own configuration, so Config::timeout_ms gets a consumer.

    TimeoutTransport::new

    fn[T] TimeoutTransport::new(inner : T, timeout_ms : Int) -> TimeoutTransport[T]

    Wraps inner, limiting each request to timeout_ms milliseconds.

    A non-positive value means "no deadline", in which case only the header stamping is skipped and requests pass through untouched.

    TimeoutTransport::send

    fn[T : Transport] TimeoutTransport::send(self : TimeoutTransport[T], request : Request) -> Response raise

    WriteOptions

    pub(all) struct WriteOptions {
    max_batch_size : Int
    } derive(Eq,
    Debug
    )

    How a write batch is split before it is sent.

    Chroma refuses a batch larger than the server's max_batch_size (it answers 400), and the limit differs per deployment: GET /pre-flight-checks reports it when the server implements the endpoint. The client cannot guess it, so it does not guess — a caller who knows the number sets it here, and write_records slices the batch into chunks of that size.

    max_batch_size <= 0 means "send it as one request", which is the default and the behaviour every earlier version had.

    WriteOptions::equal

    WriteOptions::from_preflight

    fn WriteOptions::from_preflight(checks : PreflightChecks) -> WriteOptions

    Returns a copy sized for the server's advertised limit.

    Pairs with preflight: WriteOptions::from_preflight(client.preflight()) turns what the server says into what the client sends, and falls back to "do not chunk" when an older server does not advertise a limit.

    WriteOptions::new

    fn WriteOptions::new(max_batch_size? : Int) -> WriteOptions

    Builds write options; max_batch_size defaults to "do not chunk".

    WriteOptions::not_equal

    fn WriteOptions::not_equal(x : WriteOptions, y : WriteOptions) -> Bool

    MAX_RESPONSE_BYTES

    let MAX_RESPONSE_BYTES : Int

    The largest response body the client will decode.

    The client reads a whole body into one String, and MoonBit strings are UTF-8, so the limit is stated in bytes: 32 MiB is far above any realistic result set (a full Include::all() page of 1024-dimension vectors is a few megabytes) yet small enough that a runaway response cannot exhaust memory before the caller sees an error.

    Truncating is not an option, because a half-parsed result set would look like data loss. A response past the limit fails loudly instead, and the caller can narrow include, lower n_results or page with limit.

    PEEK_LIMIT

    let PEEK_LIMIT : Int

    The largest limit peek will send.

    RETRY_DELAY_HEADER

    let RETRY_DELAY_HEADER : String

    The header carrying the delay a real transport should wait before re-sending.

    TIMEOUT_HEADER

    let TIMEOUT_HEADER : String

    The header a transport reads to learn its deadline.

    api_error

    fn api_error(status : Int, body : String) -> ChromaError

    Builds the error to raise for a non-2xx response.

    The body is probed for the Chroma error envelope; only when it carries neither error nor message does the error stay a raw Status.

    append_query

    fn append_query(url : String, params : Array[(String, String)]) -> String

    Appends key=value pairs to a URL, percent-encoding both sides.

    debug_collection_config

    fn debug_collection_config(config : CollectionConfig) -> Json

    Serializes a collection configuration, useful for logging and for building custom requests.

    Same value as CollectionConfig::to_json; the name is kept because the logging use case reads better with it.

    embedding_from_json

    fn embedding_from_json(json : Json) -> Array[Double] raise ChromaError

    Reads a JSON array of numbers into an embedding.

    embedding_to_json

    fn embedding_to_json(embedding : Array[Double]) -> Json

    Converts an embedding to a JSON array of numbers.

    encode_path_segment

    fn encode_path_segment(segment : String) -> String

    Percent-encodes a single path segment.

    Collection names may contain spaces and other characters that are not legal in a URL path, so every dynamic segment goes through here.

    encode_query_component

    fn encode_query_component(value : String) -> String

    Percent-encodes a query component. Unlike a path segment a space may be written as + here.

    is_retryable_status

    fn is_retryable_status(code : Int) -> Bool

    Whether the status code is worth retrying: 429 and 5xx, i.e. the server was reachable but too busy or broken.

    This is the single spelling of the rule. Api and Status both answer through it, and RetryTransport uses it to decide whether to re-send, so the error a caller inspects and the decision the transport takes cannot drift apart.

    join_url

    fn join_url(base : String, path : String) -> String

    Joins a base URL and a path with exactly one separating slash, so join_url("http://localhost:8000/", "/api/v2") is http://localhost:8000/api/v2.

    metadata_from_json

    fn metadata_from_json(json : Json) -> Array[(String, MetadataValue)]

    Reads a JSON object into a Metadata.

    Values Chroma cannot store (arrays and objects) are skipped rather than failing the whole response.

    metadata_to_json

    fn metadata_to_json(metadata : Array[(String, MetadataValue)]) -> Json

    Converts a Metadata list into a JSON object.

    no_progress

    fn no_progress(sent : Int, total : Int) -> Unit

    The no-op progress handler, so a call site never branches on "was a handler passed": an optional callback with a default value would make every wrapper wrap it in Some.

    normalize_where

    fn normalize_where(clause : Json) -> Json

    Closes a where clause Chroma would misread rather than reject.

    Chroma treats a flat where object as a conjunction of its keys ({"a": .., "b": ..} is a AND b). {"$and": .., "b": ..} is not a conjunction Chroma understands: the server answers 400 ("Invalid where clause") for most shapes and silently ignores the extra top-level key for some, which is worse because the caller gets records they did not filter for. This folds a top-level key that sits next to $and / $or into that operator's operand list, so both spellings mean the same thing and nothing is dropped.

    A clause with no operator at the top level is returned unchanged.

    pad_query_result

    fn pad_query_result(result : QueryResult) -> QueryResult

    Pads a query result so every row has one entry per column.

    A query response is columnar and nested: each field is an array with one row per query embedding. For an embedding that matched nothing, Chroma sends [] for that row — or omits the field for that row — while the other fields may still carry a row. QueryResult::from_query_json reads the first row, and a field that is shorter than ids there means "no value for those rows", not "this field was not requested".

    Without the padding, result.documents[i] on such a result aborts at runtime (index out of bounds) in a caller that loops over ids, which is the natural way to read a result. Here the missing cells become None, so the arrays line up with ids no matter what the server sent.

    records_to_columns

    fn records_to_columns(records : Array[Record]) -> Json

    Serializes a batch of records into the columnar body Chroma expects.

    Chroma does not take records one by one: the add/upsert/update models are parallel arrays (ids, embeddings, documents, metadatas) that must be the same length and aligned by index. A column is emitted only when it carries data, and a null marks a record that has no value for it — that position-preserving null is what keeps the arrays aligned.

    embeddings is the exception: Chroma requires the field and rejects a batch where it is missing, null or empty, so a record without coordinates has no representation here. validate_records refuses such a batch before it is serialized.

    request_retry_delay_ms

    fn request_retry_delay_ms(request : Request) -> Int?

    The wait a RetryTransport asks for before a re-send, if any. Same rules as request_timeout_ms.

    request_timeout_ms

    fn request_timeout_ms(request : Request) -> Int?

    The deadline a request asks for, if it carries one.

    Both the header and an explicit value are read by AsyncTransport, so the parsing lives here next to the header's definition rather than in every transport. A malformed value is treated as "no deadline": the header is an internal contract, and a garbage one means the wrapper was misused rather than that the caller wants a zero deadline.

    validate_batch

    fn validate_batch(records : Array[Record], reject_duplicate_ids? : Bool) -> Unit raise ChromaError

    Rejects a batch Chroma would refuse for reasons the record itself cannot show, before a request is built.

    validate_records checks each record in isolation; these two checks are about the batch as a whole:

    • duplicate ids inside one request — Chroma rejects an add that names the same id twice, because an insert cannot satisfy "insert twice". The same batch split into two requests works, which is why this is a batch rule and not a per-record one. upsert is unaffected: an upsert on an id it just wrote is well defined.
    • vector width — every embedding in a batch must have the length the collection's index was created with. Mismatched widths are a 400 from the server with no hint about which record is wrong, so the first offending record is named here.

    validate_limit_offset

    fn validate_limit_offset(limit : Int?, offset : Int?) -> Unit raise ChromaError

    Validates a pagination window on a get.

    Only the signs are checked, which is what chroma-go does (limit must begreater than or equal to 0, offset must be greater than or equal to 0). limit and offset are independent: limit=100, offset=200 is a legal page. An earlier version of this client also required offset < limit, which rejected the very paging pattern chroma-go documents.

    validate_n_results

    fn validate_n_results(n_results : Int) -> Unit raise ChromaError

    Rejects an n_results that Chroma cannot answer.

    validate_paging

    fn validate_paging(limit : Int?, offset : Int?) -> Unit raise ChromaError

    The get spelling of validate_limit_offset, kept as its own name so the call site reads as "this is paging a result set".

    validate_query_inputs

    fn validate_query_inputs(label : String, inputs : Array[Json]) -> Unit raise ChromaError

    Rejects a query whose inputs would be refused server-side.

    label names the field in the error message so a caller passing both texts and vectors can tell which one was empty.

    validate_records

    fn validate_records(records : Array[Record], require_embedding? : Bool) -> Unit raise ChromaError

    Rejects record batches Chroma would refuse, before a request is built.

    Chroma answers 400 for an empty batch and for a blank id, and its add/upsert models make embeddings mandatory, so a batch whose records have no vectors is refused too. All of these are local mistakes with local fixes, so they are caught here instead of after a round-trip.

    require_embedding is false for update, where omitting a field is how a caller says "change only what I passed".

    where_and

    fn where_and(clauses : Array[Json]) -> Json

    Combines clauses with $and, the counterpart of chroma-go's And(...).

    where_document_and

    fn where_document_and(clauses : Array[Json]) -> Json

    Combines where_document clauses with $and.

    where_document_contains

    fn where_document_contains(text : String) -> Json

    Builds a where_document clause matching documents that contain text.

    The counterpart of chroma-go's Contains(text).

    where_document_not_contains

    fn where_document_not_contains(text : String) -> Json

    Builds a where_document clause matching documents that do not contain text.

    where_document_or

    fn where_document_or(clauses : Array[Json]) -> Json

    Combines where_document clauses with $or.

    where_document_regex

    fn where_document_regex(pattern : String) -> Json

    Builds a where_document clause matching documents by regular expression.

    where_eq

    fn where_eq(key : String, value : MetadataValue) -> Json

    Builds a where clause for a metadata equality test.

    The counterpart of chroma-go's EqString(key, value): the clause filters records whose metadata field key equals value, and can be combined with where_and / where_or.

    where_gt

    fn where_gt(key : String, value : MetadataValue) -> Json

    Builds a where clause matching records whose metadata field is greater than value.

    where_gte

    fn where_gte(key : String, value : MetadataValue) -> Json

    Builds a where clause matching records whose metadata field is greater than or equal to value.

    where_in

    fn where_in(key : String, values : Array[MetadataValue]) -> Json

    Builds a where clause matching records whose metadata field is one of values.

    where_lt

    fn where_lt(key : String, value : MetadataValue) -> Json

    Builds a where clause matching records whose metadata field is less than value.

    where_lte

    fn where_lte(key : String, value : MetadataValue) -> Json

    Builds a where clause matching records whose metadata field is less than or equal to value.

    where_ne

    fn where_ne(key : String, value : MetadataValue) -> Json

    Builds a where clause for a metadata inequality test.

    where_or

    fn where_or(clauses : Array[Json]) -> Json

    Combines clauses with $or, the counterpart of chroma-go's Or(...).

    without_internal_headers

    fn without_internal_headers(request : Request) -> Request

    Strips the two client-internal headers from a request.

    They are addressed to the transport, not to Chroma, and a server is free to reject an unknown header (some proxies answer 400). Every real transport sends the stripped copy.