← Volver a proyectos

Job Radar

Las ofertas de empleo de distintas fuentes necesitan una estructura común para poder compararlas, filtrarlas y ordenarlas.

Job Radar importa ofertas de Remotive y Python.org, almacena registros normalizados en PostgreSQL y los expone mediante una REST API con FastAPI y una interfaz web sencilla. Su perfil de scoring está orientado a puestos junior remotos de Python/backend, el tipo de trabajo que busco.

El trabajo de ingeniería se centra en interpretar los datos de origen, decidir qué se considera un duplicado y conseguir que el orden de los resultados sea predecible.

Abrir proyecto en línea →

01 / FUENTES

Dos fuentes, una estructura de registro

Remotive proporciona JSON con descripciones de las ofertas. Python.org ofrece una página HTML con un listado. Su importador extrae los campos y las categorías disponibles sin acceder a las páginas individuales de cada oferta.

Ambos importadores producen registros con la misma estructura. Los campos de empresa y ubicación propios de cada fuente se convierten en campos comunes. Las descripciones de Remotive se convierten de HTML a texto plano: se eliminan scripts, estilos y otros elementos ajenos al contenido, conservando el texto legible.

Ambas fuentes usan palabras clave del título para inferir el nivel de experiencia, con una precedencia explícita cuando coinciden varias categorías. Un título no reconocido se clasifica como unknown. Los registros de Remotive se marcan como remotos; en Python.org, la condición de trabajo remoto se infiere del texto del título y la ubicación, comprobando también las negaciones explícitas.

Los registros mal formados se omiten. Una solicitud fallida o una respuesta de origen no reconocida hace fallar la importación. Son resultados distintos: los datos incompletos no deben confundirse con una obtención exitosa desde una fuente que está fallando.

RECORRIDO DEL SISTEMADe los datos de origen a las respuestas ordenadas

IMPORTACIÓN

01 / FUENTE
JSON de Remotive / HTML del listado de Python.org
02 / NORMALIZAR
Extracción y normalización por fuente → registro de oferta común
03 / IDENTIFICAR
Comprobaciones de URL exacta → inserciones por registro
04 / ALMACENAR
PostgreSQLrestricción de unicidad de URL

LECTURA

01 / SOLICITAR
GET /jobs
02 / FILTRAR
Filtros en la base de datos
03 / ORDENAR
Fecha
Orden en SQL → paginación
Score
Cargar candidatos filtrados → calcular scores → ordenar en memoria → seleccionar página
04 / DEVOLVER
Respuesta con el valor calculado de match_score
05 / CONSUMIR
Frontend / consumidor de la API

Los endpoints HTTP de importación y la CLI comparten los servicios de importación. Los scores se calculan al devolver los registros; al ordenar por score, también se calculan antes de paginar. La creación manual de ofertas escribe directamente en el almacenamiento sin pasar por la normalización de los importadores.

02 / IDENTIDAD

Qué se considera un duplicado

Para el MVP, la prioridad práctica era evitar que las importaciones repetidas generaran ofertas duplicadas.

La función compartida de persistencia comprueba las URLs entrantes frente a los registros almacenados y las URLs ya procesadas en el lote. Una restricción de unicidad en la base de datos añade una segunda comprobación. Cada inserción usa un savepoint, lo que permite omitir un conflicto de URL reconocido y continuar procesando los demás registros.

La identidad se basa en la URL exacta. Las URLs existentes se omiten sin actualizar sus registros. No se unifican los parámetros de seguimiento ni los alias de URL.

Esto evita insertar repetidamente una misma URL, pero no identifica todas las vacantes duplicadas. Un mismo puesto puede aparecer en distintos sitios con URLs diferentes —y posiblemente títulos distintos— y conservarse como registros separados.

03 / API Y PERSISTENCIA

De los registros almacenados a las respuestas de la API

SQLAlchemy mapea un único modelo de oferta a PostgreSQL, y Alembic gestiona los cambios del esquema. La marca de tiempo almacenada indica cuándo se añadió la oferta localmente, no cuándo la publicó la fuente.

