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

# Query operators

> How to combine AND/OR logic, exact phrases, and other OpenSearch DSL operators when searching profiles and companies.

This page is an operator-first reference for building queries against [Search Profiles](/docs/endpoints/v3/search-profiles) and [Search Companies](/docs/endpoints/v3/search-companies). If you're looking for full end-to-end scenarios instead, see [Search endpoint examples](/docs/examples/search-endpoint).

<Tip>
  Every JSON block below is copy-pasteable into the [API Query Tester](/docs/examples/api-query-tester). If you're not sure which operator you need, start with the decision table.
</Tip>

## Choosing the right operator

| You want to match…                         | Use                                                   | Field variant                    |
| ------------------------------------------ | ----------------------------------------------------- | -------------------------------- |
| Any of these words (default OR)            | `match`                                               | `text` (e.g. `current_title`)    |
| All of these words                         | `match` with `"operator": "AND"`                      | `text`                           |
| This exact phrase, in order                | `match_phrase`                                        | `text`                           |
| This exact value, character-for-character  | `term`                                                | `keyword` (e.g. `job_title.raw`) |
| Any of several exact values                | `terms`                                               | `keyword` / `.raw`               |
| A numeric or date range                    | `range`                                               | numeric / date                   |
| Field is present                           | `exists`                                              | any                              |
| Combine multiple conditions                | `bool` with `must` / `should` / `must_not` / `filter` | any                              |
| Same text across multiple fields           | `multi_match`                                         | `text`                           |
| Anything under `profile_info.experience.*` | wrap in `nested`                                      | nested                           |

## Text vs. keyword: why "General Manager" is tricky

Most string fields in the Swarm index are mapped as **both** `text` (analyzed — lowercased, tokenized into words) and `keyword` under a `.raw` subfield (stored verbatim). Which one you query changes what "match" means.

<Warning>
  Many fields in the mappings are **aliases** (e.g. `current_title` → `job_title`, `current_company_name` → `job_company_name`). Aliases work for `match`, but for `term`/`terms` you should target the underlying field's `.raw` — for example `profile_info.job_title.raw`, not `profile_info.current_title.raw`.
</Warning>

Take `profile_info.job_title` (aliased as `current_title`) and the value `"General Manager"`:

* **`match`** — full-text search on the analyzed field. Matches any profile whose title contains the words *general* or *manager* (default OR). "Assistant General Manager", "General Counsel", and "Product Manager" all match.
* **`match_phrase`** — full-text with word order preserved. Matches titles containing the words *general* then *manager* adjacent to each other. "Senior General Manager" and "General Manager, EMEA" both match; "Manager of General Affairs" does not.
* **`term` on `.raw`** — exact keyword match. Only titles that are literally `"General Manager"` match. Case-sensitive against the stored value.

### match — any of the words

```json theme={null}
{
  "query": {
    "match": {
      "profile_info.current_title": {
        "query": "General Manager"
      }
    }
  },
  "limit": 10
}
```

### match with operator AND — all of the words, any order

```json theme={null}
{
  "query": {
    "match": {
      "profile_info.current_title": {
        "query": "General Manager",
        "operator": "AND"
      }
    }
  },
  "limit": 10
}
```

### match\_phrase — exact phrase, adjacent, in order

```json theme={null}
{
  "query": {
    "match_phrase": {
      "profile_info.current_title": {
        "query": "General Manager"
      }
    }
  },
  "limit": 10
}
```

### term on .raw — strict exact match

```json theme={null}
{
  "query": {
    "term": {
      "profile_info.job_title.raw": {
        "value": "General Manager"
      }
    }
  },
  "limit": 10
}
```

<Warning>
  `term` on a `text` field almost always returns zero results. Always target the `.raw` subfield for exact matches.
</Warning>

## AND vs. OR inside one field

### All words required

Use `match` with `"operator": "AND"` when every token in the query must appear in the field.

```json theme={null}
{
  "query": {
    "match": {
      "profile_info.current_title": {
        "query": "senior software engineer",
        "operator": "AND"
      }
    }
  },
  "limit": 10
}
```

