> ## Documentation Index
> Fetch the complete documentation index at: https://docs.minerva.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Person Search

> Find people from a natural-language audience description. Returns matching Minerva PIDs plus contact-channel coverage stats.

Person Search accepts a plain-English audience description ("CFOs at Series
B fintech startups in NYC") and returns the matching Minerva PIDs plus
coverage statistics for how many of them have email, phone, LinkedIn, etc.

```python theme={null}
from minerva import Minerva

mc = Minerva()

# 1. Kick off a search
resp = mc.api.person_search(
    query="CFOs at Series B fintech startups in NYC",
    size=100,
)
resp.search_id                                  # str — store to re-fetch later
resp.results                                    # list[str] of Minerva PIDs
resp.total_count                                # full match count (may exceed size)
resp.total_contact_coverage.professional_email  # CoverageStat(count, percent)

# 2. Fetch the same search later by id
again = mc.api.person_search_get(resp.search_id)

# 3. Check your per-day usage
usage = mc.api.person_search_usage()
usage.current_day_remaining                     # remaining records in today's quota
```

## SDK wrapper is sync-only

`AsyncMinerva.api.person_search` (and its `_get` / `_usage` siblings)
raise `NotImplementedError`. The endpoint runs for several seconds per
call server-side and is metered per UTC day, so allowing concurrent
fan-out via `asyncio.gather(...)` would make it trivial to drain the
daily quota in a single client-side mistake. Use the sync `Minerva`
client.

```python theme={null}
async with AsyncMinerva() as mc:
    await mc.api.person_search("...")
    # raises NotImplementedError: AsyncMinerva.api.person_search is not
    # supported — person-search is sync-only by design. Use Minerva().
```

## Async REST workflow

The REST API also provides a segment-backed async workflow for searches that
need the complete matching PID set. It is not yet wrapped by `Minerva` or
`AsyncMinerva`.

1. [Submit an async search](/api-reference/person-search-job-submit).
2. [Poll status and inspect the segment-size estimate](/api-reference/person-search-job-status).
3. [Unlock the full segment and page its PIDs](/api-reference/person-search-job-results).

The results endpoint returns at most 100 PIDs per page. The initial unlock
applies to the entire completed segment—not only the first page—and charges
only organization-level net-new PIDs for the current billing year. Subsequent
`GET` pages do not re-bill.

## Validation (before any HTTP call)

The wrapper validates locally first, so customer-side bugs surface as
`MinervaValidationError` without burning a quota credit:

* `query` must be 1–500 characters
* `size` must be in 1–100

```python theme={null}
mc.api.person_search("", dry_run=True)
# MinervaValidationError: query — String should have at least 1 character
```

Pass `dry_run=True` to validate + get back the `PersonSearchRequest` model
without firing the call.

## Retrying transient errors

Person Search is one of Minerva's slowest endpoints — it makes its own
upstream LLM calls to parse your query, which can take several seconds
and is more exposed to transient network drops than the other data-plane
methods. The SDK does not auto-retry, but it classifies network drops,
read/connect timeouts, and 5xx responses as `MinervaTransientError` — a
subclass of `MinervaAPIError` — so you can write a tight retry loop
without catching things you don't want to retry (auth failures, input
validation, etc.).

The recommended pattern uses [`tenacity`](https://tenacity.readthedocs.io):

```python theme={null}
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type,
)
from minerva import Minerva, MinervaTransientError

mc = Minerva()

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=1, max=10),
    retry=retry_if_exception_type(MinervaTransientError),
)
def search_with_retry(query: str, size: int = 100):
    return mc.api.person_search(query, size=size)

resp = search_with_retry("CFOs at Series B fintech startups in NYC")
```

`MinervaTransientError` covers:

* Connection drops mid-response (the `RemoteDisconnected` family)
* Connect / read / write timeouts
* All `5xx` HTTP responses

It does **not** cover (correctly, since retrying makes no difference):

* `MinervaValidationError` — local input validation failed
* `MinervaAuthError` — `401/403` API-key issue
* `MinervaRateLimitError` — `429`, use the `retry_after` field on the
  exception to back off, then retry; don't bury it in a generic retry loop
* Other `MinervaAPIError` subclasses for `4xx` responses — your input is
  the problem

## Writing good queries

See the API reference for the canonical guide:
**[Writing good queries →](/api-reference/person-search#writing-good-queries)**

The page documents:

* Supported attribute categories (role, industry, company, career history, location, education, age, income, contact channels)
* Three worked examples with progressively more complexity
* Tips on how to phrase queries the parser handles well

## Feeding results into other SDK methods

Person Search returns PIDs. Plug them straight into `mc.api.enrich` to get
full profiles for the matched audience — no second resolve hop needed:

```python theme={null}
search = mc.api.person_search("Marketing directors at SaaS in the Bay Area")
profiles = mc.api.enrich(
    [{"record_id": pid, "minerva_pid": pid} for pid in search.results],
)
```

This is the fastest path from "describe an audience" → "have their profile
data" — the enrich call is keyed by PID, so it skips the match pipeline
server-side (same optimization as
[`mc.workflows.resolve_then_enrich`](/sdk/guides/workflows)).

## Full schema

The request body fields (`query`, `size`), response shape (`search_id`,
`results`, `total_count`, contact-coverage stats), and error responses are
documented under the API Reference:

* **[POST /person-search/v0/search →](/api-reference/person-search)** — kick off a search
* **[GET /person-search/v0/search/\<id> →](/api-reference/person-search-get)** — fetch a prior search
* **[Async Person Search →](/api-reference/person-search-job-submit)** — create, poll, unlock, and page a full segment (REST; not yet wrapped in the SDK)
* **[GET /person-search/v0/usage →](/api-reference/person-search-usage)** — per-day usage tallies
* **[Contact Append / Prospect →](/api-reference/person-search-contact-append)** — async ABM / NAICS contact jobs (REST; not yet wrapped in the SDK)
