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

# Searching by job_title

> How to search PDL's job_title field effectively, using exact matches, wildcards, the role/subrole taxonomy, and the Job Title Enrichment API.

export const MetricCard = ({title, value, barPercent, query, note}) => {
  const renderInlineCode = text => {
    const parts = text.split("`");
    return parts.map((part, index) => index % 2 === 1 ? <code key={index} style={{
      fontSize: "0.8125rem"
    }}>
          {part}
        </code> : <span key={index}>{part}</span>);
  };
  return <Card title={title}>
      <div style={{
    display: "flex",
    gap: "1.5rem",
    alignItems: "flex-start",
    flexWrap: "wrap"
  }}>
        <div style={{
    flex: "0 0 7rem"
  }}>
          <div style={{
    fontSize: "1.5rem",
    fontWeight: 700,
    color: "var(--chart-accent-text)"
  }}>{value}</div>
          <div style={{
    height: "0.4rem",
    borderRadius: "999px",
    backgroundColor: "var(--chart-accent-wash-strong)",
    marginTop: "0.4rem"
  }}>
            <div style={{
    height: "100%",
    width: `${barPercent}%`,
    borderRadius: "999px",
    backgroundColor: "var(--chart-accent)"
  }} />
          </div>
        </div>
        <div style={{
    flex: "1 1 18rem"
  }}>
          <div style={{
    marginBottom: "0.4rem"
  }}>{renderInlineCode(query)}</div>
          <Note icon="">
            <span style={{
    fontStyle: "italic"
  }}>{renderInlineCode(note)}</span>
          </Note>
        </div>
      </div>
    </Card>;
};

export const RankedBarList = ({items}) => {
  const max = Math.max(...items.map(item => item.value));
  return <div className="not-prose" style={{
    margin: "1.5rem 0"
  }}>
      {items.map(item => <div key={item.label} style={{
    display: "flex",
    alignItems: "center",
    gap: "0.75rem",
    padding: "0.3rem 0"
  }}>
          <div style={{
    width: "14rem",
    flexShrink: 0,
    fontSize: "0.8125rem",
    color: item.isOther ? "var(--color-text-secondary, inherit)" : "inherit",
    fontStyle: item.isOther ? "italic" : "normal"
  }}>
            {item.label}
          </div>
          <div style={{
    flex: 1,
    height: "0.85rem",
    backgroundColor: "var(--chart-accent-wash)",
    borderRadius: "999px"
  }}>
            <div style={{
    height: "100%",
    width: `${Math.max(item.value / max * 100, 1)}%`,
    borderRadius: "999px",
    backgroundColor: item.isOther ? "var(--chart-muted)" : "var(--chart-accent)"
  }} />
          </div>
          <div style={{
    width: "4.5rem",
    textAlign: "right",
    fontSize: "0.8125rem",
    fontWeight: 600,
    color: "inherit",
    flexShrink: 0
  }}>
            {item.value.toLocaleString("en-US")}
          </div>
        </div>)}
    </div>;
};

export const StatTiles = ({stats}) => {
  return <div className="not-prose" style={{
    display: "flex",
    flexWrap: "wrap",
    gap: "1rem",
    margin: "1.5rem 0 2rem"
  }}>
      {stats.map(stat => <div key={stat.label} style={{
    flex: "1 1 12rem",
    padding: "1rem 1.25rem",
    borderRadius: "0.75rem",
    border: "1px solid var(--chart-accent-border)",
    backgroundColor: "var(--chart-accent-wash)"
  }}>
          <div style={{
    fontSize: "1.75rem",
    fontWeight: 700,
    color: "var(--chart-accent-text)",
    lineHeight: 1.1
  }}>
            {stat.value}
          </div>
          <div style={{
    fontSize: "0.8125rem",
    color: "var(--color-text-secondary, inherit)",
    marginTop: "0.25rem"
  }}>
            {stat.label}
          </div>
        </div>)}
    </div>;
};

