"""What the data-plane read and write operations hand back."""
from __future__ import annotations
from collections.abc import Iterator
from typing import Any
from msgspec import Struct
from pinecone.models._mixin import DictLikeStruct, StructDictMixin
from pinecone.models.batch import BatchError
from pinecone.models.response_info import ResponseInfo as ResponseInfo # re-export
from pinecone.models.vectors.usage import Usage
from pinecone.models.vectors.vector import ScoredVector, Vector
[docs]
class UpsertResponse(DictLikeStruct, Struct, rename="camel", kw_only=True, gc=False):
"""What an upsert wrote, and — for a batched upsert — what it failed to write.
Which fields carry information depends on how you called upsert. Without
``batch_size`` the client sends one request, so ``upserted_count`` is the whole
answer and every batch counter is ``0``. With ``batch_size`` the client splits the
vectors into requests and sends them one at a time; a later request can fail after
earlier ones succeeded, so the batch counters and ``errors`` describe a partial
success and ``upserted_count`` covers only the batches that landed.
Attributes:
upserted_count (int): Vectors the server accepted. Equals ``total_item_count``
when every batch succeeded, and for a non-batched call.
response_info (ResponseInfo | None): HTTP response metadata (request ID, LSN
values), or ``None`` if not populated.
total_item_count (int): Vectors you submitted, across every batch. ``0`` for a
non-batched call.
failed_item_count (int): Vectors that were in a batch that failed. Not all of
them were necessarily rejected individually — the batch is the unit.
total_batch_count (int): Batches the client sent. ``0`` for a non-batched call.
successful_batch_count (int): Batches the server accepted.
failed_batch_count (int): Batches that failed.
errors (list[BatchError]): One entry per failed batch, carrying the underlying
error and the items that batch held. Empty when nothing failed.
Examples:
A single upsert reports one number.
.. code-block:: python
response = idx.upsert(vectors=[("article-101", [0.12, 0.34, 0.56])])
print(response.upserted_count)
A batched upsert can partly succeed, so check :attr:`has_errors` before treating
``upserted_count`` as the full count. :attr:`failed_items` flattens the items from
every failed batch back into a list you can resubmit.
.. code-block:: python
response = idx.upsert(vectors=vectors, batch_size=100)
if response.has_errors:
print(response.upserted_count, "of", response.total_item_count)
retry = idx.upsert(vectors=response.failed_items, batch_size=100)
.. seealso::
:doc:`/guides/performance` — choosing a ``batch_size``.
"""
upserted_count: int
response_info: ResponseInfo | None = None
total_item_count: int = 0
failed_item_count: int = 0
total_batch_count: int = 0
successful_batch_count: int = 0
failed_batch_count: int = 0
errors: list[BatchError] = []
@property
def _response_info(self) -> ResponseInfo | None:
return self.response_info
@property
def has_errors(self) -> bool:
"""``True`` if any batch failed, so ``upserted_count`` is a partial count."""
return len(self.errors) > 0
@property
def error_count(self) -> int:
"""Alias for :attr:`failed_item_count`, spelled as ``BatchResult`` spells it."""
return self.failed_item_count
@property
def success_count(self) -> int:
"""Alias for :attr:`upserted_count`, spelled as ``BatchResult`` spells it."""
return self.upserted_count
@property
def successful_item_count(self) -> int:
"""Alias for :attr:`upserted_count`, spelled as ``BatchResult`` spells it."""
return self.upserted_count
@property
def failed_items(self) -> list[dict[str, Any]]:
"""Every item from every failed batch, flattened into one list you can resubmit.
Empty when :attr:`has_errors` is ``False``. Items that were in a successful batch
are never included, so passing this straight back to ``upsert`` retries only the
writes that did not land.
"""
items: list[dict[str, Any]] = []
for error in self.errors:
items.extend(error.items)
return items
def __repr__(self) -> str:
if not self.has_errors and self.total_batch_count == 0:
return f"UpsertResponse(upserted_count={self.upserted_count})"
status = "PARTIAL FAILURE" if self.has_errors else "SUCCESS"
return (
f"UpsertResponse({status}: "
f"{self.upserted_count}/{self.total_item_count} items, "
f"{self.successful_batch_count}/{self.total_batch_count} batches)"
)
[docs]
class QueryResponse(DictLikeStruct, Struct, rename="camel", kw_only=True, gc=False):
"""The ranked matches a query found.
Almost everything you want is in ``matches``: a list of
:class:`~pinecone.models.vectors.vector.ScoredVector`, already ordered so
``matches[0]`` is the closest hit. Read each match through ``.id``, ``.score``,
``.values`` and ``.metadata``. The last two come back empty or ``None`` unless the
query passed ``include_values=True`` / ``include_metadata=True``, so a missing value
there is far more often an unset flag than an empty stored vector.
A query that matched nothing returns an empty ``matches`` rather than raising, so
check the length instead of catching an exception.
Attributes:
matches (list[ScoredVector]): The hits, ordered from most to least similar.
namespace (str): The namespace that was queried; ``""`` for the default namespace.
usage (Usage | None): Read units this query consumed, or ``None`` if not reported.
response_info (ResponseInfo | None): HTTP response metadata (request ID, LSN
values), or ``None`` if not populated.
Examples:
.. code-block:: python
response = idx.query(
top_k=5,
vector=[0.012, -0.087, 0.153],
namespace="articles-en",
include_metadata=True,
)
for match in response.matches:
print(match.id, match.score, match.metadata)
No match is not an error:
.. code-block:: python
if not response.matches:
print("nothing above the cutoff in", response.namespace)
.. seealso::
:class:`~pinecone.models.vectors.search.SearchRecordsResponse` — what ``search``
returns instead, where the hits sit under ``result.hits`` and carry ``fields``
rather than ``values`` and ``metadata``.
"""
matches: list[ScoredVector] = []
namespace: str | None = ""
usage: Usage | None = None
response_info: ResponseInfo | None = None
@property
def _response_info(self) -> ResponseInfo | None:
return self.response_info
def __post_init__(self) -> None:
"""Read a null ``namespace`` back as ``""``, so the default namespace has one spelling."""
if self.namespace is None:
self.namespace = ""
[docs]
class FetchResponse(DictLikeStruct, Struct, rename="camel", kw_only=True, gc=False):
"""The vectors a fetch retrieved, keyed by ID.
``vectors`` is a dict, not a list, so look a vector up by the ID you asked for. An ID
that does not exist in the namespace is simply absent from the dict — fetching a
missing ID is not an error — so use ``.get()`` or test membership rather than
indexing blind. Unlike a query, a fetch always returns values and metadata; there is
nothing to opt into.
Attributes:
vectors (dict[str, Vector]): Vector ID to :class:`~pinecone.models.vectors.vector.Vector`,
for the requested IDs that exist.
namespace (str): The namespace the vectors were fetched from.
usage (Usage | None): Read units this fetch consumed, or ``None`` if not reported.
response_info (ResponseInfo | None): HTTP response metadata (request ID, LSN
values), or ``None`` if not populated.
Examples:
.. code-block:: python
wanted = ["article-101", "article-102"]
response = idx.fetch(ids=wanted, namespace="articles-en")
for vector_id, vector in response.vectors.items():
print(vector_id, len(vector.values), vector.metadata)
print("not stored:", [vid for vid in wanted if vid not in response.vectors])
.. seealso::
:class:`FetchByMetadataResponse` — what you get when you select the vectors by
metadata filter rather than by ID, which can span more than one page.
"""
vectors: dict[str, Vector] = {}
namespace: str = ""
usage: Usage | None = None
response_info: ResponseInfo | None = None
@property
def _response_info(self) -> ResponseInfo | None:
return self.response_info
[docs]
class NamespaceSummary(StructDictMixin, Struct, rename="camel", kw_only=True, gc=False):
"""The per-namespace entry in :class:`DescribeIndexStatsResponse`.
Attributes:
vector_count (int): Vectors in this namespace.
"""
vector_count: int = 0
[docs]
class DescribeIndexStatsResponse(StructDictMixin, Struct, rename="camel", kw_only=True, gc=False):
"""How much is in an index, and how it is configured, as of this call.
The usual reason to call ``describe_index_stats`` is to find out which namespaces
exist and how many vectors each holds — ``namespaces`` answers both, and its keys are
the namespace names you can pass to a query. Counts are eventually consistent, so a
vector you just upserted may not be reflected yet.
Attributes:
namespaces (dict[str, NamespaceSummary]): Namespace name to its
:class:`NamespaceSummary`. The default namespace appears under ``""``.
dimension (int | None): Length of the dense vectors this index stores, or ``None``
for an index with no dense field.
index_fullness (float): How full the index is, from ``0.0`` to ``1.0``.
total_vector_count (int): Vectors across every namespace.
metric (str | None): The similarity function used when ranking, e.g. ``"cosine"``,
or ``None`` if not reported.
vector_type (str | None): ``"dense"`` or ``"sparse"``, or ``None`` if not reported.
memory_fullness (float | None): How full memory is, or ``None`` if not reported.
storage_fullness (float | None): How full storage is, or ``None`` if not reported.
response_info (ResponseInfo | None): HTTP response metadata (request ID, LSN
values), or ``None`` if not populated.
Examples:
.. code-block:: python
stats = idx.describe_index_stats()
print(stats.total_vector_count, stats.dimension)
for name, summary in stats.namespaces.items():
print(name or "(default)", summary.vector_count)
"""
namespaces: dict[str, NamespaceSummary] = {}
dimension: int | None = None
index_fullness: float = 0.0
total_vector_count: int = 0
metric: str | None = None
vector_type: str | None = None
memory_fullness: float | None = None
storage_fullness: float | None = None
response_info: ResponseInfo | None = None
@property
def _response_info(self) -> ResponseInfo | None:
return self.response_info
def __repr__(self) -> str:
parts = []
if self.dimension is not None:
parts.append(f"dimension={self.dimension!r}")
parts.append(f"total_vector_count={self.total_vector_count!r}")
if self.metric is not None:
parts.append(f"metric={self.metric!r}")
parts.append(f"namespaces={len(self.namespaces)!r}")
return f"DescribeIndexStatsResponse({', '.join(parts)})"
def __getitem__(self, key: str) -> Any:
"""Read a field by name, so ``stats["dimension"]`` works as well as ``stats.dimension``.
Raises:
KeyError: If *key* is not one of this model's fields.
"""
if key not in self.__struct_fields__:
raise KeyError(key)
return getattr(self, key)
def __contains__(self, key: object) -> bool:
"""Report whether *key* names a field, so ``"dimension" in stats`` works."""
return key in self.__struct_fields__
[docs]
class ListItem(StructDictMixin, Struct, kw_only=True, gc=False):
"""One entry in :attr:`ListResponse.vectors` — an ID and nothing else.
``list`` walks the IDs in a namespace without reading the vectors themselves, so
there are no values or metadata here. Fetch the IDs you care about to get those.
Attributes:
id (str | None): The vector identifier, or ``None`` if the entry carried none.
"""
id: str | None = None
[docs]
class ListResponse(StructDictMixin, Struct, rename="camel", kw_only=True, gc=False):
"""One page of vector IDs from a namespace.
Each element of ``vectors`` is a :class:`ListItem` carrying only an ``id``. The
response is also directly iterable and sized, so ``for item in response`` and
``len(response)`` walk that same page.
Attributes:
vectors (list[ListItem]): The ID entries on this page.
pagination (Pagination | None): Token for the next page, or ``None`` when this is
the last page.
namespace (str): The namespace the IDs were listed from.
usage (Usage | None): Read units this page consumed, or ``None`` if not reported.
response_info (ResponseInfo | None): HTTP response metadata (request ID, LSN
values), or ``None`` if not populated.
Examples:
.. code-block:: python
page = idx.list_paginated(prefix="article-", namespace="articles-en")
for item in page.vectors:
print(item.id)
.. seealso::
:doc:`/guides/pagination` — and ``Index.list``, which yields every page for you.
"""
vectors: list[ListItem] = []
pagination: Pagination | None = None
namespace: str = ""
usage: Usage | None = None
response_info: ResponseInfo | None = None
@property
def _response_info(self) -> ResponseInfo | None:
return self.response_info
def __getitem__(self, key: int | str) -> Any:
"""Index into the page's items, or read a field by name.
Args:
key (int | str): An integer position in ``vectors``, or the name of a field
on this response.
Returns:
The :class:`ListItem` at that position, or the named field's value.
Raises:
KeyError: If a string *key* does not name a field.
IndexError: If an integer *key* is past the end of this page.
"""
if isinstance(key, int):
return self.vectors[key]
if key not in self.__struct_fields__:
raise KeyError(key)
return getattr(self, key)
def __contains__(self, key: object) -> bool:
"""Report field-name membership for a string, and item membership otherwise."""
if isinstance(key, str):
return key in self.__struct_fields__
return key in self.vectors
def __len__(self) -> int:
return len(self.vectors)
def __iter__(self) -> Iterator[ListItem]: # type: ignore[override]
return iter(self.vectors)
[docs]
class UpsertRecordsResponse(StructDictMixin, Struct, kw_only=True, gc=False):
"""Acknowledgement that ``upsert_records`` was accepted.
``upsert_records`` embeds text server-side and the response body carries no counts, so
``record_count`` is what the client sent rather than what the server confirmed. Read
it as "the request went out with this many records", and call
``describe_index_stats`` if you need a count the index vouches for.
Attributes:
record_count (int): Records the client submitted. A client-side count.
response_info (ResponseInfo | None): HTTP response metadata (request ID, LSN
values), or ``None`` if not populated.
"""
record_count: int
response_info: ResponseInfo | None = None
@property
def _response_info(self) -> ResponseInfo | None:
return self.response_info
def __getitem__(self, key: str) -> Any:
"""Read a field by name, so ``response["record_count"]`` works too.
Raises:
KeyError: If *key* is not one of this model's fields.
"""
if key not in self.__struct_fields__:
raise KeyError(key)
return getattr(self, key)
def __contains__(self, key: object) -> bool:
"""Report whether *key* names a field on this response."""
return key in self.__struct_fields__
[docs]
class UpdateResponse(DictLikeStruct, Struct, rename="camel", kw_only=True, gc=False):
"""Acknowledgement that an update was accepted, and how many vectors it matched.
Attributes:
matched_records (int | None): Vectors the update matched, or ``None`` when no
count was reported. A by-filter update is the case that reports one; pass
``dry_run=True`` to get the count without applying the change. Updates apply
asynchronously, so a count here is a point-in-time figure rather than a
guarantee that the writes have landed.
response_info (ResponseInfo | None): HTTP response metadata (request ID, LSN
values), or ``None`` if not populated.
"""
matched_records: int | None = None
response_info: ResponseInfo | None = None
@property
def _response_info(self) -> ResponseInfo | None:
return self.response_info