yogl commited on
Commit
c920195
·
verified ·
1 Parent(s): c56387d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -80
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import os
2
  import io
3
  from typing import Optional, Tuple, List
 
4
 
5
  import numpy as np
6
  import pandas as pd
@@ -22,7 +23,6 @@ MODEL_NAME = os.getenv("MODEL_NAME")
22
 
23
  SLIDER_MIN_YEAR = 2005 # фиксированный минимум
24
 
25
- # Для Excel (полный набор)
26
  COLUMN_LABELS_RU_EXCEL = {
27
  "№": "№",
28
  "score": "Сходство",
@@ -97,7 +97,6 @@ DEFAULT_SCIENCES = {
97
  "Биологические",
98
  }
99
 
100
- # Паттерны для сопоставления с degree_pursued (на случай разных формулировок)
101
  SCIENCE_PATTERNS = {
102
  "Архитектура": ["архитектур"],
103
  "Биологические": ["биолог"],
@@ -123,7 +122,6 @@ SCIENCE_PATTERNS = {
123
 
124
 
125
  def _keyify(label: str) -> str:
126
- # стабильный ключ для streamlit
127
  return "k_" + "".join(ch if ch.isalnum() else "_" for ch in label).strip("_")
128
 
129
 
@@ -133,13 +131,11 @@ def _keyify(label: str) -> str:
133
 
134
  @st.cache_data(show_spinner="Загрузка данных...")
135
  def load_data():
136
- # 1) META
137
  ds_meta = load_dataset(HF_MERGED_REPO, split="train")
138
  df_meta = ds_meta.to_pandas()
139
  df_meta["registration_number"] = df_meta["registration_number"].astype(str)
140
  df_meta = df_meta.set_index("registration_number", drop=False)
141
 
142
- # 2) EMB
143
  ds_emb = load_dataset(HF_EMB_REPO, split="train")
144
  df_emb = ds_emb.to_pandas()
145
  df_emb["registration_number"] = df_emb["registration_number"].astype(str)
@@ -150,7 +146,6 @@ def load_data():
150
  norms = np.linalg.norm(emb_matrix, axis=1, keepdims=True)
151
  emb_matrix = emb_matrix / np.maximum(norms, 1e-8)
152
 
153
- # выравниваем meta под reg_nums
154
  meta_aligned = df_meta.reindex(reg_nums)
155
 
156
  # dissertation_type masks
@@ -208,15 +203,13 @@ def build_filter_mask(
208
  ) -> np.ndarray:
209
  mask = np.ones(len(reg_nums), dtype=bool)
210
 
211
- # 1) Тип диссертации
212
  type_mask = np.zeros(len(reg_nums), dtype=bool)
213
  if candidate_selected:
214
  type_mask |= is_candidate
215
  if doctor_selected:
216
  type_mask |= is_doctor
217
- mask &= type_mask # если оба выключены -> всё False
218
 
219
- # 2) Науки
220
  if science_selected:
221
  sci_mask = np.zeros(len(reg_nums), dtype=bool)
222
  for label in science_selected:
@@ -224,7 +217,6 @@ def build_filter_mask(
224
  sci_mask |= _contains_any(degree_lower, patterns)
225
  mask &= sci_mask
226
 
227
- # 3) Годы (NaN пропускаем)
228
  if year_range is not None:
229
  y0, y1 = int(year_range[0]), int(year_range[1])
230
  yr = year_arr
@@ -321,18 +313,11 @@ def run_search(
321
  science_selected: List[str],
322
  year_range: Optional[Tuple[int, int]],
323
  ):
324
- query = query.strip()
325
- if not query:
326
- return pd.DataFrame(), None, pd.DataFrame()
327
-
328
  mask = build_filter_mask(candidate_selected, doctor_selected, science_selected, year_range)
329
  results = search_core(query, top_k, mask=mask)
330
  df_raw = build_result_df(results)
331
 
332
- if df_raw.empty:
333
- return pd.DataFrame(), None, df_raw
334
-
335
- # --- UI dataframe (скрываем №, рег.номер, тип) ---
336
  df_ui = df_raw.copy()
337
  df_ui = df_ui.rename(
338
  columns={
@@ -344,11 +329,9 @@ def run_search(
344
  "vak_link": "ВАК",
345
  }
346
  )
347
-
348
- # Убираем не нужные колонки (как вы просили)
349
  df_ui = df_ui.drop(columns=["№", "registration_number", "dissertation_type"])
350
 
351
- # --- Excel: полный набор ---
352
  df_excel_ru = df_raw.rename(columns=COLUMN_LABELS_RU_EXCEL)
353
  output = io.BytesIO()
354
  with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
@@ -367,6 +350,16 @@ st.markdown(
367
  unsafe_allow_html=True,
368
  )
369
 
 
 
 
 
 
 
 
 
 
 
370
  data_has_years = np.isfinite(year_arr).any()
371
  year_max = int(np.nanmax(year_arr)) if data_has_years else None
372
 
@@ -417,74 +410,135 @@ with st.form("search_form"):
417
  with c2:
418
  do_search = st.form_submit_button("🔍 Поиск", type="primary", use_container_width=True)
419
 
 
420
  if do_search:
421
  if not candidate_selected and not doctor_selected:
422
  st.warning("Выключены оба типа диссертаций. Включите «Кандидатские» и/или «Докторские».")
423
- st.stop()
424
-
425
- with st.spinner("Идёт поиск по базе диссертаций..."):
426
- df_ui, excel_bytes, df_raw = run_search(
427
- query=query,
428
- top_k=top_k,
429
- candidate_selected=candidate_selected,
430
- doctor_selected=doctor_selected,
431
- science_selected=science_selected,
432
- year_range=year_range,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  )
434
 
435
- if df_ui.empty:
436
- st.warning("Ничего не найдено. Попробуйте изменить запрос и/или ослабьте фильтры.")
437
- else:
438
- st.success(f"Найдено записей: {len(df_ui)}")
439
-
440
- # ВАЖНО:
441
- # st.dataframe даёт сортировку по клику, но перенос текста в ячейках не поддерживает.
442
- # Чтобы читать длинные названия без горизонтального скролла — ниже блок "Детали по выбранной строке".
443
- df_ui_show = df_ui[["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК"]].copy()
444
-
445
- event = st.dataframe(
446
- df_ui_show,
447
- use_container_width=True,
448
- hide_index=True,
449
- column_order=["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК"],
450
- column_config={
451
- "Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
452
- "ФИО": st.column_config.TextColumn("ФИО", width="medium"),
453
- "Название диссертации": st.column_config.TextColumn("Название диссертации", width="large"),
454
- "Организация": st.column_config.TextColumn("Организация", width="medium"),
455
- "Год": st.column_config.NumberColumn("Год", width="small"),
456
- "ВАК": st.column_config.LinkColumn("ВАК", display_text="открыть", width="small"),
457
- },
458
- on_select="rerun",
459
- selection_mode="single-row",
460
  )
461
 
462
- # Детали с переносом строк ез горизонтального скролла)
463
- sel = getattr(event, "selection", None)
464
- sel_rows = sel.rows if sel is not None else []
465
- if sel_rows:
466
- i = sel_rows[0]
467
- row = df_ui_show.iloc[i].to_dict()
468
- st.markdown("### Детали по выбранной записи")
469
- st.markdown(f"**ФИО:** {row.get('ФИО', '')}")
470
- st.markdown(f"**Организация:** {row.get('Организация', '')}")
471
- st.markdown(f"**Год:** {row.get('Год', '')}")
472
- st.markdown(f"**Сходство:** {row.get('Сходство', '')}")
473
- st.markdown("**Название диссертации:**")
474
- st.write(row.get("Название диссертации", "")) # переносится естественно
475
- if row.get("ВАК"):
476
- st.markdown(f"**ВАК:** {row.get('ВАК')}")
477
-
478
- if excel_bytes is not None:
479
- st.download_button(
480
- label="💾 Скачать результаты в Excel",
481
- data=excel_bytes,
482
- file_name="search_results.xlsx",
483
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
484
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  else:
486
- st.info("Введите запрос выше и нажмите кнопку «Поиск» или нажмите Ctrl+Enter в поле ввода.")
487
 
 
488
  st.markdown(
489
  "<p style='font-size: 0.8rem; text-align: right; color: gray;'>(с) Антон Лощилов, 2025</p>",
490
  unsafe_allow_html=True,
 
1
  import os
2
  import io
3
  from typing import Optional, Tuple, List
4
+ from datetime import datetime, timezone
5
 
6
  import numpy as np
7
  import pandas as pd
 
23
 
24
  SLIDER_MIN_YEAR = 2005 # фиксированный минимум
25
 
 
26
  COLUMN_LABELS_RU_EXCEL = {
27
  "№": "№",
28
  "score": "Сходство",
 
97
  "Биологические",
98
  }
99
 
 
100
  SCIENCE_PATTERNS = {
101
  "Архитектура": ["архитектур"],
102
  "Биологические": ["биолог"],
 
122
 
123
 
124
  def _keyify(label: str) -> str:
 
125
  return "k_" + "".join(ch if ch.isalnum() else "_" for ch in label).strip("_")
126
 
127
 
 
131
 
132
  @st.cache_data(show_spinner="Загрузка данных...")
133
  def load_data():
 
134
  ds_meta = load_dataset(HF_MERGED_REPO, split="train")
135
  df_meta = ds_meta.to_pandas()
136
  df_meta["registration_number"] = df_meta["registration_number"].astype(str)
137
  df_meta = df_meta.set_index("registration_number", drop=False)
138
 
 
139
  ds_emb = load_dataset(HF_EMB_REPO, split="train")
140
  df_emb = ds_emb.to_pandas()
141
  df_emb["registration_number"] = df_emb["registration_number"].astype(str)
 
146
  norms = np.linalg.norm(emb_matrix, axis=1, keepdims=True)
147
  emb_matrix = emb_matrix / np.maximum(norms, 1e-8)
148
 
 
149
  meta_aligned = df_meta.reindex(reg_nums)
150
 
151
  # dissertation_type masks
 
203
  ) -> np.ndarray:
204
  mask = np.ones(len(reg_nums), dtype=bool)
205
 
 
206
  type_mask = np.zeros(len(reg_nums), dtype=bool)
207
  if candidate_selected:
208
  type_mask |= is_candidate
209
  if doctor_selected:
210
  type_mask |= is_doctor
211
+ mask &= type_mask
212
 
 
213
  if science_selected:
214
  sci_mask = np.zeros(len(reg_nums), dtype=bool)
215
  for label in science_selected:
 
217
  sci_mask |= _contains_any(degree_lower, patterns)
218
  mask &= sci_mask
219
 
 
220
  if year_range is not None:
221
  y0, y1 = int(year_range[0]), int(year_range[1])
222
  yr = year_arr
 
313
  science_selected: List[str],
314
  year_range: Optional[Tuple[int, int]],
315
  ):
 
 
 
 
316
  mask = build_filter_mask(candidate_selected, doctor_selected, science_selected, year_range)
317
  results = search_core(query, top_k, mask=mask)
318
  df_raw = build_result_df(results)
319
 
320
+ # UI: скрываем №, reg, type
 
 
 
321
  df_ui = df_raw.copy()
322
  df_ui = df_ui.rename(
323
  columns={
 
329
  "vak_link": "ВАК",
330
  }
331
  )
 
 
332
  df_ui = df_ui.drop(columns=["№", "registration_number", "dissertation_type"])
333
 
334
+ # Excel: полный набор
335
  df_excel_ru = df_raw.rename(columns=COLUMN_LABELS_RU_EXCEL)
336
  output = io.BytesIO()
337
  with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
 
350
  unsafe_allow_html=True,
351
  )
352
 
353
+ # Подготовка session_state
354
+ if "last_df_ui" not in st.session_state:
355
+ st.session_state.last_df_ui = None
356
+ if "last_df_raw" not in st.session_state:
357
+ st.session_state.last_df_raw = None
358
+ if "last_excel" not in st.session_state:
359
+ st.session_state.last_excel = None
360
+ if "requests_log" not in st.session_state:
361
+ st.session_state.requests_log = [] # заглушка "БД"
362
+
363
  data_has_years = np.isfinite(year_arr).any()
364
  year_max = int(np.nanmax(year_arr)) if data_has_years else None
365
 
 
410
  with c2:
411
  do_search = st.form_submit_button("🔍 Поиск", type="primary", use_container_width=True)
412
 
413
+ # Выполняем поиск и сохраняем результаты (чтобы они не исчезали на rerun)
414
  if do_search:
415
  if not candidate_selected and not doctor_selected:
416
  st.warning("Выключены оба типа диссертаций. Включите «Кандидатские» и/или «Докторские».")
417
+ else:
418
+ with st.spinner("Идёт поиск по базе диссертаций..."):
419
+ df_ui, excel_bytes, df_raw = run_search(
420
+ query=query,
421
+ top_k=top_k,
422
+ candidate_selected=candidate_selected,
423
+ doctor_selected=doctor_selected,
424
+ science_selected=science_selected,
425
+ year_range=year_range,
426
+ )
427
+ st.session_state.last_df_ui = df_ui
428
+ st.session_state.last_df_raw = df_raw
429
+ st.session_state.last_excel = excel_bytes
430
+
431
+ # ======= БЛОК РЕЗУЛЬТАТОВ (показывается, если есть сохранённые данные) =======
432
+ df_ui_saved = st.session_state.last_df_ui
433
+ df_raw_saved = st.session_state.last_df_raw
434
+ excel_saved = st.session_state.last_excel
435
+
436
+ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
437
+ st.success(f"Найдено записей: {len(df_ui_saved)}")
438
+
439
+ df_ui_show = df_ui_saved[["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК"]].copy()
440
+
441
+ # Сортировка по клику на заголовок + чекбоксы выбора строк
442
+ event = st.dataframe(
443
+ df_ui_show,
444
+ use_container_width=True,
445
+ hide_index=True,
446
+ column_order=["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК"],
447
+ column_config={
448
+ "Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
449
+ "ФИО": st.column_config.TextColumn("ФИО", width="medium"),
450
+ "Название диссертации": st.column_config.TextColumn("Название диссертации", width="large"),
451
+ "Организация": st.column_config.TextColumn("Организация", width="medium"),
452
+ "Год": st.column_config.NumberColumn("Год", width="small"),
453
+ "ВАК": st.column_config.LinkColumn("ВАК", display_text="открыть", width="small"),
454
+ },
455
+ on_select="rerun",
456
+ selection_mode="multi-row",
457
+ )
458
+
459
+ selected_rows = getattr(event, "selection", None).rows if getattr(event, "selection", None) else []
460
+ selected_rows = list(selected_rows or [])
461
+
462
+ # Детали (перенос текста) для выбранных
463
+ if selected_rows:
464
+ st.markdown("### Выбранные записи (предпросмотр)")
465
+ for i in selected_rows[:10]:
466
+ r = df_ui_show.iloc[i]
467
+ st.markdown(f"- **{r['ФИО']}** — {r['Название диссертации']} ({r['Год']})")
468
+ if len(selected_rows) > 10:
469
+ st.caption(f"Показаны первые 10 из {len(selected_rows)} выбранных.")
470
+
471
+ # Кнопка Excel
472
+ if excel_saved is not None:
473
+ st.download_button(
474
+ label="💾 Скачать результаты в Excel",
475
+ data=excel_saved,
476
+ file_name="search_results.xlsx",
477
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
478
  )
479
 
480
+ # ======= ЗАГЛУШКА "Запрос расширенной информации" =======
481
+ st.markdown("---")
482
+ st.subheader("Запрос расширенной информации")
483
+
484
+ with st.form("request_form"):
485
+ requester_fio = st.text_input("Ваше ФИО", placeholder="Иванов Иван Иванович")
486
+ requester_email = st.text_input("Email", placeholder="name@example.com")
487
+ requester_note = st.text_area(
488
+ "Дополнительная информация",
489
+ height=120,
490
+ placeholder="Что именно вы хотите уточнить/получить по выбранным диссертациям?",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491
  )
492
 
493
+ st.caption(f"Выбрано записей: {len(selected_rows)}")
494
+ send_request = st.form_submit_button("📨 Отправить запрос", type="primary", use_container_width=True)
495
+
496
+ if send_request:
497
+ if not requester_fio.strip() or not requester_email.strip():
498
+ st.warning("Заполните «Ваше ФИО» и «Email».")
499
+ elif len(selected_rows) == 0:
500
+ st.warning("Выберите хотя бы одну запись в таблице (чекбоксами слева).")
501
+ else:
502
+ # Собираем payload (заглушка для отправки в БД)
503
+ # Берём данные из df_raw_saved по тем же индексам.
504
+ selected_payload_items = []
505
+ for i in selected_rows:
506
+ raw = df_raw_saved.iloc[i].to_dict()
507
+ selected_payload_items.append(
508
+ {
509
+ "author_fio": raw.get("fio"),
510
+ "title": raw.get("title"),
511
+ "org": raw.get("author_org_short"),
512
+ "year": raw.get("protection_year"),
513
+ "vak_link": raw.get("vak_link"),
514
+ "registration_number": raw.get("registration_number"),
515
+ }
516
+ )
517
+
518
+ payload = {
519
+ "created_at_utc": datetime.now(timezone.utc).isoformat(),
520
+ "requester": {
521
+ "fio": requester_fio.strip(),
522
+ "email": requester_email.strip(),
523
+ "note": requester_note.strip(),
524
+ },
525
+ "items": selected_payload_items,
526
+ }
527
+
528
+ # "Отправка в БД" — заглушка: сохраняем в session_state и показываем
529
+ st.session_state.requests_log.append(payload)
530
+
531
+ st.success("Запрос принят (заглушка). Ниже — данные, которые будут отправляться в БД.")
532
+ st.json(payload)
533
+
534
+ with st.expander("История запросов (заглушка)", expanded=False):
535
+ st.write(f"Всего запросов в текущей сессии: {len(st.session_state.requests_log)}")
536
+ st.json(st.session_state.requests_log[-1])
537
+
538
  else:
539
+ st.info("Введите запрос и нажмите «Поиск». После этого результаты будут доступны для выбора и отправки запроса.")
540
 
541
+ # Футер
542
  st.markdown(
543
  "<p style='font-size: 0.8rem; text-align: right; color: gray;'>(с) Антон Лощилов, 2025</p>",
544
  unsafe_allow_html=True,