Om-Shandilya commited on
Commit
92feaec
Β·
1 Parent(s): f6d8deb

Make the pipelines functional with HF Hub + Local Files

Browse files
environment.yml CHANGED
@@ -3,343 +3,36 @@
3
 
4
  name: resume-matcher
5
  channels:
6
- - pytorch
7
- - nvidia
8
  - conda-forge
9
  - defaults
10
  dependencies:
11
- - _openmp_mutex=4.5
12
- - _python_abi3_support=1.0
13
- - aiohappyeyeballs=2.6.1
14
- - aiohttp=3.12.15
15
- - aiosignal=1.4.0
16
- - anyio=4.9.0
17
- - argon2-cffi=25.1.0
18
- - argon2-cffi-bindings=21.2.0
19
- - arrow=1.3.0
20
- - asttokens=3.0.0
21
- - async-lru=2.0.5
22
- - async-timeout=5.0.1
23
- - attrs=25.3.0
24
- - aws-c-auth=0.9.0
25
- - aws-c-cal=0.9.2
26
- - aws-c-common=0.12.4
27
- - aws-c-compression=0.3.1
28
- - aws-c-event-stream=0.5.5
29
- - aws-c-http=0.10.4
30
- - aws-c-io=0.21.2
31
- - aws-c-mqtt=0.13.3
32
- - aws-c-s3=0.8.6
33
- - aws-c-sdkutils=0.2.4
34
- - aws-checksums=0.2.7
35
- - aws-crt-cpp=0.33.1
36
- - aws-sdk-cpp=1.11.606
37
- - babel=2.17.0
38
- - beautifulsoup4=4.13.4
39
- - blas=1.0
40
- - bleach=6.2.0
41
- - bleach-with-css=6.2.0
42
- - bottleneck=1.4.2
43
- - brotli=1.1.0
44
- - brotli-bin=1.1.0
45
- - brotli-python=1.1.0
46
- - bzip2=1.0.8
47
- - c-ares=1.34.5
48
- - ca-certificates=2025.8.3
49
- - cached-property=1.5.2
50
- - cached_property=1.5.2
51
- - cairo=1.18.4
52
- - certifi=2025.8.3
53
- - cffi=1.17.1
54
- - click=8.1.8
55
- - colorama=0.4.6
56
- - comm=0.2.3
57
- - contourpy=1.3.2
58
- - cpython=3.10.18
59
- - cryptography=45.0.6
60
- - cuda-cccl=13.0.50
61
- - cuda-cccl_win-64=13.0.50
62
- - cuda-cudart=12.1.105
63
- - cuda-cudart-dev=12.1.105
64
- - cuda-cupti=12.1.105
65
- - cuda-libraries=12.1.0
66
- - cuda-libraries-dev=12.1.0
67
- - cuda-nvrtc=12.1.105
68
- - cuda-nvrtc-dev=12.1.105
69
- - cuda-nvtx=12.1.105
70
- - cuda-opencl=13.0.39
71
- - cuda-opencl-dev=13.0.39
72
- - cuda-profiler-api=13.0.39
73
- - cuda-runtime=12.1.0
74
- - cuda-version=13.0
75
- - cycler=0.12.1
76
- - datasets=2.14.4
77
- - debugpy=1.8.15
78
- - decorator=5.2.1
79
- - defusedxml=0.7.1
80
- - dill=0.3.7
81
- - double-conversion=3.3.1
82
- - exceptiongroup=1.3.0
83
- - executing=2.2.0
84
- - font-ttf-dejavu-sans-mono=2.37
85
- - font-ttf-inconsolata=3.000
86
- - font-ttf-source-code-pro=2.038
87
- - font-ttf-ubuntu=0.83
88
- - fontconfig=2.15.0
89
- - fonts-conda-ecosystem=1
90
- - fonts-conda-forge=1
91
- - fonttools=4.59.0
92
- - fqdn=1.5.1
93
- - freetype=2.13.3
94
- - frozenlist=1.7.0
95
- - fsspec=2025.7.0
96
- - gmp=6.3.0
97
- - gmpy2=2.2.1
98
- - graphite2=1.3.14
99
- - h11=0.16.0
100
- - h2=4.2.0
101
- - harfbuzz=11.3.2
102
- - hf-xet=1.1.7
103
- - hpack=4.1.0
104
- - httpcore=1.0.9
105
- - httpx=0.28.1
106
- - huggingface_hub=0.34.4
107
- - hyperframe=6.1.0
108
- - icc_rt=2022.1.0
109
- - icu=75.1
110
- - idna=3.10
111
- - importlib-metadata=8.7.0
112
- - intel-openmp=2023.1.0
113
- - ipykernel=6.29.5
114
- - ipython=8.37.0
115
- - ipywidgets=8.1.7
116
- - isoduration=20.11.0
117
- - jedi=0.19.2
118
- - jinja2=3.1.6
119
- - joblib=1.5.1
120
- - json5=0.12.0
121
- - jsonpointer=3.0.0
122
- - jsonschema=4.25.0
123
- - jsonschema-specifications=2025.4.1
124
- - jsonschema-with-format-nongpl=4.25.0
125
- - jupyter=1.1.1
126
- - jupyter-lsp=2.2.6
127
- - jupyter_client=8.6.3
128
- - jupyter_console=6.6.3
129
- - jupyter_core=5.8.1
130
- - jupyter_events=0.12.0
131
- - jupyter_server=2.16.0
132
- - jupyter_server_terminals=0.5.3
133
- - jupyterlab=4.4.5
134
- - jupyterlab_pygments=0.3.0
135
- - jupyterlab_server=2.27.3
136
- - jupyterlab_widgets=3.0.15
137
- - khronos-opencl-icd-loader=2024.05.08
138
- - kiwisolver=1.4.8
139
- - krb5=1.21.3
140
- - lark=1.2.2
141
- - lcms2=2.17
142
- - lerc=4.0.0
143
- - libabseil=20250512.1
144
- - libarrow=21.0.0
145
- - libarrow-acero=21.0.0
146
- - libarrow-compute=21.0.0
147
- - libarrow-dataset=21.0.0
148
- - libarrow-substrait=21.0.0
149
- - libbrotlicommon=1.1.0
150
- - libbrotlidec=1.1.0
151
- - libbrotlienc=1.1.0
152
- - libclang13=20.1.8
153
- - libcrc32c=1.1.2
154
- - libcublas=12.1.0.26
155
- - libcublas-dev=12.1.0.26
156
- - libcufft=11.0.2.4
157
- - libcufft-dev=11.0.2.4
158
- - libcurand=10.4.0.35
159
- - libcurand-dev=10.4.0.35
160
- - libcurl=8.14.1
161
- - libcusolver=11.4.4.55
162
- - libcusolver-dev=11.4.4.55
163
- - libcusparse=12.0.2.55
164
- - libcusparse-dev=12.0.2.55
165
- - libdeflate=1.24
166
- - libevent=2.1.12
167
- - libexpat=2.7.1
168
- - libffi=3.4.6
169
- - libfreetype=2.13.3
170
- - libfreetype6=2.13.3
171
- - libgcc=15.1.0
172
- - libglib=2.84.2
173
- - libgomp=15.1.0
174
- - libgoogle-cloud=2.39.0
175
- - libgoogle-cloud-storage=2.39.0
176
- - libgrpc=1.73.1
177
- - libhwloc=2.11.2
178
- - libiconv=1.18
179
- - libintl=0.22.5
180
- - libjpeg-turbo=3.1.0
181
- - liblzma=5.8.1
182
- - libnpp=12.0.2.50
183
- - libnpp-dev=12.0.2.50
184
- - libnvjitlink=12.1.105
185
- - libnvjitlink-dev=12.1.105
186
- - libnvjpeg=12.1.1.14
187
- - libnvjpeg-dev=12.1.1.14
188
- - libparquet=21.0.0
189
- - libpng=1.6.50
190
- - libprotobuf=6.31.1
191
- - libre2-11=2025.07.22
192
- - libsodium=1.0.20
193
- - libsqlite=3.50.3
194
- - libssh2=1.11.1
195
- - libthrift=0.22.0
196
- - libtiff=4.7.0
197
- - libutf8proc=2.10.0
198
- - libuv=1.48.0
199
- - libwebp-base=1.6.0
200
- - libwinpthread=12.0.0.r4.gg4f2fc60ca
201
- - libxcb=1.17.0
202
- - libxml2=2.13.8
203
- - libxslt=1.1.43
204
- - libzlib=1.3.1
205
- - lxml=6.0.0
206
- - lz4-c=1.10.0
207
- - markupsafe=3.0.2
208
- - matplotlib=3.10.3
209
- - matplotlib-base=3.10.3
210
- - matplotlib-inline=0.1.7
211
- - mistune=3.1.3
212
- - mkl=2023.1.0
213
- - mkl-service=2.4.0
214
- - mkl_fft=1.3.11
215
- - mkl_random=1.2.8
216
- - mpc=1.3.1
217
- - mpfr=4.2.1
218
- - mpmath=1.3.0
219
- - multidict=6.6.3
220
- - multiprocess=0.70.15
221
- - munkres=1.1.4
222
- - nbclient=0.10.2
223
- - nbconvert-core=7.16.6
224
- - nbformat=5.10.4
225
- - nest-asyncio=1.6.0
226
- - networkx=3.3
227
- - nltk=3.7
228
- - notebook=7.4.4
229
- - notebook-shim=0.2.4
230
- - numexpr=2.11.0
231
- - openjpeg=2.5.3
232
- - openssl=3.5.2
233
- - orc=2.2.0
234
- - overrides=7.7.0
235
- - packaging=25.0
236
- - pandas=1.5.3
237
- - pandocfilters=1.5.0
238
- - parso=0.8.4
239
- - patsy=1.0.1
240
- - pcre2=10.45
241
- - pdfminer.six=20250506
242
- - pickleshare=0.7.5
243
- - pillow=11.3.0
244
- - pip=25.1.1
245
- - pixman=0.46.4
246
- - platformdirs=4.3.8
247
- - prometheus_client=0.22.1
248
- - prompt_toolkit=3.0.51
249
- - propcache=0.3.1
250
- - psutil=7.0.0
251
- - pthread-stubs=0.4
252
- - pure_eval=0.2.3
253
- - pycparser=2.22
254
- - pygments=2.19.2
255
- - pyparsing=3.2.3
256
- - pyside6=6.9.1
257
- - pysocks=1.7.1
258
- - python=3.10.18
259
- - python-dateutil=2.9.0.post0
260
- - python-docx=1.2.0
261
- - python-fastjsonschema=2.21.1
262
- - python-gil=3.10.18
263
- - python-json-logger=2.0.7
264
- - python-xxhash=3.5.0
265
- - python_abi=3.10
266
- - pytorch-cuda=12.1
267
- - pytorch-mutex=1.0
268
- - pytz=2025.2
269
- - pywin32=311
270
- - pywinpty=2.0.15
271
- - pyyaml=6.0.2
272
- - pyzmq=27.0.0
273
- - qhull=2020.2
274
- - qt6-main=6.9.1
275
- - re2=2025.07.22
276
- - referencing=0.36.2
277
- - rfc3339-validator=0.1.4
278
- - rfc3986-validator=0.1.1
279
- - rfc3987-syntax=1.1.0
280
- - rpds-py=0.26.0
281
- - safetensors=0.6.2
282
- - scikit-learn=1.7.1
283
- - scipy=1.15.3
284
- - seaborn=0.13.2
285
- - seaborn-base=0.13.2
286
- - send2trash=1.8.3
287
- - sentence-transformers=5.1.0
288
- - setuptools=72.1.0
289
- - six=1.17.0
290
- - snappy=1.2.2
291
- - sniffio=1.3.1
292
- - soupsieve=2.7
293
- - stack_data=0.6.3
294
- - statsmodels=0.14.5
295
- - tbb=2021.13.0
296
- - terminado=0.18.1
297
- - threadpoolctl=3.6.0
298
- - tinycss2=1.4.0
299
- - tk=8.6.13
300
- - tomli=2.2.1
301
- - tornado=6.5.1
302
- - tqdm=4.67.1
303
- - traitlets=5.14.3
304
- - types-python-dateutil=2.9.0.20250708
305
- - typing_extensions=4.14.1
306
- - typing_utils=0.1.0
307
- - tzdata=2025b
308
- - ucrt=10.0.22621.0
309
- - unicodedata2=16.0.0
310
- - uri-template=1.3.0
311
- - urllib3=2.5.0
312
- - vc=14.3
313
- - vc14_runtime=14.44.35208
314
- - vs2015_runtime=14.44.35208
315
- - wcwidth=0.2.13
316
- - webcolors=24.11.1
317
- - webencodings=0.5.1
318
- - websocket-client=1.8.0
319
- - wheel=0.45.1
320
- - widgetsnbextension=4.0.14
321
- - win_inet_pton=1.1.0
322
- - winpty=0.4.3
323
- - wordcloud=1.9.4
324
- - xorg-libxau=1.0.12
325
- - xorg-libxdmcp=1.1.5
326
- - xxhash=0.8.3
327
- - yaml=0.2.5
328
- - yarl=1.20.1
329
- - zeromq=4.3.5
330
- - zipp=3.23.0
331
- - zstandard=0.23.0
332
- - zstd=1.5.7
333
  - pip:
