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.
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.