Spaces:
Running
Running
File size: 9,521 Bytes
abc6ea2 3cae53e abc6ea2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | from __future__ import annotations
import re
from src.core.constants import INDIAN_CITIES
from src.core.models import (
ExperienceRequirements,
LocationRequirements,
ParsedQuery,
PreferredSkill,
QueryFilters,
RequiredSkill,
SkillImportance,
)
from src.matching.skill_matcher import SKILL_ALIASES
_SINGLE_WORD_SKILLS: set[str] = set()
_MULTI_WORD_SKILLS: set[str] = set()
_MULTI_WORD_MAP: dict[str, str] = {}
for skill in SKILL_ALIASES:
norm = skill.strip().lower()
word_count = len(norm.split())
if word_count == 1:
_SINGLE_WORD_SKILLS.add(norm)
else:
_MULTI_WORD_SKILLS.add(norm)
_MULTI_WORD_MAP[norm] = skill
for alias in SKILL_ALIASES[skill]:
alias_norm = alias.strip().lower()
alias_words = len(alias_norm.split())
if alias_words == 1:
_SINGLE_WORD_SKILLS.add(alias_norm)
else:
_MULTI_WORD_SKILLS.add(alias_norm)
_MULTI_WORD_MAP[alias_norm] = skill
_EXTRA_SKILLS: set[str] = {
"python", "java", "javascript", "typescript", "golang", "go", "rust",
"c++", "c#", "ruby", "php", "scala", "kotlin", "swift", "dart",
"html", "css", "sass", "less", "webpack", "vite", "babel",
"node.js", "node", "express", "nestjs", "graphql", "grpc",
"react", "vue", "angular", "svelte", "next.js", "nuxt", "gatsby",
"tailwind", "bootstrap", "material ui", "shadcn", "chakra",
"postgresql", "postgres", "mysql", "mongodb", "redis", "elasticsearch",
"cassandra", "dynamodb", "couchbase", "mariadb", "sqlite",
"aws", "gcp", "azure", "cloud", "terraform", "pulumi",
"docker", "kubernetes", "k8s", "helm", "istio", "envoy",
"ci/cd", "jenkins", "github actions", "gitlab ci", "circleci",
"argocd", "prometheus", "grafana", "datadog", "new relic",
"kafka", "rabbitmq", "pulsar", "nats",
"flask", "django", "fastapi", "spring boot", "spring",
"pytorch", "tensorflow", "keras", "jax", "scikit-learn",
"pandas", "numpy", "scipy", "matplotlib", "seaborn", "plotly",
"spark", "hadoop", "airflow", "prefect", "dbt", "snowflake",
"bigquery", "redshift", "databricks", "lakehouse",
"machine learning", "deep learning", "reinforcement learning",
"natural language processing", "nlp", "computer vision",
"llm", "large language model", "rag", "langchain",
"gen ai", "generative ai", "diffusion", "transformer",
"selenium", "cypress", "playwright", "puppeteer",
"pytest", "jest", "mocha", "chai", "vitest",
"rest api", "restful", "rest", "graphql",
"microservices", "distributed systems", "system design",
"android", "ios", "flutter", "react native", "kotlin",
"mlops", "devops", "data engineering", "data science",
"cybersecurity", "penetration testing", "security",
"tableau", "power bi", "powerbi", "looker", "excel",
"agile", "scrum", "jira", "confluence",
"sap", "oracle", "salesforce", "servicenow",
"numpy", "pandas", "scipy", "matplotlib", "seaborn", "plotly",
"hibernate", "jdbc", "jpa", "struts", "jee",
"asp.net", ".net", "c#", "dotnet",
"blockchain", "solidity", "web3", "smart contract",
"api", "soap", "json", "xml", "protobuf",
}
_SINGLE_WORD_SKILLS.update(s for s in _EXTRA_SKILLS if len(s.split()) == 1)
_MULTI_WORD_SKILLS.update(s for s in _EXTRA_SKILLS if len(s.split()) > 1)
for s in _EXTRA_SKILLS:
norm = s.strip().lower()
if len(norm.split()) > 1 and norm not in _MULTI_WORD_MAP:
_MULTI_WORD_MAP[norm] = norm
_MULTI_WORD_SKILLS_SORTED = sorted(_MULTI_WORD_SKILLS, key=lambda x: -len(x.split()))
_SENIORITY_LEVELS = {"junior": "junior", "jr": "junior", "mid": "mid",
"senior": "senior", "sr": "senior", "lead": "lead",
"principal": "principal", "staff": "staff",
"head": "head", "vp": "vp", "director": "director",
"c-level": "c-level", "cto": "c-level", "ceo": "c-level",
"chief": "c-level", "architect": "architect",
"manager": "manager", "head of": "head"}
_NON_SKILL_WORDS: frozenset = frozenset({
"find", "search", "look", "looking", "need", "wanted", "hiring",
"and", "the", "a", "an", "in", "at", "on", "to", "of", "is", "are",
"for", "with", "from", "by", "as", "or", "but", "not", "be",
"i", "we", "you", "he", "she", "it", "they", "me", "my", "our",
"senior", "junior", "lead", "head", "principal", "staff",
"developer", "engineer", "manager", "architect", "analyst",
"experience", "role", "position", "job", "opening", "hire",
"dev", "sr", "jr", "intern", "fresher",
"year", "years", "yr", "yrs", "exp", "experience",
"building", "designing", "managing", "leading", "working",
"team", "teams", "people", "product", "projects", "company",
"good", "strong", "hands-on", "handson", "expert",
"preferred", "required", "must", "should", "ability",
"proven", "track", "record", "etc", "including",
"platform", "service", "system", "application",
"engineering", "technology", "technologies",
})
_EXPERIENCE_RE = re.compile(
r'(?P<min>\d+)\+?\s*(?:years?|yrs?|yr)\s*(?:of\s*)?(?:experience)?',
re.IGNORECASE,
)
def detect_multi_word_skills(text: str) -> list[str]:
lower = text.lower()
found: list[str] = []
start = 0
while start < len(lower):
matched = 0
for phrase in _MULTI_WORD_SKILLS_SORTED:
n = len(phrase)
segment = lower[start:start + len(phrase)]
if segment == phrase:
pre_ok = start == 0 or not lower[start - 1].isalnum()
post_ok = (start + len(phrase) >= len(lower)
or not lower[start + len(phrase)].isalnum())
if pre_ok and post_ok:
found.append(_MULTI_WORD_MAP.get(phrase, phrase))
start += len(phrase)
matched = len(phrase)
break
if matched == 0:
start += 1
return found
def parse_query(text: str) -> ParsedQuery:
original = text.strip()
lower = original.lower()
multi_skills = detect_multi_word_skills(lower)
remaining = lower
for phrase in sorted(multi_skills, key=len, reverse=True):
remaining = remaining.replace(phrase.lower(), "", 1)
remaining = re.sub(r'\s+', ' ', remaining).strip()
seniority: str | None = None
for label, level in _SENIORITY_LEVELS.items():
if label in remaining.split():
seniority = level
break
if label == "head of" and "head of" in remaining:
seniority = "head"
break
exp_min: int | None = None
exp_max: int | None = None
exp_match = _EXPERIENCE_RE.search(remaining)
if exp_match:
exp_min = int(exp_match.group("min"))
range_match = re.search(
rf'{exp_match.group("min")}\s*-\s*(\d+)\s*(?:years?|yrs?)',
remaining,
)
if range_match:
exp_max = int(range_match.group(1))
city: str | None = None
for c in sorted(INDIAN_CITIES, key=len, reverse=True):
if c.lower() in remaining:
city = c
remaining = remaining.replace(c.lower(), "")
break
words = remaining.split()
skill_words: list[str] = []
for w in words:
w_clean = w.strip(",.!?;:'\"()[]{}")
if len(w_clean) <= 1:
continue
if w_clean in _NON_SKILL_WORDS:
continue
if w_clean in _SINGLE_WORD_SKILLS:
skill_words.append(w_clean)
seen: set[str] = set()
all_skills: list[str] = []
for s in multi_skills + skill_words:
s_norm = s.strip().lower()
if s_norm not in seen:
seen.add(s_norm)
all_skills.append(s)
required_skills = [RequiredSkill(name=s, importance=SkillImportance.REQUIRED)
for s in all_skills[:8]]
preferred_skills = [PreferredSkill(name=s, importance=SkillImportance.PREFERRED)
for s in all_skills[8:]]
if not all_skills:
required_skills = [RequiredSkill(name=original,
importance=SkillImportance.REQUIRED)]
exp_req = ExperienceRequirements(
min_years=float(exp_min) if exp_min else None,
max_years=float(exp_max) if exp_max else None,
)
loc = LocationRequirements(city=city or "", remote_ok="remote" in lower)
return ParsedQuery(
required_skills=required_skills,
preferred_skills=preferred_skills,
experience=exp_req,
location=loc,
filters=QueryFilters(),
original_query=original,
)
def expand_with_aliases(text: str) -> list[str]:
lower = text.lower()
expansions: list[str] = [text]
detected_multi = detect_multi_word_skills(lower)
detected_multi_norm = set(s.lower() for s in detected_multi)
words = lower.split()
for i, w in enumerate(words):
if w in _NON_SKILL_WORDS:
continue
for skill, aliases in SKILL_ALIASES.items():
all_forms = [skill] + aliases
if w in all_forms or skill == w or w in aliases:
for form in all_forms:
if form != w and form not in detected_multi_norm:
alt_words = list(words)
alt_words[i] = form
expansions.append(" ".join(alt_words))
break
return expansions
|