export const MirroredFrequencyChart = ({buckets}) => {
  const [hovered, setHovered] = useState(null);
  const leftMax = Math.max(...buckets.map(bucket => bucket.uniquePercent));
  const rightMax = Math.max(...buckets.map(bucket => bucket.profilePercent));
  const formatCount = value => value.toLocaleString("en-US");
  const rowStyle = isHovered => ({
    display: "flex",
    alignItems: "center",
    padding: "0.3rem 0.5rem",
    borderRadius: "0.5rem",
    backgroundColor: isHovered ? "var(--chart-accent-hover)" : "transparent",
    outline: "none",
    transition: "background-color 120ms ease"
  });
  const barStyle = (percent, max, isHovered, side) => ({
    height: isHovered ? "1.35rem" : "1.1rem",
    width: `${Math.max(percent / max * 100, 1)}%`,
    backgroundColor: side === "left" ? "var(--pdl-purple-300)" : "var(--pdl-purple-700)",
    borderRadius: side === "left" ? "4px 0 0 4px" : "0 4px 4px 0",
    transition: "height 120ms ease",
    flexShrink: 0
  });
  return <div className="not-prose" style={{
    margin: "1.5rem 0"
  }}>
      <div style={{
    display: "flex",
    fontSize: "0.75rem",
    fontWeight: 600,
    textTransform: "uppercase",
    letterSpacing: "0.03em",
    color: "var(--color-text-secondary, inherit)",
    marginBottom: "0.5rem",
    padding: "0 0.5rem"
  }}>
        <div style={{
    flex: 1,
    textAlign: "right"
  }}>Share of unique job titles</div>
        <div style={{
    width: "6.5rem",
    textAlign: "center"
  }}>job_title occurs on</div>
        <div style={{
    flex: 1,
    textAlign: "left"
  }}>Share of person profiles</div>
      </div>

      {buckets.map((bucket, index) => {
    const isHovered = hovered === index;
    return <div key={bucket.label} tabIndex={0} role="group" aria-label={`job_title occurs ${bucket.label} times: ${bucket.uniquePercent}% of unique job titles, ${bucket.profilePercent}% of person profiles`} onMouseEnter={() => setHovered(index)} onMouseLeave={() => setHovered(null)} onFocus={() => setHovered(index)} onBlur={() => setHovered(null)} style={rowStyle(isHovered)}>
            <div style={{
      flex: 1,
      display: "flex",
      justifyContent: "flex-end",
      alignItems: "center",
      gap: "0.6rem"
    }}>
              <span style={{
      fontSize: "0.8125rem",
      color: "var(--color-text-secondary, inherit)",
      whiteSpace: "nowrap"
    }}>
                {bucket.uniquePercent}%
              </span>
              <div style={barStyle(bucket.uniquePercent, leftMax, isHovered, "left")} />
            </div>

            <div style={{
      width: "6.5rem",
      textAlign: "center",
      flexShrink: 0
    }}>
              <div style={{
      fontSize: "0.8125rem",
      fontWeight: 600,
      color: "inherit"
    }}>{bucket.label}</div>
              <div style={{
      fontSize: "0.6875rem",
      color: "var(--color-text-secondary, inherit)"
    }}>
                {bucket.label === "1" ? "profile" : "profiles"}
              </div>
            </div>

            <div style={{
      flex: 1,
      display: "flex",
      alignItems: "center",
      gap: "0.6rem"
    }}>
              <div style={barStyle(bucket.profilePercent, rightMax, isHovered, "right")} />
              <span style={{
      fontSize: "0.8125rem",
      color: "var(--color-text-secondary, inherit)",
      whiteSpace: "nowrap"
    }}>
                {bucket.profilePercent}%
              </span>
            </div>
          </div>;
  })}

      <div style={{
    minHeight: "1.5rem",
    marginTop: "0.5rem",
    padding: "0 0.5rem",
    fontSize: "0.8125rem",
    color: "var(--color-text-secondary, inherit)",
    textAlign: "center"
  }}>
        {hovered !== null ? <span>
            job_title occurs on <strong style={{
    color: "inherit"
  }}>{buckets[hovered].label}</strong>{" "}
            {buckets[hovered].label === "1" ? "profile" : "profiles"} for{" "}
            {formatCount(buckets[hovered].uniqueCount)} unique job titles ({buckets[hovered].uniquePercent}%), covering{" "}
            {formatCount(buckets[hovered].profileCount)} person profiles ({buckets[hovered].profilePercent}%)
          </span> : <span>Hover or tab through a row for exact counts.</span>}
      </div>
    </div>;
};

