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

# Search

> How POST /v1/search works: query variants, result limits, filters, freshness and source policies, and the ranking contract.

`POST /v1/search` runs ranked retrieval over canonical web documents. Every result includes a `doc_id` and `canonical_url` you can pass straight to [/v1/document](/concepts/documents), plus [provenance](/concepts/provenance) fields you can cite when you need auditability.

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -s https://alpha.api.trycaesar.com/v1/search \
    -H "Authorization: Bearer $CAESAR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "query": "postgres 17 logical replication failover",
      "max_results": 5
    }'
  ```

  ```python Python theme={"system"}
  from caesar_search import Caesar

  client = Caesar()  # reads CAESAR_API_KEY
  results = client.search(
      "postgres 17 logical replication failover",
      max_results=5,
  )
  for r in results.results or []:
      print(r.rank, r.title, r.canonical_url)
  ```

  ```typescript TypeScript theme={"system"}
  import { Caesar } from "caesar-search";

  const caesar = new Caesar(); // reads CAESAR_API_KEY

  const results = await caesar.search("postgres 17 logical replication failover", {
    maxResults: 5,
  });

  for (const result of results.results ?? []) {
    console.log(result.rank, result.title, result.canonical_url);
  }
  ```

  ```bash CLI theme={"system"}
  caesar-search search "postgres 17 logical replication failover" --max-results 5 --json
  ```
</CodeGroup>

These examples use `CAESAR_API_KEY`; see [authentication](/authentication) for key setup.

Reranking is a modular second stage and never fails a request: if it is unavailable, results fall back to first-stage order with warning `rerank_unavailable` and `ranking.ranker_version` reports `first_stage_order_v1`.

## Query and query variants

`query` is the only required field — the original user or agent question. `search_queries` optionally supplies your own rewrites as an array of strings: the first entry, when non-empty, replaces `query` as the candidate first-stage query, and the full list is given as context to the server-side query transform. If your agent already produced good rewrites, send them — you skip the server-side rewrite step.

## max\_results

`max_results` is an integer from 1 to 50, default 10. It is the complete result window — there is no cursor or offset pagination. The server fills the window itself by paging first-stage retrieval internally and deduplicating URLs, so asking for 50 returns the largest available result window; you never page manually.

You may still receive fewer than `max_results` when sources run dry. That is not an error and does not set the envelope `truncated` flag — `truncated` refers only to [response-shaping budget sheds](/concepts/response-shaping).

## Filters

| Field                 | Type                  | Effect                                                                                |
| --------------------- | --------------------- | ------------------------------------------------------------------------------------- |
| `filters.country`     | string (single value) | forwarded to first-stage retrieval                                                    |
| `filters.language`    | string (single value) | forwarded to first-stage retrieval                                                    |
| `filters.exact_match` | boolean               | wraps the candidate query in double quotes (exact phrase) unless it is already quoted |

## Freshness policy

| Field                              | Accepts                                                               | Behavior                                                                                                                                                                       |
| ---------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `freshness_policy.published_after` | RFC3339 timestamp or `YYYY-MM-DD`                                     | mapped to a coarse recency window — past day, past week, past month, past year, or an explicit `YYYY-MM-DDtoYYYY-MM-DD` range for older dates. Unparseable values are ignored. |
| `freshness_policy.freshness`       | raw window token: `pd`, `pw`, `pm`, `py`, or `YYYY-MM-DDtoYYYY-MM-DD` | passed through to the search infrastructure; used only when `published_after` is absent                                                                                        |

## Source policy

| Field                                | Type      | Effect                                                                                                                                |
| ------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `source_policy.include_domains`      | string\[] | with exactly one domain, the first-stage query is scoped with a `site:` operator; all entries also act as a post-retrieval allow list |
| `source_policy.exclude_domains`      | string\[] | post-retrieval deny list                                                                                                              |
| `source_policy.require_domain_match` | boolean   | when true and `include_domains` is non-empty, results not matching the allow list are dropped                                         |

Domain matching is host-suffix based: `example.com` matches `example.com` and any subdomain; a leading `www.` is ignored. When the policy removes results, the response carries warning `source_policy_filtered` with `details.filtered_results` counting what was dropped — so a thin result set is explainable rather than silent.

## The ranking block

Successful responses include a `ranking` object describing how results were ordered:

| Field            | Values                                                                                                           |
| ---------------- | ---------------------------------------------------------------------------------------------------------------- |
| `ranker_version` | `first_stage_order_v1` (first-stage order, no rerank) or `reranked_v1` (second-stage reranker reordered results) |
| `score_scope`    | always `response_local`                                                                                          |

<Warning>
  Scores are response-local. A result `score` (an object like `{"value": 0.87}`, present only when the rerank stage scored that result) is comparable to other scores in the same response only — never across responses or ranker versions. Do not persist scores as absolute relevance.
</Warning>

Replayed searches may be served from cache; deterministic ordering, identical scores, and identical `search_id` values are not guaranteed across calls.

## Analytics request field

`client_model` is accepted for analytics and tuning, but is **not forwarded into retrieval**. Sending it is harmless; expecting retrieval behavior from it is a bug.

## A full request

Every implemented request field in one call (the `response` block is covered in [response shaping](/concepts/response-shaping)):

```bash cURL theme={"system"}
curl -s https://alpha.api.trycaesar.com/v1/search \
  -H "Authorization: Bearer $CAESAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "postgres 17 logical replication failover",
    "search_queries": ["postgres 17 failover slot synchronization"],
    "max_results": 20,
    "filters": { "country": "us", "language": "en", "exact_match": false },
    "freshness_policy": { "published_after": "2024-09-01" },
    "source_policy": {
      "include_domains": ["postgresql.org"],
      "require_domain_match": true
    },
    "response": { "verbosity": "standard" }
  }'