334
- - charset-normalizer==3.4.3
335
- - filelock==3.19.1
336
- - numpy==2.2.6
337
- - pyarrow==14.0.2
338
- - regex==2025.7.34
339
- - requests==2.32.5
340
- - sympy==1.13.1
341
- - tokenizers==0.19.1
342
- - torch==2.8.0
343
- - torchaudio==2.5.1+cu121
344
- - torchvision==0.20.1+cu121
345
- - transformers==4.41.2
 
3
 
4
  name: resume-matcher
5
  channels:
 
 
6
  - conda-forge
7
  - defaults
8
  dependencies:
9
+ - python=3.10
10
+ - pip
11
+ # Core scientific stack
12
+ - numpy
13
+ - pandas
14
+ - scikit-learn
15
+ - scipy
16
+ - joblib
17
+ - tqdm
18
+ - matplotlib
19
+ - seaborn
20
+ - wordcloud
21
+ # NLP / ML essentials
22
+ - faiss-cpu
23
+ - nltk
24
+ - statsmodels
25
+ - huggingface_hub
26
+ # File handling
27
+ - openpyxl
28
+ - lxml
29
+ - pillow
30
+ - pyyaml
31
+ - python-docx
32
+ - pdfminer.six
33
+ # pip-only packages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  - pip:
35
+ - sentence-transformers
36
+ - transformers
37
+ - accelerate
38
+ - datasets
 
 
 
 
 
 
 
 
pipelines/app_pipeline.py CHANGED
@@ -1,97 +1,138 @@
1
  import argparse
