Spaces:
Running
Running
| """Embed resumes and score them against the job corpus. | |
| Critical consistency rule: resumes must be embedded the *same way* JobAtlas | |
| embedded the jobs β model ``BAAI/bge-small-en-v1.5``, no instruction prefix, | |
| ``normalize_embeddings=True`` β so the resume vector lives in the same space as | |
| the corpus vectors and cosine similarity is meaningful. | |
| """ | |
| from __future__ import annotations | |
| from collections.abc import Callable | |
| from functools import lru_cache | |
| import numpy as np | |
| from core.data import MODEL_NAME, JobCorpus | |
| from core.types import ResumeText, ScoringResult | |
| Embedder = Callable[[str], np.ndarray] | |
| def _model(): | |
| # Imported lazily so importing this module (and the stats engine/tests) | |
| # never requires torch unless an embedding is actually needed. | |
| from sentence_transformers import SentenceTransformer | |
| return SentenceTransformer(MODEL_NAME) | |
| def embed_text(text: str) -> np.ndarray: | |
| """Embed one resume into a 384-dim L2-normalized vector.""" | |
| vec = _model().encode([text], normalize_embeddings=True, show_progress_bar=False)[0] | |
| return np.asarray(vec, dtype=np.float32) | |
| def _resume_chunks(text: str) -> list[str]: | |
| """Split a resume into bullet/line chunks for late-interaction scoring.""" | |
| lines = [ln.strip(" \t\u2022-\u2013*\u00b7").strip() for ln in text.splitlines()] | |
| chunks = [ln for ln in lines if len(ln) >= 20] | |
| return chunks or [text.strip() or " "] | |
| def embed_chunks(text: str) -> np.ndarray: | |
| """Embed a resume into a (C, 384) matrix of L2-normalized chunk vectors. | |
| Scoring takes each job's best-matching chunk (see ``score_against_jobs``), so a | |
| specialist bullet lifts that resume in its own cluster instead of being averaged | |
| away by whole-document mean pooling. | |
| """ | |
| chunks = _resume_chunks(text) | |
| vecs = _model().encode(chunks, normalize_embeddings=True, show_progress_bar=False) | |
| return np.asarray(vecs, dtype=np.float32) | |
| def score_against_jobs(resume_vec: np.ndarray, jobs_matrix: np.ndarray) -> np.ndarray: | |
| """Cosine similarity of one resume against every job. | |
| Both sides are L2-normalized, so cosine similarity reduces to a dot product. | |
| Returns an (N,) array in roughly [-1, 1]. | |
| """ | |
| rv = resume_vec.astype(np.float32) | |
| if rv.ndim == 1: | |
| return jobs_matrix @ rv | |
| # (C, 384) chunk matrix: each job scores against its best-matching resume | |
| # chunk (asymmetric late interaction) β rewards depth over vocabulary breadth. | |
| return (jobs_matrix @ rv.T).max(axis=1) | |
| def _as_text(resume: ResumeText | str) -> str: | |
| return resume.text if isinstance(resume, ResumeText) else str(resume) | |
| def compare_resumes( | |
| resume_a: ResumeText | str, | |
| resume_b: ResumeText | str, | |
| corpus: JobCorpus, | |
| *, | |
| embed: Embedder = embed_chunks, | |
| ) -> ScoringResult: | |
| """Score resumes A and B against the corpus and return paired results. | |
| ``embed`` is injectable so tests can supply a deterministic synthetic | |
| embedder without loading the transformer model. | |
| """ | |
| vec_a = embed(_as_text(resume_a)) | |
| vec_b = embed(_as_text(resume_b)) | |
| scores_a = score_against_jobs(vec_a, corpus.matrix) | |
| scores_b = score_against_jobs(vec_b, corpus.matrix) | |
| return ScoringResult( | |
| job_ids=corpus.job_ids, | |
| cluster_ids=corpus.cluster_ids, | |
| cluster_labels=corpus.cluster_labels, | |
| scores_a=scores_a, | |
| scores_b=scores_b, | |
| ) | |