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:
- 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é.
- 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.
- 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:
- Más del 50% de la carrera transcurrió en empresas no-tech
- Ningún producto lanzado notable ni logros de construcción
- 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:
| Eje | Qué capta |
|---|---|
skills | Superficie de habilidades técnicas y funcionales |
trajectory | Arco profesional, exposición a etapas, constructor frente a escalador |
comp_signals | Expectativa de compensación, apalancamiento de negociación, tolerancia al equity |
geo | Preferencias de ubicación, disposición de zona horaria |
work_mode | Preferencia de remoto / híbrido / presencial |
stage_fit | En qué etapa de empresa esta persona opera mejor |
founder_dna | Para 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:
- 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.
- 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.
- 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.
- 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.
- 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.