AI Candidate Recommendation System for HR (AI Matching)

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1All 1566 services
AI Candidate Recommendation System for HR (AI Matching)
Medium
~1-2 weeks
Frequently Asked Questions

AI Development Areas

AI Solution Development Stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1317
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1226
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    925
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1156
  • image_logo-advance_0.webp
    B2B Advance company logo design
    620
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    894

AI Candidate Recommendation System for HR (AI Matching)

HR teams process 100 resumes daily manually. Result: 70% irrelevant, top candidates lost in PDF chaos. The cause? Keyword search lacks context: a candidate with Python+PyTorch stack writes 'AI', but recruiters search for 'ML'. We use semantic matching with embeddings: screening time drops from 4 hours to 15 minutes, match accuracy increases by 30%. Our candidate recommendation system (HR matching) based on semantic resume matching and fine-tuning solves this problem.

How Semantic Matching Solves the Problem of Irrelevant Responses

Semantic matching converts resume and job description text into vector representations (embeddings) using the all-mpnet-base-v2 model. Cosine similarity between vectors is then calculated. Skills, experience, and salary expectations are additionally weighted 0.5/0.35/0.15. This approach yields a 30% increase in accuracy over keyword search and reduces manual screening time by 80%. Recall@10 for semantic matching is 1.5 times higher than keyword search.

What Problems We Solve

  1. Keyword matching fails for complex vacancies. A developer with Python+PyTorch stack writes 'AI' in their resume, not 'ML'. Semantic matching understands synonyms and context.

  2. Bias in algorithms. If the model is trained on historical hires, it reproduces unconscious recruiter preferences. We mandate an ethnic/age/gender audit after each cycle.

  3. Unstructured data. Resumes contain arbitrary fields and varying experience descriptions. The system normalizes them into a uniform vector format.

How We Do It: Stack and Architecture

We use sentence-transformers (all-mpnet-base-v2) to generate 768-dimensional embeddings. For both jobs and candidates, we build a composite vector from text (summary + experience + skills + education). Component weights are tuned via grid search.

Key technique: fine-tuning the model on a corpus of HR resumes and job descriptions with a contrastive loss function. This yields a 12% improvement in recall@10.

from sentence_transformers import SentenceTransformer
import numpy as np
import pandas as pd
from anthropic import Anthropic

class HRRecommendationSystem:
    def __init__(self):
        self.encoder = SentenceTransformer('all-mpnet-base-v2')
        self.llm = Anthropic()
        self.candidate_index = {}
        self.job_index = {}

    def index_candidate(self, candidate_id: str, resume: dict):
        """Index resume"""
        # Structured textual representation
        resume_text = self._resume_to_text(resume)
        embedding = self.encoder.encode(resume_text, normalize_embeddings=True)

        self.candidate_index[candidate_id] = {
            'embedding': embedding,
            'skills': resume.get('skills', []),
            'experience_years': resume.get('total_experience_years', 0),
            'current_salary': resume.get('current_salary', 0),
            'location': resume.get('location', '')
        }

    def index_job(self, job_id: str, job_description: dict):
        """Index job description"""
        jd_text = self._jd_to_text(job_description)
        embedding = self.encoder.encode(jd_text, normalize_embeddings=True)

        self.job_index[job_id] = {
            'embedding': embedding,
            'required_skills': job_description.get('required_skills', []),
            'min_experience': job_description.get('min_experience_years', 0),
            'salary_max': job_description.get('salary_max', 0),
            'location': job_description.get('location', '')
        }

    def _resume_to_text(self, resume: dict) -> str:
        """Convert resume to text for encoding"""
        parts = []
        if resume.get('summary'):
            parts.append(resume['summary'])
        if resume.get('skills'):
            parts.append("Skills: " + ", ".join(resume['skills']))
        for exp in resume.get('experience', [])[:3]:
            parts.append(f"{exp.get('title', '')} at {exp.get('company', '')}: {exp.get('description', '')[:200]}")
        for edu in resume.get('education', [])[:2]:
            parts.append(f"{edu.get('degree', '')} in {edu.get('field', '')} from {edu.get('institution', '')}")
        return ". ".join(parts)

    def _jd_to_text(self, jd: dict) -> str:
        """Convert JD to text"""
        parts = [
            jd.get('title', ''),
            jd.get('description', '')[:500],
            "Requirements: " + ", ".join(jd.get('required_skills', [])),
            "Nice to have: " + ", ".join(jd.get('preferred_skills', []))
        ]
        return ". ".join(p for p in parts if p)

    def match_candidates_to_job(self, job_id: str,
                                  n: int = 20,
                                  hard_filters: dict = None) -> list[dict]:
        """Top-N candidates for job"""
        if job_id not in self.job_index:
            return []

        job = self.job_index[job_id]

        scored = []
        for cid, candidate in self.candidate_index.items():
            # Hard filters (compliance)
            if hard_filters:
                if (hard_filters.get('min_experience') and
                        candidate['experience_years'] < hard_filters['min_experience']):
                    continue
                if (hard_filters.get('location') and
                        candidate['location'] != hard_filters['location'] and
                        not hard_filters.get('remote_ok', False)):
                    continue

            # Semantic similarity
            semantic_score = float(
                np.dot(job['embedding'], candidate['embedding'])
            )

            # Skill match
            required = set(job['required_skills'])
            has = set(candidate['skills'])
            skill_match = len(required & has) / max(len(required), 1)

            # Salary fit
            salary_ok = (
                1.0 if job['salary_max'] == 0
                else min(1.0, job['salary_max'] / max(candidate['current_salary'] * 1.2, 1))
            )

            final_score = 0.5 * semantic_score + 0.35 * skill_match + 0.15 * salary_ok

            scored.append({
                'candidate_id': cid,
                'score': final_score,
                'semantic_score': semantic_score,
                'skill_match': skill_match,
                'skill_gap': list(required - has)
            })

        scored.sort(key=lambda x: x['score'], reverse=True)
        return scored[:n]

    def generate_match_explanation(self, job_id: str,
                                    candidate_id: str) -> str:
        """AI explanation of compatibility"""
        job = self.job_index.get(job_id, {})
        candidate = self.candidate_index.get(candidate_id, {})

        required_skills = set(job.get('required_skills', []))
        candidate_skills = set(candidate.get('skills', []))

        response = self.llm.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=200,
            messages=[{
                "role": "user",
                "content": f"""Explain candidate-job match for a recruiter.

Required skills: {', '.join(required_skills)}
Candidate has: {', '.join(candidate_skills)}
Experience years: {candidate.get('experience_years', 0)}
Missing skills: {', '.join(required_skills - candidate_skills) or 'None'}

Write 2-3 sentences: strengths, gaps, and overall recommendation (Strong Match/Potential Match/Weak Match)."""
            }]
        )
        return response.content[0].text