2
  import os
3
  import pandas as pd
4
- import faiss
5
- from sentence_transformers import SentenceTransformer
6
- from src.feature_engg.tfidf_vectorizing_data import load_vectorizer, load_vector_data
7
- from src.processing.text_cleaning import clean_text
8
  from src.matching.matching_engine import compute_similarity_matrix, top_n_tfidf_matches, top_n_bert_matches
9
  from src.utils.file_reader import extract_text_from_file
10
 
11
 
12
  def load_job_titles(job_csv_path: str):
13
  df = pd.read_csv(job_csv_path)
14
- if 'title' not in df.columns:
15
  raise ValueError("Job CSV must contain a 'title' column.")
16
  return df
17
 
18
 
 
19
  def run_tfidf_pipeline(args, raw_resume: str):
20
 
21
- # Step 2: Clean resume text
22
  cleaned_resume = clean_text(raw_resume)
23
 
24
- # Step 3: Load vectorizer and job matrix
25
- vectorizer = load_vectorizer(args.vectorizer_path)
26
- job_matrix = load_vector_data(args.job_matrix_path)
27
-
28
- # Step 4: Vectorize cleaned resume text
 
 
 
 
 
 
 
 
29
  resume_vector = vectorizer.transform([cleaned_resume])
30
 
31
- # Step 5: Compute similarity
32
  sim_matrix = compute_similarity_matrix(resume_vector, job_matrix)
33
 
34
- # Step 6: Load job titles
35
- job_df = load_job_titles(args.job_title_csv)
 
 
 
 
 
 
 
36
 
37
- # Step 7: Get top-N job matches
38
- matches = top_n_tfidf_matches(sim_matrix, top_n=args.top_k, job_df=job_df)
 
39
 
40
- print(f"\n🎯 Top {args.top_k} Job Matches for the Resume (TF-IDF):")
 
 
41
  for job_idx, score in matches[0]:
42
  print(f"πŸ”Ή {job_df.iloc[job_idx]['title']} (score: {score:0.4f})")
43
 
44
- # Optional debug
45
  if args.debug:
46
  print("\n================ DEBUG MODE ================")
47
  print("\nπŸ“„--- [DEBUG - TFIDF] Cleaned Resume Preview:\n", cleaned_resume[:1000], "---")
48
- print(f"\n--- [DEBUG - TFIDF] Raw Similarity Scores (top {args.top_k}) ---")
49
  for job_idx, score in matches[0]:
50
  print(f"[{job_idx}] {job_df.iloc[job_idx]['title']} β†’ {score:0.6f}")
51
  print("==============================================")
52
 
53
 
 
54
  def run_bert_pipeline(args, raw_resume: str):
55
- # Step 2: Load SentenceTransformer model
56
- model = SentenceTransformer(args.bert_model_path)
57
 
58
- # Step 3: Load FAISS job index
59
- job_index = faiss.read_index(args.bert_faiss_index)
 
 
 
 
 
 
 
 
 
 
60
 
61
- # Step 4: Encode resume into embedding
62
- resume_embedding = model.encode([raw_resume], normalize_embeddings=True)
 
 
 
63
 
64
- # Step 5: Search deeply in FAISS index in order to eliminate duplicate job titles
65
- # Search across all job embeddings in FAISS
66
  n_jobs = job_index.ntotal
67
  D, I = job_index.search(resume_embedding, n_jobs)
68
 
69
  # Step 6: Load job titles
70
- job_df = load_job_titles(args.job_title_csv)
71
 
72
- print(f"\nοΏ½οΏ½ Top {args.top_k} Job Matches for the Resume (BERT):")
73
- matches = top_n_bert_matches(I, D, job_df, top_n=args.top_k)
74
 
 
 
 
 
 
 
 
 
 
 
 
75
  for idx, score in matches:
76
  print(f"πŸ”Ή {job_df.iloc[idx]['title']} (score: {score:0.4f})")
77
 
78
- # Optional debug
79
  if args.debug:
80
  print("\n================ DEBUG MODE ================")
81
- print(f"\n--- [DEBUG - BERT/FAISS] Raw Similarity Scores (top {args.top_k}) ---")
82
  for idx, score in matches:
83
- print(f"πŸ”Ή {job_df.iloc[idx]['title']} (score: {score})")
84
  print("==============================================")
85
 
86
 
 
87
  def main(args):
88
  try:
89
- # Step 1: Load raw resume text
90
  if not os.path.exists(args.resume_path):
91
- raise FileNotFoundError(f"Resume file not found: {args.resume_path}")
 
92
  raw_resume = extract_text_from_file(args.resume_path)
 
93
 
94
- # Run chosen pipeline
 
95
  if args.model == "bert":
96
  run_bert_pipeline(args, raw_resume)
97
  else:
@@ -103,28 +144,36 @@ def main(args):
103
 
104
  if __name__ == "__main__":
105
  parser = argparse.ArgumentParser(description="Match a resume to top relevant job titles")
106
- parser.add_argument('--resume_path', type=str, required=True, help="Path to resume file")
107
- parser.add_argument('--model', type=str, choices=['tfidf', 'bert'], default='tfidf',
108
- help="Which model pipeline to use: 'tfidf' or 'bert'")
109
-
110
 
111
- # TF-IDF arguments
112
- parser.add_argument('--vectorizer_path', type=str, default='models/tfidf/app_tfidf/job_tfidf_vectorizer.pkl')
113
- parser.add_argument('--job_matrix_path', type=str, default='models/tfidf/app_tfidf/job_tfidf_matrix.npz')
114
-
115
- # BERT arguments
116
- parser.add_argument('--bert_model_path', type=str, default='models/bert/dapt_minilm_sentence_transformer',
117
- help="Path to fine-tuned SentenceTransformer model")
118
- parser.add_argument('--bert_faiss_index', type=str, default='models/bert/app_bert/jobs_bert_embeddings.faiss',
119
- help="Path to FAISS index of job embeddings")
120
-
121
- # Shared arguments
122
- parser.add_argument('--job_title_csv', type=str, default='data/app_data/job_titles.csv')
123
- parser.add_argument('--top_k', type=int, default=5,
124
- help="Number of top job matches to return")
125
- parser.add_argument('--debug', action='store_true',
126
- help="Print raw similarity scores and cleaned resume for tfidf pipeline")
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  args = parser.parse_args()
129
  main(args)
