1313
1414from pinecone ._internal .adapters .imports_adapter import ImportsAdapter
1515from pinecone ._internal .adapters .vectors_adapter import VectorsAdapter , extract_response_info
16- from pinecone ._internal .batching import chunked , validate_batch_size , with_progress
16+ from pinecone ._internal .batch import async_batch_execute
17+ from pinecone ._internal .batching import validate_batch_size
1718from pinecone ._internal .config import PineconeConfig
1819from pinecone ._internal .constants import DATA_PLANE_API_VERSION
1920from pinecone ._internal .data_plane_helpers import _validate_host , _vector_to_dict
21+ from pinecone ._internal .validation import require_in_range
2022from pinecone ._internal .vector_factory import VectorFactory
2123from pinecone .errors .exceptions import PineconeValueError , ValidationError
2224from pinecone .models .imports .list import ImportList
2325from pinecone .models .imports .model import ImportModel , StartImportResponse
2426from pinecone .models .namespaces .models import ListNamespacesResponse , NamespaceDescription
27+ from pinecone .models .response_info import ResponseInfo
2528from pinecone .models .vectors .query_aggregator import QueryNamespacesResults , QueryResultsAggregator
2629from pinecone .models .vectors .responses import (
2730 DescribeIndexStatsResponse ,
@@ -224,6 +227,7 @@ async def upsert(
224227 namespace : str = "" ,
225228 batch_size : int | None = None ,
226229 show_progress : bool = True ,
230+ max_concurrency : int = 4 ,
227231 timeout : float | None = None ,
228232 ) -> UpsertResponse :
229233 """Upsert a batch of vectors into a namespace.
@@ -246,19 +250,23 @@ async def upsert(
246250 display a progress bar across batches. Has no effect when
247251 ``batch_size`` is ``None`` or ``tqdm`` is not installed.
248252 Defaults to ``True``.
253+ max_concurrency (int): Asyncio concurrency limit for concurrent batch
254+ requests (range 1–64, default 4). Only used when ``batch_size``
255+ is set.
249256 timeout (float | None): Per-request timeout in seconds. Overrides
250257 the client-level default for this call only.
251258
252259 Returns:
253260 :class:`UpsertResponse` with the count of vectors upserted.
254261 When ``batch_size`` triggers multiple requests, ``response_info``
255- on the aggregated response is ``None`` — there is no single
256- response to attribute it to .
262+ carries the aggregate LSN from all successful batches (or ``None``
263+ if no LSN headers were returned) .
257264
258265 Raises:
259266 :exc:`PineconeTypeError`: If a vector element is not a recognized format.
260267 :exc:`PineconeValueError`: If a vector element is malformed.
261268 :exc:`PineconeValueError`: If *batch_size* is not a positive integer.
269+ :exc:`PineconeValueError`: If *max_concurrency* is outside [1, 64].
262270 :exc:`ApiError`: If the API returns an error response.
263271 :exc:`PineconeConnectionError`: If a network-level connection
264272 fails (DNS, refused, transport error).
@@ -292,11 +300,14 @@ async def upsert(
292300 print(response.upserted_count)
293301
294302 .. note::
295- Batches are sent **sequentially** (one ``await`` at a time). For
296- parallel emission, fan out at the call site with
297- ``asyncio.gather(*[idx.upsert(vectors=b) for b in batches])``.
298- For very large datasets (millions of vectors), consider
299- :meth:`start_import` for bulk import from cloud storage.
303+ When ``batch_size`` is set, batches are submitted **concurrently** via an
304+ ``asyncio.Semaphore`` of ``max_concurrency`` slots (default 4, range 1–64).
305+ Per-batch HTTP retries are handled by the client's configured
306+ ``RetryConfig``. **Partial failures do not raise** — per-batch errors are
307+ captured on the returned :class:`UpsertResponse` (see
308+ ``response.has_errors``, ``response.errors``, ``response.failed_items``).
309+ To retry only the failures, pass ``response.failed_items`` back to
310+ ``upsert(...)``.
300311
301312 .. seealso::
302313 - :meth:`upsert_records` — for indexes with integrated inference
@@ -308,16 +319,46 @@ async def upsert(
308319 return await self ._upsert_one_batch (
309320 vectors = vectors , namespace = namespace , timeout = timeout
310321 )
322+
311323 validate_batch_size (batch_size )
312- built = list (vectors )
313- batches = chunked (built , batch_size )
314- total_count = 0
315- for batch in with_progress (batches , show_progress = show_progress ):
316- result = await self ._upsert_one_batch (
317- vectors = batch , namespace = namespace , timeout = timeout
318- )
319- total_count += result .upserted_count
320- return UpsertResponse (upserted_count = total_count )
324+ require_in_range ("max_concurrency" , max_concurrency , 1 , 64 )
325+
326+ built = [VectorFactory .build (v ) for v in vectors ]
327+ items : list [dict [str , Any ]] = [_vector_to_dict (v ) for v in built ]
328+
329+ async def _operation (chunk : list [dict [str , Any ]]) -> UpsertResponse :
330+ return await self ._upsert_dict_batch (items = chunk , namespace = namespace , timeout = timeout )
331+
332+ batch_result = await async_batch_execute (
333+ items = items ,
334+ operation = _operation ,
335+ batch_size = batch_size ,
336+ max_concurrency = max_concurrency ,
337+ show_progress = show_progress ,
338+ desc = "Upserting" ,
339+ )
340+
341+ synth_headers : dict [str , str ] = {}
342+ if batch_result .response_info is not None :
343+ if batch_result .response_info .lsn_reconciled is not None :
344+ synth_headers ["x-pinecone-lsn-reconciled" ] = str (
345+ batch_result .response_info .lsn_reconciled
346+ )
347+ if batch_result .response_info .lsn_committed is not None :
348+ synth_headers ["x-pinecone-lsn-committed" ] = str (
349+ batch_result .response_info .lsn_committed
350+ )
351+ synth_response_info = ResponseInfo (raw_headers = synth_headers ) if synth_headers else None
352+ return UpsertResponse (
353+ upserted_count = batch_result .successful_item_count ,
354+ response_info = synth_response_info ,
355+ total_item_count = batch_result .total_item_count ,
356+ failed_item_count = batch_result .failed_item_count ,
357+ total_batch_count = batch_result .total_batch_count ,
358+ successful_batch_count = batch_result .successful_batch_count ,
359+ failed_batch_count = batch_result .failed_batch_count ,
360+ errors = batch_result .errors ,
361+ )
321362
322363 async def _upsert_one_batch (
323364 self ,
@@ -345,6 +386,21 @@ async def _upsert_one_batch(
345386 logger .debug ("Upserted %d vectors" , result .upserted_count )
346387 return result
347388
389+ async def _upsert_dict_batch (
390+ self ,
391+ * ,
392+ items : list [dict [str , Any ]],
393+ namespace : str ,
394+ timeout : float | None ,
395+ ) -> UpsertResponse :
396+ body : dict [str , Any ] = {"vectors" : items }
397+ if namespace :
398+ body ["namespace" ] = namespace
399+ response = await self ._http .post ("/vectors/upsert" , timeout = timeout , json = body )
400+ result = self ._adapter .to_upsert_response (response .content )
401+ result .response_info = extract_response_info (response )
402+ return result
403+
348404 async def upsert_from_dataframe (
349405 self ,
350406 df : pd .DataFrame ,
0 commit comments