gnudevx/Recommendation-System
0
1from fastapi import FastAPI, UploadFile, File2from fastapi.middleware.cors import CORSMiddleware3import os4from pymongo import MongoClient5import numpy as np6from sklearn.metrics.pairwise import cosine_similarity7from sklearn.preprocessing import normalize8 9from cv_utils import (10 convert_objectid_to_str,11 extract_text_from_file,12 extract_skills,13 extract_location_text,14 extract_city,15 parse_experience_text,16 parse_job_experience,17 prepare_job_skills,18 generate_job_embedding,19 generate_cv_embedding,20 check_location_match,21 model,22)23 24app = FastAPI()25 26app.add_middleware(27 CORSMiddleware,28 allow_origins=["*"],29 allow_credentials=True,30 allow_methods=["*"],31 allow_headers=["*"],32)33 34client = MongoClient(os.getenv("MONGO_URL"))35db = client["ITJOBS"]36job_collection = db["jobs"]37 38@app.post("/recommend")39async def recommend_jobs(file: UploadFile = File(...)):40 try:41 if not any(file.filename.lower().endswith(ext) for ext in [".pdf", ".docx"]):42 return {"error": "File không hợp lệ. Vui lòng tải lên file CV định dạng PDF hoặc DOCX.", "skills_found": [], "recommendations": []}43 44 file_bytes = await file.read()45 text = extract_text_from_file(file_bytes=file_bytes, filename=file.filename)46 if not text or not text.strip():47 return {"error": "Không thể đọc nội dung file. Vui lòng kiểm tra file CV.", "skills_found": [], "recommendations": []}48 49 cv_skills = extract_skills(text)50 cv_skills_set = set(cv_skills)51 52 emb_skill = np.zeros(384) if len(cv_skills) == 0 else model.encode(" ".join(cv_skills))53 emb_full = model.encode(text)54 cv_emb = 0.7 * emb_skill + 0.3 * emb_full55 56 jobs = list(job_collection.find({}, {57 "title": 1,58 "embedding": 1,59 "mustHaveSkills": 1,60 "optionalSkills": 1,61 "domainKnowledge": 1,62 "location": 1,63 "salary_raw": 1,64 "company": 1,65 "requirements": 1,66 "description": 1,67 "experience": 1,68 "experienceLevel": 1,69 "work_location_detail": 170 }))71 72 if not jobs:73 return {"skills_found": cv_skills, "recommendations": [], "message": "Không có công việc nào trong hệ thống"}74 75 job_vectors = []76 fallback_flags = []77 for job in jobs:78 emb = job.get("embedding")79 if emb and len(emb) == 384:80 job_vectors.append(emb)81 fallback_flags.append(False)82 else:83 reqs = " ".join(job.get("requirements", [])) or job.get("description", "")84 job_vectors.append(generate_job_embedding(reqs) if reqs else np.zeros(384).tolist())85 fallback_flags.append(True)86 87 job_vectors = np.array(job_vectors)88 cv_emb = normalize([cv_emb])[0]89 job_vectors = normalize(job_vectors)90 scores = cosine_similarity([cv_emb], job_vectors)[0]91 92 cv_full_location = extract_location_text(text)93 cv_city = extract_city(cv_full_location) if cv_full_location else ""94 cv_experience_years = parse_experience_text(text)95 96 results = []97 for job, score, used_fallback in zip(jobs, scores, fallback_flags):98 skills = prepare_job_skills(job)99 job_all_skills = skills["must_have"] | skills["optional"] | skills["domain"]100 matched_total = cv_skills_set & job_all_skills101 skill_score = (len(matched_total) / len(job_all_skills) * 100) if job_all_skills else 0102 103 job_experience_years = parse_job_experience(job.get("experience") or job.get("experienceLevel"))104 experience_match = None105 experience_score = 50106 if cv_experience_years is not None and job_experience_years is not None:107 experience_match = cv_experience_years >= job_experience_years108 experience_score = 100 if experience_match else 0109 110 parts = []111 location_field = job.get("location")112 if isinstance(location_field, dict):113 parts.append(location_field.get("name", ""))114 elif isinstance(location_field, str):115 parts.append(location_field)116 parts.append(job.get("work_location_detail", ""))117 job_full_location = " | ".join([p for p in parts if p])118 job_city = extract_city(job_full_location) if job_full_location else ""119 location_match = check_location_match(cv_city, job_city) if cv_city and job_city else False120 location_score = 100 if location_match else (50 if (cv_city and job_city) else 0)121 122 match_percentage = round(skill_score * 0.7 + experience_score * 0.15 + location_score * 0.15, 1)123 124 reason_parts = []125 if len(cv_skills_set & skills["must_have"]) > 0:126 reason_parts.append(f"Match {len(cv_skills_set & skills['must_have'])}/{len(skills['must_have'])} kỹ năng bắt buộc")127 if experience_match is True:128 reason_parts.append("Kinh nghiệm đủ yêu cầu")129 elif experience_match is False:130 reason_parts.append("Kinh nghiệm có thể thấp hơn yêu cầu")131 if location_match:132 reason_parts.append("Địa điểm phù hợp")133 134 reason = "; ".join(reason_parts) if reason_parts else "Nội dung CV phù hợp với yêu cầu công việc"135 136 combined_score = float(score) + match_percentage / 100.0137 if experience_match is True:138 combined_score += 0.10139 if location_match:140 combined_score += 0.05141 142 results.append({143 "id": str(job["_id"]),144 "title": str(job.get("title", "")),145 "location": convert_objectid_to_str(job.get("location")),146 "salary": str(job.get("salary_raw", "")),147 "company": str(job.get("company", "")),148 "score": float(score),149 "similarity_percentage": round(float(score) * 100, 1),150 "match_percentage": match_percentage,151 "experience_required": job_experience_years,152 "experience_match": experience_match,153 "cv_experience_years": cv_experience_years,154 "location_match": location_match,155 "cv_location": cv_full_location,156 "cv_city": cv_city,157 "job_location_text": job_full_location,158 "job_city": job_city,159 "combined_score": round(combined_score, 4),160 "matched_skills": {161 "required": list(cv_skills_set & skills["must_have"]),162 "optional": list(cv_skills_set & skills["optional"]),163 "domain": list(cv_skills_set & skills["domain"]),164 "total_matched": len(matched_total),165 "total_job_skills": len(job_all_skills),166 },167 "reason": reason,168 })169 170 return convert_objectid_to_str({171 "skills_found": cv_skills,172 "recommendations": sorted(results, key=lambda x: x["combined_score"], reverse=True)[:100],173 "summary": f"Tìm được {len(cv_skills)} kỹ năng trong CV của bạn",174 })175 176 except Exception as e:177 import traceback178 traceback.print_exc()179 return convert_objectid_to_str({"error": f"Lỗi xử lý: {str(e)}", "skills_found": [], "recommendations": []})180 181@app.post("/job-embedding")182async def create_job_embedding(payload: dict):183 try:184 text = payload.get("text", "")185 186 if not text.strip():187 return {"embedding": []}188 189 embedding = model.encode(text)190 191 return {192 "embedding": embedding.tolist()193 }194 195 except Exception as e:196 return {197 "error": str(e)198 }199 200@app.post("/cv-embedding")201async def create_cv_embedding(payload: dict):202 203 raw_text = payload.get("rawText", "")204 skills = payload.get("skills", [])205 206 if not raw_text:207 return {208 "success": False,209 "embedding": []210 }211 212 embedding = generate_cv_embedding(213 raw_text,214 skills215 )216 217 return {218 "success": True,219 "embedding": embedding220 }