yogl commited on
Commit
68d18df
·
verified ·
1 Parent(s): 9907263

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -64
app.py CHANGED
@@ -22,7 +22,6 @@ HF_MERGED_REPO = os.getenv("HF_MERGED_REPO")
22
  HF_EMB_REPO = os.getenv("HF_EMB_REPO")
23
  MODEL_NAME = os.getenv("MODEL_NAME")
24
 
25
- # Отображаемые заголовки (русские имена колонок)
26
  COLUMN_LABELS_RU = {
27
  "№": "№",
28
  "score": "Сходство",
@@ -35,7 +34,6 @@ COLUMN_LABELS_RU = {
35
  "vak_link": "Ссылка ВАК", # только в Excel
36
  }
37
 
38
- # Порядок колонок в выдаче (для интерфейса)
39
  DISPLAY_COLUMNS = [
40
  "№",
41
  "score",
@@ -54,7 +52,6 @@ if HF_TOKEN is None:
54
  )
55
  st.stop()
56
 
57
- # Логин в Hugging Face Hub (для приватных датасетов)
58
  try:
59
  login(token=HF_TOKEN)
60
  except Exception:
@@ -82,10 +79,10 @@ def load_data():
82
  norms = np.linalg.norm(emb_matrix, axis=1, keepdims=True)
83
  emb_matrix = emb_matrix / np.maximum(norms, 1e-8)
84
 
85
- # --- ВЫРАВНИВАЕМ МЕТА-ИНФОРМАЦИЮ ПОД ПОРЯДОК reg_nums ---
86
  meta_aligned = df_meta.reindex(reg_nums)
87
 
88
- # dissertation_type -> маски кандидат/доктор (устойчиво к формулировкам)
89
  if "dissertation_type" in meta_aligned.columns:
90
  type_s = meta_aligned["dissertation_type"].fillna("").astype(str)
91
  else:
@@ -94,7 +91,7 @@ def load_data():
94
  is_candidate = type_s.str.contains("кандид", case=False, na=False).to_numpy()
95
  is_doctor = type_s.str.contains("доктор", case=False, na=False).to_numpy()
96
 
97
- # degree_pursued -> массив строк (для np.isin)
98
  if "degree_pursued" in meta_aligned.columns:
99
  degree_arr = meta_aligned["degree_pursued"].fillna("").astype(str).to_numpy()
100
  else:
@@ -105,8 +102,7 @@ def load_data():
105
 
106
  @st.cache_resource(show_spinner="Загрузка модели...")
107
  def load_model():
108
- model = SentenceTransformer(MODEL_NAME)
109
- return model
110
 
111
 
112
  try:
@@ -120,20 +116,21 @@ except Exception as e:
120
  # ЛОГИКА ФИЛЬТРОВ И ПОИСКА
121
  # ==========================
122
 
123
- def build_filter_mask(type_selected, degree_selected):
 
 
 
 
 
124
  mask = np.ones(len(reg_nums), dtype=bool)
125
 
126
  # Тип диссертации
127
- type_selected = set(type_selected or [])
128
- if type_selected:
129
- type_mask = np.zeros(len(reg_nums), dtype=bool)
130
- if "Кандидатские" in type_selected:
131
- type_mask |= is_candidate
132
- if "Докторские" in type_selected:
133
- type_mask |= is_doctor
134
- mask &= type_mask
135
-
136
- # Тип наук / степень (degree_pursued)
137
  if degree_selected:
138
  mask &= np.isin(degree_arr, degree_selected)
139
 
@@ -146,11 +143,7 @@ def search_core(query: str, top_k: int = 10, mask=None):
146
  return []
147
 
148
  query_text = "query: " + query
149
-
150
- q_emb = model.encode(
151
- query_text,
152
- normalize_embeddings=True,
153
- )
154
 
155
  if mask is None:
156
  idx_pool = np.arange(len(reg_nums))
@@ -169,17 +162,12 @@ def search_core(query: str, top_k: int = 10, mask=None):
169
  results = []
170
  for rank, (idx, sc) in enumerate(zip(top_idx, top_scores), start=1):
171
  results.append(
172
- {
173
- "rank": rank,
174
- "registration_number": reg_nums[idx],
175
- "score": float(sc),
176
- }
177
  )
178
  return results
179
 
180
 
181
  def extract_year(value):
182
- """Аккуратно вытаскиваем год защиты из поля protection_date."""
183
  if value is None:
184
  return None
185
  try:
@@ -206,14 +194,12 @@ def build_result_df(results):
206
 
207
  if reg in df_all.index:
208
  meta = df_all.loc[reg]
209
- # если вдруг дубликаты по индексу (редко), берём первую строку
210
- if isinstance(meta, pd.DataFrame):
211
  meta = meta.iloc[0]
212
  else:
213
  meta = pd.Series({}, index=df_all.columns)
214
 
215
- protection_date = meta.get("protection_date", None)
216
- protection_year = extract_year(protection_date)
217
 
218
  row = {
219
  "№": r["rank"],
@@ -226,38 +212,33 @@ def build_result_df(results):
226
  "registration_number": meta.get("registration_number", reg),
227
  "vak_link": meta.get("vak_link", ""),
228
  }
229
-
230
  rows.append(row)
231
 
232
  if not rows:
233
  return pd.DataFrame(columns=DISPLAY_COLUMNS + ["vak_link"])
234
 
235
  df_res = pd.DataFrame(rows)
236
- df_res = df_res[DISPLAY_COLUMNS + ["vak_link"]]
237
- return df_res
238
 
239
 
240
- def run_search(query: str, top_k: int, type_selected, degree_selected):
241
  query = query.strip()
242
  if not query:
243
  empty_df = pd.DataFrame(columns=DISPLAY_COLUMNS + ["vak_link"])
244
- empty_df_ru = empty_df.rename(columns=COLUMN_LABELS_RU)
245
- return empty_df_ru, None
246
 
247
- mask = build_filter_mask(type_selected, degree_selected)
248
 
249
  results = search_core(query, top_k, mask=mask)
250
  df_res = build_result_df(results)
251
 
252
  if df_res.empty:
253
- empty_df_ru = df_res.rename(columns=COLUMN_LABELS_RU)
254
- return empty_df_ru, None
255
 
256
  # --- Разделяем датафреймы для отображения и для Excel ---
257
  df_excel = df_res.copy() # vak_link оставляем
258
  df_display = df_res.copy()
259
 
260
- # HTML-ссылка в колонке "Тип"
261
  def make_type_cell(row):
262
  t = row.get("dissertation_type", "")
263
  link = row.get("vak_link") or ""
@@ -271,7 +252,6 @@ def run_search(query: str, top_k: int, type_selected, degree_selected):
271
  df_display_ru = df_display.rename(columns=COLUMN_LABELS_RU)
272
  df_excel_ru = df_excel.rename(columns=COLUMN_LABELS_RU)
273
 
274
- # Excel (с отдельной колонкой "Ссылка ВАК")
275
  output = io.BytesIO()
276
  with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
277
  df_excel_ru.to_excel(writer, index=False)
@@ -283,13 +263,48 @@ def run_search(query: str, top_k: int, type_selected, degree_selected):
283
  # UI НА STREAMLIT
284
  # ==========================
285
 
286
- # Заголовок по центру
287
  st.markdown(
288
  "<h1 style='text-align: center; margin-bottom: 0.5rem;'>Поиск постдока🎓</h1>",
289
  unsafe_allow_html=True,
290
  )
291
 
292
- # Форма: Ctrl+Enter = отправка
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  with st.form("search_form"):
294
  top_k = st.slider(
295
  "Сколько результатов показать",
@@ -306,14 +321,8 @@ with st.form("search_form"):
306
  key="query",
307
  )
308
 
309
- # Скрываемые точные настройки
310
  with st.expander("Точные настройки", expanded=False):
311
- type_selected = st.multiselect(
312
- "Тип диссертаций для поиска",
313
- options=["Кандидатские", "Докторские"],
314
- default=["Кандидатские", "Докторские"],
315
- )
316
-
317
  if "degree_pursued" in df_all.columns:
318
  degree_options = (
319
  df_all["degree_pursued"]
@@ -328,13 +337,20 @@ with st.form("search_form"):
328
  else:
329
  degree_options = []
330
 
 
 
331
  degree_selected = st.multiselect(
332
- "Тип наук (degree_pursued)",
333
  options=degree_options,
334
- default=[],
 
 
 
 
 
 
335
  )
336
 
337
- # Кнопка по центру через три колонки
338
  c1, c2, c3 = st.columns([1, 1, 1])
339
  with c1:
340
  st.write("")
@@ -343,10 +359,9 @@ with st.form("search_form"):
343
  with c3:
344
  st.write("")
345
 
346
- # Результаты
347
  if do_search:
348
  with st.spinner("Идёт поиск по базе диссертаций..."):
349
- df_res_ru, excel_bytes = run_search(query, top_k, type_selected, degree_selected)
350
 
351
  if df_res_ru.empty:
352
  st.warning("Ничего не найдено. Проверьте запрос и/или ослабьте фильтры в «Точных настройках».")
@@ -382,10 +397,7 @@ if do_search:
382
  label="💾 Скачать результаты в Excel",
383
  data=excel_bytes,
384
  file_name="search_results.xlsx",
385
- mime=(
386
- "application/vnd.openxmlformats-officedocument."
387
- "spreadsheetml.sheet"
388
- ),
389
  )
390
  else:
391
  st.info(
@@ -393,7 +405,6 @@ else:
393
  "или нажмите Ctrl+Enter в поле ввода."
394
  )
395
 
396
- # Футер
397
  st.markdown(
398
  "<p style='font-size: 0.8rem; text-align: right; color: gray;'>"
399
  "(с) Антон Лощилов, 2025"
 
22
  HF_EMB_REPO = os.getenv("HF_EMB_REPO")
23
  MODEL_NAME = os.getenv("MODEL_NAME")
24
 
 
25
  COLUMN_LABELS_RU = {
26
  "№": "№",
27
  "score": "Сходство",
 
34
  "vak_link": "Ссылка ВАК", # только в Excel
35
  }
36
 
 
37
  DISPLAY_COLUMNS = [
38
  "№",
39
  "score",
 
52
  )
53
  st.stop()
54
 
 
55
  try:
56
  login(token=HF_TOKEN)
57
  except Exception:
 
79
  norms = np.linalg.norm(emb_matrix, axis=1, keepdims=True)
80
  emb_matrix = emb_matrix / np.maximum(norms, 1e-8)
81
 
82
+ # --- Выравниваем мета-инфо под порядок reg_nums ---
83
  meta_aligned = df_meta.reindex(reg_nums)
84
 
85
+ # dissertation_type -> маски кандидат/доктор
86
  if "dissertation_type" in meta_aligned.columns:
87
  type_s = meta_aligned["dissertation_type"].fillna("").astype(str)
88
  else:
 
91
  is_candidate = type_s.str.contains("кандид", case=False, na=False).to_numpy()
92
  is_doctor = type_s.str.contains("доктор", case=False, na=False).to_numpy()
93
 
94
+ # degree_pursued -> массив строк
95
  if "degree_pursued" in meta_aligned.columns:
96
  degree_arr = meta_aligned["degree_pursued"].fillna("").astype(str).to_numpy()
97
  else:
 
102
 
103
  @st.cache_resource(show_spinner="Загрузка модели...")
104
  def load_model():
105
+ return SentenceTransformer(MODEL_NAME)
 
106
 
107
 
108
  try:
 
116
  # ЛОГИКА ФИЛЬТРОВ И ПОИСКА
117
  # ==========================
118
 
119
+ def build_filter_mask(degree_selected, include_doctors: bool):
120
+ """
121
+ По умолчанию: только кандидатские.
122
+ Если include_doctors=True: кандидатские + докторские.
123
+ Плюс фильтр по degree_pursued (Науки), если выбран.
124
+ """
125
  mask = np.ones(len(reg_nums), dtype=bool)
126
 
127
  # Тип диссертации
128
+ type_mask = is_candidate.copy()
129
+ if include_doctors:
130
+ type_mask |= is_doctor
131
+ mask &= type_mask
132
+
133
+ # Науки (degree_pursued)
 
 
 
 
134
  if degree_selected:
135
  mask &= np.isin(degree_arr, degree_selected)
136
 
 
143
  return []
144
 
145
  query_text = "query: " + query
146
+ q_emb = model.encode(query_text, normalize_embeddings=True)
 
 
 
 
147
 
148
  if mask is None:
149
  idx_pool = np.arange(len(reg_nums))
 
162
  results = []
163
  for rank, (idx, sc) in enumerate(zip(top_idx, top_scores), start=1):
164
  results.append(
165
+ {"rank": rank, "registration_number": reg_nums[idx], "score": float(sc)}
 
 
 
 
166
  )
167
  return results
168
 
169
 
170
  def extract_year(value):
 
171
  if value is None:
172
  return None
173
  try:
 
194
 
195
  if reg in df_all.index:
196
  meta = df_all.loc[reg]
197
+ if isinstance(meta, pd.DataFrame): # на случай дублей
 
198
  meta = meta.iloc[0]
199
  else:
200
  meta = pd.Series({}, index=df_all.columns)
201
 
202
+ protection_year = extract_year(meta.get("protection_date", None))
 
203
 
204
  row = {
205
  "№": r["rank"],
 
212
  "registration_number": meta.get("registration_number", reg),
213
  "vak_link": meta.get("vak_link", ""),
214
  }
 
215
  rows.append(row)
216
 
217
  if not rows:
218
  return pd.DataFrame(columns=DISPLAY_COLUMNS + ["vak_link"])
219
 
220
  df_res = pd.DataFrame(rows)
221
+ return df_res[DISPLAY_COLUMNS + ["vak_link"]]
 
222
 
223
 
224
+ def run_search(query: str, top_k: int, degree_selected, include_doctors: bool):
225
  query = query.strip()
226
  if not query:
227
  empty_df = pd.DataFrame(columns=DISPLAY_COLUMNS + ["vak_link"])
228
+ return empty_df.rename(columns=COLUMN_LABELS_RU), None
 
229
 
230
+ mask = build_filter_mask(degree_selected, include_doctors)
231
 
232
  results = search_core(query, top_k, mask=mask)
233
  df_res = build_result_df(results)
234
 
235
  if df_res.empty:
236
+ return df_res.rename(columns=COLUMN_LABELS_RU), None
 
237
 
238
  # --- Разделяем датафреймы для отображения и для Excel ---
239
  df_excel = df_res.copy() # vak_link оставляем
240
  df_display = df_res.copy()
241
 
 
242
  def make_type_cell(row):
243
  t = row.get("dissertation_type", "")
244
  link = row.get("vak_link") or ""
 
252
  df_display_ru = df_display.rename(columns=COLUMN_LABELS_RU)
253
  df_excel_ru = df_excel.rename(columns=COLUMN_LABELS_RU)
254
 
 
255
  output = io.BytesIO()
256
  with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
257
  df_excel_ru.to_excel(writer, index=False)
 
263
  # UI НА STREAMLIT
264
  # ==========================
265
 
 
266
  st.markdown(
267
  "<h1 style='text-align: center; margin-bottom: 0.5rem;'>Поиск постдока🎓</h1>",
268
  unsafe_allow_html=True,
269
  )
270
 
271
+ def _pick_default_sciences(options):
272
+ """
273
+ Требуемые по умолчанию: технические, физмат, химические, биологические.
274
+ Подбираем по подстрокам (на случай отличий в формулировках).
275
+ """
276
+ want = [
277
+ ("технич",),
278
+ ("физ", "мат"), # физ-мат/физмат/физико-математические
279
+ ("хим",),
280
+ ("биолог",),
281
+ ]
282
+ opts_l = [o.lower() for o in options]
283
+
284
+ picked = []
285
+ for keys in want:
286
+ found = None
287
+ for o, ol in zip(options, opts_l):
288
+ if all(k in ol for k in keys):
289
+ found = o
290
+ break
291
+ if found:
292
+ picked.append(found)
293
+
294
+ # если по каким-то причинам не нашли ничего — оставим первые 4 (чтобы фильтр не был пустым)
295
+ if not picked and options:
296
+ picked = options[: min(4, len(options))]
297
+
298
+ # убираем дубли, сохраняя порядок
299
+ seen = set()
300
+ uniq = []
301
+ for x in picked:
302
+ if x not in seen:
303
+ seen.add(x)
304
+ uniq.append(x)
305
+ return uniq
306
+
307
+
308
  with st.form("search_form"):
309
  top_k = st.slider(
310
  "Сколько результатов показать",
 
321
  key="query",
322
  )
323
 
 
324
  with st.expander("Точные настройки", expanded=False):
325
+ # Науки (degree_pursued)
 
 
 
 
 
326
  if "degree_pursued" in df_all.columns:
327
  degree_options = (
328
  df_all["degree_pursued"]
 
337
  else:
338
  degree_options = []
339
 
340
+ default_sciences = _pick_default_sciences(degree_options)
341
+
342
  degree_selected = st.multiselect(
343
+ "Науки",
344
  options=degree_options,
345
+ default=default_sciences,
346
+ )
347
+
348
+ # По умолчанию: только кандидатские (чекбокс выключен)
349
+ include_doctors = st.checkbox(
350
+ "Включать докторские диссертации",
351
+ value=False,
352
  )
353
 
 
354
  c1, c2, c3 = st.columns([1, 1, 1])
355
  with c1:
356
  st.write("")
 
359
  with c3:
360
  st.write("")
361
 
 
362
  if do_search:
363
  with st.spinner("Идёт поиск по базе диссертаций..."):
364
+ df_res_ru, excel_bytes = run_search(query, top_k, degree_selected, include_doctors)
365
 
366
  if df_res_ru.empty:
367
  st.warning("Ничего не найдено. Проверьте запрос и/или ослабьте фильтры в «Точных настройках».")
 
397
  label="💾 Скачать результаты в Excel",
398
  data=excel_bytes,
399
  file_name="search_results.xlsx",
400
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
 
 
 
401
  )
402
  else:
403
  st.info(
 
405
  "или нажмите Ctrl+Enter в поле ввода."
406
  )
407
 
 
408
  st.markdown(
409
  "<p style='font-size: 0.8rem; text-align: right; color: gray;'>"
410
  "(с) Антон Лощилов, 2025"