['software', 'developer', 'engineer', 'programming', 'coding', 'web', 'mobile', 'frontend', 'backend', 'fullstack', 'full-stack'], 'Data Science' => ['data', 'analytics', 'scientist', 'machine learning', 'ml', 'ai', 'artificial intelligence', 'statistician'], 'Design' => ['designer', 'ux', 'ui', 'graphic', 'creative', 'figma', 'sketch', 'product design'], 'Marketing' => ['marketing', 'seo', 'social media', 'content', 'growth', 'digital marketing', 'brand'], 'Sales' => ['sales', 'account executive', 'business development', 'account manager', 'sales representative'], 'Customer Support' => ['support', 'customer service', 'help desk', 'technical support', 'customer success'], 'Human Resources' => ['hr', 'human resources', 'recruiter', 'talent', 'recruiting'], 'Finance' => ['finance', 'accountant', 'accounting', 'financial', 'controller', 'analyst'], 'Operations' => ['operations', 'logistics', 'supply chain', 'project manager', 'program manager'], 'DevOps' => ['devops', 'infrastructure', 'cloud', 'aws', 'kubernetes', 'docker', 'sre'], 'Product Management' => ['product manager', 'product owner', 'product lead'], 'Quality Assurance' => ['qa', 'quality', 'testing', 'test engineer', 'sdet'], 'Writing' => ['writer', 'content writer', 'copywriter', 'technical writer', 'editor'], ]; /** * Auto-categorize a job based on title and description */ public function categorizeJob(Job $job): ?JobCategory { if ($job->job_category_id) { return $job->category; } $text = strtolower($job->title . ' ' . $job->description); foreach ($this->categoryKeywords as $categoryName => $keywords) { foreach ($keywords as $keyword) { if (str_contains($text, $keyword)) { $category = JobCategory::firstOrCreate( ['slug' => \Str::slug($categoryName)], ['name' => $categoryName, 'description' => "Jobs related to {$categoryName}"] ); $job->update(['job_category_id' => $category->id]); Log::debug("Auto-categorized job #{$job->id} as '{$categoryName}'"); return $category; } } } return null; } /** * Enrich job with additional data */ public function enrichJob(Job $job): void { // Auto-categorize if not categorized if (!$job->job_category_id) { $this->categorizeJob($job); } // Detect if remote from title/description if (!$job->remote_allowed) { $this->detectRemote($job); } // Parse salary if not set if (!$job->salary_min && !$job->salary_max) { $this->parseSalaryFromDescription($job); } // Infer experience level if not set if (!$job->experience_level) { $this->inferExperienceLevel($job); } } /** * Detect if job is remote based on title/description */ private function detectRemote(Job $job): void { $text = strtolower($job->title . ' ' . $job->description . ' ' . ($job->location ?? '')); $remoteKeywords = ['remote', 'work from home', 'wfh', 'telecommute', 'distributed', 'anywhere']; foreach ($remoteKeywords as $keyword) { if (str_contains($text, $keyword)) { $job->update(['remote_allowed' => true]); Log::debug("Detected remote job #{$job->id}"); return; } } } /** * Try to parse salary from description */ private function parseSalaryFromDescription(Job $job): void { $description = $job->description; // Match patterns like $50,000-$80,000, $50k-80k, 50-80k, etc. $patterns = [ '/\$?([\d,]+)k?\s*-\s*\$?([\d,]+)k?/i', '/salary:\s*\$?([\d,]+)k?\s*-\s*\$?([\d,]+)k?/i', '/compensation:\s*\$?([\d,]+)k?\s*-\s*\$?([\d,]+)k?/i', ]; foreach ($patterns as $pattern) { if (preg_match($pattern, $description, $matches)) { $min = (float) str_replace(',', '', $matches[1]); $max = (float) str_replace(',', '', $matches[2]); // If values look like they're in 'k' format (e.g., 50 instead of 50000) if ($min < 1000) { $min *= 1000; $max *= 1000; } $job->update([ 'salary_min' => $min, 'salary_max' => $max, ]); Log::debug("Parsed salary for job #{$job->id}: {$min}-{$max}"); return; } } } /** * Infer experience level from title/description */ private function inferExperienceLevel(Job $job): void { $text = strtolower($job->title); $levelKeywords = [ 'entry' => ['junior', 'entry', 'associate', 'intern', 'graduate', 'trainee'], 'mid' => ['mid', 'intermediate', 'regular'], 'senior' => ['senior', 'sr', 'lead developer', 'staff'], 'lead' => ['lead', 'principal', 'architect', 'head of'], 'executive' => ['director', 'vp', 'vice president', 'chief', 'cto', 'ceo', 'cfo', 'coo'], ]; foreach ($levelKeywords as $level => $keywords) { foreach ($keywords as $keyword) { if (str_contains($text, $keyword)) { $job->update(['experience_level' => $level]); Log::debug("Inferred experience level for job #{$job->id}: {$level}"); return; } } } // Default to mid-level if no keywords found $job->update(['experience_level' => 'mid']); } /** * Batch enrich multiple jobs */ public function enrichJobs(int $batchSize = 100): int { $jobs = Job::whereNull('job_category_id') ->orWhereNull('experience_level') ->limit($batchSize) ->get(); $enrichedCount = 0; foreach ($jobs as $job) { try { $this->enrichJob($job); $enrichedCount++; } catch (\Exception $e) { Log::error("Failed to enrich job #{$job->id}: " . $e->getMessage()); } } return $enrichedCount; } /** * Normalize salary to USD (if we had currency conversion) */ public function normalizeSalary(Job $job): void { // Placeholder for currency conversion logic // Would use an API like exchangerate-api.com or similar if ($job->salary_currency !== 'USD') { // Convert to USD // $job->update(['salary_min' => $converted, 'salary_max' => $converted]); } } }