130
-
 
1
  import argparse
2
  import os
3
  import pandas as pd
4
+ from src.feature_engg.tfidf_vectorizing_data import load_tfidf_vectorizer, load_tfidf_matrix
5
+ from src.feature_engg.bert_embedding_data import get_bert_model, load_faiss_index
6
+ from src.processing.text_cleaning import clean_text, clean_text_for_bert
 
7
  from src.matching.matching_engine import compute_similarity_matrix, top_n_tfidf_matches, top_n_bert_matches
8
  from src.utils.file_reader import extract_text_from_file
9
 
10
 
11
  def load_job_titles(job_csv_path: str):
12
  df = pd.read_csv(job_csv_path)
13
+ if "title" not in df.columns:
14
  raise ValueError("Job CSV must contain a 'title' column.")
15
  return df
16
 
17
 
18
+ # ------------------------- TF-IDF PIPELINE -------------------------
19
  def run_tfidf_pipeline(args, raw_resume: str):
20
 
21
+ # Step 1: Clean resume
22
  cleaned_resume = clean_text(raw_resume)
23
 
24
+ # Step 2: Load vectorizer + job matrix (local first, fallback HF)
25
+ vectorizer = load_tfidf_vectorizer(
26
+ local_vectorizer_path=args.local_vectorizer_path,
27
+ repo_id=args.tfidf_repo_id,
28
+ filename=args.vectorizer_filename
29
+ )
30
+ job_matrix = load_tfidf_matrix(
31
+ local_matrix_path=args.local_matrix_path,
32
+ repo_id=args.tfidf_repo_id,
33
+ filename=args.matrix_filename
34
+ )
35
+
36
+ # Step 3: Vectorize resume
37
  resume_vector = vectorizer.transform([cleaned_resume])
38
 
39
+ # Step 4: Compute cosine similarity
40
  sim_matrix = compute_similarity_matrix(resume_vector, job_matrix)
41
 
42
+ # Step 5: Load job titles
43
+ job_df = load_job_titles("data/app_data/tfidf_job_titles.csv")
44
+
45
+ # Step 6: Get top-N job matches
46
+ top_k = args.top_k
47
+
48
+ if args.top_k > len(job_df['title'].unique()):
49
+ print(f"⚠️ Requested top_k={args.top_k} exceeds unique job titles={len(job_df['title'].unique())}. Reducing top_k.")
50
+ top_k = len(job_df['title'].unique())
51
 
52
+ elif args.top_k is None:
53
+ top_k = len(job_df['title'].unique())
54
+ print(f"\nℹ️ Showing all {top_k} job titles.\n")
55
 
56
+ matches = top_n_tfidf_matches(sim_matrix, top_n=top_k, job_df=job_df)
57
+
58
+ print(f"\n🎯 Top {top_k} Job Matches for the Resume (TF-IDF):")
59
  for job_idx, score in matches[0]:
60
  print(f"πŸ”Ή {job_df.iloc[job_idx]['title']} (score: {score:0.4f})")
61
 
 
62
  if args.debug:
63
  print("\n================ DEBUG MODE ================")
64
  print("\nπŸ“„--- [DEBUG - TFIDF] Cleaned Resume Preview:\n", cleaned_resume[:1000], "---")
65
+ print(f"\n--- [DEBUG - TFIDF] Raw Similarity Scores (top {top_k}) ---")
66
  for job_idx, score in matches[0]:
67
  print(f"[{job_idx}] {job_df.iloc[job_idx]['title']} β†’ {score:0.6f}")
68
  print("==============================================")
69
 
70
 
71
+ # ------------------------- BERT PIPELINE -------------------------
72
  def run_bert_pipeline(args, raw_resume: str):
 
 
73
 
74
+ # Step 1: Load fine-tuned ST model (local or HF Hub)
75
+ model = get_bert_model(args.local_bert_path or args.bert_repo_id)
76
+
77
+ # Step 2: Load FAISS index (local or HF Hub)
78
+ job_index = load_faiss_index(
79
+ local_index_path=args.local_index_path,
80
+ repo_id=args.bert_repo_id,
81
+ filename=args.index_filename
82
+ )
83
+
84
+ # Step 3: Clean resume text for transformer
85
+ cleaned_resume = clean_text_for_bert(raw_resume)
86
 
87
+ # Step 4: Embed
88
+ resume_embedding = model.encode(
89
+ [cleaned_resume],
90
+ normalize_embeddings=True
91
+ )
92
 
93
+ # Step 5: Search
 
94
  n_jobs = job_index.ntotal
95
  D, I = job_index.search(resume_embedding, n_jobs)
96
 
97
  # Step 6: Load job titles
98
+ job_df = load_job_titles("data/app_data/bert_job_titles.csv")
99
 
100
+ # Step 7: Rank top-N
101
+ top_k = args.top_k
102
 
103
+ if args.top_k > len(job_df['title'].unique()):
104
+ print(f"⚠️ Requested top_k={args.top_k} exceeds unique job titles={len(job_df['title'].unique())}. Reducing top_k.")
105
+ top_k = len(job_df['title'].unique())
106
+
107
+ elif args.top_k is None:
108
+ top_k = len(job_df['title'].unique())
109
+ print(f"\nℹ️ Showing all {top_k} job titles.\n")
110
+
111
+ matches = top_n_bert_matches(I, D, job_df, top_n=top_k)
112
+
113
+ print(f"\n🎯 Top {top_k} Job Matches for the Resume (BERT):")
114
  for idx, score in matches:
115
  print(f"πŸ”Ή {job_df.iloc[idx]['title']} (score: {score:0.4f})")
116
 
 
117
  if args.debug:
118
  print("\n================ DEBUG MODE ================")
119
+ print(f"\n--- [DEBUG - BERT/FAISS] Raw Similarity Scores (top {top_k}) ---")
120
  for idx, score in matches:
121
+ print(f"[{idx}] {job_df.iloc[idx]['title']} β†’ {score:0.6f}")
122
  print("==============================================")
123
 
124
 
125
+ # ------------------------- MAIN -------------------------
126
  def main(args):
127
  try:
 
128
  if not os.path.exists(args.resume_path):
129
+ raise FileNotFoundError(f"⚠️ Resume file not found at: {args.resume_path}")
130
+
131
  raw_resume = extract_text_from_file(args.resume_path)
132
+ print(f"\nπŸ“„ Resume: {args.resume_path}")
133
 
134
+ # Pipeline selector
135
+ print(f"βš™οΈ Using model: {args.model.upper()}")
136
  if args.model == "bert":
137
  run_bert_pipeline(args, raw_resume)
138
  else:
 
144
 
145
  if __name__ == "__main__":
146
  parser = argparse.ArgumentParser(description="Match a resume to top relevant job titles")
 
 
 
 
147
 