La API permite filtrar por trabajo remoto y nivel de experiencia, buscar por texto y paginar con limit/offset. La búsqueda de texto consulta título, empresa, ubicación y descripción; los caracteres comodín de búsqueda se tratan literalmente. Los resultados se pueden ordenar por fecha de incorporación o match score, con reglas explícitas de desempate.

OpenAPI y Swagger exponen la estructura de las solicitudes y respuestas. El frontend consulta el mismo endpoint de listado, por lo que la recuperación de datos y la lógica de ordenación permanecen en el backend.

Las importaciones se ejecutan mediante endpoints HTTP protegidos o una CLI de ejecución única. La CLI intenta importar ambas fuentes con sesiones de base de datos separadas, de modo que el fallo de una fuente no impide intentar la otra.

04 / SCORING

Un score para los puestos que busco

El score utiliza cuatro datos: título, descripción, condición de trabajo remoto y nivel de experiencia.

Primero se comprueba la relevancia. Para recibir puntos, un registro debe tener un título que coincida con un puesto de desarrollo o una señal de Python, FastAPI o SQLAlchemy. Los registros elegibles suman puntos por coincidencias de tecnologías, un título relevante, trabajo remoto y nivel junior o de práctica. Los niveles senior y de liderazgo restan puntos.

Python aporta 20 puntos; FastAPI y PostgreSQL aportan 10 cada uno. Cada tecnología cuenta una sola vez, por lo que repetir palabras clave no aumenta el score. Las URLs y los metadatos de origen se eliminan del texto que se evalúa.

El resultado se calcula al devolver los registros, en lugar de almacenarse. Los mismos datos, bajo las mismas reglas, producen el mismo score, limitado al rango de 0 a 100. Expresa relevancia por palabras clave para este perfil, no probabilidad de contratación ni idoneidad semántica.

La ordenación implica un compromiso práctico. El orden por fecha y la paginación se resuelven en SQL. El orden por score carga todos los candidatos filtrados, calcula sus scores en Python, los ordena y después selecciona la página solicitada.

05 / VERIFICACIÓN

Qué se verifica

La suite de pytest cubre normalización, importaciones repetidas, conflictos de URL, reglas de scoring, filtros, paginación y ordenación. También comprueba respuestas de origen mal formadas, autenticación de importaciones y aislamiento de fallos entre fuentes en la CLI.

Las respuestas externas se simulan con mocks y las pruebas de base de datos usan SQLite. Se genera el SQL de las migraciones de PostgreSQL y se comprueba el driver, pero la suite no prueba contra un servidor PostgreSQL en ejecución.

GitHub Actions está configurado para ejecutar pytest con Python 3.12 en pushes y pull requests dirigidos a main. Docker Compose proporciona un entorno local con la aplicación y PostgreSQL; las migraciones de la base de datos siguen siendo un paso explícito.

06 / LÍMITES

Hasta dónde llega el enfoque actual

  • Las ofertas existentes no se actualizan. Las reimportaciones dejan intacto el contenido almacenado y las ofertas no se eliminan cuando desaparecen de una fuente.
  • El nivel de detalle varía según la fuente. Los resúmenes del listado de Python.org aportan menos señales para el scoring que las descripciones completas. La inferencia del nivel de experiencia y del trabajo remoto por palabras clave puede pasar por alto textos ambiguos.
  • El orden por score procesa todos los candidatos filtrados. El trabajo aumenta con el tamaño de ese conjunto de resultados, aunque la página solicitada sea pequeña.

Estos son límites de la implementación actual; no todos responden a estrategias deliberadas desde el inicio.

07 / REFLEXIÓN

Qué aprendí y qué cambiaría después

El scoring y la gestión de duplicados fueron las partes del proyecto de las que más aprendí. Exigieron trabajar tanto con los datos disponibles como con los límites de lo que permitían determinar.

El siguiente paso sería añadir más fuentes de empleo: más ofertas útiles, pero también datos más variados para poner a prueba los supuestos de normalización, deduplicación y scoring. Esa variedad podría revelar casos límite que no aparecen con solo dos fuentes.