GrimSqueaker commited on
Commit
ff29b7e
·
verified ·
1 Parent(s): 5fa9571

UX: search auto-populates and auto-ranks (no Enter needed); perf: precomputed embeddings with weights-md5 guard (cold start ~90s -> ~15s), single-pass lowercase search blobs

Browse files
Files changed (2) hide show
  1. app.py +78 -31
  2. data/proc/embeddings.npz +3 -0
app.py CHANGED
@@ -88,6 +88,22 @@ target_df.rename(
88
 
89
  BATCH_SIZE = 1024
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  def _load_weights_file() -> str:
93
  last_error = None
@@ -166,6 +182,38 @@ def _load_model_with_weights():
166
  return loaded_model
167
 
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  def _precompute_candidate_embeddings(loaded_model):
170
  print("Precomputing candidate embeddings (batched)...")
171
  target_texts = target_df["target_text"].astype(str).to_numpy()
@@ -211,10 +259,18 @@ def _precompute_disease_embeddings(loaded_model):
211
  @lru_cache(maxsize=1)
212
  def _get_runtime_cached():
213
  loaded_model = _load_model_with_weights()
 
 
 
 
 
214
  cand_embs = _precompute_candidate_embeddings(loaded_model)
215
  return loaded_model, cand_embs
216
 
217
 
 
 
 
218
  def get_runtime():
219
  with RUNTIME_LOCK:
220
  return _get_runtime_cached()
@@ -223,6 +279,8 @@ def get_runtime():
223
  @lru_cache(maxsize=1)
224
  def _get_disease_runtime_cached():
225
  loaded_model, _ = get_runtime()
 
 
226
  dis_embs = _precompute_disease_embeddings(loaded_model)
227
  return loaded_model, dis_embs
228
 
@@ -937,13 +995,7 @@ def search_targets(query):
937
  return gr.update(choices=[], value=None)
938
  query = query.strip()
939
  lowered = query.lower()
940
- mask = (
941
- target_df["approvedSymbol"].str.contains(query, case=False, na=False)
942
- | target_df["targetId"].str.contains(query, case=False, na=False)
943
- | target_df["approvedName"]
944
- .astype(str)
945
- .str.contains(query, case=False, na=False)
946
- )
947
  matches = target_df.loc[mask].copy()
948
  if matches.empty:
949
  return gr.update(choices=[], value=None)
@@ -962,7 +1014,7 @@ def search_targets(query):
962
  )
963
  for _, row in matches.iterrows()
964
  ]
965
- return gr.update(choices=choices, value=None)
966
 
967
 
968
  # ============================================
@@ -976,17 +1028,7 @@ def search_diseases(query):
976
 
977
  query = query.strip()
978
  lowered_query = query.lower()
979
- mask = (
980
- disease_df["name"].str.contains(query, case=False, na=False)
981
- | disease_df["diseaseId"].str.contains(query, case=False, na=False)
982
- | disease_df["synonyms"].astype(str).str.contains(query, case=False, na=False)
983
- | disease_df["ExactSynonyms"]
984
- .astype(str)
985
- .str.contains(query, case=False, na=False)
986
- | disease_df["description"]
987
- .astype(str)
988
- .str.contains(query, case=False, na=False)
989
- )
990
 
991
  matches = disease_df.loc[mask].copy()
992
  if matches.empty:
@@ -1007,7 +1049,9 @@ def search_diseases(query):
1007
  for _, row in matches.iterrows()
1008
  ]
1009
 
1010
- return gr.update(choices=choices, value=None)
 
 
1011
 
1012
 
1013
  def launch():
@@ -1091,13 +1135,6 @@ def launch():
1091
 
1092
  gr.Examples(examples=examples, inputs=search_box)
1093
 
1094
- search_box.input(
1095
- fn=search_diseases, inputs=search_box, outputs=did_dropdown
1096
- )
1097
- search_box.submit(
1098
- fn=search_diseases, inputs=search_box, outputs=did_dropdown
1099
- )
1100
-
1101
  inputs = [
1102
  search_box,
1103
  did_dropdown,
@@ -1114,7 +1151,16 @@ def launch():
1114
  out_file_full,
1115
  ]
1116
 
 
 
 
 
 
 
 
 
1117
  btn.click(fn=run_disease_query, inputs=inputs, outputs=outputs)
 
1118
  topk.change(fn=run_disease_query, inputs=inputs, outputs=outputs)
1119
  filter_mode.change(fn=run_disease_query, inputs=inputs, outputs=outputs)
