This page is an operator-first reference for building queries against Search Profiles and Search Companies. If you’re looking for full end-to-end scenarios instead, see Search endpoint examples.
Start here — the single most common gotcha. match splits your query into words and matches any of them by default. Searching match for "General Manager" returns profiles whose title contains general or manager (so “Product Manager” and “General Counsel” both come back).
- For both words, in order (“General Manager” as a phrase), use
match_phrase.
- For both words, any order, add
"operator": "AND" — see All words required.
- For a strict exact string (“General Manager” and nothing else), use
term on .raw.
Every JSON block below is copy-pasteable into the API Query Tester. If you’re not sure which operator you need, start with the decision table.Prefer natural language? The API Query Builder converts descriptions like “General Managers at Acme in the US” into DSL. It covers most common cases, but for mixed AND/OR, exact phrases, or nested experience queries you may still need to hand-edit the generated JSON — this page is the reference for those edits.
Choosing the right operator
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.
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.
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
match with operator AND — all of the words, any order
match_phrase — exact phrase, adjacent, in order
term on .raw — strict exact match
term on a text field almost always returns zero results. Always target the .raw subfield for exact matches.
Worked example: “General Manager” at a specific company
Suppose you want profiles whose current title is General Manager and whose current company is a specific one (job_company_id — a keyword field, no normalizer, no .raw needed).
The wrong query
A common first attempt uses match on both fields inside must:
Why this is wrong: the first clause uses match with default OR, so it matches any title containing general or manager — not the phrase “General Manager”. Splitting the clause into two separate match clauses (one per word) also isn’t the right fix — that’s "operator": "AND" written the long way, and it still doesn’t require word order.
Fix — exact phrase (recommended)
Use match_phrase on the title. Company id is a keyword, so term is the right operator there:
Two things to note:
- The company id clause lives in
filter, not must. It’s a strict narrowing check with no relevance component, so filter is faster and doesn’t distort scoring.
job_company_id is the underlying field — current_company_id is an alias. Both work for match, but for term prefer the underlying name.
Alternative — both words, any order
If order doesn’t matter ("Manager, General Merchandise" is fine), swap match_phrase for match with "operator": "AND":
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.
At least N of the words
minimum_should_match lets you require a fraction of the tokens instead of all-or-nothing.
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.
Chaining conditions readably
Most real queries are just a flat list of ANDed conditions plus, occasionally, an ORed group. You don’t need nested bool inside bool for that — one level is enough. Think of it as three named buckets inside a single bool:
filter: [ … ] — everything you want ANDed. One condition per array element, one line each.
should: [ … ] + minimum_should_match: 1 — the ORed group.
must_not: [ … ] — exclusions.
Example — profiles who are (Product Managers or Engineering Managers) at Google or Meta, with an email on file, currently in the US, not at “Stealth”:
Each condition is one line in a flat array — no tree of nested bools. Only reach for a nested bool when you genuinely need “OR of two AND groups” (e.g. (Product Manager at Google) OR (Engineering Manager at Meta)).
AND across fields (data scientists in the US)
Because bool with only should requires at least one to match, this behaves like OR.
For a list of exact values, terms is shorter than multiple should clauses:
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:
Both queries return the same profiles; the second is faster because the location clause is not scored.
job_title_role and job_seniorities accept a fixed set of lowercase values — see 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.
Negation and existence
Exclude a value (must_not)
Product managers not currently at Google:
Field must be present (exists)
Profiles with at least one email on file:
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:
Combine a range with a text search — profiles that changed to a “Head of” role in the last 90 days:
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.
Multi-field text search
multi_match runs the same query text across several fields at once.
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
Held a “Product Manager” role at Google in the past (not necessarily current)
For a more complete “worked there but not currently” pattern, see Search endpoint examples.
Debugging queries that return the wrong results
When a compound query returns zero results — or way too many — don’t stare at the whole tree. Peel it apart:
- Simplify to one clause at a time. Start with a single
match or term. Confirm it returns something. Add clauses one at a time and watch what happens to the count.
- Check the
total_count in the response. You don’t need to look at profiles to debug — the count alone tells you which clause is over- or under-filtering.
- Suspect
match first when you get too many results. match splits into words and ORs them by default. If “General Manager” is pulling in Generals, that’s the cause — switch to match_phrase or add "operator": "AND".
- Test
must clauses standalone when you get zero results. Replace the whole bool with just one of its clauses. If that returns zero, the clause itself is broken — not the combination. Common culprits: term on a text field (should be .raw), aliased field name in a term query, wrong case on a lowercase-normalized keyword field.
- Once it works, move narrowing clauses into
filter. Locations, IDs, seniorities, exists checks — none of these need scoring. Moving them out of must into filter gives the same results, faster.
If a specific value returns zero and you expected results, check whether the field is canonical (fixed value set, lowercase) or free-form (varies by enrichment source). Free-form fields like job_company_industry often have surprising real values like "Computer Software" instead of the guessable "Software".
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); 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