samwjya commited on
Commit
e27e5ce
Β·
1 Parent(s): 646256c

update ui

Browse files
Files changed (1) hide show
  1. ui/app.py +184 -106
ui/app.py CHANGED
@@ -5,124 +5,202 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
5
 
6
  import streamlit as st
7
  from retriever import fetch_papers
8
- from reranker import load_models, ensemble_rerank, ENSEMBLE_MODELS
9
- from evaluate import evaluate
10
 
11
  st.set_page_config(page_title="PaperRank", layout="wide")
12
 
13
- @st.cache_resource
14
- def get_models():
15
- return load_models()
 
 
 
16
 
17
 
18
- # ── Sidebar ───────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  with st.sidebar:
20
- st.title("Paper Reranking System")
21
- st.caption("OpenAlex baseline vs CrossEncoder ensemble reranking on live papers.")
22
  st.divider()
 
23
  query = st.text_input("Search query", placeholder="e.g. transformer models for information retrieval")
24
  limit = st.slider("Candidates to fetch", min_value=10, max_value=200, value=50, step=10)
25
- k = st.slider("Top-k results", min_value=5, max_value=20, value=10)
26
- run = st.button("Run pipeline", type="primary", use_container_width=True, disabled=not query.strip())
27
-
28
- # ── Main area ─────────────────────────────────────────────────────────────────
29
- st.title("Paper Reranking System")
30
- st.caption("Compare OpenAlex baseline retrieval with CrossEncoder ensemble reranking on live papers.")
31
 
