Skip to departures
Hpjadul
MAC END

Migrating a Chroma Cloud Collection to Pinecone: A Cutover and Validation Guide

Departs
Crew
James Williams
Passengers
303

A six-script workflow moves Chroma Cloud vectors to Pinecone, with a planned write pause, data checks and a coordinated application cutover.

Migrating a Chroma Cloud Collection to Pinecone: A Cutover and Validation Guide

Moving a vector collection between managed services is a data and application change, not just an export and import. Chroma Cloud is scheduled to stop accepting writes on November 22, 2026, then stop serving queries on December 22. A migration plan needs to account for both deadlines, confirm that records and searches behave as expected, and switch application reads and writes together.

Aaron Kao

The workflow described here moves one Chroma Cloud collection, including its existing dense vectors, document text and metadata, into a Pinecone serverless index. It reuses the vectors, so it doesn’t re-embed the collection. The planned cutover includes a period when the application stops writing to Chroma while reads continue there. For multiple collections, repeat the process for each one.

Chroma’s wind-down notice recommends starting export and validation before November 22 and finishing the export before December 22. The open-source Chroma project isn’t affected and remains available under Apache 2.0. Teams that want a hosted service can use the Pinecone migration toolkit, a set of six Python scripts for preflight checks, index creation, export, conversion, loading and validation. The toolkit is provided as-is: teams need to check its findings against their own data and application requirements before changing production traffic.

Translate the data model before moving records

Chroma tenants and databases correspond broadly to Pinecone organizations and projects. A Chroma collection can map to a Pinecone index, or to a namespace in a shared index if collections use the same vector dimension and similarity metric. Pinecone’s vector field is a dense_vector named embedding; document text goes into a text field, and metadata becomes top-level fields that can be filtered.

That mapping has limits. Pinecone IDs must be 1 to 512 ASCII characters and can’t contain a NUL character. IDs that fail these conditions receive replacement IDs during conversion, with the original recorded for reference. Metadata values can be strings, numbers, booleans or lists of strings. Some metadata keys need renaming, and unsupported values can block the conversion or require a deliberate transformation.

The distance metrics also need to match: Chroma’s l2, cosine and ip correspond to Pinecone’s euclidean, cosine and dotproduct. Chroma where filters have Pinecone equivalents for operators including $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $and, $or and $not. Array membership checks can map to $in or $nin. Chroma’s text-content filters and contains checks on non-array values don’t have direct equivalents; applications that depend on them need redesigned queries.

These differences affect search behavior as well as data shape. Inspect the toolkit’s field mapping and transformation reports, then check that your application’s filters still express the same intent.

Rehearse the complete migration

Get the scripts from the migration toolkit repository and install the pinned dependencies with uv sync. Set PINECONE_API_KEY for Pinecone steps, and set CHROMA_API_KEY, CHROMA_TENANT and CHROMA_DATABASE for Chroma Cloud. The examples below use collection my_collection, index my-index and namespace my-collection-v1; substitute the names for your setup. Scripts can also read a local open-source Chroma copy when given --chroma-path PATH.

Begin with preflight.py. It scans the collection in pages and records the collection count, vector dimension and distance metric, along with the recorded embedding function. It also reports metadata keys and value types, empty or missing documents, and IDs that Pinecone may reject. Findings are labeled BLOCKING, TRANSFORM, WARNING or INFO and saved to preflight.json.

uv run preflight.py --collection my_collection --workdir migration_work
uv run preflight.py --collection my_collection --workdir migration_work --allow-transforms

Read the initial report before allowing transformations. Blocking findings include all-zero vectors, mixed dimensions, metadata exceeding 40 KB, unsupported metadata types and numeric or boolean arrays. Correct blocking issues in Chroma before the write pause. Transform findings include replacement IDs, renamed keys and dropped None values. The script exits with an error for these until you explicitly accept the changes with --allow-transforms.

If long document text causes the metadata-size finding, the guide offers a full-text-search path, or you can split the text in the source data. The optional --array-policy stringify flag handles non-string arrays by converting their values to strings; use it only if that representation suits the application. To check application filters, pass a JSON file of {"name", "where"} or {"name", "where_document"} objects with --app-filters FILE. Keep the selected flags consistent when converting records.

Run the remaining steps against a copy or a small collection before scheduling the production pause, and time export and loading. That rehearsal gives your team an estimate for the required window; the toolkit doesn’t provide a general duration because collection size and operating conditions vary.

Create the index and prepare the application switch

uv run create_index.py --index my-index --workdir migration_work

The script takes the dimension and metric from preflight.json and creates a document index with a vector field. It defaults to AWS us-east-1; on Standard, --cloud and --region select another location. You can enable deletion protection. If an index with that name already exists, the script checks its schema and exits with an error if it doesn’t match.

For documents over about 40 KB, the toolkit describes a full-text-search variant. Use its matching preflight command with --text-mode fts, create the index with --full-text-search, and convert with --text-mode fts. This path changes how text is represented and indexed, so make the choice during rehearsal.

Prepare an application configuration switch that controls both reads and writes. Deploy it while the application still uses Chroma. The Pinecone integration opens the index with pc.index(name=...), writes through index.documents.upsert and searches through index.documents.search. Use field names from the preflight report and account for any remapped IDs.

Pause writes, export and load

At the start of the cutover window, make the application read-only while leaving reads on Chroma. Rerun preflight with the same transformation and data-policy flags. Then export:

uv run export.py --collection my_collection --workdir migration_work

The exporter records collection.count() and streams pages from collection.get into export.jsonl. It keeps the set of IDs in memory. A successful exit requires the number of unique exported IDs to match the recorded count, no repeated IDs and no count change during export.

Those checks can catch writes that alter the collection count, but they can’t detect an update that replaces a record without changing the count. The application-level write pause is what protects against that case. Keep export.jsonl as the archive copy.

Next, convert.py reads the export twice and produces Pinecone documents, a metadata field map, an ID map and a record of transformations. Keys beginning with _ or $, keys longer than 64 bytes, and reserved names such as embedding, text and chroma_id are renamed. If a generated name conflicts or exceeds the length limit, the toolkit truncates it and adds a hash suffix. The mapping is deterministic and records why each rename occurred.

Rejected IDs become values in the form chroma-sha256-<sha256 of the ID>, with a numeric suffix if needed to avoid a collision. The converted record also retains its original ID in chroma_id. Every per-record change or drop is written to transforms.jsonl; conversion doesn’t silently discard a record. If any record fails, the script doesn’t write documents.jsonl.

Load the converted file into a new or empty namespace:

uv run load.py --index my-index --namespace my-collection-v1 --workdir migration_work

The loader streams batches to Pinecone, keeping one batch in memory at a time. A batch is sent before it would exceed 1,000 documents or 1,900,000 bytes, below the stated 2 MB request limit. The loader retries rate-limit responses, server errors, connection errors and timeouts with backoff. Rerunning against the same namespace is allowed only when load_state.json records the same index, namespace and checksum for documents.jsonl. Since upserts replace records by ID, that verified rerun produces the same content.

Validate records and search results

Validation checks both the moved data and application-level search behavior. Prepare golden queries using the same embedding function that the application uses for Chroma queries. The toolkit doesn’t create embeddings; replace the example function with your own and save each query ID and vector in golden.jsonl.

uv run validate.py --collection my_collection --index my-index --namespace my-collection-v1 \
  --workdir migration_work --golden-queries golden.jsonl

The validator checks that the Chroma collection is unchanged and its live dimension and metric agree with the Pinecone index schema. It waits up to 600 seconds for the namespace count to reach the exported count, then compares the mapped source ID set with the IDs listed in Pinecone. It also checks fields in 200 randomly selected documents and up to 200 records with per-record transformations, comparing them with the source and attempting to reconstruct the Chroma records.

Search visibility is polled for up to 180 seconds. Golden-query results are compared with Chroma’s collection.query: the tie-adjusted top-k overlap must average at least 0.9, and no query can fall below 0.6. These are heuristics, adjustable with --min-mean-overlap and --min-query-overlap. The test-only --golden-from-sample N option builds queries from perturbed stored vectors.

A self-retrieval check looks for each sampled vector’s own record. It’s diagnostic and doesn’t fail the run. A miss can point to duplicate vectors, unnormalized vectors used with dotproduct, close result scores, approximate search or data that isn’t yet visible.

If a data check fails, stop and fix the issue, then load into a fresh namespace and validate again. An upsert-only repair can add missing IDs but can’t remove records deleted from Chroma. If only the application-filter check fails, address the filter translation; the records themselves may still be correct.

Cut over and retire the source

Before switching, decide whether the validation results meet the needs of the application. Until the application writes to Pinecone, you can resume writes on Chroma and schedule another cutover window. Once ready, change the configuration so reads and writes move together, then resume writes. Keeping them paired means a write won’t land in a store that the application’s reads can’t see.

After the switch, monitor errors, latency and search quality. Returning to Chroma after Pinecone has received new writes requires replaying those writes. Chroma Cloud stops accepting writes on November 22; if that date arrives first, the export, load and cutover steps remain available until reads stop on December 22, but application writes to Chroma will fail.

Before December 22, remove Chroma client code from applications and jobs, confirm that nothing calls Chroma Cloud, and rotate or delete Chroma Cloud credentials. Archive export.jsonl together with field_map.json, id_map.jsonl and transforms.jsonl. The mapping files are needed to reverse field renames and recover original IDs.

Options after the migration

The migration retains the existing embedding model. After the cutover, teams can consider Pinecone integrated embedding, which uses a hosted model such as llama-text-embed-v2 or multilingual-e5-large at document upsert and query time. That removes the client-side embedding call, but requires a new index with a text field and an embed block, followed by a reload from stored text. Because a new embedding setup changes results, evaluate relevance rather than expecting identical IDs.

The source guide also describes a Pinecone Standard trial lasting three weeks and providing $300 in credits, ending after 21 days or when the credits are used. A trial started in October ends before November 22, so teams need to account for the trial period when scheduling the move. Standard includes object-storage import, backup and restore, role-based access control, cloud and region choice, higher limits and Developer Support. After the trial, the options described are adding a payment method to continue on Standard, upgrading to Enterprise or moving to the free Starter plan. Standard has a $50 monthly minimum usage charge, followed by pay-as-you-go pricing.

The practical measure of a successful migration is whether the application can serve its real queries with the intended data and filters after the switch. Rehearse the whole path, preserve the mapping files, and make validation the decision point before sending production traffic to Pinecone.

Source: James Williams · www.pinecone.io
Arrived · Hpjadul

Also departing from this gate