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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +165 -119
app.py CHANGED
@@ -57,6 +57,66 @@ try:
57
  except Exception:
58
  pass
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  # ==========================
61
  # ЗАГРУЗКА ДАННЫХ И МОДЕЛИ
62
  # ==========================
@@ -79,7 +139,7 @@ def load_data():
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 -> маски кандидат/доктор
@@ -91,13 +151,15 @@ def load_data():
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:
98
  degree_arr = np.array([""] * len(reg_nums), dtype=object)
99
 
100
- return df_meta, reg_nums, emb_matrix, is_candidate, is_doctor, degree_arr
 
 
101
 
102
 
103
  @st.cache_resource(show_spinner="Загрузка модели...")
@@ -106,7 +168,7 @@ def load_model():
106
 
107
 
108
  try:
109
- df_all, reg_nums, emb_matrix, is_candidate, is_doctor, degree_arr = load_data()
110
  model = load_model()
111
  except Exception as e:
112
  st.error(f"Ошибка при загрузке данных или модели: {e}")
@@ -116,23 +178,40 @@ except Exception as e:
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
 
137
  return mask
138
 
@@ -221,14 +300,19 @@ def build_result_df(results):
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
 
@@ -268,43 +352,6 @@ st.markdown(
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
  "Сколько результатов показать",
@@ -322,34 +369,23 @@ with st.form("search_form"):
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"]
329
- .dropna()
330
- .astype(str)
331
- .map(lambda x: x.strip())
332
- .loc[lambda s: s != ""]
333
- .unique()
334
- .tolist()
335
- )
336
- degree_options = sorted(degree_options)
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:
@@ -360,45 +396,55 @@ with st.form("search_form"):
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("Ничего не найдено. Проверьте запрос и/или ослабьте фильтры в «Точных настройках».")
368
  else:
369
- st.success(f"Найдено записей: {len(df_res_ru)}")
370
-
371
- table_html = df_res_ru.to_html(
372
- escape=False,
373
- index=False,
374
- classes="result-table",
375
- )
 
376
 
377
- st.markdown(
378
- """
379
- <style>
380
- table.result-table {
381
- width: 100%;
382
- border-collapse: collapse;
383
- }
384
- table.result-table th {
385
- text-align: center !important;
386
- vertical-align: middle;
387
- }
388
- </style>
389
- """,
390
- unsafe_allow_html=True,
391
- )
392
 
393
- st.markdown(table_html, unsafe_allow_html=True)
 
 
 
 
394
 