32
- if not query.strip():
33
- st.info("Enter a query in the sidebar to get started.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  st.stop()
35
 
36
- if not run:
37
- st.stop()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- # Stage 1 β€” fetch papers from OpenAlex
40
- with st.spinner(f"[1/3] Fetching papers from OpenAlex..."):
41
- try:
42
- baseline_papers = fetch_papers(query, limit=limit)
43
- except Exception as e:
44
- st.error(f"Fetch failed: {e}")
45
- st.stop()
46
-
47
- st.success(f"[1/3] Retrieved {len(baseline_papers)} papers with abstracts.")
48
-
49
- # Stage 2 β€” load models (cached) and rerank
50
- with st.spinner(f"[2/3] Running ensemble reranking ({len(ENSEMBLE_MODELS)} models)..."):
51
- models = get_models()
52
- reranked_papers = ensemble_rerank(query, baseline_papers, models)
53
- st.success(f"[2/3] Ensemble: scored {len(reranked_papers)} papers Γ— {len(ENSEMBLE_MODELS)} models.")
54
-
55
- # Stage 3 β€” side-by-side results
56
- st.subheader(f"[3/3] Top-{k} results for: \"{query}\"")
57
-
58
- top_baseline = baseline_papers[:k]
59
- top_ensemble = reranked_papers[:k]
60
-
61
- # Build rank-change lookups so each column can show movement
62
- ensemble_rank_by_id = {p["paperId"]: p["ce_rank"] for p in top_ensemble if p.get("paperId")}
63
- openalex_rank_by_id = {p["paperId"]: p["openalex_rank"] for p in top_baseline if p.get("paperId")}
64
-
65
- col_baseline, col_ensemble = st.columns(2)
66
-
67
- with col_baseline:
68
- st.markdown("#### OpenAlex Baseline")
69
- for paper in top_baseline:
70
- rank = paper["openalex_rank"]
71
- pid = paper.get("paperId", "")
72
- title = (paper.get("title") or "")[:80]
73
-
74
- ens_rank = ensemble_rank_by_id.get(pid)
75
- if ens_rank is not None and ens_rank != rank:
76
- direction = "up" if ens_rank < rank else "down"
77
- note = f" β€” moved {direction} to Ensemble #{ens_rank}"
78
- else:
79
- note = ""
80
-
81
- url = paper.get("url", "")
82
- title_md = f"[{title}]({url})" if url else title
83
- st.markdown(f"**#{rank}**{note} \n{title_md} \n`OpenAlex rank: {rank}`")
84
-
85
- with col_ensemble:
86
- st.markdown("#### Ensemble Reranking")
87
- for paper in top_ensemble:
88
- rank = paper["ce_rank"]
89
- pid = paper.get("paperId", "")
90
- title = (paper.get("title") or "")[:80]
91
-
92
- oa_rank = openalex_rank_by_id.get(pid)
93
- if oa_rank is not None and oa_rank != rank:
94
- direction = "up" if rank < oa_rank else "down"
95
- note = f" β€” {direction} from OpenAlex #{oa_rank}"
96
- else:
97
- note = ""
98
-
99
- url = paper.get("url", "")
100
- title_md = f"[{title}]({url})" if url else title
101
-
102
- s1 = paper.get("ce_score_model1", "N/A")
103
- s2 = paper.get("ce_score_model2", "N/A")
104
- s3 = paper.get("ce_score_model3", "N/A")
105
- ens = paper.get("ce_score_ensemble", "N/A")
106
- score_md = f"`BGE: {s1}` `MiniLM: {s2}` `Electra: {s3}` **Ensemble: {ens}**"
107
-
108
- st.markdown(f"**#{rank}**{note} \n{title_md} \n{score_md}")
109
-
110
- # ── Metrics cards ─────────────────────────────────────────────────────────────
111
- st.divider()
112
- st.subheader("Evaluation Metrics")
113
-
114
- metrics = evaluate(baseline_papers, reranked_papers, k=k)
115
-
116
- m1, m2, m3, m4 = st.columns(4)
117
- m1.metric(f"NDCG@{k} β€” OpenAlex", f"{metrics['openalex_ndcg']:.4f}")
118
- m2.metric(
119
- f"NDCG@{k} β€” Ensemble",
120
- f"{metrics['ensemble_ndcg']:.4f}",
121
- delta=round(metrics["ensemble_ndcg"] - metrics["openalex_ndcg"], 4),
122
  )
123
- m3.metric("MRR β€” OpenAlex", f"{metrics['openalex_mrr']:.4f}")
124
- m4.metric(
125
- "MRR β€” Ensemble",
126
- f"{metrics['ensemble_mrr']:.4f}",
127
- delta=round(metrics["ensemble_mrr"] - metrics["openalex_mrr"], 4),
128
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  import streamlit as st
7
  from retriever import fetch_papers
8
+ from reranker import load_model, rerank, AVAILABLE_MODELS, DEFAULT_MODEL
 
9
 
10
  st.set_page_config(page_title="PaperRank", layout="wide")
11
 
12
+ TIER_CONFIG = [
13
+ ("Perfectly Relevant", "#28a745", 0.90),
14
+ ("Relevant", "#e6a817", 0.70),
15
+ ("Somewhat Relevant", "#fd7e14", 0.50),
16
+ ("Not Relevant", "#6c757d", float("-inf")),
17
+ ]
18
 
19
 
20
+ @st.cache_resource
21
+ def get_model(model_name: str):
22
+ return load_model(model_name)
23
+
24
+
25
+ def tier_badge(score: float) -> str:
26
+ if score >= 0.90:
27
+ label, color = "Perfectly Relevant", "#28a745"
28
+ elif score >= 0.70:
29
+ label, color = "Relevant", "#e6a817"
30
+ elif score >= 0.50:
31
+ label, color = "Somewhat Relevant", "#fd7e14"
32
+ else:
33
+ label, color = "Not Relevant", "#6c757d"
34
+ return (
35
+ f'<span style="background:{color};color:white;padding:2px 8px;'
36
+ f'border-radius:12px;font-size:0.75em;font-weight:bold;">{label}</span>'
37
+ )
38
+
39
+
40
+ # ── Sidebar ────────────────────────────────────────────────────────────────────
41
  with st.sidebar:
42
+ st.title("PaperRank")
43
+ st.caption("CrossEncoder reranking on live academic papers.")
44
  st.divider()
45
+
46
  query = st.text_input("Search query", placeholder="e.g. transformer models for information retrieval")
47
  limit = st.slider("Candidates to fetch", min_value=10, max_value=200, value=50, step=10)
48
+ k = st.slider("Top-k results", min_value=5, max_value=20, value=10)
 
 
 
 
 
49
 
50
+ st.divider()
51
+ model_name = st.selectbox(
52
+ "Reranker model",
53
+ AVAILABLE_MODELS,
54
+ index=AVAILABLE_MODELS.index(DEFAULT_MODEL),
55
+ )
56
+
57
+ if "reranked_papers" in st.session_state:
58
+ _years = [
59
+ p["year"] for p in st.session_state["reranked_papers"]
60
+ if p.get("year") is not None
61
+ ]
62
+ year_min = min(_years) if _years else 2000
63
+ year_max = max(_years) if _years else 2026
64
+ year_range = st.slider(
65
+ "Publication year range",
66
+ min_value=year_min,
67
+ max_value=year_max,
68
+ value=(year_min, year_max),
69
+ )
70
+
71
+ run = st.button(
72
+ "Run pipeline", type="primary",
73
+ use_container_width=True,
74
+ disabled=not query.strip(),
75
+ )
76
+
77
+
78
+ # ── Pipeline: fetch + rerank on Run click ──────────────────────────────────────
79
+ if run and query.strip():
80
+ with st.spinner(f"[1/3] Fetching up to {limit} papers from OpenAlex..."):
81
+ try:
82
+ baseline_papers = fetch_papers(query, limit=limit)
83
+ except Exception as e:
84
+ st.error(f"Fetch failed: {e}")
85
+ st.stop()
86
+
87
+ st.success(f"[1/3] Retrieved {len(baseline_papers)} papers with abstracts.")
88
+ st.session_state["baseline_papers"] = baseline_papers
89
+ st.session_state["active_query"] = query
90
+
91
+ with st.spinner(f"[2/3] Reranking {len(baseline_papers)} papers with {model_name}..."):
92
+ model = get_model(model_name)
93
+ st.session_state["reranked_papers"] = rerank(query, baseline_papers, model)
94
+ st.session_state["active_model"] = model_name
95
+
96
+ # Rerun so the year slider updates to the actual bounds before results render
97
+ st.rerun()
98
+
99
+
100
+ # ── Pipeline: re-rerank when model changes (no new fetch needed) ───────────────
101
+ if (
102
+ not run
103
+ and "baseline_papers" in st.session_state
104
+ and st.session_state.get("active_model") != model_name
105
+ ):
106
+ with st.spinner(f"Switching model to {model_name}..."):
107
+ model = get_model(model_name)
108
+ st.session_state["reranked_papers"] = rerank(
109
+ st.session_state["active_query"],
110
+ st.session_state["baseline_papers"],
111
+ model,
112
+ )
113
+ st.session_state["active_model"] = model_name
114
+ st.rerun()
115
+
116
+
117
+ # ── Main area ──────────────────────────────────────────────────────────────────
118
+ st.title("PaperRank")
119
+ st.caption("CrossEncoder reranking on live academic papers.")
120
+
121
+ if "reranked_papers" not in st.session_state:
122
+ st.info("Enter a query in the sidebar and click **Run pipeline** to get started.")
123
  st.stop()
124
 
125
+ reranked_papers = st.session_state["reranked_papers"]
126
+ active_model = st.session_state["active_model"]
127
+ active_query = st.session_state["active_query"]
128
+
129
+ # ── Year filter β€” display only, never re-fetches or re-reranks ─────────────────
130
+ year_lo, year_hi = year_range
131
+
132
+
133
+ def in_year_range(paper: dict) -> bool:
134
+ yr = paper.get("year")
135
+ return yr is None or year_lo <= yr <= year_hi
136
+
137
+
138
+ filtered_reranked = [p for p in reranked_papers if in_year_range(p)]
139
+
140
+ # Tier counts from the full year-filtered reranked list (not just top k)
141
+ tier_counts = {label: 0 for label, _, _ in TIER_CONFIG}
142
+ for paper in filtered_reranked:
143
+ score = paper.get("ce_score", 0.0)
144
+ for label, _, threshold in TIER_CONFIG:
145
+ if score >= threshold:
146
+ tier_counts[label] += 1
147
+ break
148
+
149
+ # Rank-change lookups (CE rank vs original OpenAlex rank)
150
+ oa_rank_by_id = {p["paperId"]: p["openalex_rank"] for p in filtered_reranked if p.get("paperId")}
151
 
152
+ # ── Header ─────────────────────────────────────────────────────────────────────
153
+ st.subheader(f'Results for: "{active_query}"')
154
+ st.info(f"Active model: **{active_model}**")
155
+
156
+ badges_html = " &nbsp;|&nbsp; ".join(
157
+ f'<span style="background:{color};color:white;padding:2px 8px;'
158
+ f'border-radius:12px;font-size:0.8em;font-weight:bold;">'
159
+ f'{tier_counts[label]} {label}</span>'
160
+ for label, color, _ in TIER_CONFIG
161
+ if tier_counts[label] > 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  )
163
+ st.markdown(badges_html, unsafe_allow_html=True)
164
+ st.caption(
165
+ f"Counts from all {len(filtered_reranked)} year-filtered papers "
166
+ f"({year_lo}–{year_hi}) Β· showing top {k}"
 
167
  )
168
+
169
+
170
+ # ── Card renderer ──────────────────────────────────────────────────────────────
171
+ def render_ce_card(paper: dict) -> None:
172
+ rank = paper["ce_rank"]
173
+ pid = paper.get("paperId", "")
174
+ title = (paper.get("title") or "")[:80]
175
+ url = paper.get("url", "")
176
+ title_md = f"[{title}]({url})" if url else title
177
+ score = paper.get("ce_score", 0.0)
178
+
179
+ oa_r = oa_rank_by_id.get(pid)
180
+ note = (f" {'↑' if rank < oa_r else '↓'} from OA #{oa_r}") if oa_r and oa_r != rank else ""
181
+
182
+ st.markdown(
183
+ f"**#{rank}**{note} {tier_badge(score)} \n{title_md} \n"
184
+ f"`{paper.get('year', 'N/A')}` Β· {paper.get('citationCount', 'N/A')} citations Β· score: `{score}`",
185
+ unsafe_allow_html=True,
186
+ )
187
+
188
+
189
+ # ── Top-K results ──────────────────────────────────────────────────────────────
190
+ top_reranked = filtered_reranked[:k]
191
+
192
+ if not top_reranked:
193
+ st.warning("No papers match the selected year range. Adjust the slider in the sidebar.")
194
+ st.stop()
195
+
196
+ for paper in top_reranked:
197
+ render_ce_card(paper)
198
+
199
+ # ── Expander: remaining papers (rank k+1 to end of filtered list) ──────────────
200
+ rest_reranked = filtered_reranked[k:]
201
+
202
+ if rest_reranked:
203
+ total = len(filtered_reranked)
204
+ with st.expander(f"Show all {total} papers (ranks {k + 1}–{total})"):
205
+ for paper in rest_reranked:
206
+ render_ce_card(paper)