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

# Filters Overview

> How Pubrio search filters work — the unified filter engine, AND/OR semantics, and when to use which.

Pubrio's search endpoints (`/companies/search`, `/people/search`, `/companies/advertisements/search`) share a single filter engine. You compose a request body once and the same rules apply across endpoints — including the way multi-value filters combine, how locations are matched, and how you override the default operator with `filter_conditions`.

## Why a unified filter engine?

<CardGroup cols={2}>
  <Card title="One schema, three endpoints" icon="arrows-rotate">
    Company-level filters like `technologies`, `verticals`, and `founded_dates` work identically on `/companies/search`, `/people/search`, and inside Monitor `company_filters` — you learn them once.
  </Card>

  <Card title="Per-filter AND/OR" icon="code-merge">
    The default is OR (match any). Promote individual filters to AND (match all) by adding one entry to `filter_conditions` — without touching the rest of the body.
  </Card>

  <Card title="Postgres-native operators" icon="database">
    Array filters compile to native Postgres operators — `&&` (overlap) for OR, `@>` (contains) for AND. Index-friendly, no application-side post-filtering.
  </Card>

  <Card title="Same filters in Monitors" icon="bell">
    The `company_filters` block in [Monitors](/en/developer-guides/introduction) accepts the same shape, so a working search payload is also a working monitor payload.
  </Card>
</CardGroup>

***

## Anatomy of a search request

Every search request is built from three layers in the same JSON body:

| Layer              | Where it lives                                                                    | Examples                                                                       |
| ------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| People filters     | top-level keys                                                                    | `people_titles`, `management_levels`, `departments`, `people_locations`        |
| Company filters    | nested under `company_filters: {...}` (recommended) — also accepted at top level  | `technologies`, `verticals`, `founded_dates`, `employees`, `company_locations` |
| Operator overrides | `filter_conditions` array (inside `company_filters` when overriding company keys) | `[{ "key": "technologies", "operator": "and" }]`                               |

