siddhm11 commited on
Commit
0ef3e5f
Β·
verified Β·
1 Parent(s): d3097e4

docs: comprehensive README with production results and integration guide

Browse files
Files changed (1) hide show
  1. README.md +242 -204
README.md CHANGED
@@ -1,34 +1,72 @@
1
  # ResearchIT Phase 6 β€” LightGBM Reranker
2
 
3
- > **Status:** Pipeline tested on synthetic data. Awaiting real citation edges from Semantic Scholar API to produce the production model.
4
  > **Parent project:** [siddhm11/ResearchIT](https://huggingface.co/spaces/siddhm11/ResearchIT)
5
- > **Replaces:** Hand-tuned heuristic scorer in `app/recommend/reranker.py`
 
6
 
7
  ---
8
 
9
- ## What This Is
10
 
11
- A LightGBM lambdarank model that replaces the hand-tuned heuristic scorer in ResearchIT's recommendation pipeline. The heuristic uses 5 features with fixed weights. This model uses 37 features and learns optimal weights from data.
12
 
13
- **This repo contains:**
14
- - `scripts/` β€” 3 Python scripts that build the full pipeline (data β†’ train β†’ evaluate)
15
- - `synthetic_model/` β€” A model trained on synthetic data (proof the pipeline works)
16
- - `test_results.json` β€” Full benchmark results from the test suite
 
 
 
 
17
 
18
- **This repo does NOT yet contain:**
19
- - A production model trained on real citation data (that's the next step β€” run Script 1 with your S2 API key)
 
20
 
21
  ---
22
 
23
- ## How It Works
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  ### The Problem
 
26
  ResearchIT recommends arXiv papers using a multi-stage pipeline:
27
  ```
28
  Qdrant ANN retrieval β†’ Quota fusion β†’ Reranking β†’ MMR diversity β†’ Feed
29
  ```
30
 
31
- The **reranking** step currently uses a heuristic scorer:
32
  ```python
33
  score = 0.40 Γ— cos(paper, long_term_profile)
34
  + 0.25 Γ— cos(paper, short_term_profile)
@@ -37,88 +75,111 @@ score = 0.40 Γ— cos(paper, long_term_profile)
37
  - 0.15 Γ— cos(paper, negative_profile)
38
  ```
39
 
40
- These weights are hand-tuned guesses. The model can't use citation count, co-citation, category match, or any feature interactions.
41
 
42
  ### The Solution
43
- Train a LightGBM lambdarank model on citation-graph pseudo-labels:
44
- - **Each arXiv paper acts as a "pseudo-user"** β€” its reference list simulates what that researcher would "save"
45
- - **Direct citations β†’ label 2** (strong positive)
46
- - **Co-citations β†’ label 1** (weak positive β€” papers that share references)
47
- - **ANN-retrieved but not cited β†’ label 0** (negative)
48
 
49
- The model learns: given 37 features about a (user, paper) pair, which papers should rank higher?
50
 
51
- ---
 
 
 
52
 
53
- ## The 3-Script Pipeline
54
 
55
- ### Script 1: `01_fetch_citation_edges.py`
56
- **What:** Fetches the citation graph from Semantic Scholar API.
57
- **Input:** `arxiv_ids.txt` (your 1.6M arXiv IDs, one per line)
58
- **Output:** `citations.parquet` β€” (citing_arxiv_id, cited_arxiv_id, is_influential)
59
 
60
- ```bash
61
- python scripts/01_fetch_citation_edges.py \
62
- --corpus-file arxiv_ids.txt \
63
- --output citations.parquet \
64
- --api-key YOUR_S2_API_KEY # optional, faster with key
65
  ```
66
-
67
- - Supports checkpoint/resume β€” safe to interrupt
68
- - Supports both batch API (simple) and bulk download (faster for 1.6M papers)
69
- - Filters to in-corpus edges only (both papers must be in your Qdrant collection)
70
- - Expected output: ~20-40M citation edges
71
-
72
- ### Script 2: `02_generate_training_triples.py`
73
- **What:** Generates labeled training data by running ANN searches against Qdrant.
74
- **Input:** `citations.parquet` + Qdrant access + Turso access
75
- **Output:** `ltr_dataset/train.parquet` + `ltr_dataset/eval.parquet` + `feature_schema.json`
76
-
77
- ```bash
78
- python scripts/02_generate_training_triples.py \
79
- --citations citations.parquet \
80
- --corpus-file arxiv_ids.txt \
81
- --qdrant-url "https://YOUR_QDRANT_URL" \
82
- --qdrant-api-key "YOUR_KEY" \
83
- --qdrant-collection arxiv_bgem3_dense \
84
- --turso-url "https://YOUR_TURSO_URL" \
85
- --turso-token "YOUR_TOKEN" \
86
- --output-dir ./ltr_dataset \
87
- --num-queries 100000 \
88
- --candidates-per-query 50
89
  ```
90
 
91
- - Enforces time-split: train on papers before 2023, eval on 2023+
92
- - Asserts no temporal leakage: `max(train.year) < min(eval.year)`
93
- - Uses `scroll()` + `query_points()` for Qdrant (correct API for qdrant-client 1.17+)
94
- - Expected output: ~5M train rows, ~500K eval rows
95
 
96
- ### Script 3: `03_train_lightgbm.py`
97
- **What:** Trains LightGBM and compares against the heuristic baseline.
98
- **Input:** `train.parquet` + `eval.parquet`
99
- **Output:** `reranker_v1.txt` (~100-300KB model file) + metrics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
- ```bash
102
- python scripts/03_train_lightgbm.py \
103
- --train-file ltr_dataset/train.parquet \
104
- --eval-file ltr_dataset/eval.parquet \
105
- --output-dir ./model_output \
106
- --num-boost-round 500 \
107
- --learning-rate 0.05
108
- ```
109
 
110
- - Evaluates: nDCG@5, nDCG@10, nDCG@20, Recall@10, Recall@50, HR@10, MRR
111
- - Compares LightGBM vs the exact heuristic from `reranker.py`
112
- - Reports feature importance, latency benchmark, per-query win rates
113
- - Outputs verdict: deploy / marginal / no improvement
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  ---
116
 
117
- ## Feature Schema (37 features)
118
-
119
- The model uses 37 features organized in 4 groups:
120
 
121
- ### Content/Retrieval Features (0-19) β€” Populated during pseudo-label training
122
 
123
  | # | Feature | Description | Source |
124
  |---|---------|-------------|--------|
@@ -126,40 +187,40 @@ The model uses 37 features organized in 4 groups:
126
  | 1 | `candidate_position` | Rank position in ANN results (0-indexed) | Qdrant |
127
  | 2 | `candidate_citation_count` | Total citation count | Turso |
128
  | 3 | `candidate_log_citations` | log(citation_count + 1) | Computed |
129
- | 4 | `candidate_influential_citations` | Influential citation count (Semantic Scholar) | Turso |
130
  | 5 | `candidate_age_days` | Days since publication | Turso |
131
- | 6 | `candidate_recency_score` | exp(-0.002 Γ— age_days) β€” matches heuristic formula | Computed |
132
  | 7 | `query_citation_count` | Citation count of the query/user paper | Turso |
133
- | 8 | `query_age_days` | Days since query paper was published | Turso |
134
  | 9 | `year_diff` | |query_year - candidate_year| | Computed |
135
- | 10 | `same_primary_category` | 1 if same primary arXiv category, else 0 | Turso |
136
- | 11 | `co_citation_count` | Papers that cite BOTH query and candidate | Citation graph |
137
- | 12 | `shared_author_count` | Number of shared authors (case-insensitive) | Turso |
138
  | 13 | `candidate_is_newer` | 1 if candidate published after query | Computed |
139
  | 14 | `query_log_citations` | log(query_citation_count + 1) | Computed |
140
  | 15 | `citation_count_ratio` | candidate_citations / (query_citations + 1) | Computed |
141
  | 16 | `age_ratio` | candidate_age / (query_age + 1) | Computed |
142
  | 17 | `candidate_citations_per_year` | citation_count / max(age_years, 0.5) | Computed |
143
- | 18 | `query_num_references` | How many papers the query paper cites (in-corpus) | Citation graph |
144
- | 19 | `candidate_num_cited_by` | How many corpus papers cite the candidate | Citation graph |
145
 
146
  ### User Behavior Features (20-30) β€” Zero-filled for pseudo-labels, active for real users
147
 
148
  | # | Feature | Description | Source in ResearchIT |
149
  |---|---------|-------------|---------------------|
150
- | 20 | `ewma_longterm_similarity` | cos(candidate, user long-term EWMA profile) | `profiles.py` Ξ±=0.03 |
151
- | 21 | `ewma_shortterm_similarity` | cos(candidate, user short-term EWMA profile) | `profiles.py` Ξ±=0.40 |
152
- | 22 | `ewma_negative_similarity` | cos(candidate, user negative EWMA profile) | `profiles.py` Ξ±=0.15 |
153
- | 23 | `cluster_importance` | Importance weight of the serving cluster | `clustering.py` |
154
- | 24 | `cluster_distance_to_medoid` | cos(candidate, cluster medoid embedding) | `clustering.py` |
155
- | 25 | `is_suppressed_category` | 1 if candidate's category is suppressed (β‰₯3 dismissals in 14 days) | `db.py` |
156
- | 26 | `onboarding_category_match` | 1 if candidate matches user's onboarding selections | `db.py` |
157
- | 27 | `user_total_saves` | Total papers user has saved | `interactions` table |
158
- | 28 | `user_total_dismissals` | Total papers user has dismissed | `interactions` table |
159
- | 29 | `user_days_since_last_save` | Days since user's most recent save | `interactions` table |
160
  | 30 | `user_session_save_count` | Saves in current session | In-memory state |
161
 
162
- ### Cross Features (31-36) β€” Feature interactions
163
 
164
  | # | Feature | Formula |
165
  |---|---------|---------|
@@ -170,167 +231,150 @@ The model uses 37 features organized in 4 groups:
170
  | 35 | `position_inverse` | 1 / (candidate_position + 1) |
171
  | 36 | `citations_x_recency` | candidate_log_citations Γ— candidate_recency_score |
172
 
173
- ### Why 37 Features Instead of 5?
174
-
175
- The heuristic uses features 0, 1, 5 (via recency), 20, 21 β€” that's it. LightGBM additionally uses:
176
-
177
- - **Citation count** (#2, #3, #17) β€” papers with more citations are generally higher quality
178
- - **Co-citation count** (#11) β€” the strongest signal in our tests. Papers cited by the same community as your interests are highly relevant
179
- - **Category match** (#10) β€” same arXiv category = topically aligned
180
- - **Shared authors** (#12) β€” self-citation / collaborator network signal
181
- - **Feature interactions** (#31-36) β€” e.g., a paper that is both semantically similar AND frequently co-cited is much more relevant than either signal alone
182
-
183
- ---
184
-
185
- ## Benchmark Results (Synthetic Data)
186
-
187
- > These results are from realistic synthetic data that mimics citation graph patterns. Real data results will differ.
188
-
189
- ### LightGBM vs Heuristic Baseline
190
-
191
- | Metric | Heuristic | LightGBM | Improvement |
192
- |--------|:---------:|:--------:|:-----------:|
193
- | nDCG@3 | 0.9305 | **0.9979** | +7.2% |
194
- | nDCG@5 | 0.9138 | **0.9980** | +9.2% |
195
- | **nDCG@10** | **0.9111** | **0.9985** | **+9.6%** |
196
- | nDCG@20 | 0.9472 | **0.9988** | +5.5% |
197
-
198
- ### Per-Query Win Rate (500 eval queries)
199
- - LightGBM wins: **91.4%**
200
- - Heuristic wins: 0.4%
201
- - Ties: 8.2%
202
-
203
- ### Production Metrics
204
- | Metric | Value | Target | Status |
205
- |--------|-------|--------|--------|
206
- | Latency (100 candidates) | 0.088ms | <1ms | βœ… 11Γ— under budget |
207
- | Model size | 286 KB | <500 KB | βœ… |
208
- | Model reload | Identical predictions | β€” | βœ… |
209
- | Handles NaN input | Graceful | β€” | βœ… |
210
- | Handles extreme values | No crash | β€” | βœ… |
211
- | Train-eval gap | 0.0008 | <0.05 | βœ… No overfitting |
212
-
213
- ### Top 10 Features by Importance
214
-
215
- | Rank | Feature | Importance |
216
- |------|---------|:----------:|
217
- | 1 | cosine_x_cocitation | 95,386 |
218
- | 2 | qdrant_cosine_score | 37,300 |
219
- | 3 | co_citation_count | 14,124 |
220
- | 4 | candidate_citation_count | 3,399 |
221
- | 5 | candidate_position | 2,615 |
222
- | 6 | cosine_x_citations | 1,623 |
223
- | 7 | year_diff | 1,229 |
224
- | 8 | candidate_log_citations | 894 |
225
- | 9 | age_ratio | 564 |
226
- | 10 | position_inverse | 427 |
227
-
228
- The #1 feature is `cosine_x_cocitation` β€” the interaction between embedding similarity and co-citation count. This is a signal the heuristic cannot access.
229
-
230
  ---
231
 
232
- ## How to Produce the Production Model
233
 
234
  ### Prerequisites
235
  ```bash
236
  pip install httpx pyarrow tqdm numpy qdrant-client lightgbm
237
  ```
238
 
239
- ### Step 1: Export corpus IDs
240
- Export your 1.6M arXiv IDs from Turso to a text file:
241
  ```sql
242
  SELECT arxiv_id FROM papers;
243
  ```
244
- Save as `arxiv_ids.txt`, one ID per line.
245
 
246
- ### Step 2: Run the pipeline
247
  ```bash
248
- # Fetch citation edges (~30-90 min)
249
  python scripts/01_fetch_citation_edges.py \
250
  --corpus-file arxiv_ids.txt \
251
- --output citations.parquet
 
 
252
 
253
- # Generate training data (~1-2 hours, needs Qdrant + Turso access)
 
 
 
 
 
 
254
  python scripts/02_generate_training_triples.py \
255
  --citations citations.parquet \
256
  --corpus-file arxiv_ids.txt \
257
  --qdrant-url "$QDRANT_URL" \
258
  --qdrant-api-key "$QDRANT_API_KEY" \
 
259
  --turso-url "$TURSO_URL" \
260
  --turso-token "$TURSO_DB_TOKEN" \
261
- --output-dir ./ltr_dataset
 
 
 
 
 
 
 
 
262
 
263
- # Train (~5 min)
 
264
  python scripts/03_train_lightgbm.py \
265
  --train-file ltr_dataset/train.parquet \
266
  --eval-file ltr_dataset/eval.parquet \
267
- --output-dir ./model_output
 
 
268
  ```
269
 
270
- ### Step 3: Deploy
271
- Copy `model_output/reranker_v1.txt` to your ResearchIT Space.
 
 
272
 
273
  ---
274
 
275
- ## Integration Into ResearchIT
 
 
276
 
277
- The model integrates into `app/recommend/reranker.py` with minimal changes:
278
 
279
  ```python
280
- # Load once at startup
281
  import lightgbm as lgb
282
 
 
283
  _lgb_model = None
284
  try:
285
- _lgb_model = lgb.Booster(model_file="reranker_v1.txt")
286
- print("[reranker] LightGBM model loaded")
287
  except Exception:
288
- print("[reranker] Using heuristic fallback")
289
 
290
  # In rerank_candidates():
291
- features = compute_features_v2(...) # expanded to 37 features
292
  if _lgb_model is not None:
 
293
  scores = _lgb_model.predict(features)
294
  else:
295
- scores = heuristic_score(features) # existing fallback β€” always works
296
  ```
297
 
298
- The heuristic scorer remains as a fallback. If the model file is missing or fails to load, the system silently uses the heuristic. No user-facing impact.
299
-
300
- This is **LightGBM-1** in the Doc 07 multi-stage architecture:
301
- ```
302
- Qdrant ANN β†’ LightGBM-1 (this model) β†’ [TinyBERT β†’ LightGBM-2] (Phase 8b, future)
303
- ```
304
 
305
  ---
306
 
307
- ## Time-Split Evaluation
 
 
 
 
 
 
 
 
308
 
309
- Training data uses a strict temporal split to prevent leakage:
310
- - **Train:** Query papers published before 2023-01-01
311
- - **Eval:** Query papers published on or after 2023-01-01
312
- - **Assertion:** `max(train.year) < min(eval.year)` β€” verified in Script 2
313
 
314
- This means the model never sees future papers citing past papers during training β€” matching how it would perform in production on newly published papers.
 
315
 
316
  ---
317
 
318
- ## Known Limitations
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
- ### Citation β‰  User Interest
321
- Citation pseudo-labels ("this author cited that paper in their bibliography") are different from real user signals ("this user saved that paper in their feed"). A foundational paper like "Attention Is All You Need" gets label=2 in citation data but might be dismissed by users who've already read it.
322
 
323
- **Mitigation:** The `candidate_log_citations` and `candidate_citations_per_year` features help LightGBM learn a popularity penalty. When 500+ real user interactions accumulate, retrain on actual save/dismiss data β€” the 11 user behavior features (20-30) activate and the model learns real preferences.
324
 
325
- ### Synthetic vs Real
326
- The benchmark numbers above are from synthetic data. Real citation data will have:
327
- - Noisier labels (not all citations are "relevant" β€” some are obligatory references)
328
- - Lower nDCG improvement over heuristic (expect +3-5% on real data vs +9.6% on synthetic)
329
- - Different feature importance rankings
 
 
330
 
331
  ---
332
 
333
- ## Dependencies
 
334
  ```
335
  lightgbm>=4.0
336
  httpx>=0.24
@@ -342,12 +386,6 @@ tqdm>=4.65
342
 
343
  ---
344
 
345
- ## References
346
 
347
- - ResearchIT Doc 06 Β§3.1 β€” LightGBM lambdarank architecture decision
348
- - ResearchIT Doc 07 Β§A6 β€” Time-split evaluation protocol
349
- - ResearchIT Doc 07 Β§B.4 β€” Multi-stage reranker architecture
350
- - PinnerSage (Pal et al., KDD 2020) β€” Ward clustering + importance-weighted retrieval
351
- - Taobao ULIM (Meng et al., RecSys 2025) β€” Quota allocation, +5.54% CTR
352
- - YouTube (Xia et al., 2023) β€” 3Γ— gain from negative signals in reranking
353
- - Bruch et al. (SIGIR 2022) β€” RRF optimizes Recall not nDCG
 
1
  # ResearchIT Phase 6 β€” LightGBM Reranker
2
 
3
+ > **Status:** βœ… **Production model trained and evaluated on real citation data.**
4
  > **Parent project:** [siddhm11/ResearchIT](https://huggingface.co/spaces/siddhm11/ResearchIT)
5
+ > **Replaces:** Hand-tuned heuristic scorer in `app/recommend/reranker.py`
6
+ > **Architecture position:** LightGBM-1 in the Doc 07 multi-stage pipeline
7
 
8
  ---
9
 
10
+ ## 🎯 TL;DR
11
 
12
+ A LightGBM lambdarank model that reranks arXiv paper recommendations. Trained on **242K real citation edges** from Semantic Scholar across **1.6M arXiv papers** in the ResearchIT corpus.
13
 
14
+ | Metric | Heuristic | LightGBM | Improvement |
15
+ |--------|:---------:|:--------:|:-----------:|
16
+ | **nDCG@5** | 0.1819 | **0.8250** | **+353.6%** |
17
+ | **nDCG@10** | 0.2641 | **0.8791** | **+232.8%** |
18
+ | **nDCG@20** | 0.3296 | **0.8857** | **+168.7%** |
19
+ | Recall@10 | 0.4384 | **0.9825** | +124.1% |
20
+ | HR@10 | 0.6638 | **1.0000** | +50.6% |
21
+ | MRR | 0.2906 | **0.8795** | +202.7% |
22
 
23
+ **Latency:** 0.371ms per 100 candidates (budget: <1ms) βœ…
24
+ **Model size:** 948 KB
25
+ **Verdict:** βœ… DEPLOY β€” massive improvement across all metrics.
26
 
27
  ---
28
 
29
+ ## πŸ“¦ Repository Contents
30
+
31
+ ```
32
+ researchit-reranker-phase6/
33
+ β”‚
34
+ β”œβ”€β”€ production_model/ ← PRODUCTION ARTIFACTS
35
+ β”‚ β”œβ”€β”€ reranker_v1.txt ← THE MODEL (948 KB, LightGBM text format)
36
+ β”‚ β”œβ”€β”€ eval_metrics.json ← Full benchmark results + training metadata
37
+ β”‚ β”œβ”€β”€ baseline_comparison.json ← LightGBM vs heuristic comparison
38
+ β”‚ β”œβ”€β”€ feature_importance.csv ← All 37 features ranked by gain
39
+ β”‚ └── feature_schema.json ← 37-feature schema definition (ordered)
40
+ β”‚
41
+ β”œβ”€β”€ scripts/ ← REPRODUCIBLE PIPELINE (3 scripts)
42
+ β”‚ β”œβ”€β”€ 01_fetch_citation_edges.py ← S2 API β†’ citations.parquet
43
+ β”‚ β”œβ”€β”€ 02_generate_training_triples.py ← Qdrant ANN + Turso β†’ train/eval.parquet
44
+ β”‚ └── 03_train_lightgbm.py ← LightGBM lambdarank training + eval
45
+ β”‚
46
+ β”œβ”€β”€ synthetic_model/ ← PROOF OF CONCEPT (synthetic data)
47
+ β”‚ β”œβ”€β”€ reranker_v1_synthetic.txt ← Model trained on synthetic data (286 KB)
48
+ β”‚ └── test_results.json ← Synthetic eval results
49
+ β”‚
50
+ β”œβ”€β”€ tests/
51
+ β”‚ └── test_full_pipeline.py ← Comprehensive test suite (6 categories)
52
+ β”‚
53
+ β”œβ”€β”€ INTEGRATION_GUIDE.md ← Step-by-step integration into ResearchIT
54
+ β”œβ”€β”€ CHANGELOG.md ← Version history
55
+ └── README.md ← This file
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 🧠 How It Works
61
 
62
  ### The Problem
63
+
64
  ResearchIT recommends arXiv papers using a multi-stage pipeline:
65
  ```
66
  Qdrant ANN retrieval β†’ Quota fusion β†’ Reranking β†’ MMR diversity β†’ Feed
67
  ```
68
 
69
+ The **reranking** step uses a hand-tuned heuristic scorer with 5 features and fixed weights:
70
  ```python
71
  score = 0.40 Γ— cos(paper, long_term_profile)
72
  + 0.25 Γ— cos(paper, short_term_profile)
 
75
  - 0.15 Γ— cos(paper, negative_profile)
76
  ```
77
 
78
+ This heuristic can't use citation count, co-citation networks, category match, or any feature interactions. The weights are guesses.
79
 
80
  ### The Solution
 
 
 
 
 
81
 
82
+ Train a **LightGBM lambdarank model** on **citation-graph pseudo-labels**:
83
 
84
+ 1. **Each arXiv paper acts as a "pseudo-user"** β€” its bibliography simulates what that researcher would "save"
85
+ 2. **Direct citations β†’ label 2** (strong positive β€” this paper was important enough to cite)
86
+ 3. **Co-citations β†’ label 1** (weak positive β€” papers sharing community context)
87
+ 4. **ANN-retrieved but not cited β†’ label 0** (negative β€” topically related but not worth citing)
88
 
89
+ The model learns: given 37 features about a (user, paper) pair, which papers should rank higher?
90
 
91
+ ### Where It Fits in Doc 07
 
 
 
92
 
93
+ This is **LightGBM-1** in the multi-stage architecture:
 
 
 
 
94
  ```
95
+ Qdrant ANN (Phase 2) β†’ LightGBM-1 (THIS MODEL) β†’ [TinyBERT β†’ LightGBM-2] (Phase 8b, future)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  ```
97
 
98
+ ---
 
 
 
99
 
100
+ ## πŸ“Š Production Results
101
+
102
+ ### Training Data (Real Citation Graph)
103
+
104
+ | Metric | Value |
105
+ |--------|-------|
106
+ | Corpus size | 1,597,097 arXiv papers |
107
+ | Papers sampled for S2 API | 50,000 |
108
+ | In-corpus citation edges | 242,179 |
109
+ | Training rows | 90,993 (1,857 queries, pre-2023) |
110
+ | Eval rows | 7,007 (143 queries, 2023+) |
111
+ | Label distribution | 4.6% direct citation, 0.2% co-citation, 95.1% negative |
112
+ | Time split | Train: pre-2023, Eval: 2023+ (verified: no temporal leakage) |
113
+
114
+ ### Model Training
115
+
116
+ | Parameter | Value |
117
+ |-----------|-------|
118
+ | Objective | lambdarank |
119
+ | Num boost rounds | 500 (early stopped at 141) |
120
+ | Learning rate | 0.05 |
121
+ | Num leaves | 63 |
122
+ | Min data in leaf | 50 |
123
+ | Feature fraction | 0.8 |
124
+ | Bagging fraction | 0.8 |
125
+ | Training time | ~7 minutes |
126
+
127
+ ### Evaluation: LightGBM vs Heuristic Baseline
128
+
129
+ The heuristic baseline uses `qdrant_cosine_score` as proxy for `ewma_longterm_similarity` (since EWMA profiles don't exist for pseudo-users). This is a **fair comparison** β€” both models see the same zero-filled user features.
130
+
131
+ | Metric | Heuristic | LightGBM | Delta | % Improvement |
132
+ |--------|:---------:|:--------:|:-----:|:-------------:|
133
+ | nDCG@5 | 0.1819 | **0.8250** | +0.6432 | **+353.6%** |
134
+ | nDCG@10 | 0.2641 | **0.8791** | +0.6150 | **+232.8%** |
135
+ | nDCG@20 | 0.3296 | **0.8857** | +0.5561 | **+168.7%** |
136
+ | Recall@10 | 0.4384 | **0.9825** | +0.5442 | **+124.1%** |
137
+ | Recall@50 | 1.0000 | 1.0000 | 0.0000 | 0.0% |
138
+ | HR@10 | 0.6638 | **1.0000** | +0.3362 | **+50.6%** |
139
+ | MRR | 0.2906 | **0.8795** | +0.5889 | **+202.7%** |
140
+
141
+ ### Production Readiness
142
+
143
+ | Check | Result | Target | Status |
144
+ |-------|--------|--------|--------|
145
+ | Latency (100 candidates) | 0.371ms | <1ms | βœ… 2.7Γ— under budget |
146
+ | Model size | 948 KB | <2 MB | βœ… |
147
+ | Model reload | Identical predictions | β€” | βœ… |
148
+ | Handles NaN input | Graceful | β€” | βœ… |
149
+ | Handles extreme values | No crash | β€” | βœ… |
150
+ | Best iteration | 141/500 | β€” | βœ… Early stopping healthy |
151
 
152
+ ---
 
 
 
 
 
 
 
153
 
154
+ ## πŸ† Feature Importance (Top 15)
155
+
156
+ | Rank | Feature | Importance | Description |
157
+ |------|---------|:----------:|-------------|
158
+ | 1 | `candidate_num_cited_by` | 75,203 | How many corpus papers cite this candidate |
159
+ | 2 | `age_ratio` | 7,597 | candidate_age / (query_age + 1) |
160
+ | 3 | `candidate_position` | 6,765 | Rank position in ANN results |
161
+ | 4 | `cosine_x_citations` | 2,383 | cosine Γ— log(citations) interaction |
162
+ | 5 | `qdrant_cosine_score` | 2,353 | BGE-M3 cosine similarity |
163
+ | 6 | `candidate_citation_count` | 2,042 | Raw citation count |
164
+ | 7 | `citation_count_ratio` | 2,001 | candidate/query citation ratio |
165
+ | 8 | `query_age_days` | 1,749 | Age of the query paper |
166
+ | 9 | `query_num_references` | 1,726 | How many papers the query cites |
167
+ | 10 | `candidate_citations_per_year` | 1,633 | Citation velocity |
168
+ | 11 | `candidate_influential_citations` | 1,564 | S2 influential citation count |
169
+ | 12 | `query_citation_count` | 1,290 | Query paper's citation count |
170
+ | 13 | `category_x_recency` | 1,188 | category_match Γ— recency interaction |
171
+ | 14 | `citations_x_recency` | 1,143 | log_citations Γ— recency interaction |
172
+ | 15 | `position_inverse` | 1,108 | 1 / (position + 1) |
173
+
174
+ **Key insight:** `candidate_num_cited_by` (how many corpus papers cite this candidate) is the dominant signal β€” 10Γ— more important than any other feature. This is a "corpus-wide popularity" signal that the heuristic cannot access.
175
+
176
+ **User behavior features (20-30):** All 11 have zero importance (correctly β€” they're zero-filled for pseudo-labels). When real user data arrives (500+ interactions), retrain and these features will activate.
177
 
178
  ---
179
 
180
+ ## πŸ”¬ The 37-Feature Schema
 
 
181
 
182
+ ### Content/Retrieval Features (0-19) β€” Active in pseudo-label training
183
 
184
  | # | Feature | Description | Source |
185
  |---|---------|-------------|--------|
 
187
  | 1 | `candidate_position` | Rank position in ANN results (0-indexed) | Qdrant |
188
  | 2 | `candidate_citation_count` | Total citation count | Turso |
189
  | 3 | `candidate_log_citations` | log(citation_count + 1) | Computed |
190
+ | 4 | `candidate_influential_citations` | Influential citation count (S2) | Turso |
191
  | 5 | `candidate_age_days` | Days since publication | Turso |
192
+ | 6 | `candidate_recency_score` | exp(-0.002 Γ— age_days) β€” matches heuristic | Computed |
193
  | 7 | `query_citation_count` | Citation count of the query/user paper | Turso |
194
+ | 8 | `query_age_days` | Days since query paper published | Turso |
195
  | 9 | `year_diff` | |query_year - candidate_year| | Computed |
196
+ | 10 | `same_primary_category` | 1 if same primary arXiv category | Turso |
197
+ | 11 | `co_citation_count` | Papers citing BOTH query and candidate | Citation graph |
198
+ | 12 | `shared_author_count` | Shared authors (case-insensitive) | Turso |
199
  | 13 | `candidate_is_newer` | 1 if candidate published after query | Computed |
200
  | 14 | `query_log_citations` | log(query_citation_count + 1) | Computed |
201
  | 15 | `citation_count_ratio` | candidate_citations / (query_citations + 1) | Computed |
202
  | 16 | `age_ratio` | candidate_age / (query_age + 1) | Computed |
203
  | 17 | `candidate_citations_per_year` | citation_count / max(age_years, 0.5) | Computed |
204
+ | 18 | `query_num_references` | Papers the query cites (in-corpus) | Citation graph |
205
+ | 19 | `candidate_num_cited_by` | Corpus papers that cite the candidate | Citation graph |
206
 
207
  ### User Behavior Features (20-30) β€” Zero-filled for pseudo-labels, active for real users
208
 
209
  | # | Feature | Description | Source in ResearchIT |
210
  |---|---------|-------------|---------------------|
211
+ | 20 | `ewma_longterm_similarity` | cos(candidate, long-term EWMA profile) | `profiles.py` Ξ±=0.03 |
212
+ | 21 | `ewma_shortterm_similarity` | cos(candidate, short-term EWMA profile) | `profiles.py` Ξ±=0.40 |
213
+ | 22 | `ewma_negative_similarity` | cos(candidate, negative EWMA profile) | `profiles.py` Ξ±=0.15 |
214
+ | 23 | `cluster_importance` | Importance weight of serving cluster | `clustering.py` |
215
+ | 24 | `cluster_distance_to_medoid` | cos(candidate, cluster medoid) | `clustering.py` |
216
+ | 25 | `is_suppressed_category` | 1 if category suppressed (β‰₯3 dismissals in 14d) | `db.py` |
217
+ | 26 | `onboarding_category_match` | 1 if matches onboarding selections | `db.py` |
218
+ | 27 | `user_total_saves` | Total papers saved | `interactions` table |
219
+ | 28 | `user_total_dismissals` | Total papers dismissed | `interactions` table |
220
+ | 29 | `user_days_since_last_save` | Days since last save | `interactions` table |
221
  | 30 | `user_session_save_count` | Saves in current session | In-memory state |
222
 
223
+ ### Cross Features (31-36) β€” Interaction terms
224
 
225
  | # | Feature | Formula |
226
  |---|---------|---------|
 
231
  | 35 | `position_inverse` | 1 / (candidate_position + 1) |
232
  | 36 | `citations_x_recency` | candidate_log_citations Γ— candidate_recency_score |
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  ---
235
 
236
+ ## πŸ”„ Reproducing the Pipeline
237
 
238
  ### Prerequisites
239
  ```bash
240
  pip install httpx pyarrow tqdm numpy qdrant-client lightgbm
241
  ```
242
 
243
+ ### Step 1: Export Corpus IDs
244
+ Export arXiv IDs from Turso:
245
  ```sql
246
  SELECT arxiv_id FROM papers;
247
  ```
248
+ Save as `arxiv_ids.txt` (one ID per line). Our corpus: 1,597,097 IDs.
249
 
250
+ ### Step 2: Fetch Citation Edges (~30 min for 50K papers)
251
  ```bash
 
252
  python scripts/01_fetch_citation_edges.py \
253
  --corpus-file arxiv_ids.txt \
254
+ --output citations.parquet \
255
+ --max-papers 50000 # sample for rate limits; remove for full corpus
256
+ ```
257
 
258
+ - Supports checkpoint/resume (safe to interrupt)
259
+ - S2 API key optional but recommended (faster rate limit)
260
+ - Filters to in-corpus edges (both papers must be in Qdrant)
261
+ - Our run: 50K papers β†’ 242,179 in-corpus edges
262
+
263
+ ### Step 3: Generate Training Triples (~80 min)
264
+ ```bash
265
  python scripts/02_generate_training_triples.py \
266
  --citations citations.parquet \
267
  --corpus-file arxiv_ids.txt \
268
  --qdrant-url "$QDRANT_URL" \
269
  --qdrant-api-key "$QDRANT_API_KEY" \
270
+ --qdrant-collection arxiv_bgem3_dense \
271
  --turso-url "$TURSO_URL" \
272
  --turso-token "$TURSO_DB_TOKEN" \
273
+ --output-dir ./ltr_dataset \
274
+ --num-queries 2000 \
275
+ --candidates-per-query 50
276
+ ```
277
+
278
+ - Enforces time-split: train on pre-2023, eval on 2023+
279
+ - Asserts no temporal leakage: `max(train.year) < min(eval.year)`
280
+ - Uses `scroll()` + `query_points()` for qdrant-client 1.17+
281
+ - Our run: 90,993 train rows + 7,007 eval rows
282
 
283
+ ### Step 4: Train Model (~5 min)
284
+ ```bash
285
  python scripts/03_train_lightgbm.py \
286
  --train-file ltr_dataset/train.parquet \
287
  --eval-file ltr_dataset/eval.parquet \
288
+ --output-dir ./model_output \
289
+ --num-boost-round 500 \
290
+ --learning-rate 0.05
291
  ```
292
 
293
+ - Evaluates nDCG@5/10/20, Recall@10/50, HR@10, MRR
294
+ - Compares LightGBM vs exact heuristic baseline
295
+ - Reports feature importance, latency benchmark, per-query win rates
296
+ - Our run: early stopped at iteration 141, 948 KB model
297
 
298
  ---
299
 
300
+ ## πŸ”Œ Integration Into ResearchIT
301
+
302
+ See [INTEGRATION_GUIDE.md](INTEGRATION_GUIDE.md) for the complete step-by-step guide.
303
 
304
+ **Quick summary** β€” add to `app/recommend/reranker.py`:
305
 
306
  ```python
 
307
  import lightgbm as lgb
308
 
309
+ # Load once at startup
310
  _lgb_model = None
311
  try:
312
+ _lgb_model = lgb.Booster(model_file="production_model/reranker_v1.txt")
313
+ print("[reranker] LightGBM model loaded (948 KB)")
314
  except Exception:
315
+ print("[reranker] LightGBM unavailable β€” using heuristic fallback")
316
 
317
  # In rerank_candidates():
 
318
  if _lgb_model is not None:
319
+ features = compute_features_v2(user, candidates) # 37-dim feature vector
320
  scores = _lgb_model.predict(features)
321
  else:
322
+ scores = heuristic_score(candidates) # existing fallback
323
  ```
324
 
325
+ The heuristic scorer remains as a permanent fallback. If the model file is missing or fails to load, the system silently uses the heuristic. No user-facing impact.
 
 
 
 
 
326
 
327
  ---
328
 
329
+ ## ⚠️ Known Limitations
330
+
331
+ ### Citation β‰  User Interest
332
+ Citation pseudo-labels ("cited in bibliography") β‰  real user signals ("saved in feed"). A foundational paper like "Attention Is All You Need" gets label=2 in citation data but might be dismissed by users who've already read it.
333
+
334
+ **Mitigation:** The `candidate_log_citations` and `candidate_citations_per_year` features help learn a popularity curve. When 500+ real user interactions accumulate, retrain on actual save/dismiss data β€” the 11 user behavior features (20-30) activate and the model learns real preferences.
335
+
336
+ ### Sampled Corpus (50K of 1.6M)
337
+ We sampled 50K papers for S2 API calls due to rate limiting, yielding 242K in-corpus edges. The full corpus would produce ~8-10M edges with a valid API key or S2 bulk download. More edges β†’ more training data β†’ better model.
338
 
339
+ ### S2 API Key
340
+ The provided API key returned 403 Forbidden. We ran unauthenticated at ~1.5s delay per request. A working key or the S2 bulk dataset download would be significantly faster.
 
 
341
 
342
+ ### Pseudo-Label Heuristic Baseline
343
+ The heuristic baseline uses `qdrant_cosine_score` as proxy for `ewma_longterm_similarity` (feature 20) since real EWMA profiles don't exist for pseudo-users. This is fair but means the heuristic baseline nDCG (0.264) is lower than what the real heuristic achieves in production with actual user profiles.
344
 
345
  ---
346
 
347
+ ## πŸ—ΊοΈ Roadmap
348
+
349
+ | Step | Description | Status |
350
+ |------|-------------|--------|
351
+ | ~~1. Citation edges~~ | S2 API scraping | βœ… Done (242K edges) |
352
+ | ~~2. Training triples~~ | Qdrant ANN + Turso β†’ labeled data | βœ… Done (98K rows) |
353
+ | ~~3. LightGBM training~~ | lambdarank + eval | βœ… Done (nDCG@10: 0.879) |
354
+ | ~~4. Synthetic testing~~ | Test suite on synthetic data | βœ… Done (6 categories pass) |
355
+ | 5. `compute_features()` expansion | 5β†’37 features in reranker.py | πŸ”œ Next (Opus) |
356
+ | 6. Model loading + fallback | Wire `lgb.Booster` into reranker | πŸ”œ Next (Opus) |
357
+ | 7. `requirements.txt` update | Add `lightgbm>=4.0` | πŸ”œ Next (Opus) |
358
+ | 8. Integration testing + deploy | End-to-end verification | πŸ”œ Next (Opus) |
359
+ | 9. Real user data retrain | 500+ interactions β†’ retrain with features 20-30 | Future |
360
+ | 10. Phase 8b: TinyBERT + LightGBM-2 | Cross-encoder reranker stage | Future |
361
 
362
+ ---
 
363
 
364
+ ## πŸ“š References
365
 
366
+ - **ResearchIT Doc 06 Β§3.1** β€” LightGBM lambdarank architecture decision
367
+ - **ResearchIT Doc 07 Β§A6** β€” Time-split evaluation protocol
368
+ - **ResearchIT Doc 07 Β§B.4** β€” Multi-stage reranker architecture (LightGBM-1 β†’ TinyBERT β†’ LightGBM-2)
369
+ - **PinnerSage** (Pal et al., KDD 2020) β€” Ward clustering + importance-weighted retrieval
370
+ - **Taobao ULIM** (Meng et al., RecSys 2025) β€” Quota allocation, +5.54% CTR
371
+ - **YouTube DNN** (Xia et al., 2023) β€” 3Γ— gain from negative signals in reranking
372
+ - **RRF Analysis** (Bruch et al., SIGIR 2022) β€” RRF optimizes Recall not nDCG
373
 
374
  ---
375
 
376
+ ## πŸ› οΈ Dependencies
377
+
378
  ```
379
  lightgbm>=4.0
380
  httpx>=0.24
 
386
 
387
  ---
388
 
389
+ ## πŸ“„ License
390
 
391
+ This model and pipeline are part of the ResearchIT project by [@siddhm11](https://huggingface.co/siddhm11).