# `Perplexed.Search`
[🔗](https://github.com/agoodway/perplexed/blob/v0.2.0/lib/perplexed/search.ex#L1)

Swappable web search: every provider takes a query and returns normalized
`Perplexed.Search.Hit`s, so callers change engines by changing one option.
Built-in providers, by name, with list price per successful request (USD,
as of 2026-11-10) used for per-run cost accounting:

| Name              | Route                                | Per request |
| ----------------- | ------------------------------------ | ----------- |
| `:ceramic`        | Cloudflare Web Search, Ceramic       | $0.00025    |
| `:deepseek`       | DeepSeek Search API                  | $0.00075    |
| `:firecrawl`      | Firecrawl search, 2 Standard credits | $0.00198    |
| `:linkup`         | Cloudflare Web Search, Linkup fast   | $0.005      |
| `:cloudflare_exa` | Cloudflare Web Search, Exa auto      | $0.007      |
| `:searxng`        | Your SearXNG instances, keyless      | free        |

Firecrawl bills in credits, so its price follows the `firecrawl_credit_usd`
setting (default the Standard plan rate).

Any module implementing this behaviour can be passed instead, as `Module`
or `{Module, opts}`; its optional `c:price/1` and `c:free_when_empty?/0`
feed cost accounting.

# `spec`

```elixir
@type spec() :: atom() | module() | {module(), keyword()} | [spec(), ...]
```

# `free_when_empty?`
*optional* 

```elixir
@callback free_when_empty?() :: boolean()
```

Whether the provider does not bill a request that returns no hits.
Optional; defaults to false, so an empty result costs the list price.

# `price`
*optional* 

```elixir
@callback price(keyword()) :: float()
```

List price in USD of one successful request made with `opts` (the spec's
options). Optional; defaults to 0.

# `search`

```elixir
@callback search(String.t(), keyword()) ::
  {:ok, [Perplexed.Search.Hit.t()]} | {:error, term()}
```

# `default_provider`

```elixir
@spec default_provider() :: spec()
```

The provider used when a call names none: `config :perplexed, provider:`,
else `:ceramic`.

# `name`

```elixir
@spec name(spec()) :: atom()
```

The name a provider is reported by in results, attempts and telemetry:
a built-in name or the module, never a `{Module, opts}` spec's options,
which can hold credentials. A fallback chain is `:chain`.

# `price`

```elixir
@spec price(spec()) :: float()
```

Price in USD of one successful request to `provider`: the
`config :perplexed, prices:` override for that spec when set, else the
provider's list price (0 for a provider without `c:price/1`).

# `providers`

```elixir
@spec providers() :: [atom()]
```

The built-in provider names.

# `run`

```elixir
@spec run(spec(), String.t(), keyword()) ::
  {:ok, Perplexed.Search.Result.t()} | {:error, Perplexed.error()}
```

Searches `query` on `provider` and returns the hits with the call's cost.

`:limit` (default 8) is common to the built-in providers; Cloudflare caps
it at 10. `:credentials` (a keyword list of setting keys, such as
`firecrawl_api_key:`) overrides the configured credentials for this call.
`:req_options` is merged into each built-in provider's Req call last, so
tests can pass `plug:`. Other options go to the provider.

An unknown provider is `{:error, {:invalid_request, %{reason: {:unknown_provider, spec}}}}`.

A list of specs is a fallback chain, tried in order until one answers,
with per-provider and overall time limits (`:provider_timeout_ms`,
`:deadline_ms`, `:fallback_on`); see `Perplexed.Search.Fallback`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
