""" Keyword extractor: curated tech keyword list + regex matching. Extracts: - Which JD keywords appear in the resume (matched) - Which JD keywords are absent from the resume (missing) - Which matched keywords appear only weakly / once (weak) """ from __future__ import annotations import re from collections import Counter from dataclasses import dataclass # ── Curated tech keyword catalogue ──────────────────────────────────────── TECH_KEYWORDS: list[str] = [ # Languages "Python", "JavaScript", "TypeScript", "Java", "Kotlin", "Swift", "Go", "Golang", "Rust", "C++", "C#", "Ruby", "PHP", "Scala", "R", "MATLAB", "Bash", "Shell", "Perl", "Dart", "Elixir", "Haskell", "Clojure", # Web / Frontend "React", "Next.js", "Vue", "Vue.js", "Angular", "Svelte", "HTML", "CSS", "Tailwind", "Bootstrap", "SASS", "SCSS", "Redux", "Zustand", "GraphQL", "REST", "REST APIs", "WebSocket", "gRPC", # Backend Frameworks "FastAPI", "Django", "Flask", "Express", "Node.js", "Spring Boot", "Rails", "Laravel", "Gin", "Echo", "Fiber", "Actix", # Databases "PostgreSQL", "MySQL", "SQLite", "MongoDB", "Cassandra", "DynamoDB", "Redis", "Elasticsearch", "Firestore", "Supabase", "PlanetScale", "CockroachDB", "SQL", "NoSQL", # Cloud & DevOps "AWS", "GCP", "Azure", "Docker", "Kubernetes", "K8s", "Terraform", "Ansible", "CI/CD", "GitHub Actions", "Jenkins", "CircleCI", "ArgoCD", "Helm", "Linux", "Nginx", "Caddy", "Vercel", "Render", "Fly.io", "Railway", # AI / ML / Data "PyTorch", "TensorFlow", "JAX", "Keras", "scikit-learn", "sklearn", "LangChain", "LlamaIndex", "Hugging Face", "Transformers", "FAISS", "Chroma", "Pinecone", "Qdrant", "Weaviate", "Embeddings", "Vector DB", "Vector Database", "RAG", "LLM", "GPT", "OpenAI", "Gemini", "Anthropic", "Claude", "BERT", "Sentence Transformers", "spaCy", "NLTK", "XGBoost", "LightGBM", "CatBoost", "Random Forest", "NLP", "Computer Vision", "CNN", "RNN", "LSTM", "Transformer", "Reinforcement Learning", "Fine-tuning", "PEFT", "LoRA", "MLflow", "DVC", "Weights & Biases", "wandb", # Data Engineering "Pandas", "NumPy", "Polars", "Spark", "PySpark", "Airflow", "Kafka", "dbt", "Snowflake", "BigQuery", "Redshift", "Databricks", "ETL", "Data Pipeline", # Testing & Quality "Pytest", "Jest", "Cypress", "Playwright", "Selenium", "JUnit", "TDD", "BDD", "Unit Testing", "Integration Testing", # Architecture "Microservices", "System Design", "API Design", "Event-Driven", "CQRS", "Event Sourcing", "Message Queue", "RabbitMQ", # Tools & Practices "Git", "GitHub", "GitLab", "Jira", "Agile", "Scrum", "Kanban", "OpenAPI", "Swagger", "Postman", "Linux", ] # Build a lookup: lowercase → canonical name _KEYWORD_MAP: dict[str, str] = {kw.lower(): kw for kw in TECH_KEYWORDS} # Also handle multi-word keywords joined differently (e.g. "ci cd" → "CI/CD") _ALIASES: dict[str, str] = { "ci cd": "CI/CD", "ci/cd": "CI/CD", "machine learning": "ML", "deep learning": "DL", "neural network": "DL", "node": "Node.js", "react.js": "React", "nextjs": "Next.js", "vuejs": "Vue.js", "typescript": "TypeScript", "javascript": "JavaScript", "postgres": "PostgreSQL", "mongo": "MongoDB", "k8s": "Kubernetes", "golang": "Go", } @dataclass class KeywordResult: matched: list[str] missing: list[str] weak: list[str] # matched but low frequency (mentioned once) jd_keywords: list[str] # all keywords found in JD resume_keywords: list[str] def extract_keywords(resume_text: str, jd_text: str) -> KeywordResult: """ Find which keywords from the JD are present in the resume. """ jd_kws = _find_keywords(jd_text) res_kws = _find_keywords(resume_text) res_freq = _keyword_frequencies(resume_text) jd_set = set(jd_kws) res_set = set(res_kws) matched = sorted(jd_set & res_set) missing = sorted(jd_set - res_set) # "Weak" = matched but mentioned only once in the resume weak = [kw for kw in matched if res_freq.get(kw, 0) <= 1] return KeywordResult( matched=matched, missing=missing, weak=weak, jd_keywords=sorted(jd_set), resume_keywords=sorted(res_set), ) def _find_keywords(text: str) -> list[str]: """Return all canonical tech keywords found in text.""" text_lower = text.lower() found: set[str] = set() # Check aliases first for alias, canonical in _ALIASES.items(): if alias in text_lower: found.add(canonical) # Check keyword map for kw_lower, kw_canonical in _KEYWORD_MAP.items(): # Use word-boundary-like check (avoid matching "react" inside "reactivation") pattern = r"(? dict[str, int]: """Count occurrences of each keyword in text.""" text_lower = text.lower() freq: Counter[str] = Counter() for kw_lower, kw_canonical in _KEYWORD_MAP.items(): count = len(re.findall(re.escape(kw_lower), text_lower)) if count: freq[kw_canonical] = count return dict(freq)