1120
  sort_by.change(fn=run_disease_query, inputs=inputs, outputs=outputs)
@@ -1151,15 +1197,16 @@ def launch():
1151
  )
1152
  )
1153
  t_out_file = gr.File(label="Download full disease ranking (CSV)")
 
 
1154
  t_search_box.input(
1155
  fn=search_targets, inputs=t_search_box, outputs=tid_dropdown
1156
- )
1157
  t_search_box.submit(
1158
  fn=search_targets, inputs=t_search_box, outputs=tid_dropdown
1159
- )
1160
- t_inputs = [t_search_box, tid_dropdown, t_topk, t_min_score]
1161
- t_outputs = [target_summary, t_out_df, t_out_file]
1162
  t_btn.click(fn=run_target_query, inputs=t_inputs, outputs=t_outputs)
 
1163
  t_topk.change(fn=run_target_query, inputs=t_inputs, outputs=t_outputs)
1164
  t_min_score.change(
1165
  fn=run_target_query, inputs=t_inputs, outputs=t_outputs
 
88
 
89
  BATCH_SIZE = 1024
90
 
91
+ # Precomputed lowercase search blobs: one plain-substring scan per keystroke
92
+ # instead of five case-insensitive regex scans. The newline separator prevents
93
+ # accidental cross-field matches (queries never contain newlines).
94
+ _DISEASE_SEARCH_BLOB = (
95
+ disease_df["name"].astype(str)
96
+ + "\n" + disease_df["diseaseId"].astype(str)
97
+ + "\n" + disease_df["synonyms"].astype(str)
98
+ + "\n" + disease_df["ExactSynonyms"].astype(str)
99
+ + "\n" + disease_df["description"].astype(str)
100
+ ).str.lower()
101
+ _TARGET_SEARCH_BLOB = (
102
+ target_df["approvedSymbol"].astype(str)
103
+ + "\n" + target_df["targetId"].astype(str)
104
+ + "\n" + target_df["approvedName"].astype(str)
105
+ ).str.lower()
106
+
107
 
108
  def _load_weights_file() -> str:
109
  last_error = None
 
182
  return loaded_model
183
 
184
 
185
+ def _weights_md5() -> str:
186
+ import hashlib
187
+ try:
188
+ return hashlib.md5(Path(_load_weights_file()).read_bytes()).hexdigest()
189
+ except Exception:
190
+ return ""
191
+
192
+
193
+ def _load_packaged_embeddings(loaded_model):
194
+ """Load precomputed candidate/disease embeddings if present AND produced
195
+ from the exact weights file we just loaded (md5 guard). Returns
196
+ (cand_embs, dis_embs) or None -> caller computes live. Cuts the cold-start
197
+ embedding precompute (~1 min on the free CPU tier) to a file read."""
198
+ path = DATA_DIR / "embeddings.npz"
199
+ if not path.exists():
200
+ return None
201
+ try:
202
+ data = np.load(path, allow_pickle=False)
203
+ if str(data["weights_md5"]) != _weights_md5():
204
+ print("Packaged embeddings are for different weights; recomputing live.")
205
+ return None
206
+ cand, dis = data["candidate_embs"], data["disease_embs"]
207
+ if len(cand) != len(target_df) or len(dis) != len(disease_df):
208
+ print("Packaged embeddings shape mismatch; recomputing live.")
209
+ return None
210
+ print(f"Loaded packaged embeddings: candidates {cand.shape}, diseases {dis.shape}")
211
+ return cand.astype(np.float32), dis.astype(np.float32)
212
+ except Exception as error:
213
+ print(f"Packaged embeddings unreadable ({error}); recomputing live.")
214
+ return None
215
+
216
+
217
  def _precompute_candidate_embeddings(loaded_model):
218
  print("Precomputing candidate embeddings (batched)...")
219
  target_texts = target_df["target_text"].astype(str).to_numpy()
 
259
  @lru_cache(maxsize=1)
260
  def _get_runtime_cached():
261
  loaded_model = _load_model_with_weights()
262
+ packaged = _load_packaged_embeddings(loaded_model)
263
+ if packaged is not None:
264
+ cand_embs, dis_embs = packaged
265
+ _PACKAGED_DISEASE_EMBS.append(dis_embs)
266
+ return loaded_model, cand_embs
267
  cand_embs = _precompute_candidate_embeddings(loaded_model)
268
  return loaded_model, cand_embs
269
 
270
 
271
+ _PACKAGED_DISEASE_EMBS: list = []
272
+
273
+
274
  def get_runtime():
275
  with RUNTIME_LOCK:
276
  return _get_runtime_cached()
 
279
  @lru_cache(maxsize=1)
280
  def _get_disease_runtime_cached():
281
  loaded_model, _ = get_runtime()
282
+ if _PACKAGED_DISEASE_EMBS:
283
+ return loaded_model, _PACKAGED_DISEASE_EMBS[0]
284
  dis_embs = _precompute_disease_embeddings(loaded_model)
285
  return loaded_model, dis_embs
286
 
 
995
  return gr.update(choices=[], value=None)
996
  query = query.strip()
997
  lowered = query.lower()
998
+ mask = _TARGET_SEARCH_BLOB.str.contains(lowered, regex=False, na=False)
 
 
 
 
 
 
999
  matches = target_df.loc[mask].copy()
1000
  if matches.empty:
1001
  return gr.update(choices=[], value=None)
 
1014
  )
