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
chromadbandtopk-sdkinstalled - Your Chroma Cloud tenant ID, database name and API key
- 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.
topk importloads each spec into a new collection.- You switch your queries to the TopK SDK.
- A second script compares record counts and sample query results between Chroma and TopK.
How Chroma concepts map to TopK
Step 1: Export every collection
Save this aschroma_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 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 calleddocs, it looks like this:
docs.toml
- 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 withfn.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:Step 4: Update your queries
Chroma’squery() becomes a TopK query that selects a score, sorts by it and limits the results:
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
--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_internalbecomesmeta_internal. Keys calledid,documentorembeddingbecomemeta_id,meta_documentandmeta_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 defaultall-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
documentfield is optional. - Squared L2. Chroma’s
l2is squared Euclidean distance. TopK’seuclideanranks results in the same order, but the scores differ.