Refery.

02. El motor de emparejamiento

El motor de emparejamiento responde a una pregunta engañosamente compleja: dado el perfil completo de un candidato y un conjunto de puestos abiertos, ¿para qué puestos esta persona es genuinamente un ajuste dentro del 1% superior, y qué puestos podemos descartar con confianza?

Un enfoque ingenuo consistiría en pedirle a un LLM que evalúe cada par (candidato, puesto). Esta es la arquitectura que despliegan la mayoría de las startups de "reclutamiento con IA". No escala, produce resultados inconsistentes entre ejecuciones y es drásticamente más costosa de lo necesario.

En cambio, el motor de emparejamiento de Refery opera como un pipeline de tres etapas:

  1. El motor de señales calcula señales deterministas y estructuradas a partir del historial del candidato. Se calculan una sola vez y se almacenan en caché.
  2. El recuperador multivectorial utiliza embeddings semánticos combinados con filtros duros para reducir millones de pares candidato-puesto a los 20-30 mejores candidatos por puesto.
  3. El panel adversarial (tratado en el capítulo 03) ejecuta la costosa evaluación con LLM solo sobre este conjunto reducido.

Este capítulo cubre las etapas 1 y 2.

El motor de señales

Antes de que se realice cualquier operación vectorial, Refery extrae un conjunto de señales estructuradas y deterministas del historial de cada candidato. Estas señales son la base del sistema de emparejamiento. Las calcula el código, no los LLM, lo que significa que son reproducibles, depurables y gratuitas de calcular.

Nivel de logo (pipeline crudo → modificado)

Cada empresa en el currículum de un candidato recibe una puntuación en una lista de niveles. La lista de niveles es consciente de la función: los candidatos de ingeniería se puntúan contra una lista de niveles de ingeniería; los candidatos de ventas contra una lista de niveles de ventas. Un pequeño subconjunto de empresas se ubica en lo más alto de una lista pero no de la otra, y esa asimetría importa.

// signal-engine/logo-tier.ts

type Tier = 'S+' | 'S' | 'A' | 'B' | 'C' | 'D';

const ENG_TIERS: Record<Tier, string[]> = {
  'S+': ['Google', 'Meta', 'Apple', 'Amazon', 'Netflix', 'OpenAI',
         'Anthropic', 'DeepMind', 'Stripe', 'Databricks', 'Figma'],
  'S':  ['Airbnb', 'Uber', 'Coinbase', 'Notion', 'Linear', 'Vercel',
         'Plaid', 'Ramp', 'Brex', 'Scale', 'Cursor', 'Perplexity'],
  'A':  ['Atlassian', 'Slack', 'Snowflake'],
  'B':  [], // calculado dinámicamente: startups financiadas menos conocidas
  'C':  [], // startups desconocidas, tecnología genérica
  'D':  [], // empresas no-tech, consultoría tradicional
};

const TIER_SCORE: Record<Tier, number> = {
  'S+': 6, 'S': 5, 'A': 4, 'B': 3, 'C': 2, 'D': 1,
};

export function computeRawLogoTier(
  companies: CompanyExperience[],
  fnRole: 'eng' | 'sales'
): { average: Tier; peak: Tier; reasoning: string } {
  const tierList = fnRole === 'eng' ? ENG_TIERS : SALES_TIERS;
  const scored = companies.map(c => ({
    company: c.name,
    tier: classifyCompany(c.name, tierList),
    yearsAt: c.tenure,
  }));

  const weightedAvg = weightedAverage(scored.map(s => ({
    score: TIER_SCORE[s.tier],
    weight: s.yearsAt,
  })));

  return {
    average: scoreToTier(weightedAvg),
    peak: scored.reduce((max, s) =>
      TIER_SCORE[s.tier] > TIER_SCORE[max.tier] ? s : max
    ).tier,
    reasoning: formatReasoning(scored),
  };
}

El modificador de pedigrí

El nivel de logo crudo está sesgado intencionalmente hacia nombres reconocidos. Pero una startup de 50 personas respaldada por Sequoia es materialmente diferente de un negocio de 50 personas autofinanciado, y el nivel de logo por sí solo pasa eso por alto. El modificador de pedigrí lo corrige.

// signal-engine/pedigree.ts

const TOP_TIER_VCS = new Set([
  'Sequoia', 'Andreessen Horowitz', 'a16z', 'Benchmark', 'Founders Fund',
  'Accel', 'Greylock', 'Index', 'Lightspeed', 'Bessemer', 'Kleiner',
  'Khosla', 'Thrive', 'NEA', 'GV', 'Conviction', 'Radical', 'Spark',
  'Ribbit', 'Initialized', 'USV',
]);