### At least N of the words

`minimum_should_match` lets you require a fraction of the tokens instead of all-or-nothing.

```json theme={null}
{
  "query": {
    "match": {
      "profile_info.current_title": {
        "query": "vice president marketing growth",
        "minimum_should_match": "75%"
      }
    }
  },
  "limit": 10
}
```

## AND vs. OR across multiple fields

Use `bool` to combine clauses. The four clause types:

* **`must`** — AND. Contributes to score.
* **`filter`** — AND. Does not contribute to score; faster and cacheable.
* **`should`** — OR. If used alongside `must`, it only boosts score; on its own it acts as OR (at least one must match).
* **`must_not`** — NOT.

### AND across fields (data scientists in the US)

```json theme={null}
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "profile_info.current_title": {
              "query": "data scientist",
              "operator": "AND"
            }
          }
        },
        {
          "match": {
            "profile_info.current_location": {
              "query": "United States"
            }
          }
        }
      ]
    }
  },
  "limit": 10
}
```

### OR across fields (Google or Meta, current employees)

Because `bool` with only `should` requires at least one to match, this behaves like OR.

```json theme={null}
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "profile_info.job_company_name.raw": { "value": "Google" }
          }
        },
        {
          "term": {
            "profile_info.job_company_name.raw": { "value": "Meta" }
          }
        }
      ],
      "minimum_should_match": 1
    }
  },
  "limit": 10
}
```

<Tip>
  For a list of exact values, `terms` is shorter than multiple `should` clauses:

  ```json theme={null}
  {
    "query": {
      "terms": {
        "profile_info.job_company_name.raw": ["Google", "Meta", "Apple"]
      }
    },
    "limit": 10
  }
  ```
</Tip>

### filter vs. must

`must` and `filter` both combine clauses with AND. The difference is scoring: `must` clauses contribute to a relevance score, `filter` clauses don't (and are faster + cacheable).

Rule of thumb: put the clauses that decide **relevance** in `must`, and the clauses that just narrow the set (locations, dates, seniorities, exists checks) in `filter`.

Same query, two shapes — take the earlier "data scientists in the US" example. The location doesn't need scoring, so moving it into `filter` is more efficient:

```json theme={null}
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "profile_info.current_title": {
              "query": "data scientist",
              "operator": "AND"
            }
          }
        }
      ],
      "filter": [
        {
          "match": {
            "profile_info.current_location": {
              "query": "United States"
            }
          }
        }
      ]
    }
  },
  "limit": 10
}
```

Both queries return the same profiles; the second is faster because the location clause is not scored.

<Tip>
  `job_title_role` and `job_seniorities` accept a fixed set of lowercase values — see [Canonical values](/docs/miscellaneous/canonical-values). `job_company_industry` is free-form (e.g. `"Software Development"`, `"Computer Software"`); values vary by source, so `term` matches can miss unless you know the exact string.
</Tip>

## Negation and existence

### Exclude a value (must\_not)

Product managers **not** currently at Google:

```json theme={null}
{
  "query": {
    "bool": {
      "must": [
        {
          "match_phrase": {
            "profile_info.current_title": { "query": "Product Manager" }
          }
        }
      ],
      "must_not": [
        {
          "term": {
            "profile_info.job_company_name.raw": { "value": "Google" }
          }
        }
      ]
    }
  },
  "limit": 10
}
```

### Field must be present (exists)

Profiles with at least one email on file:

```json theme={null}
{
  "query": {
    "exists": { "field": "profile_info.emails" }
  },
  "limit": 10
}
```

## Ranges and dates

`range` supports `gte`, `gt`, `lte`, `lt`. Dates accept either `YYYY-MM-DD` or relative expressions like `now-30d`, `now-1y`, `now`.

Profiles whose current job was updated in the last year:

```json theme={null}
{
  "query": {
    "range": {
      "profile_info.current_job_updated_at": {
        "gte": "now-1y",
        "lte": "now"
      }
    }
  },
  "limit": 10
}
```

