Before you migrate
You need:- A Pinecone account and API key.
- Network access to your PostgreSQL database.
- Python 3.9 or later.
- A PostgreSQL role that can read the tables you want to migrate.
- Write access to PostgreSQL if you use the synchronization script. The script creates bookkeeping tables or an outbox table and triggers, depending on the synchronization strategy.
Terminal
The repositoryβs
requirements.txt installs the pinecone, psycopg, pgvector, and pyarrow packages used throughout this guide.Terminal
1. Inspect the pgvector data
For every table you want to migrate, identify the primary key, vector column, vector dimension, distance metric, metadata columns, and row count. Find the vector columns and their dimensions:PostgreSQL
PostgreSQL
If the table doesnβt have a pgvector index, inspect the distance operator used by your application queries. The Pinecone index dimension and metric must match the source data.
Also record the source count for each table. Youβll compare these counts with Pinecone after the backfill:
PostgreSQL
2. Map access controls
If your tables use PostgreSQL row-level security (RLS), review every policy before choosing a Pinecone layout:PostgreSQL
3. Choose the index and namespace layout
By default,sync.py uses one Pinecone index and maps each PostgreSQL table to a namespace with the same name. The steps below use this layout. Tables can share an index only when their vectors have the same dimension and use the same distance metric.
To use a shared namespace, tenant-based namespaces, or multiple indexes, update the namespace and index routing in
sync.py before you run the migration.
Use a string for every record ID. The migration repository prefixes source IDs with the table name, such as documents#4021, to prevent collisions. Each Pinecone record contains the vector in values and selected source columns in metadata.
Metadata values must be strings, numbers, booleans, or lists of strings. Omit NULL values and convert PostgreSQL numeric values to floats. Donβt name a source metadata field metadata; metadata is the top-level Parquet column that contains the JSON-encoded metadata object.
4. Create the Pinecone index
The examples below use two PostgreSQL tables,
documents and products, with 768-dimensional vectors stored in an embedding column and queried using cosine distance. Replace all example table names, column names, dimensions, metrics, metadata fields, and namespaces with values from your workload.Python
5. Configure the source tables
In the cloned migration repository, edit theTABLES configuration near the top of sync.py to match your PostgreSQL schema. Each entry defines the primary key, vector column, metadata columns, and optional change timestamp:
Python
sync.py maps each configured table to a namespace with the same name.
Run all sync.py commands below from the root of the cloned pgvector-migration repository.
6. Backfill the records
Initialize change tracking
The following workflow uses the default change-log strategy. If the source stays writable during the backfill, initialize change tracking before copying records. This captures changes made during the backfill so you can apply them afterward:Terminal
Bulk import
For a production backfill, use bulk import. Create anexport.py file with the following code from the toolβs migration workflow. It writes one import-ready Parquet file per table.
The export script reuses the PostgreSQL connection,
TABLES configuration, and record-mapping helpers from sync.py. Configure TABLES in sync.py before running the export.Python
Terminal
Output
Python
describe_import operation to monitor progress. Each import takes at least 10 minutes. Wait until the import status is Completed before validating record counts in the next step.
The target namespaces must not already exist. A private bucket or container requires a storage integration.
Streaming backfill
For a small dataset, you can instead use the streaming backfill insync.py:
Terminal
Apply captured changes
If you initialized change tracking, apply the changes captured while the backfill was running:Terminal
7. Validate the backfill
Compare the source row count for each table with the record count for its target namespace:Python
documents table:
Python
<=> for cosine distance, <#> for inner product, or <-> for Euclidean distance.
Compare the returned record IDs and ordering. Small ordering differences for near-ties are expected with approximate search. Large differences can indicate a mismatched metric or inconsistent vector normalization.
8. Keep Pinecone synchronized
After the initial copy, synchronize inserts, updates, and deletes until cutover. The repositoryβssync.py supports two strategies:
The change-log strategy is the default. Run the synchronization command on a schedule until cutover:
Terminal
updated_at column, the tool also supports a watermark strategy. This strategy uses its first synchronization run to backfill records. For setup, limitations, and commands, see the watermark strategy in the pgvector-migration tool.
Periodically reconcile all record IDs, and run reconciliation immediately before cutover:
Terminal
9. Cut over application traffic
Keep pgvector serving traffic and continue synchronization while you cut over:- Shadow a sample of production searches to Pinecone without returning those results to users. Compare search quality and latency.
- Route a small percentage of reads to Pinecone and monitor results.
- Increase the percentage after validation.
- Immediately before switching the primary read path, run
syncwith your selected strategy, runpython sync.py reconcile, and then runsyncagain to apply changes captured during reconciliation. - Run
synconce more and confirm that it reports no outstanding changes. Then make Pinecone the primary read path. - Keep pgvector available as a rollback target until the new read path is stable. You can point reads back to pgvector as it never stopped serving and is still authoritative.