export function applyPedigreeModifier(
  rawTier: Tier,
  company: CompanyExperience
): { modifiedTier: Tier; lift: number; reason: string } {
  let lift = 0;
  const reasons: string[] = [];

  // Respaldo de un VC de primer nivel → +1 nivel
  const topInvestors = company.investors.filter(i => TOP_TIER_VCS.has(i));
  if (topInvestors.length > 0) {
    lift += 1;
    reasons.push(`backed by ${topInvestors.join(', ')}`);
  }

  // Umbrales del tramo de financiación
  if (company.totalRaised >= 200_000_000) {
    lift += 1;
    reasons.push(`$${(company.totalRaised / 1e6).toFixed(0)}M raised`);
  } else if (company.totalRaised >= 50_000_000) {
    lift += 1;
    reasons.push(`well-funded at $${(company.totalRaised / 1e6).toFixed(0)}M`);
  }

  // Limitar el incremento a +2 para evitar una puntuación descontrolada
  lift = Math.min(lift, 2);

  return {
    modifiedTier: shiftTier(rawTier, lift),
    lift,
    reason: reasons.join(', '),
  };
}

La bonificación de IA

Apilada sobre el pedigrí. Los laboratorios de modelos fundacionales (OpenAI, Anthropic, Mistral, Cohere, xAI, DeepMind) reciben un incremento completo de +1 nivel. Las empresas de infraestructura de IA (Scale, Pinecone, Weights & Biases, Modal, Replicate, LangChain, Together, Hugging Face) reciben +1. Las aplicaciones nativas de IA (Cursor, Perplexity, Harvey, Sierra, Decagon, Glean, Hebbia, ElevenLabs, Suno) reciben de +0,5 a +1 según su tracción.

Este es un sesgo afinado intencionalmente para Refery. La cartera de clientes de Refery está fuertemente orientada a la IA, por lo que el sistema de emparejamiento se inclina hacia candidatos con exposición relevante a la IA. El sesgo se expone explícitamente en los informes de candidatos para que pueda revisarse y recalibrarse.

Análisis de trayectoria

La trayectoria es un resumen de una línea sobre cómo un candidato se ha movido entre etapas de empresas a lo largo de su carrera. Capta algo que la pura puntuación por niveles pasa completamente por alto: si alguien ha vivido realmente una transición de etapa o solo se ha incorporado siempre después de la salida a bolsa.

// signal-engine/trajectory.ts

type Stage = 'pre-seed' | 'seed' | 'series-a' | 'series-b'
           | 'series-c' | 'late-stage' | 'public' | 'non-tech';

export function computeTrajectory(history: CompanyExperience[]): string {
  const transitions = history.map(c => ({
    company: c.name,
    joinedAt: classifyStageAtTime(c.name, c.startDate),
    leftAt:   classifyStageAtTime(c.name, c.endDate ?? new Date()),
    yearsAt:  c.tenure,
  }));

  const stageRides = transitions.filter(t => t.joinedAt !== t.leftAt);
  const earlyStageCount = transitions.filter(t =>
    ['pre-seed', 'seed', 'series-a'].includes(t.joinedAt)
  ).length;

  if (stageRides.length >= 2 && earlyStageCount >= 2) {
    return `${stageRides.length} stage rides, ${earlyStageCount} early-stage joins. Pure builder DNA.`;
  }

  if (transitions.every(t => t.joinedAt === 'public')) {
    return `Always joined post-IPO. No 0-to-1 exposure.`;
  }

  // ... la lógica de clasificación completa continúa
}

Indicador no-tech

Un valor booleano que expone una preocupación estructural sobre el ajuste de un candidato para tecnología de etapa temprana. Se activa cuando se cumplen simultáneamente tres condiciones:

  1. Más del 50% de la carrera transcurrió en empresas no-tech
  2. Ningún producto lanzado notable ni logros de construcción
  3. Ninguna narrativa de pivote clara que explique el cambio

El indicador se suprime si el candidato lideró una transformación digital real, fue no-tech solo en su carrera temprana con más de 5 años en tecnología desde entonces, estuvo en una función adyacente a la tecnología (quant o ciencia de datos en un banco aún representa una señal fuerte para puestos de ML), o fundó su propio emprendimiento durante cualquier brecha.

Esto no es un veto. El indicador expone la preocupación al panel. La persona del Escéptico es responsable de su interpretación.

Perfil de cliente de ventas

Solo para candidatos de ventas/GTM, una clasificación adicional de 3 dimensiones:

  • Segmento de cliente: Empresa estratégica, Empresa, Mercado medio, PYME, Startup/PLG
  • Tramo de ACV: Estratégico ($1M+), Mayor ($250K-$1M), Medio ($50K-$250K), PYME ($10K-$50K), Velocidad (menos de $10K)
  • Concentración por industria: fintech, salud, sector público, retail/ecom, manufactura, tech/SaaS, medios, bienes raíces, legal u "horizontal"