148
+ # Shared args
149
+ parser.add_argument("--resume_path", type=str, required=True, help="Path to resume file")
150
+ parser.add_argument("--model", type=str, choices=["tfidf", "bert"], default="tfidf")
151
+ parser.add_argument("--top_k", type=int, default=None,
152
+ help="Number of top matches to return if not specified, returns all")
153
+ parser.add_argument("--debug", action="store_true",
154
+ help="print raw similarity scores for both and cleaned resume for tfidf pipeline")
155
+
156
+ # TF-IDF args
157
+ parser.add_argument("--local_vectorizer_path", type=str, default=None,
158
+ help="Local TF-IDF vectorizer .pkl file")
159
+ parser.add_argument("--local_matrix_path", type=str, default=None,
160
+ help="Local TF-IDF job matrix .npz file")
161
+ parser.add_argument("--tfidf_repo_id", type=str, default="Om-Shandilya/resume-matcher-tfidf",
162
+ help="Hub repo id for HuggingFace model")
163
+ parser.add_argument("--vectorizer_filename", type=str, default="applicant/job_vectorizer.pkl",
164
+ help="Filename of vectorizer in the HF repo")
165
+ parser.add_argument("--matrix_filename", type=str, default="applicant/job_matrix.npz",
166
+ help="Filename of matrix in the HF repo")
167
+
168
+ # BERT args
169
+ parser.add_argument("--local_bert_path", type=str, default=None,
170
+ help="Local fine-tuned ST model path")
171
+ parser.add_argument("--local_index_path", type=str, default=None,
172
+ help="Local FAISS index file path")
173
+ parser.add_argument("--bert_repo_id", type=str, default="Om-Shandilya/resume-matcher-bert",
174
+ help="fine-tuned ST model's HF repo id")
175
+ parser.add_argument("--index_filename", type=str, default="applicant/jobs.faiss",
176
+ help="Filename of FAISS index in the HF repo")
177
 
178
  args = parser.parse_args()
179
  main(args)
 
pipelines/recruiter_pipeline.py CHANGED
@@ -1,139 +1,167 @@
1
  import argparse
2
  import os
3
- import joblib
4
  from sklearn.metrics.pairwise import cosine_similarity
5
- import faiss
6
- from sentence_transformers import SentenceTransformer
7
  from src.utils.bulk_loading import bulk_load_raw_resume_files
8
  from src.utils.file_reader import extract_text_from_file
9
- from src.processing.text_cleaning import clean_text
10
 
11
 
12
- def rank_with_tfidf(args, raw_job_text, raw_resume_texts):
13
- """TF-IDF recruiter pipeline"""
14
- # Step 1: Load vectorizer
15
- if not os.path.exists(args.vectorizer_path):
16
- raise FileNotFoundError(f"⚠️ Vectorizer file not found: {args.vectorizer_path}")
17
- vectorizer = joblib.load(args.vectorizer_path)
 
 
18
 
19
- # Step 2: Process job description
20
  cleaned_job_text = clean_text(raw_job_text)
21
  job_vector = vectorizer.transform([cleaned_job_text])
22
 
23
- # Step 3: Process resumes
24
  cleaned_resumes = {fname: clean_text(txt) for fname, txt in raw_resume_texts.items()}
25
  resume_matrix = vectorizer.transform(cleaned_resumes.values())
26
 
27
  # Step 4: Compute similarity
28
  sims = cosine_similarity(job_vector, resume_matrix)[0]
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  if args.debug:
31
  print("\n================ DEBUG MODE ================")
32
- print("\n[DEBUG - TFIDF] Cleaned job description:")
33
- print(cleaned_job_text[:500], "...\n")
34
- print("[DEBUG - TFIDF] First 3 cleaned resumes:")
35
  for i, (fname, txt) in enumerate(cleaned_resumes.items()):
36
  if i >= 3: break
37
  print(f"{fname}: {txt[:300]}...\n")
38
- print("[DEBUG - TFIDF] Raw similarity scores:", sims[:10])
 
 
39
  print("==============================================")
40
 
41
- # Step 5: Rank resumes
42
- ranked = sorted(zip(cleaned_resumes.keys(), sims), key=lambda x: x[1], reverse=True)
43
- return ranked
44
 
 
 
 
 
 
 
 
 
45
 
46
- def rank_with_bert(args, raw_job_text, raw_resume_texts):
47
- """BERT recruiter pipeline using FAISS (on the fly)"""
48
- if not os.path.exists(args.bert_model_path):
49
- raise FileNotFoundError(f"⚠️ BERT model not found: {args.bert_model_path}")
50
 
51
- # Step 1: Load BERT model
52
- model = SentenceTransformer(args.bert_model_path)
 
53
 
54
- # Step 2: Encode job description
55
- job_embedding = model.encode([raw_job_text], convert_to_numpy=True, normalize_embeddings=True)
56
 
57
- # Step 3: Encode resumes
58
- resume_embeddings = model.encode(list(raw_resume_texts.values()), convert_to_numpy=True, normalize_embeddings=True)
 
59
 
60
- # Step 4: Create FAISS indices
61
- local_index = faiss.IndexFlatIP(resume_embeddings.shape[1])
62
- local_index.add(resume_embeddings)
 
 
 
63
 
64
- scores, indices = local_index.search(job_embedding, len(raw_resume_texts))
 
 
65
 
66
  if args.debug:
67
  print("\n================ DEBUG MODE ================")
68
- print("\n[DEBUG - BERT/FAISS] Raw job description:")
69
- print(raw_job_text[:500], "...\n")
70
- print("[DEBUG - BERT/FAISS] First 3 raw resumes:")
71
- for i, (fname, txt) in enumerate(raw_resume_texts.items()):
72
  if i >= 3: break
73
  print(f"{fname}: {txt[:300]}...\n")
74
- print(f"[DEBUG - BERT/FAISS] all similarity scores:", scores[0][:len(raw_resume_texts)])
 
 
75
  print("==============================================")
76
 
77
- # Step 5: Rank resumes
78
- ranked = [(list(raw_resume_texts.keys())[i], float(scores[0][j]))
79
- for j, i in enumerate(indices[0])]
80
- return ranked
81
-
82
 
 
83
  def main(args):
84
  try:
85
- # Load raw job and resumes
 
 
86
  raw_job_text = extract_text_from_file(args.job_desc_path)
 
 
 
87
  raw_resume_texts = bulk_load_raw_resume_files(args.resume_dir)
88
 
89
  if not raw_resume_texts:
90
  raise ValueError("⚠️ No valid resumes found in the given directory.")
91
 
92
- # Limit the number of resumes displayed based on the top_k argument and available resumes
93
- available_resumes = len(raw_resume_texts)
94
- top_k = min(args.top_k, available_resumes)
95
-
96
- if args.top_k > available_resumes:
97
- print(f"\n⚠️ Only {available_resumes} resumes are available. "
98
- f"Showing top {available_resumes} matches instead of {args.top_k}.\n")
99
 
