DEV Community

Cover image for How We Built an AI Recruitment Platform with MongoDB, NLP, and a Human-in-the-Loop Feedback System
KAMMARI ASHRITHA
KAMMARI ASHRITHA

Posted on

How We Built an AI Recruitment Platform with MongoDB, NLP, and a Human-in-the-Loop Feedback System

We built NeuroHire, an AI recruitment platform that goes beyond keyword matching. It validates skills with evidence, explains its decisions, and learns from recruiter feedback. Stack: Django + React + MongoDB Atlas + scikit-learn. Live demo - neurohire-bay.vercel.app


Hiring is broken. Not in a dramatic way - it just quietly fails, all the time. Recruiters spend hours manually scanning resumes for keywords, and job seekers spend hours tailoring applications to systems that reduce them to a skill checklist. We built NeuroHire to fix that - and along the way, we learned a lot about what it actually takes to build AI systems that are transparent, fair, and useful in the real world.

This is the story of how we built it, what broke, and how MongoDB ended up being the backbone of the whole thing.

The Problem We Were Solving

Traditional Applicant Tracking Systems work on simple keyword matching. If your resume says "React" and the job description says "React," you score a point. If it doesn't, you don't. That's it. There's no understanding of context, no sense of whether you actually used React to build something meaningful, and zero transparency about why you were rejected.

We wanted to build something different: a system that understands resumes the way a thoughtful human recruiter would, explains its decisions, and actually gets better over time by learning from recruiter feedback.

The Tech Stack

Before getting into the interesting parts, here's what we used:

  • Backend: Django REST Framework (Python)
  • Frontend: React with Tailwind CSS, deployed on Vercel
  • Database: MongoDB Atlas for candidate profiles + SQLite for auth and sessions
  • ML/NLP: scikit-learn (TF-IDF, cosine similarity), regex-based entity extraction, rule-based skill validation
  • Deployment: Render (backend) + Vercel (frontend) + MongoDB Atlas (database)

The NLP Pipeline - Where the Real Work Happens

The core of NeuroHire is a resume analysis pipeline built in Python. When a recruiter uploads a resume, here is what happens under the hood:

Step 1 - Text Extraction

We support both PDF and DOCX. For PDFs we use PyMuPDF (fitz), and for DOCX files we use python-docx. Both return a plain text representation that the rest of the pipeline works on.

if ext in ('.docx', '.doc'):
    import docx as python_docx
    doc = python_docx.Document(file_path)
    text = "\n".join([p.text for p in doc.paragraphs if p.text.strip()])
elif ext == '.pdf':
    import fitz
    doc = fitz.open(file_path)
    text = "".join([page.get_text() for page in doc])
Enter fullscreen mode Exit fullscreen mode

Step 2 - Entity Extraction (NLP)

This is where NLP comes in. We use a combination of regex pattern matching and keyword list matching to extract structured fields from unstructured resume text:

  • Skills are matched against a curated list of 200+ technical keywords
  • Experience years are extracted using date pattern regex
  • Education is identified using degree-level keywords (B.Tech, M.S., PhD, etc.)
  • Project descriptions are parsed from section headers

Step 3 - Evidence-Backed Skill Validation

This was the most interesting part to build. The insight was simple: just because a resume lists a skill doesn't mean the person used it. So instead of counting keywords, we scan a 120-character window around each skill mention and look for action verbs like "built", "deployed", "implemented", "designed".

action_verbs = ['built', 'developed', 'deployed', 'implemented', 
                'designed', 'created', 'led', 'architected']

for skill in skills:
    idx = text_lower.find(skill.lower())
    if idx != -1:
        window = text_lower[max(0, idx-120):idx+120]
        if any(verb in window for verb in action_verbs):
            validated['status'] = 'Valid'
        else:
            validated['status'] = 'Partial'
    else:
        validated['status'] = 'Unverified'
Enter fullscreen mode Exit fullscreen mode

Each skill ends up with one of three labels: Valid (backed by project evidence), Partial (mentioned in certifications), or Unverified (listed without context). This alone reduced false positive skill matches by around 20% in our evaluation.

Step 4 - TF-IDF Role Matching

