Search
Find records by an exact-match search over a searchable field, by value or by blind index.
POST /v1/collections/:name/search finds records whose value on one field matches
exactly. The field must be declared searchable in the schema.
How you search depends on the field's sensitivity:
- a
plainfield is searched byvalue— it is stored in the clear, and the server compares it directly; - a
masked,tokenisedorclient_encryptedfield is searched by a blind index — bytes you compute on your own device from a key the server never holds. The server matches on the index and never computes one itself. The blind index is produced on an enrolled device by thecustodiancommand line; see Using the HTTP API.
You must hold a read grant on the field being searched; without it, a search is refused (see below).
Searching a plain field by value
POST /v1/collections/policies/search
Authorization: Bearer <token>
Content-Type: application/json
{ "field": "policy_number", "value": "PY-4471" }{
"field": "policy_number",
"index_bits": null,
"truncated": false,
"results": [
{ "id": "018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70", "token": null }
]
}Searching an encrypted field by blind index
POST /v1/collections/policyholders/search
Authorization: Bearer <token>
Content-Type: application/json
{ "field": "sa_id_number", "blind_index": "<base64>" }{
"field": "sa_id_number",
"index_bits": 16,
"truncated": true,
"results": [
{ "id": "018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70", "token": "tok_9Xa1..." }
]
}Reading the result
A result is deliberately thin: for each matching record it carries the record's identifier and the searched field's token, and nothing else. Returning the records' field values would turn a search into a way to harvest values a caller never identified.
Two fields make a candidate set unmistakable from an exact match:
index_bitsis the width of the blind index the match used (nullfor aplainfield, which is matched exactly);truncatedistruewhen that width is below the full width, so the results are a candidate set, not a confirmed match. A truncated index deliberately maps many values to the same index; you resolve the candidates' tokens to filter them on your device.
A search on a client_encrypted searchable field succeeds but carries a warning, so
the trade is visible where it is made:
{
"warning": "Field \"sa_id_number\" is client_encrypted and declared searchable. The server cannot read its values, but its blind index still discloses equality and frequency to anyone holding both the database and the index key; on a structured identifier that is enough to enumerate the value. This is a trade the schema may have made knowingly — the doctor command flags it as a misconfiguration."
}The warning is advisory: the search still succeeds and returns its results. It marks
that declaring a client_encrypted field searchable weakens the field — the blind
index discloses equality and frequency even though the value itself stays unreadable.
If you did not intend that trade, review the field's declaration in the
schema; nothing about the search call itself needs to change
to clear the warning.
The refusals
-
supplying a
valueon a field that requires a blind index issearch_value_where_index_required:{ "code": "search_value_where_index_required", "message": "Field \"sa_id_number\" is client_encrypted, whose value the server never holds, so it is searched by \"blind_index\" — computed on your device — not by a value." } -
supplying a
blind_indexon aplainfield issearch_index_where_value_required:{ "code": "search_index_where_value_required", "message": "Field \"policy_number\" is plain and stored in the clear, so it is searched by \"value\", not by a blind index." }
A field not in the collection is field_not_in_collection (422). A field that is
not searchable, a missing field, or a search on a field you hold no read grant on,
is a plain 422 or 403 naming the field, with no error code.