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

# Find lookalike candidates using seed and expand

> How to find people similar to a known person by deriving search criteria from a seed profile.

## Overview

Recruiting and research teams often want to find more people similar to a known person. The known person is a candidate or contact they already rate, given as an example rather than a filter set. For example, a hiring manager may want to find engineers like the one they hired from another employer last year. But this manager can't easily write that as a query using a job title and specific skills, because it's likely to be too vague or too narrow. The example candidate represents the implicit criteria the hiring manager wants; the task is to make those criteria explicit.

This tutorial walks through a pattern for solving this problem, by resolving the seed profile and then performing an expanded search, using the Seltz [Answer](/answer/guides/basic-answer) and [Search](/api-reference/search) endpoints.

## Outline

1. Resolve the seed and derive search criteria with a single Answer call.
2. Confirm the seed before expanding.
3. Expand results by searching again using the derived criteria.
4. Filter and dedupe the results.

The examples in this tutorial find lookalike candidates for a sales hire, but the same pattern applies to any role, or to any scope where a seed profile can be turned into search criteria.

## 1. Resolve the seed and derive search criteria with a single Answer call.

Rather than calling [`search`](/api-reference/search) yourself, picking a candidate by hand, and parsing the profile's Markdown content, use `answer` with a `response_format` JSON schema. Passing [`scope: "people"`](/api-reference/answer#body-scope-one-of-0) grounds the answer in [people](/data/people) profiles, and the schema shapes the result directly into the criteria you need for the expanded search. A bare name is a weak query on this scope, and a generic anchor like an employer alone may not be distinctive enough either. Combine whatever identifying details you already have, such as employer, role, and location, similar to anchoring a company name with an industry or location.