For role-to-resume matching, we use TF-IDF vectorization from scikit-learn. When a recruiter pastes a job description, we run cosine similarity between the resume text vector and the JD vector. This is a significant upgrade from keyword counting - it catches synonyms, related terms, and context.

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

vectorizer = TfidfVectorizer(stop_words='english', max_features=200, ngram_range=(1, 2))
tfidf_matrix = vectorizer.fit_transform([resume_text, job_description])
score = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0] * 100
Enter fullscreen mode Exit fullscreen mode

Step 5 - Learning Velocity and Consistency

Learning velocity measures how much growth language appears in the resume - words like "led", "launched", "promoted", "scaled". Consistency scoring runs four contradiction checks: does someone claim senior-level skills with under a year of experience? Do they list more skills than their experience could reasonably support?

MongoDB - The Unified Candidate Profile Store

Here is where things got interesting from an architecture perspective. NeuroHire uses a dual-database approach:

  • SQLite handles authentication, sessions, scheduled interviews, and waitlist entries - all structured, relational data that doesn't change shape
  • MongoDB Atlas stores candidate profiles - the output of the analysis pipeline

The reason for this split is that candidate profiles are inherently schemaless. A resume from a fresh graduate looks nothing like one from a 10-year senior. MongoDB lets us store whatever the NLP pipeline extracts without fighting a rigid schema.

mongo_doc = {
    'name': analysis.get('name'),
    'skills': analysis.get('skills', []),
    'validated_skills': analysis.get('validated_skills', []),
    'role_match_score': analysis.get('role_match_score', 0),
    'learning_velocity': analysis.get('learning_velocity', 'Medium'),
    'consistency_score': analysis.get('consistency_score', 0),
    'final_fit': analysis.get('final_fit', 'Medium'),
    'explainability': analysis.get('explainability', ''),
    'source': 'resume_upload',
}

mongo_id = db.candidates.insert_one(mongo_doc).inserted_id
Enter fullscreen mode Exit fullscreen mode

We also use MongoDB for the chatbot - it reads from the candidates collection to answer recruiter questions like "show me the top candidates" or "which candidates know React," making it genuinely useful rather than just a static FAQ bot.

The Human-AI Feedback Loop (HAAR)

One of the things we were most proud of is the feedback system. Every time a recruiter makes a hiring decision - hire, reject, or waitlist - we log it alongside the AI recommendation. We track four metrics:

Metric What it measures
HAAR Human-AI Agreement Rate: how often the recruiter follows the AI recommendation
CDR Consistency Defect Rate: % of candidates flagged with contradictions
SVC Skill Validation Coverage: % of candidates with verified skills
LVS Learning Velocity Score: distribution of High/Medium/Low velocity candidates

Over time, the system analyzes patterns in recruiter decisions. If a recruiter consistently hires candidates the AI rated Medium, the system surfaces that as an insight: "You tend to prioritize candidates with 3+ years experience regardless of role match score." This is the feedback loop - not automatic retraining, but transparent pattern reporting that helps recruiters become more aware of their own hiring patterns.

What We Learned

The hardest bug we hit was CSRF. Django's CSRF middleware runs before any permission class, which meant our cross-origin POST requests from Vercel to Render were getting blocked before they even reached the view - even when permission_classes = []. The fix was wrapping every public endpoint in csrf_exempt() in urls.py. Simple once you know it, but it took a while to track down.

The most rewarding part was the skill validation system. Seeing a resume that listed 15 skills get broken down into 8 Verified, 4 Partial, and 3 Unverified - with specific evidence highlighted - felt like the system was actually reading the resume rather than scanning it.

What's Next

The current pipeline is entirely rule-based and unsupervised. The natural next step is to train a proper classifier on labeled hiring decisions, which would let the system learn what "good fit" actually means for a specific company rather than just applying generic heuristics. MongoDB Atlas Vector Search would also let us replace TF-IDF with semantic embeddings for much richer matching.

For now though, NeuroHire works - and it works transparently, which was always the point.


Live demo: neurohire-bay.vercel.app

Built with Django, React, MongoDB Atlas, scikit-learn, PyMuPDF, and python-docx.


Guided by @chanda_rajkumar ยท Built with @harshitha_oruganti, @sravani_annam_abadb6f66bb and @ashrita_srigayatriv_823

Top comments (0)