Export your table to a file with \copy, then upsert that file into a collection — that's the whole data move. pgvector's text serialization of a vector column ("[0.017,-0.038,...]") is parsed natively by Vector Panda's CSV ingestion, your primary key column becomes the vector key, and every other column comes along as filterable metadata. Nothing needs re-embedding: the vectors you computed for pgvector are the vectors you search with here.

Step one, on the Postgres side:

\copy (SELECT id, embedding, content FROM items) TO 'items.csv' WITH (FORMAT csv, HEADER)

Step two, on the Vector Panda side. To keep this page runnable end to end we build a three-row items.csv in exactly the shape that \copy emits — an id column, an embedding column holding pgvector's bracketed text, and a content column — using real embeddings from the all-MiniLM-L6-v2 model bundled with the SDK:

import csv
from veep import VP, samples

texts = {
    "doc-1": "Return policy: items may be returned within 30 days of delivery.",
    "doc-2": "Standard shipping takes 3 to 5 business days within the US.",
    "doc-3": "Support is available by email around the clock.",
}
with open("items.csv", "w", newline="") as fh:
    w = csv.writer(fh)
    w.writerow(["id", "embedding", "content"])
    for key, text in texts.items():
        vec = samples.encode(text)
        w.writerow([key, "[" + ",".join(f"{x:.6f}" for x in vec) + "]", text])

The migration itself is two calls — naming your id and vector columns locks the schema so the upload processes without a confirmation step:

vp = VP.from_creds()
vp.collections.create("items", tier="hot", id_field="id", vector_field="embedding")
vp.vectors.upsert("items", "items.csv")

qv = samples.encode("how long does delivery take?")
hit = vp.vectors.query("items", vector=qv, top_k=1)[0]
print(hit.key, f"{hit.score:.4f}", hit.metadata["content"])
doc-2 0.6509 Standard shipping takes 3 to 5 business days within the US.

For large tables, export to Parquet instead of CSV if you have the tooling handy — it uploads and processes faster — but CSV works at any size.

What changes in your query code

Distance becomes similarity. pgvector's <=> operator returns cosine distance in [0, 2], where smaller is closer. Vector Panda returns cosine similarity in [-1, 1], where larger is closer: similarity = 1 - distance. If you had a distance cutoff like embedding <=> q < 0.3, the equivalent here is min_score=0.7.

ORDER BY ... LIMIT becomes top_k.

SELECT id, content FROM items ORDER BY embedding <=> $1 LIMIT 10;

is:

vp.vectors.query("items", vector=qv, top_k=10)

WHERE clauses become filter predicates, applied during the vector search rather than as SQL:

SELECT id FROM items WHERE year >= 2000 ORDER BY embedding <=> $1 LIMIT 10;

is:

vp.vectors.query("items", vector=qv, top_k=10, filter={"year": {"$gte": 2000}})

The full operator set ($eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, plus $and/$or/$not) is covered in Filter syntax in depth.

Index selection disappears. There is no CREATE INDEX ... USING hnsw step and no lists/ef_construction tuning: Vector Panda measures candidate index configurations against a sample of your actual vectors and serves the fastest one that meets your collection's recall target (default 0.95, adjustable via vp.collections.update()). How auto-optimize picks your index shows what gets measured.

If you need to come back

Migration anxiety runs both directions, so: vp.collections.export("items", "out/") writes every vector and its metadata to parquet files you can load anywhere — including back into pgvector. No export fees, no ceilings, at any scale. Exporting your data has the details.

vp.collections.delete("items")