```

## The response

Trimmed to one result, at the default `standard` verbosity:

```json theme={"system"}
{
  "request_id": "5f0c2c4e-1d2a-4f6b-9e0a-7c1b2d3e4f50",
  "search_id": "a7e2b9c1-3d4f-4a5b-8c6d-9e0f1a2b3c4d",
  "session_id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "access": { "rate_limit": { "limit_rps": 100, "remaining": 99, "reset_at": "2026-06-12T10:00:01Z" } },
  "ranking": { "ranker_version": "reranked_v1", "score_scope": "response_local" },
  "results": [
    {
      "rank": 1,
      "doc_id": "0c944fa8-4c8f-4f48-9b08-0fb2fd3438ec",
      "canonical_url": "https://www.postgresql.org/docs/17/logical-replication-failover.html",
      "source_url": "https://www.postgresql.org/docs/17/logical-replication-failover.html",
      "title": "Logical Replication Failover",
      "description": "How to ensure logical replication continues after failover…",
      "snippet": "When the publisher server fails, the subscriber can be redirected…",
      "score": { "value": 0.87 },
      "metadata": {
        "first_seen_at": "2026-06-12T09:58:00Z",
        "last_seen_at": "2026-06-12T09:58:00Z",
        "last_crawled_at": "2026-06-12T09:58:00Z",
        "extracted_at": "2026-06-12T09:58:00Z",
        "content_digest": "sha256:3f6e…",
        "published_at": "2024-09-26T00:00:00Z"
      },
      "passages": [
        { "passage_id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70", "doc_id": "0c944fa8-4c8f-4f48-9b08-0fb2fd3438ec", "ordinal": 1, "text": "When the publisher server fails…" }
      ]
    }
  ],
  "usage": { "requests": 1, "bytes_returned": 2148 }
}
```

Notes on the envelope:

* `search_id` identifies the ranked list and can be used with `/v1/feedback`. `session_id` echoes yours or is server-generated — see [sessions](/concepts/sessions).
* Per-result `provenance` (`capture_id`, `capture_time`) appears only at `response.verbosity: "full"` — the default `standard` omits it. See [response shaping](/concepts/response-shaping).
* Error responses use a shared envelope with stable machine codes — see [errors](/concepts/errors). Full field schemas live in the [API reference](/api-reference/index).
