Skip to main content

Migrate with a coding agent

Paste this into Claude Code, Cursor, Codex, or any agent with terminal access. It inventories your Chroma collections, shows you a plan, then exports, imports and verifies each one.
Prerequisites
  • TopK account (Sign up here) and the TopK CLI
  • TopK API key (Get an API key here), for the verification step
  • Python with chromadb and topk-sdk installed
  • Your Chroma Cloud tenant ID, database name and API key
Chroma Cloud stops accepting writes on November 22, 2026 and stops serving reads on December 22, 2026 (wind-down notice). The export below only reads, so it works until December 22, but your application’s writes fail from November 22.
Here is how the migration works:
  1. A script exports every collection to JSONL and writes a TopK import spec for it, with the vector dimension, the distance metric and a typed field for each metadata key.
  2. topk import loads each spec into a new collection.
  3. You switch your queries to the TopK SDK.
  4. A second script compares record counts and sample query results between Chroma and TopK.
Your embeddings are copied as they are. Nothing is re-embedded.

How Chroma concepts map to TopK

Step 1: Export every collection

Save this as chroma_to_topk.py in a new, empty directory, and run it there with your Chroma credentials in the environment. It walks every non-empty collection in the database and writes <collection>.jsonl and <collection>.toml next to it.
chroma_to_topk.py
Output
On self-hosted Chroma, replace chromadb.CloudClient(...) with chromadb.HttpClient(host=..., port=...). Everything else stays the same.
The export pages through each collection by offset, so records your application writes while it runs can be skipped or exported twice. Pause writes to Chroma during the export, or run the export and import again just before you switch writes to TopK.
The script reads each collection’s distance function from its configuration, so a collection created with cosine or ip keeps that metric in TopK. Collections without an explicit space use Chroma’s default, l2.

Step 2: Review the import spec

Each generated spec is a plain import spec. For a collection called docs, it looks like this:
docs.toml
Two edits are worth considering before you import:
  • Let TopK embed the text too. Change the document’s index from "keyword" to "semantic" and TopK generates multi-vector embeddings for it, alongside your existing vectors. You can then query with fn.semantic_similarity() and skip the query embedding call.
  • Drop fields you don’t query. The spec is a whitelist: only the fields it declares are imported.

Step 3: Import into TopK

From the export directory, log in, select the project to import into, then import every spec:
Pick the region closest to your application from Regions. Each spec creates its collection and loads it; documents are searchable as soon as they are written.
  • Add --dry-run to print the plan and a sample of documents without writing anything.
  • If a run stops, re-run it with --resume <run-id>, using the run id printed when it started. Upserts are idempotent, so re-running from scratch is safe too.
  • To merge per-tenant collections into one collection with a partition per tenant: in the export, add "tenant": col.name to each record. In every spec, use the same collection name in the table headers and add partition = "tenant" under the first header. Every spec must declare the same fields, because an import into an existing collection with a different schema is rejected.

Step 4: Update your queries

Chroma’s query() becomes a TopK query that selects a score, sorts by it and limits the results:
Sort direction depends on the metric. For cosine and dot_product, a higher score is closer, so sort with asc=False. For euclidean, a lower score is closer, so sort with asc=True.

Filter operators

Wrap each comparison in parentheses when you combine them with & or |, because Python binds & and | more tightly than ==, > and the other comparisons. not_ is imported from topk_sdk.query. Both $contains and contains() are case-sensitive substring matches.

Step 5: Check that results match

Run this while Chroma still serves reads. It compares record counts, then runs 20 of your own vectors as queries against both systems and prints how many of the top 10 results they share.
verify.py
Both systems use approximate nearest neighbor indexes, so expect close to 10 shared results per query rather than identical lists every time. A count mismatch usually means the import stopped early: re-run it with --resume.

Things to know

  • Only plain metadata values move. The script exports string, integer, float and boolean metadata, and skips lists and other values.
  • A key with mixed types becomes text. If a metadata key holds different types in different records, such as a number in some and a string in others, it’s imported as text and every value is stored as a string. A mix of integers and floats becomes float.
  • Renamed keys. TopK field names can’t start with _, so a key like _internal becomes meta_internal. Keys called id, document or embedding become meta_id, meta_document and meta_embedding, because the script uses those names itself. Use the new names in your filters.
  • Queries by text. If your application calls Chroma with query_texts, Chroma embedded the query with the collection’s embedding function: the default all-MiniLM-L6-v2, one you configured, or Chroma Cloud’s hosted Qwen or SPLADE. Your stored vectors move as they are, but TopK needs a query vector from the same model. Either embed queries with that model yourself (all of these are open models you can run), or add a semantic index so TopK embeds both documents and queries.
  • Sparse vectors aren’t exported. Chroma Cloud stores sparse vectors (for example from SPLADE) in metadata, and the script drops them. Use a keyword index for BM25 on the document instead, or a semantic index, which covers keyword matching too.
  • Records without a document import without one; the document field is optional.
  • Squared L2. Chroma’s l2 is squared Euclidean distance. TopK’s euclidean ranks results in the same order, but the scores differ.

Need help?

If you’d rather not migrate alone, talk to us. We’ll help with the export, the import and the side-by-side check.