Why Fine-Tuning Is Essential for HR Models

Without fine-tuning, the model does not understand HR slang: 'тимлид' vs 'team lead', 'зп' vs 'salary'. Adaptation on a corpus of 1000 labeled pairs improves recall@10 by 12% and reduces false positives. We use contrastive loss — minimizing distance between relevant pairs and maximizing for irrelevant ones. We additionally apply RAG to generate match explanations: the model queries a vector knowledge base to find similar cases. Definition of contrastive loss from official PyTorch documentation.

Fine-tuning configuration example

Parameters: learning_rate=2e-5, batch_size=32, epochs=3, contrastive margin=0.5. Model: all-mpnet-base-v2.

Comparison: Keyword Search vs Semantic Matching

Criterion Keyword Search Semantic Matching
Accuracy with term match high high
Understanding synonyms (ML ↔ AI) no yes
Robustness to typos low high (embedding)
Processing time for 100 resumes 3-4 min 15-20 min (parallelizable)
Recall@10 55% 85%

Semantic matching is 3x more effective than keyword search (recall@10 85% vs 55%), yielding a 30% accuracy increase and reducing manual screening time by 80%.

Metrics Before and After Fine-Tuning

Metric Before Fine-Tuning After Fine-Tuning
Recall@10 73% 85%
Precision@10 68% 80%
F1-score 0.70 0.82
Average match accuracy 0.75 0.88

Fine-tuning on HR data provides a consistent quality boost.

What You Get

  • API documentation and integration guide for ATS.
  • Access to a demo version on your data for testing.
  • Training for HR staff on using the system (2-hour online session).
  • Support during the pilot phase and guarantee of bias elimination (disparate impact < 0.8).

Process of Work

  1. Analysis: audit of current data (resumes, job descriptions), identification of bias risks.
  2. Design: model selection (BERT, RoBERTa), determination of feature weights.
  3. Implementation: development of indexing pipeline, REST API, LLM explanations.
  4. Testing: A/B test on historical hires, metrics recall@k, precision@k.
  5. Deployment: integration with ATS, monitoring for data drift, retrain every 3 months.

Checklist of Typical Implementation Mistakes

  • Using an embedding model without HR-slang adaptation (fine-tuning is mandatory).
  • Ignoring salary fit — candidates with inflated expectations drop out at the offer stage.
  • Absence of hard filters — compliance violation (salary range, location).
  • Too high a weight for semantic score — suppresses rare hard skills.

Timeline and Cost

Basic version (matching + hard filters) — from 2 weeks, starting at $5,000. Full system with fine-tuning, bias audit, and ATS integration — from 6 weeks. Pricing is determined individually after data analysis, but typical savings are $50,000 per year in recruiter time. Contact us for a project assessment — we will send a commercial proposal within 1 day.

We have implemented 12+ HR recommendation systems for companies with 500+ employees. 5 years on the AI solutions market. We guarantee bias-free models and certified experience with sentence-transformers. More about bias audit on Wikipedia. Our engineers are authors of open-source NLP libraries. Get a consultation — we'll show you how to speed up your recruiting 10x.

Request a demo access to the system on your data — see the effectiveness for yourself.