100
- # Choose model
101
- if args.model == "tfidf":
102
- ranked = rank_with_tfidf(args, raw_job_text, raw_resume_texts)
103
- elif args.model == "bert":
104
- ranked = rank_with_bert(args, raw_job_text, raw_resume_texts)
105
  else:
106
- raise ValueError("❌ Invalid model. Choose 'tfidf' or 'bert'.")
107
 
108
- # Display ranked resumes
109
- print(f"\n🎯 Top {top_k} Candidate Matches for the Job ({args.model.upper()}):")
110
- for i, (fname, score) in enumerate(ranked[:top_k], 1):
111
- print(f"{i}. {fname} β†’ score: {score:.4f}")
112
-
113
  except Exception as e:
114
  print(f"❌ Error: {str(e)}")
115
 
116
 
117
  if __name__ == "__main__":
118
- parser = argparse.ArgumentParser(description="Recruiter Pipeline: Rank user uploaded resumes for a given job description")
119
-
120
- # Shared arguments
121
- parser.add_argument('--job_desc_path', type=str, required=True, help="Path to job description file")
122
- parser.add_argument('--resume_dir', type=str, required=True, help="Directory containing applicant resumes")
123
- parser.add_argument('--model', type=str, choices=['tfidf', 'bert'], default='tfidf',
124
- help="Model to use: tfidf or bert")
125
- parser.add_argument('--top_k', type=int, default=10, help="Number of top resumes to return")
126
- parser.add_argument('--debug', action='store_true', help="Print cleaned/raw texts and raw similarity scores")
127
-
128
- # TF-IDF specific
129
- parser.add_argument('--vectorizer_path', type=str,
130
- default='models/tfidf/recruiter_tfidf/combined_tfidf_vectorizer.pkl',
131
- help="Path to pre-trained TF-IDF vectorizer")
132
-
133
- # BERT specific
134
- parser.add_argument('--bert_model_path', type=str,
135
- default='models/bert/dapt_minilm_sentence_transformer',
136
- help="Path to fine-tuned BERT/SBERT model")
 
 
 
 
 
137
 
138
  args = parser.parse_args()
139
  main(args)
 
1
  import argparse
2
  import os
3
+ import numpy as np
4
  from sklearn.metrics.pairwise import cosine_similarity
5
+ from src.feature_engg.tfidf_vectorizing_data import load_tfidf_vectorizer
6
+ from src.feature_engg.bert_embedding_data import get_bert_model
7
  from src.utils.bulk_loading import bulk_load_raw_resume_files
8
  from src.utils.file_reader import extract_text_from_file
9
+ from src.processing.text_cleaning import clean_text, clean_text_for_bert
10
 
11
 
12
+ # ------------------------- TF-IDF PIPELINE -------------------------
13
+ def run_tfidf_pipeline(args, raw_job_text, raw_resume_texts):
14
+ # Step 1: Load vectorizer (local or HF Hub)
15
+ vectorizer = load_tfidf_vectorizer(
16
+ local_vectorizer_path=args.local_vectorizer_path,
17
+ repo_id=args.tfidf_repo_id,
18
+ filename=args.vectorizer_filename
19
+ )
20
 
21
+ # Step 2: Clean job description
22
  cleaned_job_text = clean_text(raw_job_text)
23
  job_vector = vectorizer.transform([cleaned_job_text])
24
 
25
+ # Step 3: Clean and vectorize resumes
26
  cleaned_resumes = {fname: clean_text(txt) for fname, txt in raw_resume_texts.items()}
27
  resume_matrix = vectorizer.transform(cleaned_resumes.values())
28
 
29
  # Step 4: Compute similarity
30
  sims = cosine_similarity(job_vector, resume_matrix)[0]
31
 
32
+ # Step 5: Rank resumes
33
+ ranked = sorted(zip(cleaned_resumes.keys(), sims), key=lambda x: x[1], reverse=True)
34
+
35
+ # Step 6: Top-K handling
36
+ top_k = args.top_k
37
+ available_resumes = len(ranked)
38
+
39
+ if args.top_k is None:
40
+ top_k = available_resumes
41
+ print(f"\nℹ️ Showing all {available_resumes} resumes.\n")
42
+ elif args.top_k > available_resumes:
43
+ top_k = available_resumes
44
+ print(f"\n⚠️ Requested top_k={args.top_k} exceeds available resumes={available_resumes}. Reducing top_k.\n")
45
+
46
+ print(f"\n🎯 Top {top_k} Candidate Matches for the Job (TF-IDF):")
47
+ for i, (fname, score) in enumerate(ranked[:top_k], 1):
48
+ print(f"{i}. {fname} β†’ score: {score:.4f}")
49
+
50
  if args.debug:
51
  print("\n================ DEBUG MODE ================")
52
+ print("\nπŸ“„--- [DEBUG - TFIDF] Cleaned Job Description Preview:\n", cleaned_job_text[:1000], "---")
53
+ print("\n--- [DEBUG - TFIDF] First 3 Cleaned Resumes ---")
 
54
  for i, (fname, txt) in enumerate(cleaned_resumes.items()):
55
  if i >= 3: break
56
  print(f"{fname}: {txt[:300]}...\n")
57
+ print(f"\n--- [DEBUG - TFIDF] Raw Similarity Scores (top {top_k}) ---")
58
+ for fname, score in ranked[:top_k]:
59
+ print(f"{fname} β†’ {score:0.6f}")
60
  print("==============================================")
61
 
 
 
 
62
 
63
+ # ------------------------- BERT PIPELINE -------------------------
64
+ def run_bert_pipeline(args, raw_job_text, raw_resume_texts):
65
+ # Step 1: Load fine-tuned ST model (local or HF Hub)
66
+ model = get_bert_model(args.local_bert_path or args.bert_repo_id)
67
+
68
+ # Step 2: Clean job description
69
+ cleaned_job_text = clean_text_for_bert(raw_job_text)
70
+ job_embedding = model.encode([cleaned_job_text], normalize_embeddings=True)
71
 
72
+ # Step 3: Encode resumes
73
+ cleaned_resumes = {fname: clean_text_for_bert(txt) for fname, txt in raw_resume_texts.items()}
74
+ resume_embeddings = model.encode(list(cleaned_resumes.values()), normalize_embeddings=True)
 
75
 
76
+ # Step 4: Compute cosine similarity manually
77
+ # Using dot product as embeddings are normalized and not FAISS since we have small data here.
78
+ sims = np.dot(resume_embeddings, job_embedding.T).flatten()
79
 
80
+ # Step 5: Rank resumes
81
+ ranked = sorted(zip(cleaned_resumes.keys(), sims), key=lambda x: x[1], reverse=True)
82
 
83
+ # Step 6: Top-K handling
84
+ top_k = args.top_k
85
+ available_resumes = len(ranked)
86
 
87
+ if args.top_k is None:
88
+ top_k = available_resumes
89
+ print(f"\nℹ️ Showing all {available_resumes} resumes.\n")
90
+ elif args.top_k > available_resumes:
91
+ top_k = available_resumes
92
+ print(f"\n⚠️ Requested top_k={args.top_k} exceeds available resumes={available_resumes}. Reducing top_k.\n")
93
 
