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")
