Overview
Recruitier’s job matching engine is a multi-stage AI pipeline that takes a candidate’s complete profile — confirmed skills, experience, title, location, and preferences — and finds the most relevant jobs from a database of thousands of opportunities. It then scores and ranks every match using AI evaluation across four dimensions, producing detailed explanations that tell you exactly why each job is a good fit.
The entire process runs in the background. You start it and continue working while the system handles searching, filtering, and scoring. Real-time progress updates are delivered to your browser via Server-Sent Events (SSE) as each stage completes. A typical matching run processes 50-200 jobs in under 2 minutes.
The 5-Stage Matching Pipeline
When matching is triggered for a candidate, the system executes five distinct stages in sequence. Each stage builds on the output of the previous one.
Stage 1: Embedding (Analyzing)
What happens: The system reads the candidate’s full profile and generates vector embeddings — mathematical representations of the candidate’s expertise that enable semantic search.The matching engine loads the candidate’s data from the database, including:
- CV text (original from upload or synthetic from LinkedIn import)
- Confirmed skills (only confirmed skills are used)
- Job title and experience level
- Location coordinates and search radius
- Salary expectations and job type/flexibility preferences
It then generates three separate vector embeddings for the candidate’s profile:| Vector | Source Data | What It Captures | Weight in Search |
|---|
| Title vector | Job title and headline | Role alignment, seniority level, job function | 35% |
| Skills vector | Confirmed skill list | Technical capabilities and tool proficiency | 45% |
| Experience vector | Summary and CV text | Broader professional context, domain knowledge, work complexity | 20% |
These three vectors are stored in the Qdrant vector database for fast similarity comparison. If the candidate has been embedded before (is_embedded = true), existing vectors are used unless the profile has changed. If this is the first embedding or the profile has been updated, fresh vectors are generated by calling the recommendation service.Why three vectors instead of one? A single embedding would blur the signal. By separating title, skills, and experience, the system can independently evaluate different aspects of fit. A candidate might have a perfect skills match (Python, FastAPI, PostgreSQL) for a job that has a very different title (“Platform Engineer” vs. “Backend Developer”). The three-vector approach catches these nuanced matches that a single-vector system would miss.Vector embeddings are high-dimensional mathematical representations (typically 384-768 dimensions) where similar meanings are close together in vector space. “Python Developer” and “Python Engineer” produce vectors that are very close together, even though the exact words are different. “Python Developer” and “Marketing Manager” produce vectors that are far apart.
Stage 2: Generating Search Queries
What happens: The AI generates multiple diverse search strategies based on the candidate’s profile.Rather than running a single search, the system asks the AI (Gemini) to create up to 15 different search query sets. Each query set targets a different angle of the candidate’s capabilities:
- Direct title matches — Jobs with the same or similar title
- Skill-based searches — Jobs that require the candidate’s key skills in combination
- Alternative roles — Related positions the candidate could fill based on their skill set
- Industry variations — The same role in different sectors or company types
- Seniority variations — Similar roles at slightly different levels
Example: A “Senior Python Developer” with skills in FastAPI, PostgreSQL, and Docker might generate:| Query # | Search Strategy | Example Query |
|---|
| 1 | Direct title | ”senior python developer” |
| 2 | Skill combination | ”python backend engineer fastapi” |
| 3 | Framework focus | ”python developer django fastapi” |
| 4 | Alternative title | ”software engineer python” |
| 5 | Infrastructure angle | ”python developer docker kubernetes” |
| 6 | Data angle | ”python developer postgresql data” |
| 7 | Lead variation | ”lead python developer” |
| 8 | Full-stack angle | ”full-stack python developer” |
| … | … | … |
Why multiple queries matter: A single search query can miss jobs that use different terminology for the same role. “Backend Engineer”, “Software Developer”, “Platform Engineer”, and “Application Developer” might all describe positions that a Senior Python Developer would be perfect for. Multiple search strategies cast a wider net and catch opportunities that keyword-only or single-query searching would miss. Stage 3: Searching (Vector Search)
What happens: The system executes parallel vector (semantic) searches across the entire job database, applying preference-based filters.This is where the three-vector matching architecture comes into play. For the candidate’s profile, the system runs three parallel vector searches against the job database:
-
Title-to-Title search (35% weight) — The candidate’s title vector is compared against every job’s title vector using cosine similarity. This finds jobs with similar role descriptions regardless of exact wording. “Senior Python Developer” matches “Python Backend Engineer” because they are semantically similar.
-
Skills-to-Skills search (45% weight) — The candidate’s skills vector is compared against every job’s skills vector. This finds jobs requiring similar technical capabilities. A candidate with [Python, FastAPI, PostgreSQL] matches a job requiring [Python, Django, MySQL] because the skill sets overlap significantly in the vector space.
-
Experience-to-Description search (20% weight) — The candidate’s experience vector is compared against every job’s full description vector. This captures broader context like domain knowledge, industry experience, work complexity, and organizational expectations.
Each search also applies filters based on the candidate’s preferences and data quality requirements:| Filter | Purpose | Applied When |
|---|
| is_active | Only active, current job listings | Always |
| Recency (6 months) | Only jobs posted within the last 6 months | Always |
| Company name | Exclude jobs without a valid company name | Always |
| Industry exclusion | Exclude staffing and recruiting agencies | Always |
| Experience level | Match candidate’s level (junior/medior/senior/lead) | When set on candidate |
| Location + radius | Exclude jobs beyond 2x the preferred radius | When candidate has coordinates |
| Job type | Only matching types (full-time, part-time, contract) | When preferences set |
| Flexibility | Only matching arrangements (on-site, hybrid, remote) | When preferences set |
The search uses a fetch limit of 400 — it retrieves up to 400 candidate jobs from the vector database. This is intentionally larger than the display limit of 200 to provide a buffer for post-filtering (deduplication, existing match exclusion, etc.).Why semantic search outperforms keyword search: A job posting for a “Python Backend Engineer” will match a candidate titled “Senior Python Developer” even though the exact phrase differs. A job requiring “cloud infrastructure experience” will match a candidate whose CV describes “deploying applications on AWS EC2 and ECS” because the embedding model understands these are semantically related. This is dramatically better than keyword matching, which would miss these connections. Stage 4: Processing (Match Creation)
What happens: The system collects results from all searches, removes duplicates, and creates match records in the database.Since multiple search queries and three parallel vector searches may find the same job, the system performs several processing steps:
-
Merge vector scores: For each job found, the three vector similarity scores are combined using the weighted formula:
combined_score = (title_score * 0.35) + (skills_score * 0.45) + (experience_score * 0.20)
-
Deduplicate results: The same job may appear in multiple search results. The system keeps only the highest-scoring instance of each unique job.
-
Exclude existing matches: Jobs that already have a
CandidateJobMatch record for this candidate are excluded. This prevents duplicate match cards in the pipeline.
-
Exclude protected matches: Jobs that are in protected statuses (favorited, applied, contacted, interviewing, offer stage, placed) are identified and their IDs are passed to the exclude list. These matches are NEVER deleted or replaced during re-matching.
-
Validate company data: Each matched job must have a valid company name and company information. Jobs with missing company data are filtered out.
-
Create match records:
CandidateJobMatch records are created in the database with the initial vector similarity scores and a “pending” status. These records link the candidate to specific jobs and will be updated with AI scores in the next stage.
Why deduplication matters: Without it, a job found by 3 different search queries would appear 3 times in your pipeline. The deduplication ensures each opportunity appears exactly once, with the best available vector similarity score.Protected matches are sacrosanct. If you have favorited a job or started interviewing for it, that match will never be removed during re-matching. This is a core design principle — your pipeline progress is always preserved.
Stage 5: Scoring (AI Evaluation)
What happens: Every matched job is individually evaluated by Gemini, which produces a detailed score breakdown and natural language explanation.For each job match, the AI receives a carefully constructed prompt containing:| Input | Source | Character Limit |
|---|
| Candidate CV text | cv_text field (original or synthetic) | Up to 8,000 characters |
| Candidate confirmed skills | skills_with_confidence (confirmed only) | Full list |
| Candidate experience level | experience_level field | Single value |
| Job title | From the matched job | Full text |
| Company name | From the matched job’s company | Full text |
| Job location | From the matched job | Full text |
| Job description | Full description from the job listing | Up to 6,000 characters |
The AI evaluates the match across four scoring dimensions:| Dimension | Default Weight | What It Evaluates |
|---|
| Role Fit | 30% (weight_role) | How well the candidate’s background aligns with the role requirements. Career trajectory, seniority match, role type alignment. |
| Skills Fit | 35% (weight_skills) | How many required skills the candidate possesses. Identifies matching and missing skills. |
| Experience Fit | 20% (weight_experience) | Whether the candidate’s years and type of experience match expectations. Industry relevance, project complexity. |
| Secondary Fit | 15% (weight_secondary) | Additional factors: education alignment, certifications, industry knowledge, language requirements, culture fit indicators. |
Each dimension receives a score from 0 to 1. The AI also produces:
- Key matching points — The strongest reasons the candidate fits this role (displayed as green badges on match cards)
- Potential concerns — Issues to consider, such as missing skills or experience gaps (displayed as orange badges)
- Natural language explanation — A summary of why this match is good or not, with specific evidence from both the CV and job description
- Recommendation — “apply”, “consider”, or “skip” based on the final score
After AI scoring, the location penalty is applied as a multiplicative factor (see Location & Preferences for the formula). Jobs are then ranked by their final score, and ranks are assigned (1 = best match).Concurrent scoring for speed: To keep matching fast even with hundreds of potential jobs, the AI scoring runs concurrently — up to 50 jobs are scored simultaneously using parallel API calls. This concurrency level is tuned to stay within Gemini’s rate limit of 500 requests per minute. Each individual scoring call has a 90-second timeout to prevent any single slow evaluation from blocking the entire batch.A typical matching run scores 50-200 jobs in under 2 minutes.Critical mismatch caps: If a scoring dimension is extremely low, the overall score is capped to prevent misleadingly high scores:
- Role Fit below 0.30: Overall score capped at 35% — even great skills cannot compensate for a completely wrong role
- Skills Fit below 0.30: Overall score capped at 45% — a strong role match does not help if the candidate lacks the core skills
These caps ensure that fundamental mismatches are always surfaced, regardless of how well other dimensions score.
The Technology Behind Matching
Vector Embeddings Explained
At the core of Recruitier’s matching is vector embedding technology. Text (job titles, skill lists, descriptions) is converted into high-dimensional mathematical vectors where similar meanings are close together in vector space.
This means “Python Developer” and “Python Engineer” produce vectors that are very close together, even though the words are different. “Python Developer” and “Java Developer” are further apart but still closer than “Python Developer” and “Marketing Manager”. The distance between vectors represents semantic similarity.
Three-Vector Architecture
Rather than using a single embedding for the whole profile, Recruitier uses three separate vectors with different weights:
| Vector | Weight | Rationale |
|---|
| Title | 35% | The job title is the most direct signal for role alignment. It captures seniority (Senior, Junior, Lead), domain (Backend, Frontend, Full-Stack), and primary function (Developer, Engineer, Architect). |
| Skills | 45% | Skills are the strongest predictor of technical fit. They carry the most weight because a great title match is meaningless if the skills do not align. A “Senior Developer” without the right technology stack is not a fit. |
| Experience | 20% | The full experience description provides “soft signals” — domain knowledge, company types, project complexity, industry context. It is noisier than skills or title but adds useful context that the other vectors miss. |
This weighted approach outperforms single-vector matching because it allows the system to separately evaluate different aspects of fit and combine them intelligently. Research shows that multi-vector approaches improve recall by 15-25% compared to single-vector methods.
Concurrent AI Scoring Architecture
The scoring stage uses a concurrent execution model to maximize throughput:
- Batch size: Up to 50 concurrent AI scoring calls
- Rate limit: Stays within Gemini’s 500 RPM (requests per minute) limit
- Timeout: Each individual scoring call has a 90-second timeout
- Error handling: Failed scoring calls are logged but do not block other scores. A job that fails to score receives a default handling rather than crashing the entire batch.
- Typical throughput: 50-200 jobs scored in under 2 minutes
This means that even for candidates with very broad skill sets or large search radii that produce many vector matches, the scoring completes quickly.
Real-Time Progress Updates
During matching, you receive live progress updates directly on the candidate detail page. A progress panel shows each stage with real-time status indicators:
| Stage | Label | Description |
|---|
| Analyzing | Analyzing Profile | Reading the candidate’s skills and experience |
| Generating | Creating Queries | Building search parameters for the job database |
| Searching | Finding Jobs | Scanning databases for matching opportunities |
| Processing | Processing | Deduplicating and preparing match records |
| Scoring | AI Scoring | Evaluating each match for quality and fit |
Each stage shows one of three states: completed (checkmark), currently running (spinner), or pending (circle). The panel also shows a description of what the current stage is doing and a note that matching usually takes less than a minute.
The progress updates are delivered via Server-Sent Events (SSE) using Redis pub/sub. You do not need to refresh the page. You can watch the matching progress in real time, navigate away and come back later, or work on other candidates while matching runs in the background.
When Matching Runs
Matching is triggered automatically in several scenarios:
| Trigger | Type | What Happens |
|---|
| After CV upload | Automatic | Full 5-stage pipeline runs after the candidate is created and skills are confirmed |
| After LinkedIn import | Automatic | Full 5-stage pipeline runs after the profile is imported |
| After skill change | Automatic | If confirmed skills changed (added/removed), re-matching is triggered |
| After location change | Automatic | Changes to location, coordinates, or radius trigger re-matching |
| On login (incremental) | Automatic | Top 5 candidates matched against 50 new jobs each, with 6-hour cooldown |
| Manual trigger | On demand | You can manually start matching from the candidate profile at any time |
Re-Matching Behavior
When matching runs again for a candidate who already has matches:
- Protected matches are ALWAYS preserved: Jobs in these statuses are never removed:
- Favorited
- Applied
- Contacted
- Interviewing
- Offer Stage
- Placed
- Pending matches may be replaced: Unscored or unacted-upon matches (in “New Matches” status) may be replaced with fresh results
- New matches are added: Jobs that were not in the previous results are added as new matches
- Exclude list: Protected match job IDs are passed to the matching pipeline so it does not waste resources re-scoring jobs you have already engaged with
Re-matching is designed to be safe. It will never erase your pipeline progress. Only “pending” matches that you have not interacted with are affected. This means you can confidently update a candidate’s skills or location knowing that your favorited, contacted, and interviewing matches are safe.
Incremental Matching on Login
To keep match results fresh without manual intervention, Recruitier runs incremental matching when you log in:
| Parameter | Value |
|---|
| Trigger | User login |
| Cooldown | 6 hours (does not run if last run was less than 6 hours ago) |
| Candidates processed | Top 5 candidates (by activity or priority) |
| Jobs per candidate | Up to 50 new jobs |
| SSE events | login_matching_started, incremental_matching_success, login_matching_completed |
This means that every morning when you log in, your most active candidates automatically get matched against jobs that were posted overnight or recently added to the database.
Matching Configuration
Scoring Weights
The default scoring weights can be customized per candidate through the candidate profile:
| Weight | Default | Configurable | Use Case for Adjustment |
|---|
weight_role | 0.30 | Yes | Increase for roles where industry/domain alignment matters most |
weight_skills | 0.35 | Yes | Increase for highly technical roles (ML Engineer, DevOps) |
weight_experience | 0.20 | Yes | Increase for leadership or management positions |
weight_secondary | 0.15 | Yes | Increase for roles where education/certifications are critical |
All four weights must sum to 1.0. When you increase one weight, decrease others proportionally.
For most technical roles, the default weights work well. Only adjust weights when you notice that the standard scoring is not capturing what matters most for a specific type of role. Common adjustments:
- Career changers: Decrease role weight (0.15), increase skills weight (0.50)
- Leadership positions: Increase experience weight (0.30), decrease skills weight (0.25)
- Regulated industries: Increase secondary weight (0.25) to emphasize certifications and compliance
Fetch and Display Limits
The system has configurable limits for pipeline performance:
| Limit | Default | Purpose |
|---|
| Fetch limit | 400 | Maximum jobs retrieved from vector search (buffer for filtering) |
| Display limit | 200 | Maximum matches shown to the user after all processing |
| Minimum score | 0.50 | Jobs below this threshold receive a “skip” recommendation |
Advanced
The Complete Pipeline Data Flow
Here is the full data flow from trigger to results, showing how data moves through each stage:
Trigger (CV upload, skill change, location change, manual, login)
|
v
Stage 1: Embedding
Input: candidate.title, candidate.skills (confirmed), candidate.cv_text
Process: Generate 3 vectors via recommendation service
Output: Title vector, Skills vector, Experience vector -> stored in Qdrant
|
v
Stage 2: Query Generation
Input: Candidate profile (title, skills, summary, experience level)
Process: Gemini generates up to 15 diverse search query sets
Output: Array of search query strings
|
v
Stage 3: Vector Search
Input: 3 candidate vectors + query strings + filter criteria
Process: Parallel searches in Qdrant with filters
Output: Up to 400 job candidates with vector similarity scores
Filters applied: is_active, recency, company name, industry exclusion,
experience level, location/radius, job type, flexibility
|
v
Stage 4: Match Creation
Input: Raw search results (potentially duplicated across queries)
Process: Merge scores (35%/45%/20%), deduplicate, exclude existing matches,
exclude protected matches, validate company data
Output: CandidateJobMatch records in database (pending status)
|
v
Stage 5: AI Scoring
Input: Each match's candidate CV (8K chars) + confirmed skills +
job description (6K chars) + job metadata
Process: 50 concurrent Gemini calls, 90s timeout each
Output: Per-match scores (role_fit, skills_fit, experience_fit, secondary_fit),
key matching points, potential concerns, explanation, recommendation
Post-process: Apply location penalty, calculate final score, assign ranks
|
v
Result: Ranked list of up to 200 scored matches with full explanations
Delivered via: SSE notification to browser
How the Scoring Prompt Works
The AI scoring uses a carefully engineered prompt (CANDIDATE_JOB_SCORING_PROMPT) that instructs Gemini to:
- Read the candidate’s CV text and confirmed skills
- Read the job description and requirements
- Evaluate four dimensions independently (role fit, skills fit, experience fit, secondary fit)
- Identify key matching points and potential concerns
- Produce a natural language explanation with evidence
- Return a structured JSON response
The prompt is designed to produce consistent, calibrated scores. The AI is instructed to:
- Use the full 0-1 range (not cluster around 0.5)
- Provide specific evidence from both the CV and job description
- Flag fundamental mismatches explicitly
- Consider both required and preferred qualifications separately
Critical Penalty System
The critical mismatch caps serve as a safety net against score inflation:
Role Fit cap: When role_fit < 0.30, the overall score is capped at 0.35 (35%). This prevents a situation where a Marketing Manager with Python hobby projects gets an 80% match for a Senior Python Developer role just because their skills overlap.
Skills Fit cap: When skills_fit < 0.30, the overall score is capped at 0.45 (45%). This prevents a situation where a Senior Java Developer gets a high score for a Python/FastAPI role just because their seniority and experience align.
These caps ensure that fundamental mismatches in either role or skills always result in a low overall score, regardless of how well other dimensions score.
Staffing Agency Exclusion
The matching pipeline automatically excludes jobs posted by staffing and recruiting agencies. This is a deliberate business rule: recruiters using Recruitier do not want to match their candidates to competitor agencies’ job listings.
The exclusion is based on the EXCLUDED_CLIENT_INDUSTRIES list, which currently includes “Staffing and Recruiting”. Companies with this industry classification in the global_company table are excluded from search results. Companies without industry data (NULL) are still included — only explicitly excluded industries are filtered out.
SSE Event Flow During Matching
The matching pipeline publishes the following SSE events through Redis pub/sub:
| Event | When | Data |
|---|
matching_started | Pipeline begins | candidate_id, candidate_name |
matching_stage_update | Each stage transition | stage_name, stage_number, total_stages, status_message |
matching_completed | Pipeline finishes | candidate_id, total_matches, top_score |
login_matching_started | Incremental matching begins on login | user_id |
incremental_matching_success | One candidate’s incremental match finishes | candidate_id, new_matches_count |
login_matching_completed | All incremental matching finishes | total_candidates_processed |
The frontend receives these via the SSEConnectionManager and updates the UI in real time — progress indicators, match counts, and notifications are all driven by these events.
Power-User Tips
Trigger manual re-matching after weight adjustments. If you change a candidate’s scoring weights (e.g., increasing skills weight for a technical role), the existing match scores were calculated with the old weights. Trigger a manual re-match to recalculate all scores with the new weight configuration.
Use the stage-by-stage progress to diagnose issues. If matching is slow at the “Searching” stage, the candidate may have a very broad profile generating too many vector matches. If it is slow at “Scoring”, there are many jobs to evaluate. If it completes quickly but produces few matches, the candidate’s preferences may be too restrictive.
For candidates with unusual profiles, consider adjusting weights before running the first match. A career changer from finance to tech with strong Python skills should have their role weight decreased and skills weight increased BEFORE the first matching run. This saves a re-match and produces better initial results.