Esto es lo que detecta el modo de fallo del "representante de ventas dentro del 5% superior en el segmento equivocado". Un candidato que vendió acuerdos PYME de Velocidad de $20K a startups es estructuralmente un mal ajuste para un puesto de Empresa con ACV de $500K, por buenas que sean sus cifras.

El recuperador multivectorial

Una vez calculadas las señales, los candidatos y los puestos se incrustan en un espacio vectorial para la recuperación semántica. La estrategia de embeddings de Refery es multivectorial: cada candidato y cada puesto se representa mediante varios embeddings distintos, uno por eje de señal, en lugar de un único vector concatenado.

Por qué multivectorial

Un único embedding denso lo mezcla todo: habilidades, experiencia, compensación, ubicación, modalidad de trabajo, ajuste por etapa. Esto pierde información. Dos candidatos con habilidades similares pero un ajuste por etapa muy diferente producen vectores que se parecen entre sí, aunque deberían clasificarse de forma muy distinta para el mismo puesto.

La recuperación multivectorial mantiene cada eje independiente y agrega las puntuaciones de similitud por eje en el momento de la consulta, con pesos específicos por eje afinados según el tipo de puesto.

Esquema

Los embeddings se almacenan en tablas de pgvector, ubicadas junto con los datos relacionales en Supabase.

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE candidate_embeddings (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  candidate_id  uuid NOT NULL REFERENCES candidates(id) ON DELETE CASCADE,
  axis          text NOT NULL,
  embedding     vector(1536) NOT NULL,
  computed_at   timestamptz NOT NULL DEFAULT now(),
  UNIQUE (candidate_id, axis)
);

CREATE INDEX ON candidate_embeddings
  USING hnsw (embedding vector_cosine_ops);

CREATE TABLE job_embeddings (
  id           uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  job_id       uuid NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
  axis         text NOT NULL,
  embedding    vector(1536) NOT NULL,
  computed_at  timestamptz NOT NULL DEFAULT now(),
  UNIQUE (job_id, axis)
);

CREATE INDEX ON job_embeddings
  USING hnsw (embedding vector_cosine_ops);

Los siete ejes que Refery incrusta actualmente:

EjeQué capta
skillsSuperficie de habilidades técnicas y funcionales
trajectoryArco profesional, exposición a etapas, constructor frente a escalador
comp_signalsExpectativa de compensación, apalancamiento de negociación, tolerancia al equity
geoPreferencias de ubicación, disposición de zona horaria
work_modePreferencia de remoto / híbrido / presencial
stage_fitEn qué etapa de empresa esta persona opera mejor
founder_dnaPara puestos sénior: alineación con el estilo operativo del fundador

Cada eje se incrusta alimentando un prompt pequeño y estructurado al modelo de embedding de OpenAI. El prompt está estrictamente acotado: el eje skills ve solo la sección de habilidades, el eje trajectory ve solo el resumen estructurado de trayectoria, y así sucesivamente. Esto mantiene cada embedding enfocado.

Consulta de recuperación

Una recuperación contra los siete ejes para un solo puesto se ve así:

WITH job_axes AS (
  SELECT axis, embedding
  FROM job_embeddings
  WHERE job_id = $1
)
SELECT
  c.id AS candidate_id,
  c.name,
  -- Similitud coseno por eje (1 - distancia)
  AVG(1 - (ce.embedding <=> ja.embedding)) AS avg_similarity,
  -- Puntuación ponderada usando pesos de eje específicos del puesto
  SUM(
    CASE ce.axis
      WHEN 'skills'        THEN (1 - (ce.embedding <=> ja.embedding)) * 0.30
      WHEN 'trajectory'    THEN (1 - (ce.embedding <=> ja.embedding)) * 0.20
      WHEN 'stage_fit'     THEN (1 - (ce.embedding <=> ja.embedding)) * 0.20
      WHEN 'founder_dna'   THEN (1 - (ce.embedding <=> ja.embedding)) * 0.10
      WHEN 'comp_signals'  THEN (1 - (ce.embedding <=> ja.embedding)) * 0.08
      WHEN 'geo'           THEN (1 - (ce.embedding <=> ja.embedding)) * 0.07
      WHEN 'work_mode'     THEN (1 - (ce.embedding <=> ja.embedding)) * 0.05
    END
  ) AS weighted_score
FROM candidate_embeddings ce
JOIN candidates c ON c.id = ce.candidate_id
JOIN job_axes ja ON ja.axis = ce.axis
WHERE c.status IN ('active', 'reviewing')
  AND c.do_not_contact = false
GROUP BY c.id, c.name
ORDER BY weighted_score DESC
LIMIT 30;