395
- if excel_bytes is not None:
396
- st.download_button(
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(
404
  "Введите запрос выше и нажмите кнопку «Поиск» "
 
57
  except Exception:
58
  pass
59
 
60
+ # ==========================
61
+ # СПРАВОЧНИКИ ФИЛЬТРОВ (UI)
62
+ # ==========================
63
+
64
+ SCIENCE_LABELS = [
65
+ "Архитектура",
66
+ "Биологические",
67
+ "Ветеринарные",
68
+ "Географические",
69
+ "Геолого-минералогические",
70
+ "Искусствоведение",
71
+ "Исторические",
72
+ "Культурология",
73
+ "Медицинские",
74
+ "Педагогические",
75
+ "Политические",
76
+ "Сельскохозяйственные",
77
+ "Технические",
78
+ "Фармацевтические",
79
+ "Физико-математические",
80
+ "Филологические",
81
+ "Философские",
82
+ "Химические",
83
+ "Экономические",
84
+ "Юридические науки",
85
+ ]
86
+
87
+ SCIENCE_LABELS = sorted(list(dict.fromkeys(SCIENCE_LABELS)), key=lambda s: s.casefold())
88
+
89
+ DEFAULT_SCIENCES = {
90
+ "Технические",
91
+ "Физико-математические",
92
+ "Химические",
93
+ "Биологические",
94
+ }
95
+
96
+ # Паттерны для сопоставления с degree_pursued (на случай разной формулировки в базе)
97
+ SCIENCE_PATTERNS = {
98
+ "Архитектура": ["архитектур"],
99
+ "Биологические": ["биолог"],
100
+ "Ветеринарные": ["ветеринар"],
101
+ "Географические": ["географ"],
102
+ "Геолого-минералогические": ["геолого-минералог", "геол.-минералог", "геолого минералог"],
103
+ "Искусствоведение": ["искусствовед"],
104
+ "Исторические": ["историч"],
105
+ "Культурология": ["культуролог"],
106
+ "Медицинские": ["медицин"],
107
+ "Педагогические": ["педагог"],
108
+ "Политические": ["политич"],
109
+ "Сельскохозяйственные": ["сельскохозяй"],
110
+ "Технические": ["технич"],
111
+ "Фармацевтические": ["фармацевт"],
112
+ "Физико-математические": ["физико-математ", "физ-мат", "физмат"],
113
+ "Филологические": ["филолог"],
114
+ "Философские": ["философ"],
115
+ "Химические": ["химич"],
116
+ "Экономические": ["экономич"],
117
+ "Юридические науки": ["юридич"],
118
+ }
119
+
120
  # ==========================
121
  # ЗАГРУЗКА ДАННЫХ И МОДЕЛИ
122
  # ==========================
 
139
  norms = np.linalg.norm(emb_matrix, axis=1, keepdims=True)
140
  emb_matrix = emb_matrix / np.maximum(norms, 1e-8)
141
 
142
+ # --- ВЫРАВНИВАЕМ МЕТА-ИНФУ ПОД ПОРЯДОК reg_nums ---
143
  meta_aligned = df_meta.reindex(reg_nums)
144
 
145
  # dissertation_type -> маски кандидат/доктор
 
151
  is_candidate = type_s.str.contains("кандид", case=False, na=False).to_numpy()
152
  is_doctor = type_s.str.contains("доктор", case=False, na=False).to_numpy()
153
 
154
+ # degree_pursued -> массив строк (и lower-версия для быстрых contains)
155
  if "degree_pursued" in meta_aligned.columns:
156
  degree_arr = meta_aligned["degree_pursued"].fillna("").astype(str).to_numpy()
157
  else:
158
  degree_arr = np.array([""] * len(reg_nums), dtype=object)
159
 
160
+ degree_lower = np.char.lower(degree_arr.astype(str))
161
+
162
+ return df_meta, reg_nums, emb_matrix, is_candidate, is_doctor, degree_arr, degree_lower
163
 
164
 
165
  @st.cache_resource(show_spinner="Загрузка модели...")
 
168
 
169
 
170
  try:
171
+ df_all, reg_nums, emb_matrix, is_candidate, is_doctor, degree_arr, degree_lower = load_data()
172
  model = load_model()
173
  except Exception as e:
174
  st.error(f"Ошибка при загрузке данных или модели: {e}")
 
178
  # ЛОГИКА ФИЛЬТРОВ И ПОИСКА
179
  # ==========================
180
 
181
+ def _contains_any(deg_lower_arr: np.ndarray, patterns: list[str]) -> np.ndarray:
182
+ """deg_lower_arr: np.ndarray of lowercased strings"""
183
+ m = np.zeros(len(deg_lower_arr), dtype=bool)
184
+ for p in patterns:
185
+ p = (p or "").strip().lower()
186
+ if not p:
187
+ continue
188
+ m |= (np.char.find(deg_lower_arr, p) >= 0)
189
+ return m
190
+
191
+
192
+ def build_filter_mask(
193
+ candidate_selected: bool,
194
+ doctor_selected: bool,
195
+ science_selected: list[str],
196
+ ) -> np.ndarray:
197
  mask = np.ones(len(reg_nums), dtype=bool)
198
 
199
+ # 1) Тип диссертации
200
+ type_mask = np.zeros(len(reg_nums), dtype=bool)
201
+ if candidate_selected:
202
+ type_mask |= is_candidate
203
+ if doctor_selected:
204
  type_mask |= is_doctor
205
+ mask &= type_mask # если оба выключены -> всё False
206
 
207
+ # 2) Науки (degree_pursued)
208
+ # Если ничего не выбрано — не ограничиваем по наукам (только по типу диссертаций).
209
+ if science_selected:
210
+ sci_mask = np.zeros(len(reg_nums), dtype=bool)
211
+ for label in science_selected:
212
+ patterns = SCIENCE_PATTERNS.get(label, [label])
213
+ sci_mask |= _contains_any(degree_lower, patterns)
214
+ mask &= sci_mask
215
 
216
  return mask
217
 
 
300
  return df_res[DISPLAY_COLUMNS + ["vak_link"]]
301
 
302
 
303
+ def run_search(
304
+ query: str,
305
+ top_k: int,
306
+ candidate_selected: bool,
307
+ doctor_selected: bool,
308
+ science_selected: list[str],
309
+ ):
310
  query = query.strip()
311
  if not query:
312
  empty_df = pd.DataFrame(columns=DISPLAY_COLUMNS + ["vak_link"])
313
  return empty_df.rename(columns=COLUMN_LABELS_RU), None
314
 
315
+ mask = build_filter_mask(candidate_selected, doctor_selected, science_selected)
 
316
  results = search_core(query, top_k, mask=mask)
317
  df_res = build_result_df(results)
318
 
 
352
  unsafe_allow_html=True,
353
  )
354
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  with st.form("search_form"):
356
  top_k = st.slider(
357
  "Сколько результатов показать",
 
369
  )
370
 
371
  with st.expander("Точные настройки", expanded=False):
372
+ st.markdown("**Диссертации:**")
373
+ c1, c2 = st.columns(2)
374
+ with c1:
375
+ candidate_selected = st.checkbox("Кандидатские", value=True, key="dtype_candidate")
376
+ with c2:
377
+ doctor_selected = st.checkbox("Докторские", value=False, key="dtype_doctor")
378
+
379
+ st.markdown("**Науки:**")
380
+
381
+ # Чекбоксы наук в 3 колонки, строго в алфавитном порядке
382
+ cols = st.columns(3)
383
+ science_selected = []
384
+ for i, label in enumerate(SCIENCE_LABELS):
385
+ default_val = label in DEFAULT_SCIENCES
386
+ with cols[i % 3]:
387
+ if st.checkbox(label, value=default_val, key=f"sci_{label}"):
388
+ science_selected.append(label)
 
 
 
 
 
 
 
 
 
 
 
389
 
390
  c1, c2, c3 = st.columns([1, 1, 1])
391
  with c1:
 
396
  st.write("")
397
 
398
  if do_search:
399
+ # Быстрая валидация: если оба типа диссертаций выключены — смысла искать нет
400
+ if not candidate_selected and not doctor_selected:
401
+ st.warning("Выключены оба типа диссертаций (кандидатские и докторские). Включите хотя бы один тип.")
 
 
402
  else:
403
+ with st.spinner("Идёт поиск по базе диссертаций..."):
404
+ df_res_ru, excel_bytes = run_search(
405
+ query=query,
406
+ top_k=top_k,
407
+ candidate_selected=candidate_selected,
408
+ doctor_selected=doctor_selected,
409
+ science_selected=science_selected,
410
+ )
411
 
412
+ if df_res_ru.empty:
413
+ st.warning("Ничего не найдено. Попробуйте изменить запрос и/или ослабьте фильтры.")
414
+ else:
415
+ st.success(f"Найдено записей: {len(df_res_ru)}")
 
 
 
 
 
 
 
 
 
 
 
416
 
417
+ table_html = df_res_ru.to_html(
418
+ escape=False,
419
+ index=False,
420
+ classes="result-table",
421
+ )
422
 
423
+ st.markdown(
424
+ """
425
+ <style>
426
+ table.result-table {
427
+ width: 100%;
428
+ border-collapse: collapse;
429
+ }
430
+ table.result-table th {
431
+ text-align: center !important;
432
+ vertical-align: middle;
433
+ }
434
+ </style>
435
+ """,
436
+ unsafe_allow_html=True,
437
  )
438
+
439
+ st.markdown(table_html, unsafe_allow_html=True)
440
+
441
+ if excel_bytes is not None:
442
+ st.download_button(
443
+ label="💾 Скачать результаты в Excel",
444
+ data=excel_bytes,
445
+ file_name="search_results.xlsx",
446
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
447
+ )
448
  else:
449
  st.info(
450
  "Введите запрос выше и нажмите кнопку «Поиск» "