SignalHire
Technical Documentation
Version 1.0 · June 2026 · AI-Powered Candidate Ranking for the Redrob AI Challenge
Overview
SignalHire is an AI-powered candidate ranking system that processes 100,000 candidate profiles and surfaces the top 100 best matches for a Senior AI Engineer role. Built for the Redrob AI Challenge at the India Runs Hackathon.
The Problem
Given a 487 MB JSONL file containing 100K candidate profiles — including keyword stuffers, ~80 honeypot/adversarial profiles, behavioral twins, and strong engineers who describe themselves in plain language — identify and rank the top 100 candidates for a Senior AI Engineer position.
The Solution
A two-phase offline hybrid scoring pipeline:
- Phase A (Precompute) — Embedding generation + multi-signal sub-score computation (GPU optional via
EMBEDDING_DEVICE=cuda) - Phase B (Ranking) — CPU-only composite scoring via vectorized matrix operations, evidence-based reasoning, and validated CSV output
| Decision | Rationale |
|---|---|
| Offline precompute | Separates expensive GPU work from CPU-only ranking sandbox |
| Multi-signal scoring | Defeats keyword stuffers — no single signal can game the system |
| Semantic embedding backup | Catches strong engineers whose plain language misses keyword checks |
| Evidence-cited reasoning | Every score claim traces to a specific skill or sentence |
| Vectorized re-ranking | Enables live 50 ms re-ranking of all 100K candidates in the dashboard |
Quick Start
# Clone and setup git clone https://github.com/DevanshSrajput/SignalHire.git cd SignalHire python -m venv venv && source venv/bin/activate pip install -r requirements.txt # Step 1: Precompute (CPU default; GPU ~10× faster) python precompute.py # or: EMBEDDING_DEVICE=cuda python precompute.py # Step 2: Rank → output/submission.csv python rank.py # Step 3: Dashboard streamlit run app.py # http://localhost:8501
Prerequisites
| Requirement | Notes |
|---|---|
| Python ≥ 3.10 | Required |
| NVIDIA GPU + CUDA | Optional — speeds precompute ~10×. CPU is the default; set EMBEDDING_DEVICE=cuda to use GPU |
| Disk | ~500 MB for data + ~160 MB for generated artifacts |
| RAM | 16 GB recommended |
Docker
Artifacts are baked into the image — only candidates.jsonl needs to be mounted at runtime.
# Build (bakes precomputed artifacts, pre-downloads embedding model) docker build -t signalhire . # Rank (mount candidates.jsonl in, get submission.csv out) docker run --rm \ -v $(pwd)/data:/app/data \ -v $(pwd)/output:/app/output \ signalhire # Dashboard docker run --rm -p 8501:8501 \ -v $(pwd)/data:/app/data \ signalhire streamlit run app.py --server.port=8501 --server.address=0.0.0.0
--gpus all only for GPU-accelerated precompute runs.Two-Phase Pipeline
candidates.jsonl (100K, 487 MB)
│
▼
┌──────────────────────────────────────┐
│ PHASE A: Precompute (GPU, ~4 min) │
│ │
│ Stream JSONL ─► Disqualify fakes │
│ └──► Embed (MiniLM, 384-dim) │
│ └──► Compute 4 sub-scores │
│ └──► Serialize artifacts │
└──────────────┬───────────────────────┘
│ embeddings.npy + subscores.pkl (~155 MB)
▼
┌──────────────────────────────────────┐
│ PHASE B: Ranking (CPU, <10 s) │
│ │
│ Load artifacts ─► Cosine similarity │
│ └──► Weighted composite │
│ └──► argsort → top 100 │
│ └──► Evidence-based reasoning│
│ └──► Validate & write CSV │
└──────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ DASHBOARD (CPU, ~50 ms re-rank) │
│ Weight UI → Live matrix re-score │
│ 6 interactive tabs │
└──────────────────────────────────────┘
Dependency Graph
Layer 0 — leaf modules (no project imports): textmatch.py ─── re, functools config.py ─── os, re, pathlib Layer 1: disqualify.py ──► config, textmatch Layer 2: signals.py ──► config, disqualify, textmatch Layer 3: evidence.py ──► config, signals, textmatch engine.py ──► config Layer 4: precompute.py ──► config, disqualify, signals + sentence_transformers Layer 5: rank.py ──► config, engine, evidence Layer 6: app.py ──► config, engine, evidence, rank + streamlit, plotly
Data Flow
candidates.jsonl ──► precompute.py ──► artifacts/
│ ├── embeddings.npy (99965×384, float32)
│ ├── candidate_ids.npy (99965 strings)
│ ├── jd_embedding.npy (384, float32)
│ ├── subscores.pkl (99965 dicts)
│ ├── disqualified.json (~35 records)
│ └── demographics.csv
│
└── rank.py ──► output/submission.csv
app.py ──► Streamlit dashboard (http://localhost:8501)
Module Reference
config.py — Configuration Hub
Single source of truth for all paths, weights, keyword lists, penalties, and thresholds. Every other module imports from here — no magic numbers elsewhere.
Path Constants
| Constant | Value | Description |
|---|---|---|
ROOT | Project root | Base directory (resolved from config.py location) |
DATA_DIR | ROOT / "data" | Raw input data |
ARTIFACTS_DIR | ROOT / "artifacts" | Precomputed output |
OUTPUT_DIR | ROOT / "output" | Final CSV output |
CANDIDATES_PATH | DATA_DIR / "candidates.jsonl" | 100K profiles (~487 MB) |
VALIDATE_SCRIPT | DATA_DIR / "validate_submission.py" | Challenge validator script |
SAMPLE_PATH | DATA_DIR / "sample_candidates.json" | Sample data for demo mode |
Scoring Weights
WEIGHTS = { "technical_fit": 0.35, "career_quality": 0.25, "availability_signal": 0.20, "seniority_fit": 0.12, "semantic_similarity": 0.08, } # Sum is asserted to equal 1.0
Embedding Configuration
| Constant | Value | Description |
|---|---|---|
EMBEDDING_MODEL | "sentence-transformers/all-MiniLM-L6-v2" | 384-dim sentence embeddings |
EMBEDDING_DIM | 384 | Vector dimensionality |
EMBEDDING_DEVICE | os.getenv("EMBEDDING_DEVICE", "cpu") | Compute device — cpu default, override with env var |
EMBEDDING_BATCH_SIZE | 2048 | Batch size for encoding |
PROFILE_BLOB_MAX_CHARS | 1024 | Max chars for embedding input |
Penalty Constants
| Constant | Value | Description |
|---|---|---|
CONSULTING_PENALTY | 0.15 | Multiplier for all-consulting careers |
NO_CODE_PENALTY | 0.80 | Multiplier for 18+ months of inactivity |
CV_SPEECH_ROBOTICS_PENALTY | 0.85 | Multiplier for off-domain specialization |
HONEYPOT_YEAR_BUFFER | 5 | Allowed YoE vs career timeline discrepancy |
GHOST_COMPLETENESS_THRESHOLD | 5 | Profile completeness % below which → ghost |
textmatch.py — Keyword Matching Engine
Word-boundary-aware keyword matching. Prevents false positives like "rag" matching inside "storage" or "map" inside "bitmap".
Design: Keywords ≥4 characters use stem matching (\b{kw}\w*), allowing "eval" → "evaluation". Keywords <4 characters require exact word match (\b{kw}\b).
Returns a compiled regex. Cached via functools.lru_cache(maxsize=1024). kw ≥ 4 chars → stem match; kw < 4 → exact word.
Returns True if any keyword matches. Short-circuits on first match.
Returns all keywords that match within the text.
disqualify.py — Adversarial Profile Detection
Identifies and removes adversarial profiles before scoring. Three hard-disqualify checks, plus soft penalties for borderline profiles.
Checks three conditions in order: YoE inflation (claimed YoE exceeds career timeline + buffer), Ghost profile (completeness <5%, no verified contact), Pure research (all roles researcher, zero production signals). Returns (True, reason) or (False, "").
Returns cumulative penalty multiplier and reasons string. All-consulting → ×0.15, No-code-18mo → ×0.80, CV/speech/robotics-only → ×0.85. Multipliers stack multiplicatively.
| Helper | Purpose |
|---|---|
parse_year(date_str) | Extracts 4-digit year; handles "present"/"now" → CURRENT_YEAR |
_all_roles_at_consulting(candidate) | True if every role's company matches a known consulting firm |
_is_pure_research(candidate) | All roles titled "researcher" + no production signal keywords |
_is_no_code_18mo(candidate) | Last activity >548 days ago + no current role |
_is_cv_speech_robotics_only(candidate) | Has CV/speech/robotics skills but zero retrieval signals |
signals.py — Sub-Score Computation
Computes the four primary sub-scores. Each returns a float in [0, 1].
Must-have match (75% weight) across declared skills (proficiency-weighted) and career descriptions + nice-to-have match (25%) + production bonus (capped 0.10) + retrieval bonus (capped 0.10).
Non-consulting role (+0.30), ML/AI title at ≥50-person company (+0.15), median tenure ≥36mo (+0.25) / ≥24mo (+0.20) / ≥18mo (+0.05), upward title progression (+0.20).
Open-to-work flag (+0.25), recency (≤30d +0.20, ≤90d +0.12, ≤180d +0.06), recruiter response rate (×0.15), interview completion rate (×0.15), notice period (≤30d +0.05, ≤60d +0.035).
YoE bands: 6–9yr → 1.0 · 4–5/10–12yr → 0.7 · 3/13–15yr → 0.4 · else → 0.1. Tier-1 education +0.05, Tier-2 +0.02.
evidence.py — Provenance & Reasoning
Traces every JD requirement match back to the specific skill or career sentence that triggered it. Generates honest, evidence-cited reasoning strings for the output CSV.
Returns structured evidence dict with matched (criterion, match_type, skill_name, snippet), missing_must_haves, production keywords, and retrieval keywords.
Produces one-liner reasoning string (120–300 chars). Format: "6.7yr Lead AI Engineer at Razorpay; strong match on embeddings, vector search, python; production evidence (serving, ndcg); actively looking, 73% response rate, 30d notice." Only claims what evidence actually contains.
engine.py — Vectorized Ranking Engine
All ranking math as vectorized NumPy operations. Every operation works on precomputed matrices — no per-candidate loops.
Packs sub-score dicts into dense float32 arrays: subscore_matrix (N×4) and penalties (N,).
Core scoring operation: scores = penalties × (subscore_matrix @ weight_vector + w_semantic × semantic_sim)
Selects top-k candidates via np.argsort with deterministic tie-breaking by candidate index.
Maximal Marginal Relevance. MMR(d) = λ × score(d) − (1−λ) × max_similarity(d, already_selected). At λ=1.0 → pure score ranking.
200 Monte Carlo trials with ±20% weight perturbation. Returns per-candidate frequency of appearing in top-k. Candidates with >95% frequency are stable regardless of exact weight choice.
precompute.py — Phase A Pipeline
The expensive one-time computation phase. Streams 487 MB JSONL, removes adversarial profiles, embeds all valid profiles with all-MiniLM-L6-v2, computes all sub-scores, serializes to disk.
Full pipeline: create ARTIFACTS_DIR → load SentenceTransformer on configured device → embed JD → stream JSONL in 1K-line chunks (disqualify, blob, sub-scores) → batch-encode (batch=2048) → save all artifacts.
EMBEDDING_DEVICE=cuda to use GPU.rank.py — Phase B Pipeline
Loads precomputed artifacts, calculates composite scores, selects top 100, generates evidence-based reasoning, writes validated CSV.
Builds candidate_id → byte_offset by scanning the JSONL once (~1.5 s). Enables O(1) random access to any candidate's full record without loading the 487 MB file.
Load artifacts → build matrices → cosine similarity → composite scores → top-100 → byte-offset index → load full records → evidence + reasoning → enforce monotonic ordering → write CSV → validate.
app.py — Interactive Dashboard
Six-tab Streamlit workbench. All scoring is a single float32 matrix multiply over precomputed artifacts — ~50 ms for 100K candidates regardless of which tab or slider is active.
| Tab | Features |
|---|---|
| 🏆 Shortlist | Paginated candidate cards · stability badges · score bars · evidence chips · missing must-haves |
| ⚖️ Compare | Radar chart side-by-side for 2–4 candidates across all 5 score dimensions |
| 📊 Insights | Score histogram + fairness audit (education tier, country, YoE distributions vs pool) |
| 🛡️ Integrity | Disqualification log by category with concrete detection examples |
| 📤 Export | Submission CSV · personalized outreach pack (top-10) · ranking config JSON |
| 📖 Methodology | Live formula display + integrity rule documentation |
| Sidebar Control | Effect |
|---|---|
| Custom JD | Paste any job description → embedded on the fly → instant re-rank all 100K |
| Weight sliders | Drag any of 5 signals → 100K re-score in ~50 ms |
| Diversity (MMR) | λ slider → penalize near-duplicate profiles in shortlist |
| Blind screening | Hides names, companies, institutions to reduce reviewer bias |
Scoring Methodology
Composite Formula
S = penalty_multiplier × (
0.35 × technical_fit
+ 0.25 × career_quality
+ 0.20 × availability_signal
+ 0.12 × seniority_fit
+ 0.08 × semantic_similarity
)
All sub-scores ∈ [0, 1]
penalty_multiplier ∈ [0.12, 1.0]
Final score scaled to [0, 10] in output CSV
Technical Fit (0.35)
| Criterion | Weight | Keywords (sample) |
|---|---|---|
| Embeddings & Retrieval | 0.25 | embedding, retrieval, vector search, dense retrieval, RAG, faiss... |
| Vector Databases | 0.20 | vector database, faiss, pinecone, weaviate, milvus, qdrant, chroma... |
| Python | 0.15 | python, pytorch, tensorflow, jax, numpy... |
| Eval Frameworks | 0.15 | evaluation, ndcg, mrr, map, recall, precision... |
| LLM Fine-tuning nice-to-have | 0.10 | fine-tuning, lora, qlora, rlhf, peft, instruction tuning... |
| Learning to Rank nice-to-have | 0.10 | learning to rank, lambdamart, listwise, pairwise... |
| HR-Tech nice-to-have | 0.05 | recruiter, talent, hiring, ats, applicant... |
Proficiency weights: expert=1.0, advanced=0.75, intermediate=0.4, beginner=0.1. Platform assessment scores override self-reported proficiency.
Career Quality (0.25)
- Non-consulting role presence → +0.30
- ML/AI title at company with ≥50 employees → +0.15
- Median tenure ≥36 months → +0.25 · ≥24 months → +0.20 · ≥18 months → +0.05
- Upward title progression (last role seniority > first) → +0.20
Availability Signal (0.20)
open_to_work_flag→ +0.25- Last active ≤30d → +0.20 · ≤90d → +0.12 · ≤180d → +0.06
recruiter_response_rate→ × 0.15interview_completion_rate→ × 0.15- Active applications (>0) → +0.05
- Notice period ≤30d → +0.05 · ≤60d → +0.035
Seniority Fit (0.12)
| YoE Range | Score |
|---|---|
| 6–9 years (ideal for Senior AI Engineer) | 1.0 |
| 4–5 or 10–12 years | 0.7 |
| 3 or 13–15 years | 0.4 |
| <3 or >15 years | 0.1 |
Education bonus: Tier-1 (IIT, IISc, NIT, BITS...) → +0.05. Tier-2 (DTU, VIT, SRM...) → +0.02.
Semantic Similarity (0.08)
Cosine similarity between the candidate's 384-dim all-MiniLM-L6-v2 embedding and the JD embedding. This is the "safety net" — catches strong engineers whose plain-language descriptions miss keyword checks.
Penalty Multipliers
| Condition | Multiplier | Rationale |
|---|---|---|
| All roles at consulting firms (TCS, Infosys, Wipro, Capgemini, Accenture, HCL...) | × 0.15 | Pure-consulting careers rarely involve the depth of AI/ML work required |
| No coding activity in 18+ months | × 0.80 | Skills may be stale in a fast-moving field |
| CV/speech/robotics only, no retrieval signals | × 0.85 | Domain misalignment with this JD |
Data Schemas
Input: candidates.jsonl
{
"candidate_id": "CAND_0000001",
"profile": {
"name": "...",
"headline": "Senior ML Engineer | NLP & Search",
"years_of_experience": 7,
"current_title": "Senior ML Engineer",
"current_company": "Razorpay",
"company_size": "1001-5000"
},
"career_history": [{
"company": "Razorpay", "title": "Senior ML Engineer",
"start_date": "2022-03", "end_date": "present",
"is_current": true,
"description": "Built embedding-based retrieval pipeline..."
}],
"skills": [{ "name": "Python", "proficiency": "expert" }],
"redrob_signals": {
"profile_completeness_score": 92,
"open_to_work_flag": true,
"recruiter_response_rate": 0.73,
"notice_period_days": 30,
"last_active_date": "2026-05-28",
"verified_email": true
}
}
Internal: Artifacts
| File | Format | Shape | Description |
|---|---|---|---|
| embeddings.npy | float32 | (99965, 384) | Unit-normalized profile embeddings |
| candidate_ids.npy | str array | (99965,) | Index-aligned with embeddings |
| jd_embedding.npy | float32 | (384,) | JD text embedding |
| subscores.pkl | Python dict | 99965 entries | candidate_id → {technical_fit, career_quality, availability_signal, seniority_fit, penalty_multiplier, top_skills, ...} |
| disqualified.json | JSON list | ~35 entries | [{candidate_id, disqualify_type, reason}, ...] |
| demographics.csv | CSV | ~100K rows | Cached pool demographics for fairness audit |
Output: submission.csv
candidate_id,rank,score,reasoning CAND_0081846,1,8.70,"6.7yr Lead AI Engineer at Razorpay; strong match on embeddings, vector search, python, information retrieval; production evidence (serving, ndcg); actively looking, 73% response rate, 30d notice."
| Rule | Status |
|---|---|
| Exactly 100 rows (ranks 1–100) | ✅ enforced |
| Scores non-increasing by rank | ✅ monotonic enforcement in rank.py |
| Tie-breaking by candidate_id ascending | ✅ lexsort in engine.top_k_indices |
| candidate_id format CAND_XXXXXXX | ✅ validated |
| Reasoning ≤300 chars, no newlines | ✅ enforced in evidence.generate_reasoning |
Deployment
Local Development
EMBEDDING_DEVICE=cuda python precompute.py # Phase A: ~4 min GPU python rank.py # Phase B: ~3 s streamlit run app.py # Dashboard: http://localhost:8501
HuggingFace Space
Live demo at huggingface.co/spaces/DevanshSrajput/SignalHire — Docker SDK with precomputed artifacts for the 50-candidate sample dataset. All dashboard tabs work identically to the full 100K version.
Sandbox Constraints
| Constraint | How It's Met |
|---|---|
| CPU-only ranking | Phase B uses only NumPy — zero GPU dependency |
| 16 GB RAM limit | Streaming JSONL ingestion, batched embedding, vectorized ops. Peak ~14 GB |
| No network access | Embedding model pre-downloaded at Docker build time |
| <5 minute ranking | np.argsort O(N) + byte-offset seek index. Total ~3 s |
| Deterministic results | SEED=42 enforced. Reproducible on re-run |
Performance Benchmarks
Measured on NVIDIA RTX 3050 (6 GB) + 12-core CPU
| Phase | Device | Time | Throughput |
|---|---|---|---|
| Precompute (100K → 99,965) | GPU (CUDA) | ~4.2 min | ~400 candidates/s |
| Offset index build | CPU | ~1.5 s | ~67K lines/s |
| Ranking + validation | CPU | ~3.1 s | ~32K candidates/s |
| Dashboard re-rank | CPU | ~50 ms | 2M candidates/s |
Initial Documentation Archive
The original planning documents that guided SignalHire's development are preserved in the Initial-Documentation/ directory. These were the blueprints — the code is the building.
| Document | Description |
|---|---|
| RecruiterIQ_PRD.md | Product Requirements Document — problem statement, scoring formula, evaluation metrics |
| TRD.md | Technical Requirements Document — functional and non-functional requirements |
| BACKEND_SCHEMA.md | Data Models & Interfaces — complete schema definitions for all I/O |
| APP_FLOW.md | Application Flow — system diagrams, state machines, error handling |
| IMPLEMENTATION_PLAN.md | Build Execution Roadmap — 9-phase plan with code snippets and risk register |
| UIUX_DESIGN.md | UI/UX Design — dashboard layout, color palette, component specs |
PRD — Product Requirements
The RecruiterIQ PRD defines the core problem: rank 100K candidates for a Senior AI Engineer role, defeat adversarial profiles, and surface the top 100 with honest evidence-based reasoning. It established the five-signal scoring formula and the evaluation criteria (recall of known good candidates, 0 honeypots in top 100).
TRD — Technical Requirements
The TRD translates the PRD into hard constraints: CPU-only inference, 16 GB RAM limit, 5-minute wall-clock budget, no network access, deterministic reproducibility. It specified the offline precompute architecture as the solution to the GPU/CPU split.
App Flow
The APP_FLOW document traces every user interaction through the system: from JSONL load to artifact production (Phase A), from artifact load to CSV output (Phase B), and from sidebar control to live matrix multiply (Dashboard). It includes the state machine for the Streamlit session and error handling for missing artifacts.
Implementation Plan
A 9-phase build roadmap: (1) config skeleton → (2) keyword matching → (3) disqualification → (4) sub-scores → (5) evidence → (6) vectorized engine → (7) precompute → (8) rank → (9) dashboard. Each phase has acceptance criteria, code snippets, and a risk register entry.
UI/UX Design
The UIUX document specifies the exact layout of each Streamlit tab, the color palette (dark theme with indigo/violet/green/amber accents), component hierarchy (cards → badges → bars → chips), and the sidebar control order. The stability badge design and MMR slider range were specified here.
FAQ
Why two phases instead of one?
The sandbox is CPU-only with a 5-minute time limit. Precomputing embeddings (GPU) separately keeps Phase B well under the budget.
How does it handle keyword stuffers?
Multi-signal scoring. A candidate who stuffs "Python" 50 times gets high technical_fit but still needs career_quality, availability_signal, and seniority_fit. No single signal can game the composite score.
Why all-MiniLM-L6-v2 over larger models?
Best accuracy-to-speed tradeoff for 100K profiles. 384 dimensions, ~60 MB model, <200 ms per batch of 2048. Larger models would exceed the RAM/time budget.
Why is the consulting penalty so severe (×0.15)?
For this specific JD, all-consulting careers at service firms rarely involve the depth of AI/ML work required. In a production system, it would be a configurable threshold, not a constant.
How is the stability badge calculated?
200 Monte Carlo trials with ±20% random weight perturbation. Frequency in top-100: >95% → "stable", 70–95% → "moderate", <70% → "fragile".
↑ Back to top