Combine a range with a text search — profiles that changed to a "Head of" role in the last 90 days:

```json theme={null}
{
  "query": {
    "bool": {
      "must": [
        {
          "match_phrase": {
            "profile_info.current_title": { "query": "Head of" }
          }
        }
      ],
      "filter": [
        {
          "range": {
            "profile_info.current_job_updated_at": {
              "gte": "now-90d",
              "lte": "now"
            }
          }
        }
      ]
    }
  },
  "limit": 10
}
```

<Tip>
  When you combine a range with a specific canonical value (like `job_seniorities: "director"`), the intersection can be small or empty depending on your team's network. If a compound query returns zero, test each clause on its own first to isolate which one is over-filtering.
</Tip>

## Multi-field text search

`multi_match` runs the same query text across several fields at once.

```json theme={null}
{
  "query": {
    "multi_match": {
      "query": "growth marketing",
      "fields": [
        "profile_info.current_title",
        "profile_info.all_time_job_title"
      ],
      "operator": "AND"
    }
  },
  "limit": 10
}
```

## Nested fields (work experience)

Anything under `profile_info.experience.*` is a nested document — you can't match it with a flat clause. Wrap the query in `nested` with the correct `path`.

### Anyone who has ever worked at Google

```json theme={null}
{
  "query": {
    "nested": {
      "path": "profile_info.experience",
      "query": {
        "term": {
          "profile_info.experience.company.name.raw": { "value": "Google" }
        }
      }
    }
  },
  "limit": 10
}
```

### Held a "Product Manager" role at Google in the past (not necessarily current)

```json theme={null}
{
  "query": {
    "nested": {
      "path": "profile_info.experience",
      "query": {
        "bool": {
          "must": [
            {
              "term": {
                "profile_info.experience.company.name.raw": { "value": "Google" }
              }
            },
            {
              "match_phrase": {
                "profile_info.experience.title.name": { "query": "Product Manager" }
              }
            }
          ]
        }
      }
    }
  },
  "limit": 10
}
```

For a more complete "worked there but not currently" pattern, see [Search endpoint examples](/docs/examples/search-endpoint#nested-queries-work-experience).

## Common pitfalls

* **`term` on a `text` field returns nothing.** Use `.raw` on the underlying field (e.g. `job_title.raw`, `job_company_name.raw`) for exact matches. Aliased names like `current_title.raw` do not resolve — always target the real field.
* **Keyword fields with a lowercase normalizer.** Fields like `job_seniorities`, `job_title_role`, and `job_company_industry` are stored lowercased, so `term` values must be lowercase (e.g. `"senior"`, `"marketing & product management"`).
* **Free-form vs. canonical values.** `job_seniorities` and `job_title_role` accept a fixed set of values ([Canonical values](/docs/miscellaneous/canonical-values)); `job_company_industry` is enrichment-source data with varying strings — `match` on the text form is safer than `term` unless you know the exact value.
* **`match_phrase` is not case-sensitive.** The analyzer lowercases both the query and the indexed value. If you need case-sensitivity, use `term` on `.raw`.
* **Forgetting `nested`.** Any field path starting with `profile_info.experience.`, `profile_info.education.`, or `profile_info.certifications.` needs a `nested` wrapper with the matching `path`.
* **`should` without `minimum_should_match` when mixed with `must`.** Once you add a `must` clause, `should` clauses only boost scoring — they no longer require any match. Set `"minimum_should_match": 1` when you want OR behavior.
* **Using `must` for non-scored filters.** Move seniorities, industries, date ranges, and `exists` checks into `filter` for a faster query.

## Next steps

* Try the queries above in the [API Query Tester](/docs/examples/api-query-tester).
* Describe your search in plain English and let the [API Query Builder](/docs/examples/api-query-builder) generate the DSL.
* See the [Profile mappings](/docs/enrichment-data/profile/mappings-v3) for the full list of queryable fields and which have a `.raw` subfield.