1015
  for _, row in matches.iterrows()
1016
  ]
1017
+ return gr.update(choices=choices, value=choices[0][1])
1018
 
1019
 
1020
  # ============================================
 
1028
 
1029
  query = query.strip()
1030
  lowered_query = query.lower()
1031
+ mask = _DISEASE_SEARCH_BLOB.str.contains(lowered_query, regex=False, na=False)
 
 
 
 
 
 
 
 
 
 
1032
 
1033
  matches = disease_df.loc[mask].copy()
1034
  if matches.empty:
 
1049
  for _, row in matches.iterrows()
1050
  ]
1051
 
1052
+ # Auto-select the top match so the user never has to open the dropdown;
1053
+ # selecting a value does NOT trigger ranking (only Rank/sliders/select do).
1054
+ return gr.update(choices=choices, value=choices[0][1])
1055
 
1056
 
1057
  def launch():
 
1135
 
1136
  gr.Examples(examples=examples, inputs=search_box)
1137
 
 
 
 
 
 
 
 
1138
  inputs = [
1139
  search_box,
1140
  did_dropdown,
 
1151
  out_file_full,
1152
  ]
1153
 
1154
+ # Typing populates AND ranks: per-disease scoring is lru-cached,
1155
+ # so keystrokes that keep the same top match cost only a render.
1156
+ search_box.input(
1157
+ fn=search_diseases, inputs=search_box, outputs=did_dropdown
1158
+ ).then(fn=run_disease_query, inputs=inputs, outputs=outputs)
1159
+ search_box.submit(
1160
+ fn=search_diseases, inputs=search_box, outputs=did_dropdown
1161
+ ).then(fn=run_disease_query, inputs=inputs, outputs=outputs)
1162
  btn.click(fn=run_disease_query, inputs=inputs, outputs=outputs)
1163
+ did_dropdown.select(fn=run_disease_query, inputs=inputs, outputs=outputs)
1164
  topk.change(fn=run_disease_query, inputs=inputs, outputs=outputs)
1165
  filter_mode.change(fn=run_disease_query, inputs=inputs, outputs=outputs)
1166
  sort_by.change(fn=run_disease_query, inputs=inputs, outputs=outputs)
 
1197
  )
1198
  )
1199
  t_out_file = gr.File(label="Download full disease ranking (CSV)")
1200
+ t_inputs = [t_search_box, tid_dropdown, t_topk, t_min_score]
1201
+ t_outputs = [target_summary, t_out_df, t_out_file]
1202
  t_search_box.input(
1203
  fn=search_targets, inputs=t_search_box, outputs=tid_dropdown
1204
+ ).then(fn=run_target_query, inputs=t_inputs, outputs=t_outputs)
1205
  t_search_box.submit(
1206
  fn=search_targets, inputs=t_search_box, outputs=tid_dropdown
1207
+ ).then(fn=run_target_query, inputs=t_inputs, outputs=t_outputs)
 
 
1208
  t_btn.click(fn=run_target_query, inputs=t_inputs, outputs=t_outputs)
1209
+ tid_dropdown.select(fn=run_target_query, inputs=t_inputs, outputs=t_outputs)
1210
  t_topk.change(fn=run_target_query, inputs=t_inputs, outputs=t_outputs)
1211
  t_min_score.change(
1212
  fn=run_target_query, inputs=t_inputs, outputs=t_outputs
data/proc/embeddings.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4bea154adf07e9a408e1c4efba502bd7963fc1842cd0f63e47f5c956ec5e392d
3
+ size 84362056