From 0430d2464af0299f593d9a853f714170275fcede Mon Sep 17 00:00:00 2001 From: joerg84 Date: Fri, 17 Jul 2026 08:58:31 -0400 Subject: [PATCH] fix(models): teach the id/score aliases when Hit is accessed with wire keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The search wire format names these fields _id and _score, and that is what upserted records and raw REST examples show — so hit['_id'] is the natural first guess, and it failed with a bare KeyError('_id'). Point the error at the aliases that exist (hit['id'] / hit.id), and list the available keys for other unknown lookups. Fixes #692 Co-Authored-By: Claude Fable 5 --- pinecone/models/vectors/search.py | 13 ++++++++++++- tests/unit/test_search_models.py | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pinecone/models/vectors/search.py b/pinecone/models/vectors/search.py index 05e0cc95..3a334127 100644 --- a/pinecone/models/vectors/search.py +++ b/pinecone/models/vectors/search.py @@ -83,6 +83,9 @@ class SearchUsage(StructDictMixin, Struct, kw_only=True): rerank_units: int | None = None +_WIRE_KEY_ALIASES = {"_id": "id", "_score": "score"} + + class Hit(StructDictMixin, Struct, kw_only=True, rename={"id_": "_id", "score_": "_score"}): """A single search result hit. @@ -117,7 +120,15 @@ def __getitem__(self, key: str) -> Any: if key == "score": return self.score_ if key not in self.__struct_fields__: - raise KeyError(key) + alias = _WIRE_KEY_ALIASES.get(key) + if alias is not None: + raise KeyError( + f"{key!r} is the wire name for this field; use hit[{alias!r}] or hit.{alias}" + ) + raise KeyError( + f"{key!r}; available keys are 'id', 'score', and 'fields' " + "(record fields live in hit['fields'])" + ) return getattr(self, key) def __contains__(self, key: object) -> bool: diff --git a/tests/unit/test_search_models.py b/tests/unit/test_search_models.py index b157ffc0..40ed3e12 100644 --- a/tests/unit/test_search_models.py +++ b/tests/unit/test_search_models.py @@ -35,6 +35,29 @@ def test_hit_bracket_missing_key(self) -> None: with pytest.raises(KeyError, match="nonexistent"): hit["nonexistent"] + def test_hit_bracket_missing_key_lists_available_keys(self) -> None: + hit = Hit(id_="r1", score_=0.5) + with pytest.raises(KeyError) as excinfo: + hit["nonexistent"] + assert "available keys" in str(excinfo.value) + + def test_hit_bracket_wire_name_id_teaches_alias(self) -> None: + """The wire format calls the field '_id'; the error must point at hit['id'].""" + hit = Hit(id_="r1", score_=0.5) + with pytest.raises(KeyError) as excinfo: + hit["_id"] + message = str(excinfo.value) + assert "wire name" in message + assert "hit['id']" in message + + def test_hit_bracket_wire_name_score_teaches_alias(self) -> None: + hit = Hit(id_="r1", score_=0.5) + with pytest.raises(KeyError) as excinfo: + hit["_score"] + message = str(excinfo.value) + assert "wire name" in message + assert "hit['score']" in message + class TestSearchUsage: def test_search_usage_required_only(self) -> None: