Jobhunt: Automated Job Matching with LLM Scoring

Finding roles in the intersection of AI, Geoinformatics, and Data Science is genuinely hard. Job platforms keyword-match titles and miss the actual skill overlap. Jobhunt is a fully automated weekly pipeline that fixes this — it scrapes real listings from the Bundesagentur für Arbeit, scores each one against a live CV using an LLM, and produces an interactive map and ranked report. All locally. All self-hosted.

What Makes It Interesting

A Real Public API, Not a Scraper

Jobhunt pulls directly from the Bundesagentur für Arbeit REST API — no headless browser, no HTML parsing. The API requires no registration; the static key jobboerse-jobsuche is public. Listings come back as structured JSON with coordinates, work type, and start date included. Missing coordinates fall back to Nominatim geocoding, rate-limited to 1 req/sec as per OSM policy.

LLM Scoring with Three Interchangeable Backends

The scoring engine is backend-agnostic. A Scorer base class with a factory function lets you switch between:

| Backend | Use case |

|---|---|

| Ollama (llama3.1:8b) | Local GPU, zero cost, runs in Docker with NVIDIA passthrough |

| Claude (claude-sonnet-4) | Best reasoning quality, via API key |

| OpenAI (gpt-4o-mini) | Lightweight cloud fallback |

Each backend returns {"score": 0-100, "reasoning": "..."} and runs a health_check() before the pipeline starts. JSON parsing is deliberately defensive — strips markdown fences, finds the outer {...}, handles garbage output gracefully.

Incremental Runs via ChromaDB

Every scored job is stored in a ChromaDB vector store keyed by the BA job ID. On subsequent weekly runs, already-scored jobs are skipped entirely — no duplicate API calls, no wasted tokens. The same store doubles as a semantic search index: search_similar("geospatial Python role") returns nearest-neighbour matches from everything ever scored.

The Output

Each run drops a timestamped folder with three artefacts:

  • jobhunt_map.html — Folium interactive map on dark tiles, markers clustered by geography, colour-coded green/orange/red/gray by conviction score, popups with reasoning and direct links to listings
  • jobhunt_report.md — Top 15 matches in detail, then a full ranked table of all scored jobs
  • jobs_raw.json — Complete dump for any downstream use

Architecture

Bundesagentur API → scraper.py → enrichment + Nominatim geocoding
                                       ↓
                              scorer.py (Ollama / Claude / OpenAI)
                                       ↓
                         store.py (ChromaDB — dedup + semantic search)
                                       ↓
                outputs.py → Folium map + Markdown report + raw JSON

The stack runs in Docker Compose: an ollama service with GPU passthrough, an ollama-init one-shot container that pulls the model on first run, and the jobhunt container that mounts config.yaml and profile.md at runtime. Update your CV, and the next run scores against it automatically.

See It Running

The pipeline kicking off — configured searches are dispatched to the BA API and results start flowing in

The pipeline runs on a weekly cron schedule. On each invocation it fans out across configured search queries, deduplicates by job ID, and hands each listing to the scoring backend.

Ollama running llama3.1:8b inside Docker — each job description scored locally, no data leaving the machine

Zero API cost when running locally. The LLM reads the full job description alongside your profile and returns a conviction score with a two-sentence reasoning. GPU-accelerated via NVIDIA Docker runtime.

The final output: a dark-tile Folium map with clustered, colour-coded markers and the ranked markdown report

Green markers are high-conviction matches (≥75), orange medium, red low. Click any marker for the score, reasoning, and a direct link to the listing on the BA portal.

Testing Philosophy

CI covers pure logic only — config loading, data model serialisation, BA API response parsing, LLM JSON extraction robustness (markdown fences, preamble, missing keys, invalid JSON), and output file generation. No LLM mocking. Anything requiring Ollama or a live API key is integration-tested locally. This keeps CI fast and avoids mocked tests silently drifting from real inference behaviour.

---

The whole thing is containerized and ready to run. Pull the repo, drop in your profile.md, and point it at whichever scoring backend you have available — grab it on GitLab and see for yourself.