Spaces:
Sleeping
Sleeping
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- app.py +78 -31
- 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=
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|