It can be daunting to tackle a dataset as large as the PDL Person dataset, and learning to properly query the [`job_title`](/docs/fields#job_title) field is one of the trickier skills to hone. PDL standardizes the job titles it ingests, but the underlying values still come from messy, real-world human input.

<StatTiles
  stats={[
{ value: "76M+", label: "unique job titles in the PDL Person dataset" },
{ value: "60%+", label: "of profiles with a job_title are tagged with a job_title_role" },
{ value: "20", label: "maximum wildcards allowed per search" },
]}
/>

***

## Standardizing `job_title` values

PDL lowercases every `job_title` value and expands common acronyms:

| Raw input | Standardized to           |
| :-------- | :------------------------ |
| `CEO`     | `chief executive officer` |
| `VP`      | `vice president`          |
| `Sr.`     | `senior`                  |
| `Jr.`     | `junior`                  |

So for example, you don't need to search `job_title` for the literal string `"CEO"`, since it's already been expanded to `"chief executive officer"` through our standardization processes.

***

## Why `job_title` is hard to search

The biggest reason why searching on the `job_title` field can be challenging is the sheer variety of ways people describe their roles. The same role gets spelled out hundreds of different ways.

The chart below groups every job title by how many person profiles share that exact string.
For example, the "2-99" row is every title that shows up on somewhere between 2 and 99 profiles and compares that split two ways: as a share of **unique titles**, and as a share of **person profiles**.

<MirroredFrequencyChart
  buckets={[
{ label: "1", uniquePercent: 85.5, uniqueCount: 65000000, profilePercent: 11.6, profileCount: 65000000 },
{ label: "2-99", uniquePercent: 14.2, uniqueCount: 10775000, profilePercent: 12.2, profileCount: 68000000 },
{ label: "100-999", uniquePercent: 0.3, uniqueCount: 190000, profilePercent: 9.5, profileCount: 53000000 },
{ label: "1000-4999", uniquePercent: 0.03, uniqueCount: 26000, profilePercent: 9.8, profileCount: 55000000 },
{ label: "5000+", uniquePercent: 0.01, uniqueCount: 9000, profilePercent: 56.9, profileCount: 318000000 },
]}
/>

The two sides mirror each other: 85.5% of unique titles occur exactly once, but those one-off titles account for roughly 11.6% of profiles.

Flipping it around: titles occurring 5,000+ times are **only 0.01% of unique titles, yet they cover over 56.9% of profiles**.

The variety is real, but it's concentrated in a long tail of rarely-seen titles; instead, **most profiles land on a comparatively small set of common job titles**.

<Accordion title="See the exact counts behind the chart">
  | `job_title` frequency | Person profiles | Unique job titles |
  | :-------------------- | --------------: | ----------------: |
  | 1                     |      65,000,000 |        65,000,000 |
  | 2-99                  |      68,000,000 |        10,775,000 |
  | 100-999               |      53,000,000 |           190,000 |
  | 1000-4999             |      55,000,000 |            26,000 |
  | 5000+                 |     318,000,000 |             9,000 |
</Accordion>

***

## Comparing search techniques

Let's take a search for software engineers as an example.

The cards below show that the way you phrase your search can significantly impact the number of results you get.

<Note>
  *Each bar below is sized relative to the largest record count of the three (5.6M)*
</Note>

<MetricCard title="Exact match" value="1.8M records" barPercent={32} query="`job_title = &#x22;software engineer&#x22;`" note="Misses `&#x22;senior software engineer&#x22;` and `&#x22;software engineer II&#x22;`." />

<MetricCard title="Wildcard match" value="4.2M records" barPercent={75} query="`job_title = *software*engineer*`" note="Misses `&#x22;software developer&#x22;` and `&#x22;python engineer&#x22;`." />

<MetricCard title="Role + subrole" value="5.6M records" barPercent={100} query="`job_title_role = &#x22;engineering&#x22;` and `job_title_sub_role = &#x22;software&#x22;`" note={`Catches all of the above, plus titles that don't literally contain "software" or "engineer".`} />

While exact and wildcard searches on `job_title` work, it's usually easier to cast a wider net with PDL's `job_title_role`, `job_title_sub_role`, and `job_title_levels` fields.

***

### Using `job_title_role` and `job_title_sub_role`

PDL maps job titles to a canonicalized list of [job title roles](/docs/job-title-roles) and [job title subroles](/docs/job-title-subroles), using keywords in `job_title` and company industry to capture the many different titles that describe the same role. Over 60% of profiles with a listed `job_title` are tagged with a `job_title_role`.

For example, here are some of the titles that fall under `job_title_role = "engineering"` and `job_title_sub_role = "software"`:

<Columns cols={2}>
  <div>
    * software developer
    * system engineer
    * software test engineer
  </div>

  <div>
    * software development manager
    * software architect
    * ios developer
  </div>
</Columns>

In most cases, PDL has already done the work of tagging job titles to a role and subrole, so you don't need to guess every variation of a title a person might use. Instead, you can cast a wide net with the role/subrole taxonomy, then narrow in with the [`skills`](/docs/fields#skills), [`job_summary`](/docs/fields#job_summary), [`headline`](/docs/fields#headline), or [`summary`](/docs/fields#summary) fields for a specific specialty.

For example, to find someone who writes Ruby code:

##### Example: Searching for Ruby developers <a id="example-searching-for-ruby-developers" />

<CodeGroup>
  ```python Python3 SDK theme={null}
  from peopledatalabs import PDLPY

  CLIENT = PDLPY(api_key="YOUR API KEY")

  ES_QUERY = {
      "query": {
          "bool": {
              "must": [
                  {"term": {"job_title_role": "engineering"}},
                  {"term": {"job_title_sub_role": "software"}},
              ],
              "should": [
                  {"term": {"skills": "ruby"}},
                  {"wildcard": {"job_summary": "*ruby*"}},
                  {"wildcard": {"headline": "*ruby*"}},
                  {"wildcard": {"summary": "*ruby*"}},
              ],
              "minimum_should_match": 1,
          }
      },
      "size": 100,
  }

  response = CLIENT.person.search(query=ES_QUERY, size=100).json()
  ```

  ```bash cURL theme={null}
  curl -X POST \
    'https://api.peopledatalabs.com/v5/person/search' \
    -H 'X-Api-Key: YOUR API KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "query": {
        "bool": {
          "must": [
            {"term": {"job_title_role": "engineering"}},
            {"term": {"job_title_sub_role": "software"}}
          ],
          "should": [
            {"term": {"skills": "ruby"}},
            {"wildcard": {"job_summary": "*ruby*"}},
            {"wildcard": {"headline": "*ruby*"}},
            {"wildcard": {"summary": "*ruby*"}}
          ],
          "minimum_should_match": 1
        }
      },
      "size": 100
    }'
  ```
</CodeGroup>

<Warning>
  PDL has some blind spots in its job title tagging. Relying only on `job_title_role` and `job_title_sub_role` is not a viable strategy for very specialized or niche roles — pair it with a wildcard or free-text search for those cases.
</Warning>

***

### Using the Job Title Enrichment API

The [Job Title Enrichment API](/docs/job-title-enrichment-api) is useful for expanding a search when you don't have a good sense of the title variations you're looking for. It returns a cleaned title plus a list of contextually-similar titles and skills as shown below, but it comes with some important limitations as well.

##### Example Response from Job Title Enrichment API

```json JSON theme={null}
{
  "status": 200,
  "data": {
    "cleaned_job_title": "pediatric nurse",
    "similar_job_titles": [
      "pediatric registered nurse",
      "pediatric nurse practitioner",
      "pediatric licensed practical nurse",
      "nursery nurse",
      "pediatric intensive care nurse"
    ],
    "relevant_skills": [
      "pediatric nursing",
      "nursing",
      "pediatrics",
      "patient safety",
      "patient education"
    ]
  }
}
```

The important caveat to keep in mind here is that **the API only returns titles that appear in PDL's global resume data at least 100 times**. So if you rely solely on the values it returns, **you're missing out on roughly 24% of profiles**, and the long tail of the rarer title variations described [above](#why-job_title-is-hard-to-search).

Here's how a search for `"pediatric nurse"` plays out across each technique. Each bar below is sized relative to the largest match count of the four (56,000):

<MetricCard title="Exact match" value="6,000" barPercent={10.7} query="`job_title = &#x22;pediatric nurse&#x22;`" note="Exact match only." />

<MetricCard title="Wildcard match" value="33,000" barPercent={58.9} query="`job_title = *pediatric*nurse*`" note="Catches `&#x22;pediatric registered nurse&#x22;`, but not `&#x22;nursery nurse&#x22;`." />

<MetricCard title="Job Title Enrichment API" value="39,000" barPercent={69.6} query="Exact title, or any `similar_job_titles` value the API returns" note={`Catches \`"nursery nurse"\`, since it was returned by the API. Still bounded by the API's 100-occurrence floor.`} />

<MetricCard title="Role + subrole + wildcard" value="56,000" barPercent={100} query="`job_title_role = &#x22;health&#x22;` and `job_title_sub_role = &#x22;nursing&#x22;` and `job_title = (*pediatric* or *nursery*)`" note="Catches the most variations, but also over-matches titles like `&#x22;nurse manager surgical services and pediatric services&#x22;`." />

<Warning>
  Wildcards are powerful, but they slow queries down. PDL allows a maximum of [20 wildcards per search](/docs/input-parameters-person-search-api#elasticsearch-query-limitations).
</Warning>

Below is a further breakdown of the top titles in behind that last, broadest search (**Role + subrole + wildcard**), where you can see both the gain and the cost of this approach:

##### Top titles **Role + subrole + wildcard** search

<RankedBarList
  items={[
{ label: "nursery nurse", value: 15000 },
{ label: "pediatric nurse practitioner", value: 10000 },
{ label: "pediatric nurse", value: 6000 },
{ label: "pediatric registered nurse", value: 5000 },
{ label: "pediatric intensive care nurse", value: 2000 },
{ label: "pediatric licensed practical nurse", value: 600 },
{ label: "pediatric nursing", value: 500 },
{ label: "community nursery nurse", value: 400 },
{ label: "certified pediatric nurse practitioner", value: 300 },
{ label: "senior nursery nurse", value: 200 },
{ label: "Other", value: 16000, isOther: true },
]}
/>

<Note>
  "Other" is every remaining title the search over-matched into, not a single specific one.
</Note>

The Job Title Enrichment API beats a plain wildcard search, but it still misses some real variations. The broadest, role/subrole-plus-wildcard search returns nearly double the results of the API-driven search, but includes some irrelevant profiles along with it.

## Choosing an approach

Which technique to reach for depends on how familiar you are with the titles you're targeting, and your tolerance for over- or under-matching:

<Tip>
  **PDL's Recommendation**

  Default to combining the role/subrole taxonomy with a targeted wildcard or skills search (see [example](#example-searching-for-ruby-developers) above). It's the best balance of recall and precision, as long as you spot-check for over-matched profiles.
</Tip>

<Columns cols={2}>
  <Card title="Exact match" icon="crosshair">
    **Best for:** you know the precise, common phrasing

    **Trade-off:** misses every variation and synonym
  </Card>

  <Card title="Wildcard match" icon="asterisk">
    **Best for:** you know a distinctive substring

    **Trade-off:** misses titles without that substring; counts against the 20-wildcard limit
  </Card>

  <Card title="Role + subrole" icon="tags">
    **Best for:** broad recall with minimal guesswork

    **Trade-off:** blind spots for niche or highly specialized roles
  </Card>

  <Card title="Job Title Enrichment API" icon="sparkles">
    **Best for:** you don't know the title variations up front

    **Trade-off:** only surfaces titles seen 100+ times, missing \~24% of profiles
  </Card>
</Columns>


## Related topics

- [Best Practices](/learn/best-practices.md)
- [July 2022 Release Notes](/changelog/july-2022-release-notes-v19.md)
- [Examples - Job Title Enrichment API](/docs/examples-job-title-enrichment-api.md)
- [Job Title Enrichment API](/docs/job-title-enrichment-api.md)
- [Input Parameters - Job Title Enrichment API](/docs/input-parameters-job-title-enrichment-api.md)
