Large ingests with upsert_from_dataframe¶
upsert_from_dataframe() takes a pandas DataFrame, splits it
into batches, sends the batches in parallel, and reports which rows landed and which did
not. This page covers sizing the batches, bounding the job in time, and handling partial
failure.
It serves indexes created with the deprecated top-level dimension=/metric=. On a
schema-based (document) index, write through documents
instead.
The method has the same signature on Index,
AsyncIndex and
GrpcIndex, so the examples carry over apart from the await. For
how the SDK paces and retries batches, see How bulk ingest
behaves.
Note
This method writes through upsert(), so it serves indexes
created with top-level dimension and metric. The server rejects it on an index
created with a schema; use index.documents and see the
quickstart for how the two
interfaces divide.
Important
pandas is not an SDK dependency. Install it yourself; without it the call raises a
RuntimeError. To skip pandas, upsert() with batch_size
does the same parallel batching from a list of vectors; see
Large datasets.
When to use it¶
Your situation |
Reach for |
|---|---|
Vectors in a pandas DataFrame |
|
Vectors in a list, and no pandas |
|
Far more rows than you want to stream, already in cloud storage |
The DataFrame contract¶
Required columns: id (string) and values (list of floats, matching the index
dimension). Optional columns: sparse_values and metadata. Any other column is ignored,
so move fields you want stored into metadata.
A missing cell (None or NaN) in an optional column leaves that field off the vector.
That matters when you build a frame from row dicts where only some rows carry metadata:
pandas fills the gaps with NaN, and those rows are written without metadata.
import pandas as pd
df = pd.DataFrame(
[
{"id": "article-101", "values": [0.012, -0.087, 0.153], "metadata": {"lang": "en"}},
{"id": "article-102", "values": [0.045, 0.021, -0.064]},
]
)
The values are truncated to three floats here; pass your index’s full dimension.
Minimal usage¶
from pinecone import Pinecone
pc = Pinecone(api_key="your-api-key")
index = pc.index("article-search")
response = index.upsert_from_dataframe(df, namespace="articles-en")
print(response.upserted_count)
Omitting namespace writes to the default namespace; see
Working with namespaces. By default a failed batch is reported on the response
rather than raised, and there is no overall deadline.
Sizing the knobs¶
response = index.upsert_from_dataframe(
df,
namespace="articles-en",
batch_size=200,
max_concurrency=4,
timeout=30,
total_timeout=1800,
on_error="collect",
)
The reference for upsert_from_dataframe() lists each
parameter and its default. How to choose values:
batch_size: rows per request¶
Lower it when rows are heavy (high-dimension vectors or large metadata) so each request
stays inside the server’s caps on vector count and request size, and each retry re-sends
less. Raise it for small vectors when you want fewer round trips, and check the result:
the server, not the SDK, decides when a request is too big. A batch succeeds or fails as
a unit, so batch_size is also your unit of retry.
max_concurrency: parallelism ceiling¶
This is a ceiling, not a target: the SDK holds concurrency lower on its own when the server pushes back, then recovers. Lower it when the ingest shares an index with latency-sensitive query traffic. Raise it only after measuring; see The knobs.
timeout: one attempt of one batch¶
This bounds a single request attempt. The SDK retries a batch that times out like any
other transient failure, so one batch can take several multiples of timeout before it
is reported as failed. Raise timeout when batches are large or the network is slow.
Retries and Resilience explains what is retried and which retry settings reach the data
plane.
Do not use timeout to bound the whole job; that is what total_timeout is for.
total_timeout: the whole job’s deadline¶
Set it whenever the ingest runs inside a real window, such as a cron slot, a deploy step or a pipeline stage. On expiry:
The SDK stops submitting new batches.
Batches already in flight finish; none is cancelled mid-write.
Every row not sent comes back in
response.failed_items, withdisposition="unsent"on its batch’s error.
failed_items is a flat list of dicts keyed id, values, sparse_values and
metadata, so pd.DataFrame(response.failed_items) rebuilds a frame you can pass
straight back in.
There is no default deadline, because a slow but progressing ingest is healthy. The SDK’s
stall detector gives up early when the backend stops responding, but it does not cover
every outage, so total_timeout is the only guaranteed wall-clock bound.
Recipe: the nightly job¶
Bounded in time, with leftover rows saved for the next run:
response = index.upsert_from_dataframe(
df,
namespace="articles-en",
total_timeout=25 * 60, # headroom inside a 30-minute slot
)
if response.has_errors:
pd.DataFrame(response.failed_items).to_parquet("retry-tomorrow.parquet")
print(
f"upserted {response.upserted_count}/{response.total_item_count}; "
f"{response.failed_item_count} rows deferred"
)
Re-sending the leftover rows is safe: an upsert overwrites by vector ID, so a row that landed just before a timeout is overwritten with identical data.
Recipe: retry within the same run¶
Bound the number of rounds, resend only batches marked retryable, and back off between
rounds. A loop on while response.failed_items: never ends when a batch fails the same
way every time.
import time
response = index.upsert_from_dataframe(df, namespace="articles-en", total_timeout=1800)
for attempt in range(3):
retryable = [item for err in response.errors if err.retryable for item in err.items]
if not retryable:
break
time.sleep(2**attempt)
response = index.upsert_from_dataframe(
pd.DataFrame(retryable), namespace="articles-en", total_timeout=600
)
A batch with retryable=False failed deterministically, for example a validation error
or another 4xx rejection, and fails the same way if re-sent. Log it and move on.
Recipe: fail loudly instead¶
Pass on_error="raise" when a pipeline should stop on failure. Once every batch has
settled, the SDK re-raises the lowest-indexed batch’s exception with the partial
UpsertResponse attached as .response. The
exception is whatever that batch raised: PineconeTimeoutError
for a batch that ran out of retries on timeouts, or an
ApiError subclass for a rejection.
from pinecone import PineconeTimeoutError
try:
index.upsert_from_dataframe(
df, namespace="articles-en", total_timeout=1800, on_error="raise"
)
except PineconeTimeoutError as exc:
pd.DataFrame(exc.response.failed_items).to_parquet("retry.parquet")
raise
Recipe: asyncio¶
On AsyncPinecone, index() is a coroutine, so
await it:
from pinecone import AsyncPinecone
async with AsyncPinecone(api_key="your-api-key") as pc:
index = await pc.index("article-search")
async with index:
response = await index.upsert_from_dataframe(
df, namespace="articles-en", total_timeout=1800
)
Reading the response¶
Field |
Meaning |
|---|---|
|
Rows the server accepted |
|
Rows submitted / rows in a batch that failed |
|
|
|
The rows from failed batches, ready to send again |
|
One |
Each error’s disposition says how far its batch got:
"rejected": the request reached the server and came back with an error, so the write may have landed anyway."unsent":total_timeoutexpired before the batch was sent."abandoned": the stall detector judged the backend unavailable and dropped the remaining batches without sending them. Treat it like any other partial failure and retry later.
Match the values you care about and let others fall through; new ones can appear.
Checklist for a large production ingest¶
[ ]
pandasinstalled in the environment that runs the job[ ]
idandvaluescolumns present, and your own fields moved intometadata[ ]
batch_sizesized to your row weight[ ]
total_timeoutset if the job has a real window[ ]
on_errorchosen:"collect"and persistfailed_items, or"raise"[ ] Retry path bounded and filtered on
err.retryable