`answer` always returns `citations` regardless of `response_format`, so the same call also gives you the resolved profile's URL. You'll use this URL in [step 2](#2-confirm-the-seed-before-expanding) to confirm the seed, and again in [step 4](#4-filter-and-dedupe-the-results) to keep it out of the expanded results.

<CodeGroup>
  ```python Python theme={null}
  import json

  from seltz import Seltz

  client = Seltz()

  LOOKALIKE_CRITERIA_FORMAT = {
      "type": "json_schema",
      "json_schema": {
          "name": "lookalike_criteria",
          "schema": {
              "type": "object",
              "properties": {
                  "headline": {"type": "string"},
                  "role": {"type": "string"},
                  "skills": {
                      "type": "array",
                      "items": {"type": "string"},
                  },
              },
              "required": ["headline", "role", "skills"],
              "additionalProperties": False,
          },
          "strict": True,
      },
  }

  def resolve_seed(name: str, employer: str) -> dict | None:
      response = client.answer(
          f"What is the headline, job title (without the employer name), and top skills for {name}, who works at {employer}?",
          scope="people",
          response_format=LOOKALIKE_CRITERIA_FORMAT,
      )
      try:
          criteria = json.loads(response.answer)
      except json.JSONDecodeError:
          return None
      criteria["seed_url"] = response.citations[0].url if response.citations else None
      return criteria

  seed = resolve_seed("Jane Doe", "ACME Corp, Sales Leader")
  ```

  ```typescript TypeScript theme={null}
  import { Seltz } from "seltz";

  const client = new Seltz();

  const lookalikeCriteriaFormat = {
    type: "json_schema",
    json_schema: {
      name: "lookalike_criteria",
      schema: {
        type: "object",
        properties: {
          headline: { type: "string" },
          role: { type: "string" },
          skills: { type: "array", items: { type: "string" } },
        },
        required: ["headline", "role", "skills"],
        additionalProperties: false,
      },
      strict: true,
    },
  };

  async function resolveSeed(name: string, employer: string) {
    const result = await client.answer({
      query: `What is the headline, job title (without the employer name), and top skills for ${name}, who works at ${employer}?`,
      scope: "people",
      responseFormat: lookalikeCriteriaFormat,
    });
    let criteria;
    try {
      criteria = JSON.parse(result.answer);
    } catch {
      return null;
    }
    criteria.seedUrl = result.citations[0]?.url ?? null;
    return criteria;
  }

  const seed = await resolveSeed("Jane Doe", "ACME Corp, Sales Leader");
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.seltz.ai/v1/answer \
    -H "x-api-key: $SELTZ_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "query": "What is the headline, job title (without the employer name), and top skills for Jane Doe, who works at ACME Corp, Sales Leader?",
      "scope": "people",
      "response_format": {
        "type": "json_schema",
        "json_schema": {
          "name": "lookalike_criteria",
          "strict": true,
          "schema": {
            "type": "object",
            "properties": {
              "headline": {"type": "string"},
              "role": {"type": "string"},
              "skills": {"type": "array", "items": {"type": "string"}}
            },
            "required": ["headline", "role", "skills"],
            "additionalProperties": false
          }
        }
      }
    }'
  ```
</CodeGroup>

See [Answer API Reference](/api-reference/answer#body-response_format) for the full `response_format` behavior, including how a malformed schema is rejected before the request is billed.

<Note>
  Generation can also be truncated partway through, in which case `response.answer` doesn't parse as valid JSON: the example above returns `None` rather than raising, since you should retry a failed resolution with a more specific anchor, not treat it as a match. Check for this before continuing to the next step: the steps below assume a resolved seed and fail on a `None`/`null` result.
</Note>

## 2. Confirm the seed before expanding.

This step takes place in your product. It's valuable to confirm you're using the right seed before expanding the search: if you use the wrong seed, there's no way for the user to know, and the downstream results may be incorrect in a way that's hard to recognize. This confirmation step prevents that from happening.

The example below shows the resolved headline, role, skills, and source URL, and asks the user to confirm it's the right person before continuing.

<CodeGroup>
  ```python Python theme={null}
  print(f"Headline: {seed['headline']}")
  print(f"Role: {seed['role']}")
  print(f"Skills: {', '.join(seed['skills'])}")
  print(f"Source: {seed['seed_url']}")

  # In your product, this is a UI confirmation rather than a prompt.
  confirmed = input("Is this the right person? [y/n] ")
  if confirmed.lower() != "y":
      raise ValueError("Refine the query with a more specific anchor and try again.")
  ```

  ```typescript TypeScript theme={null}
  import * as readline from "node:readline/promises";

  console.log(`Headline: ${seed.headline}`);
  console.log(`Role: ${seed.role}`);
  console.log(`Skills: ${seed.skills.join(", ")}`);
  console.log(`Source: ${seed.seedUrl}`);

  // In your product, this is a UI confirmation rather than a prompt.
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  const confirmed = await rl.question("Is this the right person? [y/n] ");
  rl.close();

  if (confirmed.toLowerCase() !== "y") {
    throw new Error("Refine the query with a more specific anchor and try again.");
  }
  ```
</CodeGroup>

## 3. Expand results by searching again using the derived criteria.

Build a second query from the criteria you derived, as descriptive terms rather than a strict filter. You're looking for people who resemble the seed, not people who match it exactly.

Use `role` here rather than `headline`: the `people` scope is also used for same-company retrieval, so a query that still carries the seed's employer name, as `headline` does, biases the expansion toward the seed's coworkers instead of similar people elsewhere. `role` describes what makes someone a match, using title and skills, without the company that made `headline` useful for confirming identity in step 2.

<CodeGroup>
  ```python Python theme={null}
  def expand(seed: dict, max_results: int = 25):
      query_terms = " ".join([seed["role"] or "", *seed["skills"][:3]])
      return client.search(query_terms, scope="people", max_results=max_results)

  expanded = expand(seed)
  ```

  ```typescript TypeScript theme={null}
  async function expand(seed: { role: string; skills: string[] }, maxResults = 25) {
    const queryTerms = [seed.role, ...seed.skills.slice(0, 3)].join(" ");
    return client.search({ query: queryTerms, scope: "people", maxResults });
  }

  const expanded = await expand(seed);
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.seltz.ai/v1/search \
    -H "x-api-key: $SELTZ_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "query": "Sales Leader strategic sales planning customer relationship management driving revenue",
      "scope": "people",
      "max_results": 25
    }'
  ```
</CodeGroup>

## 4. Filter and dedupe the results.

The expanded search can return the seed itself, along with duplicate profiles. This step drops the seed by URL, and dedupes the rest of the results before showing them to the user.

<CodeGroup>
  ```python Python theme={null}
  def filter_results(documents: list, seed_url: str):
      seen = set()
      results = []
      for doc in documents:
          if doc.url == seed_url or doc.url in seen:
              continue
          seen.add(doc.url)
          results.append(doc)
      return results

  lookalikes = filter_results(expanded.documents, seed["seed_url"])
  ```

  ```typescript TypeScript theme={null}
  function filterResults(documents: { url?: string }[], seedUrl: string) {
    const seen = new Set<string>();
    const results = [];
    for (const doc of documents) {
      if (!doc.url || doc.url === seedUrl || seen.has(doc.url)) continue;
      seen.add(doc.url);
      results.push(doc);
    }
    return results;
  }

  const lookalikes = filterResults(expanded.documents, seed.seedUrl);
  ```

  ```bash cURL theme={null}
  # $SEED_URL is the citation URL from step 1's response —
  # response.citations[0].url in the JSON returned by the answer call.
  curl -s -X POST https://api.seltz.ai/v1/search \
    -H "x-api-key: $SELTZ_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "query": "Sales Leader strategic sales planning customer relationship management driving revenue",
      "scope": "people",
      "max_results": 25
    }' \
    | jq --arg seed_url "$SEED_URL" '[.documents[] | select(.url != null and .url != $seed_url)] | unique_by(.url)'
  ```
</CodeGroup>

## Beyond recruiting

The same seed-and-expand pattern works for other scopes too. Swap `scope: "people"` for [`companies`](/data/companies) to find companies similar to a known one, deriving criteria like industry and company size from the seed instead of headline, role, and skills. Only the query, the response schema, and the anchor terms change.

<Note>
  The same trap from step 3 applies here: whatever field you use to anchor the seed (like a company's own name) can end up biasing the expand query toward things related to the seed rather than things similar to it. Keep an identity-anchoring field separate from the match-describing fields you actually expand on.
</Note>

## Next steps

* [People](/data/people) — What a `people`-scope result contains
* [Answer API Reference](/api-reference/answer#body-response_format) — Full `response_format` specification
* [Handle errors](/search/guides/handle-errors) — Handling errors from the `search` endpoint
* [Data Concepts](/data/data-concepts) — How scopes work across Seltz endpoints
