Jobs Radar
Personalised job search pipeline: scrape → tag → embed → rank → rate.
Features
- Resume parsing — paste raw resume text, get structured output (target roles, skills, seniority, experience) via OpenAI structured outputs
- Parallel scraping — scrapes Indeed across multiple queries and locations concurrently using
ThreadPoolExecutor - Dedup + tagging — removes duplicate listings, tags each job for seniority, entry-level, remote, clearance, and experience range using regex
- Hybrid vector search — dense (BGE) + sparse (BM25) search with Reciprocal Rank Fusion via Qdrant in-memory
- LLM-as-judge rating — each result is scored 1–10 against the candidate's resume using
gpt-4o-mini - Feedback loop — rate jobs 1–5 stars; liked/disliked examples are injected into the judge prompt on future searches to personalise ratings over time
- Single-table persistence — search results and user feedback live in one SQLite table (
job_results), keyed byjob_url; re-submitting feedback updates the row, no duplicates - CSV export — every scrape is auto-saved to
~/.jobs-radar/jobs_{timestamp}.csv, downloadable from the UI - Streamlit UI — parse resume, search jobs with inline feedback, browse and rate recent results
- REST API — FastAPI backend, useful for scripting or other clients
Stack
- Python 3.12, FastAPI, Streamlit
- OpenAI API (
gpt-4o-miniby default) - Qdrant (in-memory) + fastembed
- jobspy (Indeed scraping)
- SQLite (results + feedback storage)
- uv (dependency management)
Installation
pip install jobs-radar
# or with uv
uv add jobs-radar
Or run from source:
git clone https://github.com/your-username/jobs-radar
cd jobs-radar
uv sync
Setup
cp .env.example .env # fill in OPENAI_API_KEY
Data (SQLite DB + CSVs) is stored in ~/.jobs-radar/ by default. Override with DATA_DIR in your .env.
Running
You need two terminals — one for the API server and one for the UI.
From a pip/uv install:
# terminal 1 — start the API server
jobs-radar-serve
# terminal 2 — open the UI
jobs-radar-ui
From source:
# terminal 1 — API
uvicorn jobs_radar.main:app --reload
# terminal 2 — UI
streamlit run jobs_radar/ui.py
Then open http://localhost:8501 in your browser. API docs at http://localhost:8000/docs.
Configuration
All settings are read from environment variables or a .env file:
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
— | Required |
DATA_DIR |
~/.jobs-radar |
Where SQLite DB and CSVs are stored |
API_HOST |
0.0.0.0 |
FastAPI bind host |
API_PORT |
8000 |
FastAPI port |
API_BASE |
http://localhost:8000 |
URL the Streamlit UI uses to reach the API |
API Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /parse |
Parse resume text → structured profile |
| POST | /search |
Scrape, embed, rank, and rate jobs |
| GET | /results/recent |
Load the most recent search results |
| POST | /feedback |
Save user rating for a job |
Project Structure
jobs_radar/
main.py # FastAPI routes
pipeline.py # scrape, dedup, tag, validate
vector_store.py # Qdrant setup, upsert, hybrid search
llm.py # resume parsing, job rating, judge prompt
feedback.py # SQLite — job_results table, save/load functions
models.py # Pydantic models
config.py # pydantic-settings — all env/config vars
cli.py # jobs-radar-serve and jobs-radar-ui entrypoints
ui.py # Streamlit UI
tests/ # pytest suite (54 tests)
Data Model
All results and feedback share one SQLite table (~/.jobs-radar/feedback.db):
| Column | Source |
|---|---|
job_url (PK) |
scraper |
title, company, location, ... |
scraper |
llm_rating, relevance_score, llm_reasoning |
system |
user_rating, notes |
user feedback |
search_id, saved_at |
system |
Re-running a search updates system fields for any URL already in the DB while preserving user_rating and notes.
Notes
- Qdrant runs in-memory — the vector index is not persisted between server restarts. The SQLite DB and CSV exports are the durable layer.
- fastembed downloads model weights on the first
upsert_jobscall (~30–60s). Subsequent calls are fast.
Metadata
Release files for jobs-radar 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jobs_radar-0.1.2.tar.gz | 133.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jobs_radar-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 148.9 kB
Release files / jobs_radar-0.1.2.tar.gz
| Download URL | jobs_radar-0.1.2.tar.gz |
|---|---|
| Size | 133.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b7481f5f27dcd29a5bc1591de230b05fb656104e273efb8e020cc37c7b043dc9
|
|
BLAKE2b-256 checksum How to use checksums |
b018f375840114cef0dd6e86940a4db4d281f8eb760c01fa489965a11bd6092f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Release files / jobs_radar-0.1.2-py3-none-any.whl
| Download URL | jobs_radar-0.1.2-py3-none-any.whl |
|---|---|
| Size | 15.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
204180ee4ad679e493a94351802ce5012197f5671b79db6f82695553066cc319
|
|
BLAKE2b-256 checksum How to use checksums |
614a436de90434f5af696c3279ae1d85663b6debbc614f5534cf42c2ec2f1438
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|