A minimal `/people/search` request that uses all three layers:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.pubrio.com/people/search \
    -H "Content-Type: application/json" \
    -H "pubrio-api-key: YOUR_API_KEY" \
    -d '{
      "people_titles": ["VP of Engineering", "CTO"],
      "company_filters": {
        "technologies": ["Kubernetes", "Docker"],
        "is_enable_similarity_search": true,
        "company_locations": ["US"]
      },
      "per_page": 25,
      "page": 1
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.pubrio.com/people/search",
      headers={
          "Content-Type": "application/json",
          "pubrio-api-key": "YOUR_API_KEY",
      },
      json={
          "people_titles": ["VP of Engineering", "CTO"],
          "company_filters": {
              "technologies": ["Kubernetes", "Docker"],
              "is_enable_similarity_search": True,
              "company_locations": ["US"],
          },
          "per_page": 25,
          "page": 1,
      },
  )
  print(response.json())
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.pubrio.com/people/search", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "pubrio-api-key": "YOUR_API_KEY",
    },
    body: JSON.stringify({
      people_titles: ["VP of Engineering", "CTO"],
      company_filters: {
        technologies: ["Kubernetes", "Docker"],
        is_enable_similarity_search: true,
        company_locations: ["US"],
      },
      per_page: 25,
      page: 1,
    }),
  });
  console.log(await response.json());
  ```
</CodeGroup>

***

## `company_filters`: keep company-level keys grouped

The `company_filters: {...}` wrapper object is the recommended way to send company-level filters — it visually separates which keys filter the *person* from which filter the *company*, and matches the shape [Monitors](/en/developer-guides/introduction) already use, so payloads transfer cleanly between search and monitor configurations.

Both styles work; the engine flattens the wrapped form to the top level before processing, and **top-level keys win on conflict**:

<CodeGroup>
  ```json Wrapped (recommended) theme={null}
  {
    "people_titles": ["VP of Engineering"],
    "company_filters": {
      "technologies": [37, 152],
      "founded_dates": [2015, 2023],
      "company_locations": ["US"]
    }
  }
  ```

  ```json Flat (also works) theme={null}
  {
    "people_titles": ["VP of Engineering"],
    "technologies": [37, 152],
    "founded_dates": [2015, 2023],
    "company_locations": ["US"]
  }
  ```
</CodeGroup>

When you add a `filter_conditions` override for a company-level key, put it **inside** `company_filters` so it travels with the keys it overrides.

### Same shape on the `/search/similar` variants

`POST /companies/search/similar` and `POST /people/search/similar` accept the **same filter body** as their non-similar counterparts (including the `company_filters` wrapper and `filter_conditions`). Each one adds a similarity step on top:

|                             | What they need extra                                                                                                 | What you get extra                                                                               |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `/companies/search/similar` | A reference company — `domain_search_id`, `domain`, `linkedin_url`, or `domains`                                     | Each result gets a `similarity_score` (float, 0-1) and rows are ordered by similarity descending |
| `/people/search/similar`    | A reference person/title — one of `people_titles`, `people_search_id`, `linkedin_url`, `linkedin_urls`, or `peoples` | Same — `similarity_score` per row, ordered by similarity                                         |

The response envelope is otherwise identical to the standard `search` endpoint. Filters narrow the candidate pool *before* similarity ranking is applied — so combining `company_locations: ["US"]` with `/people/search/similar` returns the closest US-based people to your reference titles, which is the "find more people like X within these constraints" pattern.

<Note>
  Unlike the standard `/search` endpoints, `/search/similar` does **not** return an exact `pagination.total_entries` — the value is capped because similar search ranks results by relevance and only surfaces the top matches. Use similar search to find the *best* matches, not to enumerate every one.
</Note>

***

## AND vs OR — the one decision you make per filter

Multi-value filters (`technologies`, `verticals`, `keywords`, `categories`, …) accept an array. The operator decides what "match" means:

<Tabs>
  <Tab title="OR (default)">
    **Match any value.** Returns rows whose array overlaps with the input.

    ```json theme={null}
    {
      "technologies": ["Python", "PostgreSQL", "Kubernetes"],
      "is_enable_similarity_search": true
    }
    ```

    A company is included if its tech stack contains **at least one** of `Python`, `PostgreSQL`, or `Kubernetes`. Compiles to Postgres `column && ARRAY[...]`.

    Use when: you want broad reach — "interested in *any* of these", "located in *any* of these countries".
  </Tab>

  <Tab title="AND">
    **Match every value.** Returns rows whose array contains every input value.

    ```json theme={null}
    {
      "technologies": [37, 152, 408],
      "filter_conditions": [
        { "key": "technologies", "operator": "and" }
      ]
    }
    ```

    Numeric tag IDs come from `GET /technologies?search_term=python` (and similar). **Don't combine `is_enable_similarity_search: true` with AND on the same key** — similarity expands each free-text term into many tag IDs and `@>` then requires the row to contain all of them, which almost always returns zero.

    A company is included only if its tech stack contains **all of** `[37, 152, 408]`. Compiles to Postgres `column @> ARRAY[...]`.

    Use when: you want precision — "uses *all of* these technologies together", "tagged with *all* of these verticals".
  </Tab>
</Tabs>

<Note>
  Filters not listed in `filter_conditions` use the default operator (OR within an array, AND across distinct filter keys). You only declare the overrides — never the defaults.
</Note>

***

## What you can override

Each endpoint accepts overrides for a different set of keys. The keys come from the OpenAPI enum on each `*_filter_conditions` schema:

<CardGroup cols={3}>
  <Card title="Company endpoint" icon="building" href="/en/api-reference/endpoint/companies/search">
    `company_filter_conditions` keys: `keywords`, `verticals`, `vertical_categories`, `vertical_sub_categories`, `technologies`, `categories`, `advertisement_target_locations`, `advertisement_exclude_target_locations`, `advertisement_search_terms`, `places`, `exclude_places`, `job_exclude_locations`.
  </Card>

  <Card title="People endpoint" icon="user" href="/en/api-reference/endpoint/people/search">
    `people_filter_conditions` keys (delegate to the company engine): `keywords`, `verticals`, `vertical_categories`, `vertical_sub_categories`, `technologies`, `categories`, `places`, `exclude_places`, plus `social_media`.
  </Card>

  <Card title="Ads endpoint" icon="bullhorn" href="/en/api-reference/endpoint/companies/advertisements_search">
    `ads_filter_conditions` keys: `target_locations`, `exclude_target_locations`. Smaller set because ads only filter by impression country.
  </Card>
</CardGroup>

<Tip>
  When using `/people/search`, the `filter_conditions[].key` for company-level locations uses the **bare** name from the company engine — `places`, `exclude_places` — not the prefixed people-API name (`company_places`). See [People + Company Filters](/en/developer-guides/filters/people-with-company-filters#key-remap-reference).
</Tip>

***

## Performance tips

<AccordionGroup>
  <Accordion title="Filter early on indexed columns" icon="bolt">
    Locations, employee buckets, and `founded_dates` are indexed and reduce the candidate set faster than free-text or vertical filters. Combine them with one or two precise filters before reaching for similarity search.
  </Accordion>

  <Accordion title="Don't over-AND large arrays" icon="triangle-exclamation">
    `column @> ARRAY[a, b, c, …]` requires every value to be present. Cardinality grows fast — a 10-tech AND on a category with average 3 tech tags returns near-zero rows and forces a full scan. Prefer 2-4 values per AND filter; switch to OR for exploratory queries.
  </Accordion>

  <Accordion title="Use is_enable_similarity_search for free-text input" icon="wand-magic-sparkles">
    If you can't supply slug IDs (verticals, technologies, categories) and only have free-text strings, set `is_enable_similarity_search: true` and `similarity_score: 0.7`. The engine resolves matches before applying the filter — much cheaper than scanning text.
  </Accordion>

  <Accordion title="Prefer ranges over enum lists for size and revenue" icon="arrows-left-right">
    `employees: [[201, 500], [501, 1000]]` (an array of buckets) and `revenues: [1000000, 5000000]` (a single min/max range) are faster and more idiomatic than long ID lists.
  </Accordion>
</AccordionGroup>

***

## Advertising activity filters

`/companies/search` can filter by a company's advertising footprint — how many ads they run, whether any are currently active, which platforms and formats they use, and how they rank against other advertisers in a given country. `POST /companies/advertisements/search` accepts every one of these filters too (`advertisement_active_ads`, `advertisement_running_ads`, `advertisement_total_ads`, `advertisement_platform_count`, `advertisement_format_count`, `advertisement_impressions_estimate`, `advertisement_formats`, `advertisement_country_activity`) — an ad is included if its owning company meets the bound.

| Filter                                                                             | Shape                                                                                                                                                  | Scope         |
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
| `advertisement_active_ads`, `advertisement_running_ads`, `advertisement_total_ads` | `[min, max]` range                                                                                                                                     | All countries |
| `advertisement_platform_count`, `advertisement_format_count`                       | `[min, max]` range                                                                                                                                     | All countries |
| `advertisement_impressions_estimate`                                               | `[min, max]` range                                                                                                                                     | All countries |
| `advertisement_formats`                                                            | array — `image`, `video`, `text`, `carousel`, `dynamic_product`, `document`, `message`, `event`, `article`, `spotlight`, `follow`, `job`, `engagement` | All countries |
| `advertisement_platforms`                                                          | array — `linkedin`, `facebook`, `google`, `tiktok`, `apple` (`meta` accepted as an alias for `facebook`)                                               | All countries |
| `advertisement_publisher_platforms`, `advertisement_exclude_publisher_platforms`   | array — `facebook`, `instagram`, `messenger`, `threads`, `audience_network`                                                                            | All countries |
| `advertisement_status`                                                             | array — `currently_running` (has ads on record), `active_last_30_min` (ad records changed in the last 30 minutes)                                      | All countries |
| `advertisement_country_activity`                                                   | object, see below                                                                                                                                      | One country   |

Use `null` for an open-ended bound — `[1, null]` means "at least 1", `[null, 500]` means "500 or fewer".

```json theme={null}
{
  "advertisement_active_ads": [1, null],
  "advertisement_formats": ["video"]
}
```

### Ranking within a country

`advertisement_country_activity` scopes rank, percentile, and volume score to one country at a time — a company's ad rank in the US says nothing about its rank in Germany, so `country` is required:

```json theme={null}
{
  "advertisement_country_activity": {
    "country": "US",
    "rank": [null, 500]
  }
}
```

<Warning>
  **`rank` counts down from the biggest spender, like a race.** `1` is the biggest advertiser in that country, and the number goes *up* as advertising volume goes *down*. To find the **biggest** spenders, filter `rank` with a **low** upper bound — `[null, 500]` is the top 500. Filtering `[500, null]` finds everyone **outside** the top 499, which is the opposite of "big spender". If you want a scale-free version that works the same regardless of how many advertisers are in that country, use `percentile` instead — it runs the other direction, so a **higher** number means a **bigger** spender: `[90, null]` is the top 10%.
</Warning>

Combine `advertisement_country_activity` with the all-country filters above in the same request — for example, "ranked in the top 500 in the US, and has at least 100 active ads company-wide":

```json theme={null}
{
  "advertisement_country_activity": { "country": "US", "rank": [null, 500] },
  "advertisement_active_ads": [100, null]
}
```

### Ranking the results

Filtering by `advertisement_country_activity` narrows *which* companies come back; it does not by itself decide the order. To get the biggest advertisers in that market first, ask for `sort_by: "advertisement_country_rank"`:

```json theme={null}
{
  "advertisement_country_activity": { "country": "HK", "rank": [1, 10] },
  "sort_by": "advertisement_country_rank"
}
```

Whenever `advertisement_country_activity` is present, each company also carries its standing in that country, so you can display the rank you filtered on:

| Field                              | Meaning                                                                                                   |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `advertisement_country_rank`       | Position in that country, 1 = biggest advertiser. Ties share a rank, so numbers can repeat and then skip. |
| `advertisement_country_percentile` | Share of advertisers in that country this company outranks.                                               |
| `advertisement_country_active_ads` | Active ads attributed to that country — not the company-wide `advertisement_active_ads`.                  |

`sort_by: "advertisement_country_rank"` is ignored when `advertisement_country_activity` is absent, since a rank only exists inside a country. Pass `is_ascending_order: false` to list the smallest advertisers in the market first.

### Finding new entrants to a market

`advertisement_country_activity` also accepts two date windows, so you can ask who *started* advertising in a market rather than who is already big there:

| Key          | Meaning                                                                                                            |
| ------------ | ------------------------------------------------------------------------------------------------------------------ |
| `first_seen` | `[from, to]` — when the company's earliest ad in that country ran. This is the "newly entered this market" filter. |
| `last_seen`  | `[from, to]` — when its most recent ad in that country ran. Useful for the opposite question: who has gone quiet.  |

Companies that entered Japan since the start of July:

```json theme={null}
{
  "advertisement_country_activity": { "country": "JP", "first_seen": ["2026-07-01", null] }
}
```

Either bound may be `null` for an open-ended window. Dates are interpreted in the request's timezone. A company whose ads carry no start date has no first-seen value and is treated as unknown — it is never reported as new. Combine with the ranked keys in the same object to narrow further, for example new entrants that are already spending heavily.

***

## Next steps

<CardGroup cols={2}>
  <Card title="filter_conditions" icon="code-merge" href="/en/developer-guides/filters/filter-conditions">
    Reference page — every supported key, every default, and copyable AND/OR recipes.
  </Card>

  <Card title="People + Company Filters" icon="users-rectangle" href="/en/developer-guides/filters/people-with-company-filters">
    Use any company filter inside `/people/search`. The headline new feature of the unified engine.
  </Card>

  <Card title="Company Search reference" icon="building" href="/en/api-reference/endpoint/companies/search">
    Full request/response schema for `/companies/search`.
  </Card>

  <Card title="People Search reference" icon="user" href="/en/api-reference/endpoint/people/search">
    Full request/response schema for `/people/search`.
  </Card>
</CardGroup>

<Note>
  Looking for the dashboard-side filtering walkthrough? See [Filtering & Exporting Contacts](/en/knowledge-base/concepts/search-filters) in the Knowledge Base.
</Note>