94
+ print(f"\n🎯 Top {top_k} Candidate Matches for the Job (BERT):")
95
+ for i, (fname, score) in enumerate(ranked[:top_k], 1):
96
+ print(f"{i}. {fname} β†’ score: {score:.4f}")
97
 
98
  if args.debug:
99
  print("\n================ DEBUG MODE ================")
100
+ print("\nπŸ“„--- [DEBUG - BERT] Cleaned Job Description Preview:\n", cleaned_job_text[:1000], "---")
101
+ print("\n--- [DEBUG - BERT] First 3 Cleaned Resumes ---")
102
+ for i, (fname, txt) in enumerate(cleaned_resumes.items()):
 
103
  if i >= 3: break
104
  print(f"{fname}: {txt[:300]}...\n")
105
+ print(f"\n--- [DEBUG - BERT] Raw Similarity Scores (top {top_k}) ---")
106
+ for fname, score in ranked[:top_k]:
107
+ print(f"{fname} β†’ {score:0.6f}")
108
  print("==============================================")
109
 
 
 
 
 
 
110
 
111
+ # ------------------------- MAIN -------------------------
112
  def main(args):
113
  try:
114
+ # Load job description and resumes
115
+ if not os.path.exists(args.job_desc_path):
116
+ raise FileNotFoundError(f"⚠️ Job description not found: {args.job_desc_path}")
117
  raw_job_text = extract_text_from_file(args.job_desc_path)
118
+
119
+ if not os.path.exists(args.resume_dir):
120
+ raise FileNotFoundError(f"⚠️ Resume directory not found: {args.resume_dir}")
121
  raw_resume_texts = bulk_load_raw_resume_files(args.resume_dir)
122
 
123
  if not raw_resume_texts:
124
  raise ValueError("⚠️ No valid resumes found in the given directory.")
125
 
126
+ print(f"\nπŸ“„ Job Description: {args.job_desc_path}")
127
+ print(f"πŸ“‚ Loaded {len(raw_resume_texts)} resumes from {args.resume_dir}")
 
 
 
 
 
128
 
129
+ # Pipeline selector
130
+ print(f"βš™οΈ Using model: {args.model.upper()}")
131
+ if args.model == "bert":
132
+ run_bert_pipeline(args, raw_job_text, raw_resume_texts)
 
133
  else:
134
+ run_tfidf_pipeline(args, raw_job_text, raw_resume_texts)
135
 
 
 
 
 
 
136
  except Exception as e:
137
  print(f"❌ Error: {str(e)}")
138
 
139
 
140
  if __name__ == "__main__":
141
+ parser = argparse.ArgumentParser(description="Recruiter Pipeline: Rank resumes for a given job description")
142
+
143
+ # Shared args
144
+ parser.add_argument("--job_desc_path", type=str, required=True, help="Path to job description file")
145
+ parser.add_argument("--resume_dir", type=str, required=True, help="Directory containing applicant resumes")
146
+ parser.add_argument("--model", type=str, choices=["tfidf", "bert"], default="tfidf")
147
+ parser.add_argument("--top_k", type=int, default=None,
148
+ help="Number of top matches to return if not specified, returns all")
149
+ parser.add_argument("--debug", action="store_true",
150
+ help="print raw similarity scores and cleaned texts for debugging")
151
+
152
+ # TF-IDF args
153
+ parser.add_argument("--local_vectorizer_path", type=str, default=None,
154
+ help="Local TF-IDF vectorizer .pkl file")
155
+ parser.add_argument("--tfidf_repo_id", type=str, default="Om-Shandilya/resume-matcher-tfidf",
156
+ help="Hub repo id for HuggingFace TF-IDF model")
157
+ parser.add_argument("--vectorizer_filename", type=str, default="recruiter/combined_vectorizer.pkl",
158
+ help="Filename of vectorizer in the HF repo")
159
+
160
+ # BERT args
161
+ parser.add_argument("--local_bert_path", type=str, default=None,
162
+ help="Local fine-tuned ST model path")
163
+ parser.add_argument("--bert_repo_id", type=str, default="Om-Shandilya/resume-matcher-bert",
164
+ help="fine-tuned ST model's HF repo id")
165
 
166
  args = parser.parse_args()
167
  main(args)
src/feature_engg/bert_embedding_data.py CHANGED
@@ -2,9 +2,13 @@ import os
2
  import numpy as np
3
  import pandas as pd
4
  import torch
 
 
 
5
  from typing import Optional
6
  from sentence_transformers import SentenceTransformer, models
7
- import faiss
 
8
 
9
 