El operador <=> es la distancia coseno de pgvector, que combinada con el índice HNSW se ejecuta en aproximadamente O(log N). Con el volumen de datos actual de Refery, esta consulta retorna en milisegundos de un solo dígito.

El filtro do_not_contact = false es innegociable. La lista negra (que actualmente incluye a Resolve.ai y un puñado de otros, cada uno con do_not_contact = true) se aplica en la capa SQL para hacer imposible que el código posterior exponga accidentalmente a un candidato en la lista negra.

Filtros duros

Antes de calcular la similitud vectorial, los filtros duros eliminan los emparejamientos estructuralmente inviables. Son deterministas y gratuitos.

-- Filtro de visa, aplicado por puesto
WHERE
  CASE j.visa_requirement
    WHEN 'us_authorized' THEN c.work_authorization @> ARRAY['us_authorized']
    WHEN 'eu_authorized' THEN c.work_authorization @> ARRAY['eu_authorized']
    WHEN 'sponsor_available' THEN true  -- cualquier candidato encaja
    WHEN 'global_remote' THEN true
    ELSE false
  END

-- Filtro de piso de compensación
AND j.salary_max >= c.salary_expectation_min

-- Filtro de ubicación / modalidad remota
AND (
  j.remote_policy = 'remote'
  OR (j.remote_policy = 'hybrid' AND c.location_metro = j.location_metro)
  OR (j.remote_policy = 'onsite' AND c.location_metro = j.location_metro)
)

Motor de restricción de colocación

Tras la recuperación, cada candidato tiene una puntuación de "alcance" de 3 ejes contra la bolsa de empleo en vivo:

// matching/placement-constraint.ts

export async function computePlacementConstraint(
  candidate: Candidate,
  openJobs: Job[]
): Promise<PlacementConstraint> {
  const total = openJobs.length;

  const compReach = openJobs.filter(j =>
    j.salary_max >= candidate.salary_expectation_min
  ).length / total;

  const locationReach = openJobs.filter(j =>
    j.remote_policy === 'remote' ||
    j.location_metro === candidate.location_metro ||
    candidate.relocation_open
  ).length / total;

  const visaReach = candidate.work_authorization.includes('us_authorized')
    ? 1.0
    : openJobs.filter(j =>
        ['global_remote', 'sponsor_available', 'eu_authorized']
          .includes(j.visa_requirement)
      ).length / total;

  return {
    compReach,
    locationReach,
    visaReach,
    flags: [
      compReach < 0.30 ? 'comp_floor_too_high' : null,
      locationReach < 0.40 ? 'location_too_narrow' : null,
      visaReach < 0.20 ? 'visa_too_restrictive' : null,
    ].filter(Boolean),
    verdict: classifyPlacementDifficulty(compReach, locationReach, visaReach),
  };
}

Esto se expone en el informe del candidato como algo así como:

Restricción de colocación: piso de $325K + remoto en Portland + sin autorización estadounidense = 10% de la bolsa alcanzable. Estructuralmente difícil de colocar.

Esta es una señal crítica para la clasificación. Un candidato del "5% superior" con un alcance del 10% es una realidad operativa distinta a un candidato del 25% superior con un alcance del 80%.

Por qué esto es novedoso

La combinación de estas técnicas en una plataforma de reclutamiento constituye conocimiento técnico propietario:

  1. Descomposición multivectorial afinada específicamente para los ejes de contratación. La mayoría de los sistemas de recuperación en producción usan un único embedding denso. La descomposición de siete ejes de Refery está diseñada a medida para la estructura de señales específica de la contratación de tecnología sénior.
  2. Nivel de logo consciente de la función con modificadores de pedigrí e IA. Cada modificador es auditable de forma independiente, se expone en el informe y es ajustable. No existe una biblioteca de código abierto o comercial equivalente para esto.
  3. Trayectoria como señal estructurada de primera clase. Los currículums de texto libre no captan las transiciones de etapa; Refery las extrae explícitamente y se convierten en uno de los tres pesos principales de la puntuación de emparejamiento.
  4. Motor de restricción de colocación como primitiva de clasificación. Calcular el alcance en la bolsa en vivo para cada candidato, en cada ejecución de emparejamiento, le da al operador una base cuantitativa para la priorización que ningún ATS proporciona.
  5. Arquitectura co-ubicada de vector + relacional + RLS. Toda la recuperación, el filtrado y el control de acceso ocurren en una única consulta SQL, eliminando toda una clase de errores de consistencia.

El resultado es un sistema que convierte el historial completo de un candidato en un pequeño conjunto de campos estructurados de alta señal y embeddings, que luego impulsan cada decisión posterior. Continúa en el capítulo 03, donde el panel se hace cargo de las decisiones genuinamente ambiguas.