Repro
res = index.search(namespace="ns", query={...})
hit = res.result.hits[0]
hit["_id"]
# KeyError: '_id'
Root cause
Hit is a msgspec Struct with rename={"id_": "_id", "score_": "_score"} — the wire format genuinely calls these fields _id/_score, and that's what raw-REST examples and response JSON show. Hit.__getitem__ supports "id", "score", and the struct fields, and raises KeyError(key) for everything else — so exactly the two names the API itself uses are the ones that fail, with no hint.
Proposed fix
Teach in the error. When the key is a known wire alias, say what to use:
KeyError: '_id' is the wire name for this field; on Hit use hit["id"] / hit.id (likewise '_score' → hit["score"] / hit.score)
and for other unknown keys, list what exists: available keys: 'id', 'score', 'fields' (record fields live in hit["fields"]).
Alternative worth considering: simply accept "_id"/"_score" as read aliases in __getitem__/__contains__, so code written against the wire JSON ports directly. Happy to go either way — the teaching KeyError is the smaller change and steers callers to the canonical names.
How we found it
Cold-agent audit for an upcoming blog post: 2 of 2 SDK-path agents wrote hit["_id"] (copying the shape they'd just upserted), hit the bare KeyError, and had to introspect/read models/vectors/search.py to discover .id/.score.
Repro
Root cause
Hitis a msgspec Struct withrename={"id_": "_id", "score_": "_score"}— the wire format genuinely calls these fields_id/_score, and that's what raw-REST examples and response JSON show.Hit.__getitem__supports"id","score", and the struct fields, and raisesKeyError(key)for everything else — so exactly the two names the API itself uses are the ones that fail, with no hint.Proposed fix
Teach in the error. When the key is a known wire alias, say what to use:
and for other unknown keys, list what exists:
available keys: 'id', 'score', 'fields' (record fields live in hit["fields"]).Alternative worth considering: simply accept
"_id"/"_score"as read aliases in__getitem__/__contains__, so code written against the wire JSON ports directly. Happy to go either way — the teaching KeyError is the smaller change and steers callers to the canonical names.How we found it
Cold-agent audit for an upcoming blog post: 2 of 2 SDK-path agents wrote
hit["_id"](copying the shape they'd just upserted), hit the bare KeyError, and had to introspect/readmodels/vectors/search.pyto discover.id/.score.