10
  def get_bert_model(model_name: str = "all-MiniLM-L6-v2",
@@ -53,7 +57,7 @@ def save_bert_model(vectorizer: SentenceTransformer,
53
 
54
  def bert_embed_text(df: pd.DataFrame,
55
  text_column: str,
56
- label: str,
57
  model: Optional[SentenceTransformer] = None,
58
  save_path: Optional[str] = None,
59
  save_model_file: bool = False):
@@ -89,26 +93,41 @@ def bert_embed_text(df: pd.DataFrame,
89
  )
90
 
91
  if save_path and label:
92
- save_bert_embeddings(embeddings, os.path.join(save_path, f"{label}_bert_embeddings.faiss"))
93
  if save_model_file:
94
- save_bert_model(model, os.path.join(save_path, f"{label}_bert_model"))
95
 
96
  return embeddings, model
97
 
98
 
99
- def load_bert_embeddings(path: str):
100
- """
101
- Load a FAISS index file (.faiss) from disk.
102
- """
103
- if not path.endswith('.faiss'):
104
- path += '.faiss'
105
- return faiss.read_index(path)
106
-
107
-
108
- def load_bert_model(path: str):
109
- """Load a saved SentenceTransformer model."""
110
-
111
- return SentenceTransformer(path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
 
114
  def convert_hf_model_to_st(hf_model_path: str,
 
2
  import numpy as np
3
  import pandas as pd
4
  import torch
5
+ import faiss
6
+ import torch
7
+ from faiss import read_index
8
  from typing import Optional
9
  from sentence_transformers import SentenceTransformer, models
10
+ from transformers import AutoTokenizer, AutoModel
11
+ from huggingface_hub import hf_hub_download
12
 
13
 
14
  def get_bert_model(model_name: str = "all-MiniLM-L6-v2",
 
57
 
58
  def bert_embed_text(df: pd.DataFrame,
59
  text_column: str,
60
+ label: str = 'jobs', # default is 'jobs' as most common use-case in pipeline.
61
  model: Optional[SentenceTransformer] = None,
62
  save_path: Optional[str] = None,
63
  save_model_file: bool = False):
 
93
  )
94
 
95
  if save_path and label:
96
+ save_bert_embeddings(embeddings, os.path.join(save_path, f"{label}.faiss"))
97
  if save_model_file:
98
+ save_bert_model(model, save_path)
99
 
100
  return embeddings, model
101
 
102
 
103
+ def load_faiss_index(local_index_path: str, repo_id: str, filename: str):
104
+ """Load FAISS index, preferring local then HF Hub."""
105
+ if local_index_path and os.path.exists(local_index_path):
106
+ print(f"πŸ“‚ Loading local FAISS index from {local_index_path}")
107
+ return read_index(local_index_path)
108
+ else:
109
+ print(f"🌐 Downloading FAISS index from Hugging Face Hub ({repo_id})")
110
+ faiss_path = hf_hub_download(repo_id=repo_id, filename=filename)
111
+ return read_index(faiss_path)
112
+
113
+ def load_bert_model(local_model_path: str, repo_id: str):
114
+ """Load BERT model, preferring local then HF Hub."""
115
+ if local_model_path and os.path.exists(local_model_path):
116
+ print(f"πŸ“‚ Loading local BERT model from {local_model_path}")
117
+ tokenizer = AutoTokenizer.from_pretrained(local_model_path)
118
+ model = AutoModel.from_pretrained(local_model_path)
119
+ else:
120
+ print(f"🌐 Downloading BERT model from Hugging Face Hub ({repo_id})")
121
+ tokenizer = AutoTokenizer.from_pretrained(repo_id)
122
+ model = AutoModel.from_pretrained(repo_id)
123
+ return tokenizer, model
124
+
125
+ def mean_pooling(model_output, attention_mask):
126
+ """Mean pooling for sentence embeddings."""
127
+ token_embeddings = model_output[0] # First element is [batch, seq_len, hidden_dim]
128
+ input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
129
+ return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(
130
+ input_mask_expanded.sum(1), min=1e-9)
131
 
132
 
133
  def convert_hf_model_to_st(hf_model_path: str,
src/feature_engg/tfidf_vectorizing_data.py CHANGED
@@ -4,6 +4,7 @@ import joblib
4
  import os
5
  from scipy.sparse import csr_matrix, save_npz, load_npz
6
  from typing import Optional, Tuple
 
7
 
8
 
9
  def get_tfidf_vectorizer(max_features: int = 5000,
@@ -108,19 +109,23 @@ def tfidf_vectorize_text(df: pd.DataFrame,
108
  return X, vectorizer
109
 
110
 
111
- def load_vectorizer(path: str):
112
- """
113
- Loads a vectorizer from a .pkl file.
114
- """
115
- if not path.endswith('.pkl'):
116
- path += '.pkl'
117
- return joblib.load(path)
 
 
118
 
119
 
120
- def load_vector_data(path: str):
121
- """
122
- Loads a sparse matrix from a .npz file.
123
- """
124
- if not path.endswith('.npz'):
125
- path += '.npz'
126
- return load_npz(path)
 
 
 
4
  import os
5
  from scipy.sparse import csr_matrix, save_npz, load_npz
6
  from typing import Optional, Tuple
7
+ from huggingface_hub import hf_hub_download
8
 
9
 
10
  def get_tfidf_vectorizer(max_features: int = 5000,
 
109
  return X, vectorizer
110
 
111
 
112
+ def load_tfidf_vectorizer(local_vectorizer_path: str, repo_id: str, filename: str):
113
+ """Load TF-IDF vectorizer, preferring local then HF Hub."""
114
+ if local_vectorizer_path and os.path.exists(local_vectorizer_path):
115
+ print(f"πŸ“‚ Loading local TF-IDF vectorizer from {local_vectorizer_path}")
116
+ return joblib.load(local_vectorizer_path)
117
+ else:
118
+ print(f"🌐 Downloading TF-IDF vectorizer from Hugging Face Hub ({repo_id})")
119
+ vec_path = hf_hub_download(repo_id=repo_id, filename=filename)
120
+ return joblib.load(vec_path)
121
 
122
 
123
+ def load_tfidf_matrix(local_matrix_path: str, repo_id: str, filename: str):
124
+ """Load TF-IDF matrix, preferring local then HF Hub."""
125
+ if local_matrix_path and os.path.exists(local_matrix_path):
126
+ print(f"πŸ“‚ Loading local TF-IDF matrix from {local_matrix_path}")
127
+ return load_npz(local_matrix_path)
128
+ else:
129
+ print(f"🌐 Downloading TF-IDF matrix from Hugging Face Hub ({repo_id})")
130
+ mat_path = hf_hub_download(repo_id=repo_id, filename=filename)
131
+ return load_npz(mat_path)
src/matching/matching_engine.py CHANGED
@@ -1,6 +1,6 @@
1
  import numpy as np
2
  from sklearn.metrics.pairwise import cosine_similarity
3
- from src.feature_engg.tfidf_vectorizing_data import load_vector_data
4
 
5
  def compute_similarity_matrix(X_resumes, X_jobs ):
6
  """
@@ -70,8 +70,8 @@ if __name__ == "__main__":
70
  job_vec_path = "models/dev_tfidf/jobs_tfidf_matrix.npz"
71
 
72
  # Load sparse TF-IDF matrices
73
- X_resumes = load_vector_data(resume_vec_path)
74
- X_jobs = load_vector_data(job_vec_path)
75
 
76
  print(f"βœ… Loaded resumes vector shape: {X_resumes.shape}")
77
  print(f"βœ… Loaded job descriptions vector shape: {X_jobs.shape}")
 
1
  import numpy as np
2
  from sklearn.metrics.pairwise import cosine_similarity
3
+ from src.feature_engg.tfidf_vectorizing_data import load_tfidf_matrix
4
 
5
  def compute_similarity_matrix(X_resumes, X_jobs ):
6
  """
 
70
  job_vec_path = "models/dev_tfidf/jobs_tfidf_matrix.npz"
71
 
72
  # Load sparse TF-IDF matrices
73
+ X_resumes = load_tfidf_matrix(resume_vec_path)
74
+ X_jobs = load_tfidf_matrix(job_vec_path)
75
 
76
  print(f"βœ… Loaded resumes vector shape: {X_resumes.shape}")
77
  print(f"βœ… Loaded job descriptions vector shape: {X_jobs.shape}")
src/processing/text_cleaning.py CHANGED
@@ -157,4 +157,24 @@ def clean_column(
157
  )
158
  )
159
  print(f"βœ… Cleaned column '{column_name}' and saved cleaned data to '{new_column_name}' column.")
160
- return df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  )
158
  )
159
  print(f"βœ… Cleaned column '{column_name}' and saved cleaned data to '{new_column_name}' column.")
160
+ return df
161
+
162
+ def clean_text_for_bert(text: str) -> str:
163
+ """
164
+ Minimal cleaning for BERT input.
165
+ Keeps stopwords, punctuation, and numbers because they carry meaning.
166
+ Only removes obvious noise.
167
+ """
168
+ if pd.isnull(text):
169
+ return ""
170
+
171
+ # Normalize unicode to standard form
172
+ text = unicodedata.normalize("NFKC", text)
173
+
174
+ # Remove HTML tags
175
+ text = re.sub(r"<.*?>", " ", text)
176
+
177
+ # Replace multiple spaces/newlines with a single space
178
+ text = re.sub(r"\s+", " ", text).strip()
179
+
180
+ return text