261 lines
7.2 KiB
Python
261 lines
7.2 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
from django.db.models import Q
|
|
|
|
from apps.profiles.models import SearchProfile
|
|
|
|
from .normalization import normalize_token
|
|
|
|
# Deliberately title-led: employer pages regularly mention digital systems in otherwise
|
|
# non-IT vacancies. A description hit alone must therefore never make a vacancy relevant.
|
|
IT_TITLE_TERMS = (
|
|
"it",
|
|
"ict",
|
|
"informatica",
|
|
"software",
|
|
"development engineer",
|
|
"devops",
|
|
"secops",
|
|
"cloud",
|
|
"data engineer",
|
|
"data scientist",
|
|
"data steward",
|
|
"database",
|
|
"dba",
|
|
"business intelligence",
|
|
"bi analyst",
|
|
"system engineer",
|
|
"systems engineer",
|
|
"system network engineer",
|
|
"system administrator",
|
|
"systeembeheer",
|
|
"infrastructure engineer",
|
|
"infrastructuur engineer",
|
|
"network engineer",
|
|
"network administrator",
|
|
"network architect",
|
|
"netwerkbeheer",
|
|
"netwerk engineer",
|
|
"cybersecurity",
|
|
"cyber security",
|
|
"security engineer",
|
|
"security architect",
|
|
"security analyst",
|
|
"information security",
|
|
"informatiebeveiliging",
|
|
"workplace engineer",
|
|
"endpoint engineer",
|
|
"microsoft 365 engineer",
|
|
"m365 engineer",
|
|
"intune engineer",
|
|
"modern workplace",
|
|
"digital workplace",
|
|
"service desk",
|
|
"servicedesk",
|
|
"helpdesk",
|
|
"support engineer",
|
|
"support specialist",
|
|
"it technician",
|
|
"pc technician",
|
|
"application lead",
|
|
"application engineer",
|
|
"application manager",
|
|
"application specialist",
|
|
"applicatiebeheer",
|
|
"platform engineer",
|
|
"platform expert",
|
|
"solution architect",
|
|
"solutions architect",
|
|
"technical architect",
|
|
"technisch architect",
|
|
"functional analyst",
|
|
"functioneel analist",
|
|
"business analyst",
|
|
"product owner",
|
|
"web developer",
|
|
"mobile developer",
|
|
"low-code developer",
|
|
"low code developer",
|
|
"api developer",
|
|
"idm developer",
|
|
"power platform",
|
|
"forgerock",
|
|
"embedded",
|
|
"c#",
|
|
"front end",
|
|
"frontend",
|
|
"back end",
|
|
"backend",
|
|
"full stack",
|
|
"fullstack",
|
|
"php",
|
|
".net",
|
|
"java",
|
|
"python",
|
|
"powershell",
|
|
"azure",
|
|
"aws",
|
|
"microsoft 365",
|
|
"linux",
|
|
"windows server",
|
|
"kubernetes",
|
|
"docker",
|
|
"terraform",
|
|
"erp",
|
|
"sap",
|
|
"machine learning",
|
|
"ai engineer",
|
|
"artificial intelligence",
|
|
"image processing",
|
|
"computer vision",
|
|
"qa engineer",
|
|
"test automation",
|
|
)
|
|
|
|
# These engineering titles occur both in IT and in unrelated technical sectors. They are
|
|
# accepted only when the title supplies the role context and the description independently
|
|
# supplies a strong IT signal. A description signal by itself remains insufficient.
|
|
CONTEXTUAL_IT_TITLE_TERMS = (
|
|
"field engineer",
|
|
"field service engineer",
|
|
"implementation engineer",
|
|
"implementation consultant",
|
|
"service engineer",
|
|
)
|
|
|
|
IT_DESCRIPTION_TERMS = (
|
|
"microsoft 365",
|
|
"m365",
|
|
"windows server",
|
|
"active directory",
|
|
"entra id",
|
|
"intune",
|
|
"autopilot",
|
|
"exchange online",
|
|
"sharepoint",
|
|
"vmware",
|
|
"proxmox",
|
|
"hyper-v",
|
|
"networking",
|
|
"netwerkbeheer",
|
|
"tcp/ip",
|
|
"vlan",
|
|
"vpn",
|
|
"dhcp",
|
|
"dns",
|
|
"firewall",
|
|
"switches",
|
|
"routers",
|
|
"workstations",
|
|
"desktops",
|
|
"laptops",
|
|
"voip",
|
|
"3cx",
|
|
"powershell",
|
|
)
|
|
|
|
# These phrases can contain IT vocabulary while describing commercial, recruitment,
|
|
# or educational-design work. They therefore override positive title signals.
|
|
NON_IT_TITLE_TERMS = (
|
|
"business developer",
|
|
"business development",
|
|
"it recruitment",
|
|
"it recruiter",
|
|
"learning designer",
|
|
)
|
|
|
|
IT_PROFILE_TERMS = (*IT_TITLE_TERMS, "networking", "security", "active directory", "intune")
|
|
|
|
|
|
def _term_pattern(term: str) -> str:
|
|
escaped = re.escape(term).replace(r"\ ", r"[\s/_-]+")
|
|
return rf"(?<!\w){escaped}(?!\w)"
|
|
|
|
|
|
IT_TITLE_PATTERN = "(?:" + "|".join(_term_pattern(term) for term in IT_TITLE_TERMS) + ")"
|
|
NON_IT_TITLE_PATTERN = "(?:" + "|".join(_term_pattern(term) for term in NON_IT_TITLE_TERMS) + ")"
|
|
CONTEXTUAL_IT_TITLE_PATTERN = (
|
|
"(?:" + "|".join(_term_pattern(term) for term in CONTEXTUAL_IT_TITLE_TERMS) + ")"
|
|
)
|
|
IT_DESCRIPTION_PATTERN = (
|
|
"(?:" + "|".join(_term_pattern(term) for term in IT_DESCRIPTION_TERMS) + ")"
|
|
)
|
|
_IT_TITLE_RE = re.compile(IT_TITLE_PATTERN, re.IGNORECASE)
|
|
_NON_IT_TITLE_RE = re.compile(NON_IT_TITLE_PATTERN, re.IGNORECASE)
|
|
_CONTEXTUAL_IT_TITLE_RE = re.compile(CONTEXTUAL_IT_TITLE_PATTERN, re.IGNORECASE)
|
|
_IT_DESCRIPTION_RE = re.compile(IT_DESCRIPTION_PATTERN, re.IGNORECASE)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ItRelevanceAssessment:
|
|
relevant: bool
|
|
signals: tuple[str, ...]
|
|
reason: str
|
|
|
|
|
|
def assess_it_relevance(title: str, description: str = "") -> ItRelevanceAssessment:
|
|
normalized_title = normalize_token(title)
|
|
blocked_signal = _NON_IT_TITLE_RE.search(normalized_title)
|
|
if blocked_signal:
|
|
return ItRelevanceAssessment(
|
|
relevant=False,
|
|
signals=(),
|
|
reason=f"Niet-technische titelcontext: {blocked_signal.group(0)}.",
|
|
)
|
|
signals = tuple(
|
|
term
|
|
for term in IT_TITLE_TERMS
|
|
if re.search(_term_pattern(normalize_token(term)), normalized_title, re.IGNORECASE)
|
|
)
|
|
if signals:
|
|
return ItRelevanceAssessment(
|
|
relevant=True,
|
|
signals=signals[:4],
|
|
reason="IT-signaal in functietitel: " + ", ".join(signals[:4]),
|
|
)
|
|
contextual_title = _CONTEXTUAL_IT_TITLE_RE.search(normalized_title)
|
|
if contextual_title:
|
|
description_signal = _IT_DESCRIPTION_RE.search(normalize_token(description))
|
|
if description_signal:
|
|
return ItRelevanceAssessment(
|
|
relevant=True,
|
|
signals=(contextual_title.group(0), description_signal.group(0)),
|
|
reason=(
|
|
"IT-context bevestigd via ambigue functietitel en vacaturetekst: "
|
|
f"{contextual_title.group(0)}, {description_signal.group(0)}."
|
|
),
|
|
)
|
|
return ItRelevanceAssessment(
|
|
relevant=False,
|
|
signals=(),
|
|
reason="Ambigue technische functietitel zonder aantoonbare IT-context.",
|
|
)
|
|
return ItRelevanceAssessment(
|
|
relevant=False,
|
|
signals=(),
|
|
reason="Geen aantoonbaar IT-signaal in de functietitel.",
|
|
)
|
|
|
|
|
|
def profile_requires_it_focus(profile: SearchProfile) -> bool:
|
|
configured = " ".join([profile.name, *profile.desired_titles, *profile.desired_skills])
|
|
normalized = normalize_token(configured)
|
|
return any(
|
|
re.search(_term_pattern(normalize_token(term)), normalized, re.IGNORECASE)
|
|
for term in IT_PROFILE_TERMS
|
|
)
|
|
|
|
|
|
def it_relevance_query(prefix: str = "") -> Q:
|
|
"""Return the database equivalent of the conservative title-led classifier."""
|
|
clear_title = Q(**{f"{prefix}original_title__iregex": IT_TITLE_PATTERN})
|
|
contextual_title = Q(**{f"{prefix}original_title__iregex": CONTEXTUAL_IT_TITLE_PATTERN}) & Q(
|
|
**{f"{prefix}description_text__iregex": IT_DESCRIPTION_PATTERN}
|
|
)
|
|
return (clear_title | contextual_title) & ~Q(
|
|
**{f"{prefix}original_title__iregex": NON_IT_TITLE_PATTERN}
|
|
)
|