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

# Search recipes

> Working filter combinations for both search endpoints, plus how relevance, quoted phrases, field selection, and facets behave.

There are two search endpoints over the same index:

* [`GET /api/jobs`](/docs/api-reference/jobs/list-jobs) — query-string filters, comma-separated multi-values. Best for URL-shareable queries and lightweight integrations.
* [`POST /api/jobs/search`](/docs/api-reference/jobs/search-jobs) — typed JSON body with include/exclude and range filters. Best for filter UIs and anything programmatic.

Both are page-based and relevance-ranked. To export the whole corpus, use the [feed](/docs/guides/sync-a-database) instead — it is cheaper per job and immune to records shifting between pages.

<Warning>
  Filter values are matched **literally**. An unrecognised value is not an error — it returns `200` with `total: 0`. Send the canonical keys from [Enums](/docs/api-reference/jobs/object#enums), not the display values that come back on a job.
</Warning>

***

## Parameter equivalence

| Concept          | `GET` query param                       | `POST` body field                       |
| ---------------- | --------------------------------------- | --------------------------------------- |
| Text query       | `q` (single string)                     | `queries` (string array, OR'd)          |
| Location         | `location` (single string)              | `locations` (string array)              |
| Sources          | `sources` (comma-separated)             | `sources` (string array)                |
| Work model       | `work_model` (comma-separated)          | `work_models` (string array)            |
| Employment type  | `employment_type` (comma-separated)     | `employment_types` (string array)       |
| Experience level | `experience_level` (comma-separated)    | `experience_levels` (string array)      |
| Skills           | `skills` (comma-separated)              | `skills` (include/exclude object)       |
| Industries       | `industries` (comma-separated)          | `industries` (include/exclude object)   |
| Companies        | — *(not available on GET)*              | `companies` (include/exclude object)    |
| Salary           | `min_salary_usd`, `max_salary_usd`      | `salary_usd` (`{min, max}` object)      |
| Posted window    | `posted_after`, `posted_before`         | `posted_after`, `posted_before`         |
| Indexed window   | `discovered_after`, `discovered_before` | `discovered_after`, `discovered_before` |
| Search body text | `search_description` (default `true`)   | `search_description`                    |
| Facet selection  | `include_facets` (comma-separated)      | `include_facets` (string array)         |
| Field selection  | `include_fields` (comma-separated)      | `include_fields` (string array)         |
| Pagination       | `page` (1), `page_size` (25, max 100)   | `page`, `page_size`                     |

<Note>
  `POST` array fields are **not** comma-split. `"employment_types": ["full-time,contract"]` is one literal value and matches nothing — write `["full-time", "contract"]`.
</Note>

***

## How text queries match

By default a query is a **broad relevance search**: each term is matched against the job title, company name, popular skills, and summary, with typo tolerance. That maximises recall, so `data engineer` can also surface roles whose title is not "data engineer" because the words appear elsewhere.

Set `search_description=false` to keep the query out of the description body — noticeably faster and much more precise.

### Quoted phrases match titles only

Wrap a query in **double quotes** to switch that entry to a strict match: the exact phrase must appear contiguously in the **job title**. Typo tolerance and partial-word matching are off.

```json theme={null}
{ "queries": ["\"Quantitative Developer\"", "\"Quant Developer\""] }
```

Only jobs whose *title* contains either phrase are returned. You can mix quoted and unquoted entries — each is evaluated independently and the results combined:

```json theme={null}
{ "queries": ["\"Quant Developer\"", "machine learning engineer"] }
```

On `GET /api/jobs` pass the quotes inside the value: `?q="Quantitative Developer"`.

<Note>
  A request may carry at most **10 positive terms** in `queries`. Exceeding that returns a real `400` rather than silently dropping the extras, so you always know exactly what was searched. Negative `-` exclusions do not count toward the limit.
</Note>

***

## Recipes

### Keyword plus location

```bash theme={null}
curl "https://connect.jobo.world/api/jobs?q=data+engineer&location=Seattle&page_size=10" \
  -H "X-Api-Key: $JOBO_API_KEY"
```

```python theme={null}
from jobo_enterprise import JoboClient

with JoboClient(api_key="YOUR_API_KEY") as client:
    results = client.search.search(q="data engineer", location="Seattle", page_size=10)
    print(f"{results.total} matches across {results.total_pages} pages")
    for job in results.jobs:
        print(f"  {job.title} — {job.company.name} ({job.workplace_type})")
```

### Remote senior roles in a salary band

Remember: `senior`, not `senior level`.

```bash theme={null}
curl -G "https://connect.jobo.world/api/jobs" \
  -H "X-Api-Key: $JOBO_API_KEY" \
  --data-urlencode "q=backend engineer" \
  --data-urlencode "work_model=remote" \
  --data-urlencode "experience_level=senior" \
  --data-urlencode "employment_type=full-time" \
  --data-urlencode "min_salary_usd=150000" \
  --data-urlencode "max_salary_usd=250000"
```

```python theme={null}
results = client.search.search(
    q="backend engineer",
    work_model="remote",
    experience_level="senior",
    employment_type="full-time",
    min_salary_usd=150_000,
    max_salary_usd=250_000,
)
```

<Warning>
  Do **not** use the SDK's `EmploymentType` enum — its members serialize as `full_time`/`part_time` with underscores, which the API does not accept and which silently returns zero results. Pass the hyphenated string literals `"full-time"` / `"part-time"` instead. `WorkModel` and `ExperienceLevel` are safe, except that `ExperienceLevel` is missing `intern`.
</Warning>

### Include and exclude skills

`skills`, `companies`, and `industries` accept an include/exclude object on the typed endpoint. Entries within `include` are OR'd; anything in `exclude` is removed.

```json theme={null}
{
  "queries": ["backend developer"],
  "skills": { "include": ["python", "go"], "exclude": ["php"] }
}
```

```python theme={null}
from jobo_enterprise import InclusionExclusionFilter

results = client.search.search_advanced(
    queries=["backend developer"],
    skills=InclusionExclusionFilter(include=["python", "go"], exclude=["php"]),
)
```

### Target or exclude specific employers

Only available on the typed endpoint. Excluding staffing agencies is the most common use.

```json theme={null}
{
  "queries": ["software engineer"],
  "companies": { "exclude": ["Acme Staffing", "Generic Recruiting Co"] }
}
```

### Open-ended salary range

Omit either bound.

```json theme={null}
{ "salary_usd": { "min": 200000 } }
```

```python theme={null}
from jobo_enterprise import RangeFilter

results = client.search.search_advanced(salary_usd=RangeFilter(min=200_000))
```

### Posted window vs discovered window

`posted_after` / `posted_before` filter on the employer's posting date, which some platforms do not expose. `discovered_after` / `discovered_before` filter on when Jobo indexed the job — use these when you need a guarantee that nothing is missed.

```json theme={null}
{
  "queries": ["backend engineer"],
  "posted_after": "2026-06-01T00:00:00Z",
  "posted_before": "2026-07-01T00:00:00Z"
}
```

### Everything at once

```json theme={null}
{
  "queries": ["backend engineer", "platform engineer"],
  "locations": ["San Francisco", "New York", "Seattle"],
  "sources": ["greenhouse", "lever", "ashby"],
  "skills": { "include": ["go", "rust", "kubernetes"], "exclude": ["php"] },
  "companies": { "exclude": ["Acme Staffing"] },
  "work_models": ["remote", "hybrid"],
  "employment_types": ["full-time"],
  "experience_levels": ["senior", "lead"],
  "salary_usd": { "min": 150000, "max": 300000 },
  "posted_after": "2026-01-15T00:00:00Z",
  "page": 1,
  "page_size": 50
}
```

### Iterate every match

`iter_jobs` pages for you.

```python theme={null}
for job in client.search.iter_jobs(
    queries=["backend engineer"],
    locations=["London"],
    page_size=100,
):
    print(job.title, job.company.name)
```

***

## Shrinking the response

Every job carries all its fields by default, and `description` dominates payload size. `include_fields` keeps only the non-core fields you ask for — core fields (id, title, company, locations, compensation, dates, source, flags) are always returned.

| `include_fields`             | Result                                      |
| ---------------------------- | ------------------------------------------- |
| omitted                      | Full job, every field. The default.         |
| `["description","benefits"]` | Core **plus** `description` and `benefits`. |
| `[]` (or empty on GET)       | Core fields only.                           |

The five omittable fields are `description`, `summary`, `qualifications`, `responsibilities`, and `benefits`. On `GET` pass a comma-separated string (`?include_fields=description,benefits`); send `?include_fields=` for core only. Unknown names are dropped, and any non-core field you did not request comes back **empty** (`""` / `[]`) rather than absent.

Dropping `description` is the single biggest win for high-volume search.

***

## Facets

Responses include a `facets` object of server-computed counts, keyed by canonical value.

| Facet key          | Faceted on         | Default?                                |
| ------------------ | ------------------ | --------------------------------------- |
| `work_model`       | work model         | ✅                                       |
| `experience_level` | experience level   | ✅                                       |
| `employment_type`  | employment type    | ✅                                       |
| `sources`          | `provider_id`      | ✅                                       |
| `industries`       | company industries | Opt-in                                  |
| `skills`           | skill names        | Opt-in — high cardinality, adds latency |

| `include_facets`       | Result                      |
| ---------------------- | --------------------------- |
| omitted                | The four defaults.          |
| `["skills"]`           | Only `skills`.              |
| `[]` (or empty on GET) | No facets — `"facets": {}`. |

```json theme={null}
{
  "facets": {
    "work_model": [
      { "key": "remote", "count": 5230 },
      { "key": "hybrid", "count": 2847 },
      { "key": "onsite", "count": 1540 }
    ],
    "experience_level": [
      { "key": "senior", "count": 4521 },
      { "key": "mid", "count": 3892 }
    ]
  }
}
```

Facet keys are the **canonical** values, so you can feed a `key` straight back in as a filter value. This is not true of the display values on a job — see [Enums](/docs/api-reference/jobs/object#enums).

<Warning>
  Three facet behaviours surprise people:

  * Unknown facet names are silently dropped, so a request whose `include_facets` contains *only* unknown names (e.g. the plural typo `work_models`) returns **no** facets rather than the defaults.
  * At most **8 buckets** are returned per facet, and counts are approximate. Take exact totals from `total`, never from summing facet counts.
  * A facet with no buckets is omitted from the `facets` object entirely rather than appearing as an empty array.
</Warning>
