Job search and ranking
Where the Weighted Average Ran Out of Room
A job search tool that reads employer sites directly.
- Role
- Author
- Classification
- Engineering prototype
Problem
Most job results are senior, expired or ineligible.
How it was solved
It discards what you cannot take, then scores the rest.
- Sources
- Discard
- Score
- Rank
Result
321
employers, 117 with a verified feed
Built with
- Python
- FastAPI
- TypeScript
- React
- PostgreSQL
Personal product / Search and ranking
If a job advert is a wish list rather than a minimum, a search tool should rank rather than filter. So what do you do with the two requirements that really are disqualifying?
Penalties larger than their own weight2 of 7 components
Rank rather than filter, and name every deduction
This diagram in words
- Nine adapters source — One per applicant-tracking system, read directly rather than scraped generically.Python
- Discard, narrowly gate — Only where the opportunity does not exist, or is not this profession.
- Not a posting sink — The only thing removed outright rather than ranked down.
- Postings store — Kept with their enrichment, so a score can be recomputed.PostgreSQL
- Weighted sum process — Seven components, with the weights held in configuration.YAML
- Flat penalties gate — For the two requirements no weight can express as a matter of degree.
- Shown with its reasons sink — Every deduction named on the job rather than used to remove it.TypeScript · React
Read the full background
Problem
Why this work exists
A search for machine learning roles in one country returns thousands of results, almost all of them senior, expired, part-time or somewhere you cannot legally work. Reading them is the actual work. The obvious fix - filter hard - throws away the roles a recent graduate should be applying to, because an advert lists what an employer would like rather than what it requires.
Contribution
What I can claim
Built the whole application: one adapter per applicant-tracking system rather than one scraper per company, a scoring model that penalises instead of filtering, and the interfaces around them. Then fixed the model where it could not say what needed saying - two requirements now carry a flat penalty because a weighted term could not carry it.
Method
When a preference turns out to be a constraint
The tool is built on one claim: a job advert is a wish list, not a minimum, so a search should rank rather than filter.
- 26
- 20 / −26
- 16
- 15
- 13 / −30
- 6
- 4
- not full-timeworking-student, placement, VIE and PhD posts
- not this fieldjudged from title and description together
- clearly outside Europesomewhere you could not take the job
- older than the age limitapplying is no longer worth the evening
Those four are the only conditions that discard a posting, and each one answers whether the opportunity exists rather than whether the candidate is good enough. Everything else is a penalty, because a penalty can be read and argued with and a filter cannot.
Read
Employers rent their career pages from a handful of applicant-tracking systems, each with a public endpoint.
Keep
Only four conditions throw a posting away, and each asks whether the opportunity exists or is in this profession.
Penalise
Everything else lowers the score and is named on the job.
Fail
Then two roles scored in the high seventies that nobody could take: one requiring native German, one on another continent.
Separate
A weighted term can only move the total by its own weight, so no setting of those weights could express unavailability.
What this does not show
The weights are one person's judgement about their own search, calibrated against nothing: there is no outcome data behind them, and the premise that a graduate should apply at roughly sixty percent of a posting is an argument rather than a finding. Coverage is uneven - manually watched employers are not monitored automatically - and the tool never applies on anyone's behalf.
How it was built
System
Workflow and decisions
Adapters for 9 applicant-tracking systems reaching 87 configured employers, a matching engine with seven weighted components and explicit penalties, and a full application over the top: setup, review queues, an application pipeline, employer management, automation and analytics.
- 1Read employer systems through 9 adapters, one per applicant-tracking system
- 2Discard a posting only where the opportunity does not exist or is not this profession
- 3Score the rest as a weighted sum over seven components
- 4Apply flat penalties where a weighted term cannot express unavailability
- 5Name every deduction on the job rather than removing the job
- Python
- FastAPI
- TypeScript
- React
- PostgreSQL
- YAML
Evidence
What is actually versioned
location is worth 20 points of the score and is docked up to 26 when the job is outside your tiers; language is worth 13 points of the score and is docked up to 30 when the job is native German required. A weighted term can only move the total by its own weight, so neither could express "you cannot take this job" - a mandatory German native role and a role on another continent both scored in the high seventies on technical merit before the flat penalties were added. The full ladders are German B2 8, German C1 and above 22, German native 30 and outside your tiers 26, outside, but remote 10.
not full-time, not this field, clearly outside Europe, older than the age limit. Each answers whether the opportunity is available or in this profession, never whether the candidate is good enough. Everything else is a penalty, which is visible on the job and can be argued with, where a filter is silent.
Large employers rent one of a handful of applicant-tracking systems, each with a public endpoint, so there is one adapter per system rather than one scraper per company and adding an employer is a line of configuration. The repository reports probing 279 plausible-looking candidates before writing the shipped list, of which 102 - 37% - did not exist; the tool that checked them ships with it.
How the work is checked
Quality controls
How the work is checked
- Rejection is reserved for four conditions about the opportunity, so a poor match is ranked down and explained rather than disappearing
- Every deduction is named on the job, which is what makes the ranking arguable instead of merely obeyed
- Employers were probed against their real endpoints before being configured, and the verification tool is in the repository so the next expansion can be checked the same way
- Candidate match and field relevance stay separate measures rather than being folded into one number
- It never applies on anyone's behalf, and the coverage document states which employers are monitored automatically and which remain manual
Limitations
Where the evidence stops
- The weights are one person's judgement about their own search. Nothing here is calibrated against outcomes, because the outcome data is a handful of applications and cannot support calibration
- Manual watchlist employers are not automatically monitored, so coverage is uneven in a way the dashboard has to state rather than hide
- Field relevance is graded from the title and description together, which recovers roles a title-only rule discarded and still makes mistakes in both directions
- A personal product with one user. It has not been tested against anybody else's search, and the 60% premise it is built on is an argument rather than a measurement
What this changed in my practice
What this changed in my practice
A weighted average is a statement that every consideration is tradeable against every other, and some are not. Both failures looked like scoring bugs and were actually a modelling error: no setting of a thirteen-point language weight makes a native-German requirement disqualifying, because thirteen points is the most it can ever remove. The fix was to stop pretending that constraint was a preference.