Reranking benchmark¶
What does a second stage buy a production pipeline, and does blending several of them buy
more? This compares the Switch pipelines of composing recommenders
on the users a service really has: ones it knows, and ones arriving with no history.
BM25isSwitch(KnownUser(), BM25Recommender(), MostPopularRecommender()): BM25 for known users, the most popular items for everyone else.BM25+CatBoostis the same switch with aCatBoostRankerbehind each branch. Known users get BM25's top 100, reordered on who the user is (age, gender, occupation), what the movie is (genres), the BM25 score, how many interactions the user and the movie have (InteractionCounts), EASE's score for the pair (RecommenderScores) and how popular the movie is among users of the same gender, age band and occupation (SegmentPopularity). Cold users get the 100 most popular movies, reordered on popularity and segment popularity, which is all there is to know about someone with no history. That ranker is fitted on users thatColdStartSplitholds out whole insideCascade.fit, so it learns from candidates exactly as a cold user is served them.BM25+XGBoostandBM25+LightGBMare the same pipeline withXGBRankerorLGBMRankerin place of CatBoost, on exactly the same features. Both are regularized, with a lower learning rate and larger minimum leaves. At their defaults (300 trees at a learning rate of 0.1), both overfit the few hundred users a ranker learns from. That reached 0.166 and 0.178 NDCG@10 on warm users, and fell below the popularity baseline on cold users (0.469 and 0.451).BM25+Blendputs aBlendRankerover all three boosters behind each branch, at its defaults. Each booster is fitted three times on two thirds of the users and scores the third it did not see. A logistic regression then learns how to weigh their rank-normalized scores, and the three are refitted on every user for serving.EASEis the baseline switch withEASEin place of BM25, the second member of the fusions below, shown on its own so a fusion can be compared with each of its members.BM25+EASE RRFserves known users withReciprocalRankFusionof BM25 and EASE: each retrieves its top 100, and an item scores1/(60 + rank)summed over the two lists. There is no ranker and nothing is learned. Cold users get the most popular movies, as in the baseline.BM25+RRFis theBM25+CatBoostswitch with a training-freeReciprocalRankRankerin place of CatBoost. Known users get BM25's top 100, reordered by the fused ranks of their BM25 and EASE scores. Cold users get the 100 most popular movies, reordered by the fused ranks of popularity and segment popularity.BM25+RRF boostersis theBM25+Blendswitch with the three boosters fused byReciprocalRankRankerinstead of stacked byBlendRanker. Each booster is fitted once on every user. There are no out-of-fold refits and no blender to learn: three booster fits per branch, against the blend's twelve.
The split is ColdStartSplit, so the held-out set asks both questions at once, and every
quality row is reported three times: for all held-out users, for the warm ones and for the
cold ones. Time is reported one way only, as what one user's request costs, since that is
what a reranker adds.
uv sync --extra catboost --extra xgboost --extra lightgbm
uv run python benchmarks/run.py run reranking # measure it, re-render the tables below
MovieLens 100K¶
MovieLens 100K, ColdStartSplit: 10% of users held out whole, the latest 20% of every other user's ratings held out, k=10. Regenerate with python benchmarks/run.py run reranking --dataset movielens-100k-cold.
| Pipeline | users | n users | NDCG@10 | P@10 | R@10 | hit rate | MAP | MRR | cat cov | mean pop | novelty |
|---|---|---|---|---|---|---|---|---|---|---|---|
| BM25 | all | 943 | 0.2057 | 0.1832 | 0.1004 | 0.6914 | 0.1154 | 0.3642 | 0.1039 | 270.9265 | 8.18 |
| BM25+CatBoost | all | 943 | 0.2395 | 0.2121 | 0.1183 | 0.7656 | 0.1365 | 0.4085 | 0.2611 | 227.8753 | 8.53 |
| BM25+XGBoost | all | 943 | 0.2326 | 0.2059 | 0.1152 | 0.7593 | 0.1289 | 0.4107 | 0.2780 | 214.6402 | 8.65 |
| BM25+LightGBM | all | 943 | 0.2315 | 0.2030 | 0.1138 | 0.7476 | 0.1296 | 0.4090 | 0.2837 | 214.7716 | 8.65 |
| BM25+Blend | all | 943 | 0.2352 | 0.2047 | 0.1153 | 0.7497 | 0.1330 | 0.4132 | 0.2780 | 219.2950 | 8.61 |
| EASE | all | 943 | 0.2210 | 0.1910 | 0.1070 | 0.7349 | 0.1234 | 0.4038 | 0.3062 | 221.6990 | 8.58 |
| BM25+EASE RRF | all | 943 | 0.2200 | 0.1945 | 0.1080 | 0.7158 | 0.1245 | 0.3859 | 0.1778 | 252.5215 | 8.32 |
| BM25+RRF | all | 943 | 0.2205 | 0.1943 | 0.1062 | 0.7126 | 0.1277 | 0.3874 | 0.1653 | 252.2036 | 8.31 |
| BM25+RRF boosters | all | 943 | 0.2387 | 0.2092 | 0.1175 | 0.7667 | 0.1341 | 0.4193 | 0.2743 | 219.9035 | 8.60 |
| BM25 | warm | 849 | 0.1701 | 0.1468 | 0.1010 | 0.6596 | 0.0872 | 0.3242 | 0.1039 | 256.4918 | 8.26 |
| BM25+CatBoost | warm | 849 | 0.2052 | 0.1775 | 0.1217 | 0.7420 | 0.1068 | 0.3709 | 0.2611 | 209.6465 | 8.64 |
| BM25+XGBoost | warm | 849 | 0.1994 | 0.1723 | 0.1185 | 0.7350 | 0.1008 | 0.3719 | 0.2780 | 196.1783 | 8.76 |
| BM25+LightGBM | warm | 849 | 0.1979 | 0.1688 | 0.1168 | 0.7232 | 0.1013 | 0.3708 | 0.2837 | 195.8178 | 8.77 |
| BM25+Blend | warm | 849 | 0.2013 | 0.1710 | 0.1187 | 0.7267 | 0.1038 | 0.3739 | 0.2780 | 200.4595 | 8.72 |
| EASE | warm | 849 | 0.1871 | 0.1554 | 0.1084 | 0.7079 | 0.0961 | 0.3682 | 0.3062 | 201.8139 | 8.70 |
| BM25+EASE RRF | warm | 849 | 0.1860 | 0.1592 | 0.1095 | 0.6867 | 0.0973 | 0.3483 | 0.1778 | 236.0490 | 8.41 |
| BM25+RRF | warm | 849 | 0.1847 | 0.1577 | 0.1087 | 0.6867 | 0.0965 | 0.3478 | 0.1653 | 238.6967 | 8.39 |
| BM25+RRF boosters | warm | 849 | 0.2051 | 0.1755 | 0.1209 | 0.7444 | 0.1053 | 0.3816 | 0.2743 | 201.1066 | 8.72 |
| BM25 | cold | 94 | 0.5273 | 0.5128 | 0.0945 | 0.9787 | 0.3695 | 0.7252 | 0.0063 | 401.3000 | 7.54 |
| BM25+CatBoost | cold | 94 | 0.5486 | 0.5245 | 0.0872 | 0.9787 | 0.4048 | 0.7480 | 0.0125 | 392.5160 | 7.58 |
| BM25+XGBoost | cold | 94 | 0.5323 | 0.5096 | 0.0854 | 0.9787 | 0.3826 | 0.7612 | 0.0194 | 381.3862 | 7.62 |
| BM25+LightGBM | cold | 94 | 0.5352 | 0.5117 | 0.0861 | 0.9681 | 0.3854 | 0.7538 | 0.0182 | 385.9606 | 7.61 |
| BM25+Blend | cold | 94 | 0.5411 | 0.5085 | 0.0848 | 0.9574 | 0.3962 | 0.7681 | 0.0144 | 389.4160 | 7.59 |
| EASE | cold | 94 | 0.5273 | 0.5128 | 0.0945 | 0.9787 | 0.3695 | 0.7252 | 0.0063 | 401.3000 | 7.54 |
| BM25+EASE RRF | cold | 94 | 0.5273 | 0.5128 | 0.0945 | 0.9787 | 0.3695 | 0.7252 | 0.0063 | 401.3000 | 7.54 |
| BM25+RRF | cold | 94 | 0.5440 | 0.5245 | 0.0837 | 0.9468 | 0.4096 | 0.7451 | 0.0244 | 374.1968 | 7.66 |
| BM25+RRF boosters | cold | 94 | 0.5428 | 0.5138 | 0.0868 | 0.9681 | 0.3947 | 0.7602 | 0.0175 | 389.6755 | 7.59 |
Measured on Apple M4 Pro (12 usable cores), macOS-27.0-arm64-arm-64bit-Mach-O, CPython 3.14.3, numpy 2.5.3, skrecsys 0.5.0, _core built in release mode. The quality table above is deterministic and portable; the timings below are not comparable across machines or builds.
Each row below is one call of recommend for a single user, the way a serving process sees a request: each request asks for the next user of its segment, so the samples cover every warm or cold user rather than one user's cached history. Sampled until 10s or 1,000 requests per row, after 10 untimed ones; q95 and q99 are nearest-rank quantiles, so each is a request that really happened.
| Pipeline | users | requests | median | q95 | q99 |
|---|---|---|---|---|---|
| BM25 | warm | 1,000 | 21 us | 27 us | 34 us |
| BM25+CatBoost | warm | 1,000 | 666 us | 858 us | 1.00 ms |
| BM25+XGBoost | warm | 1,000 | 800 us | 1.07 ms | 1.21 ms |
| BM25+LightGBM | warm | 1,000 | 682 us | 919 us | 1.07 ms |
| BM25+Blend | warm | 1,000 | 1.55 ms | 1.77 ms | 1.93 ms |
| EASE | warm | 1,000 | 32 us | 83 us | 117 us |
| BM25+EASE RRF | warm | 1,000 | 240 us | 328 us | 387 us |
| BM25+RRF | warm | 1,000 | 358 us | 573 us | 682 us |
| BM25+RRF boosters | warm | 1,000 | 1.49 ms | 1.74 ms | 1.97 ms |
| BM25 | cold | 1,000 | 24 us | 26 us | 35 us |
| BM25+CatBoost | cold | 1,000 | 392 us | 446 us | 486 us |
| BM25+XGBoost | cold | 1,000 | 565 us | 751 us | 845 us |
| BM25+LightGBM | cold | 1,000 | 443 us | 506 us | 567 us |
| BM25+Blend | cold | 1,000 | 1.28 ms | 1.41 ms | 1.49 ms |
| EASE | cold | 1,000 | 24 us | 26 us | 34 us |
| BM25+EASE RRF | cold | 1,000 | 26 us | 33 us | 48 us |
| BM25+RRF | cold | 1,000 | 314 us | 418 us | 588 us |
| BM25+RRF boosters | cold | 1,000 | 1.20 ms | 1.33 ms | 1.42 ms |
The first 3 cold users by id, and the top 5 of what each pipeline serves them. Bold titles are ones the user rated in the held-out set; held out is how many they rated in all.
| cold user | age, gender, occupation | held out | BM25 | BM25+CatBoost | BM25+XGBoost | BM25+LightGBM | BM25+Blend | EASE | BM25+EASE RRF | BM25+RRF | BM25+RRF boosters |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 9 | 29, M, student | 22 | Star Wars (1977) Fargo (1996) Return of the Jedi (1983) Contact (1997) Liar Liar (1997) |
Star Wars (1977) Contact (1997) Return of the Jedi (1983) Fargo (1996) Independence Day (ID4) (1996) |
Star Wars (1977) English Patient, The (1996) Contact (1997) Independence Day (ID4) (1996) Godfather, The (1972) |
Star Wars (1977) Contact (1997) Independence Day (ID4) (1996) Godfather, The (1972) Scream (1996) |
Star Wars (1977) Contact (1997) Independence Day (ID4) (1996) Liar Liar (1997) Fargo (1996) |
Star Wars (1977) Fargo (1996) Return of the Jedi (1983) Contact (1997) Liar Liar (1997) |
Star Wars (1977) Fargo (1996) Return of the Jedi (1983) Contact (1997) Liar Liar (1997) |
Twelve Monkeys (1995) Return of the Jedi (1983) Star Wars (1977) Toy Story (1995) Scream (1996) |
Star Wars (1977) Contact (1997) Independence Day (ID4) (1996) Godfather, The (1972) Twelve Monkeys (1995) |
| 15 | 49, F, educator | 104 | Star Wars (1977) Fargo (1996) Return of the Jedi (1983) Contact (1997) Liar Liar (1997) |
English Patient, The (1996) Star Wars (1977) Fargo (1996) Air Force One (1997) Contact (1997) |
English Patient, The (1996) Air Force One (1997) Fargo (1996) Contact (1997) Star Wars (1977) |
English Patient, The (1996) Air Force One (1997) Contact (1997) Star Wars (1977) Fargo (1996) |
English Patient, The (1996) Fargo (1996) Air Force One (1997) Star Wars (1977) Contact (1997) |
Star Wars (1977) Fargo (1996) Return of the Jedi (1983) Contact (1997) Liar Liar (1997) |
Star Wars (1977) Fargo (1996) Return of the Jedi (1983) Contact (1997) Liar Liar (1997) |
English Patient, The (1996) Full Monty, The (1997) Fargo (1996) Air Force One (1997) Star Wars (1977) |
English Patient, The (1996) Air Force One (1997) Star Wars (1977) Fargo (1996) Contact (1997) |
| 32 | 28, F, student | 41 | Star Wars (1977) Fargo (1996) Return of the Jedi (1983) Contact (1997) Liar Liar (1997) |
Contact (1997) Liar Liar (1997) Star Wars (1977) Scream (1996) Toy Story (1995) |
English Patient, The (1996) Contact (1997) Liar Liar (1997) Scream (1996) Fargo (1996) |
English Patient, The (1996) Contact (1997) Scream (1996) Star Wars (1977) Liar Liar (1997) |
Contact (1997) Liar Liar (1997) English Patient, The (1996) Scream (1996) Star Wars (1977) |
Star Wars (1977) Fargo (1996) Return of the Jedi (1983) Contact (1997) Liar Liar (1997) |
Star Wars (1977) Fargo (1996) Return of the Jedi (1983) Contact (1997) Liar Liar (1997) |
Scream (1996) Star Wars (1977) Toy Story (1995) Liar Liar (1997) Return of the Jedi (1983) |
Contact (1997) English Patient, The (1996) Liar Liar (1997) Scream (1996) Star Wars (1977) |
MovieLens 1M¶
The same pipelines and settings on MovieLens 1M, split the same way: 5,436 warm users and 604 cold ones, six times as many as MovieLens 100K has, so differences that are noise there can be told apart here.
MovieLens 1M, ColdStartSplit: 10% of users held out whole, the latest 20% of every other user's ratings held out, k=10. Regenerate with python benchmarks/run.py run reranking --dataset movielens-1m-cold.
| Pipeline | users | n users | NDCG@10 | P@10 | R@10 | hit rate | MAP | MRR | cat cov | mean pop | novelty |
|---|---|---|---|---|---|---|---|---|---|---|---|
| BM25 | all | 6,040 | 0.1725 | 0.1583 | 0.0613 | 0.6101 | 0.0979 | 0.3090 | 0.1092 | 1596.1259 | 8.94 |
| BM25+CatBoost | all | 6,040 | 0.1916 | 0.1744 | 0.0734 | 0.6662 | 0.1087 | 0.3431 | 0.2165 | 1492.6599 | 9.09 |
| BM25+XGBoost | all | 6,040 | 0.1871 | 0.1686 | 0.0726 | 0.6781 | 0.1034 | 0.3469 | 0.2334 | 1411.7101 | 9.21 |
| BM25+LightGBM | all | 6,040 | 0.1899 | 0.1725 | 0.0740 | 0.6879 | 0.1049 | 0.3476 | 0.2373 | 1386.8866 | 9.24 |
| BM25+Blend | all | 6,040 | 0.1927 | 0.1753 | 0.0744 | 0.6815 | 0.1082 | 0.3483 | 0.2239 | 1465.7283 | 9.13 |
| EASE | all | 6,040 | 0.1670 | 0.1515 | 0.0649 | 0.6333 | 0.0919 | 0.3097 | 0.3519 | 1245.7657 | 9.44 |
| BM25+EASE RRF | all | 6,040 | 0.1763 | 0.1610 | 0.0661 | 0.6411 | 0.0984 | 0.3213 | 0.1814 | 1444.2125 | 9.13 |
| BM25+RRF | all | 6,040 | 0.1768 | 0.1628 | 0.0656 | 0.6379 | 0.0993 | 0.3159 | 0.1609 | 1459.5701 | 9.10 |
| BM25+RRF boosters | all | 6,040 | 0.1924 | 0.1741 | 0.0741 | 0.6839 | 0.1073 | 0.3517 | 0.2313 | 1439.3425 | 9.17 |
| BM25 | warm | 5,436 | 0.1379 | 0.1249 | 0.0634 | 0.5747 | 0.0685 | 0.2636 | 0.1092 | 1518.0176 | 9.01 |
| BM25+CatBoost | warm | 5,436 | 0.1572 | 0.1401 | 0.0766 | 0.6365 | 0.0787 | 0.3027 | 0.2165 | 1412.7076 | 9.17 |
| BM25+XGBoost | warm | 5,436 | 0.1529 | 0.1346 | 0.0758 | 0.6501 | 0.0737 | 0.3058 | 0.2334 | 1325.7490 | 9.30 |
| BM25+LightGBM | warm | 5,436 | 0.1561 | 0.1388 | 0.0773 | 0.6613 | 0.0754 | 0.3066 | 0.2373 | 1300.2331 | 9.33 |
| BM25+Blend | warm | 5,436 | 0.1585 | 0.1411 | 0.0777 | 0.6538 | 0.0783 | 0.3086 | 0.2239 | 1383.5051 | 9.21 |
| EASE | warm | 5,436 | 0.1318 | 0.1173 | 0.0674 | 0.6004 | 0.0618 | 0.2644 | 0.3519 | 1128.7286 | 9.57 |
| BM25+EASE RRF | warm | 5,436 | 0.1422 | 0.1279 | 0.0687 | 0.6091 | 0.0690 | 0.2773 | 0.1814 | 1349.2249 | 9.22 |
| BM25+RRF | warm | 5,436 | 0.1424 | 0.1281 | 0.0680 | 0.6054 | 0.0696 | 0.2777 | 0.1609 | 1379.4854 | 9.18 |
| BM25+RRF boosters | warm | 5,436 | 0.1585 | 0.1403 | 0.0774 | 0.6560 | 0.0778 | 0.3117 | 0.2313 | 1355.6555 | 9.25 |
| BM25 | cold | 604 | 0.4833 | 0.4589 | 0.0421 | 0.9288 | 0.3626 | 0.7173 | 0.0027 | 2299.1000 | 8.31 |
| BM25+CatBoost | cold | 604 | 0.5010 | 0.4829 | 0.0452 | 0.9338 | 0.3779 | 0.7065 | 0.0096 | 2212.2300 | 8.37 |
| BM25+XGBoost | cold | 604 | 0.4953 | 0.4750 | 0.0441 | 0.9305 | 0.3708 | 0.7171 | 0.0112 | 2185.3604 | 8.39 |
| BM25+LightGBM | cold | 604 | 0.4947 | 0.4755 | 0.0443 | 0.9272 | 0.3698 | 0.7165 | 0.0120 | 2166.7677 | 8.40 |
| BM25+Blend | cold | 604 | 0.5006 | 0.4828 | 0.0450 | 0.9305 | 0.3776 | 0.7050 | 0.0096 | 2205.7369 | 8.37 |
| EASE | cold | 604 | 0.4833 | 0.4589 | 0.0421 | 0.9288 | 0.3626 | 0.7173 | 0.0027 | 2299.1000 | 8.31 |
| BM25+EASE RRF | cold | 604 | 0.4833 | 0.4589 | 0.0421 | 0.9288 | 0.3626 | 0.7173 | 0.0027 | 2299.1000 | 8.31 |
| BM25+RRF | cold | 604 | 0.4858 | 0.4753 | 0.0438 | 0.9305 | 0.3672 | 0.6595 | 0.0107 | 2180.3326 | 8.39 |
| BM25+RRF boosters | cold | 604 | 0.4976 | 0.4781 | 0.0444 | 0.9354 | 0.3734 | 0.7116 | 0.0099 | 2192.5253 | 8.38 |
Measured on Apple M4 Pro (12 usable cores), macOS-27.0-arm64-arm-64bit-Mach-O, CPython 3.14.3, numpy 2.5.3, skrecsys 0.5.0, _core built in release mode. The quality table above is deterministic and portable; the timings below are not comparable across machines or builds.
Each row below is one call of recommend for a single user, the way a serving process sees a request: each request asks for the next user of its segment, so the samples cover every warm or cold user rather than one user's cached history. Sampled until 10s or 1,000 requests per row, after 10 untimed ones; q95 and q99 are nearest-rank quantiles, so each is a request that really happened.
| Pipeline | users | requests | median | q95 | q99 |
|---|---|---|---|---|---|
| BM25 | warm | 1,000 | 25 us | 44 us | 57 us |
| BM25+CatBoost | warm | 1,000 | 785 us | 1.38 ms | 1.85 ms |
| BM25+XGBoost | warm | 1,000 | 901 us | 1.48 ms | 2.07 ms |
| BM25+LightGBM | warm | 1,000 | 779 us | 1.33 ms | 1.82 ms |
| BM25+Blend | warm | 1,000 | 1.63 ms | 2.26 ms | 2.80 ms |
| EASE | warm | 1,000 | 71 us | 334 us | 542 us |
| BM25+EASE RRF | warm | 1,000 | 288 us | 617 us | 869 us |
| BM25+RRF | warm | 1,000 | 388 us | 895 us | 1.41 ms |
| BM25+RRF boosters | warm | 1,000 | 1.55 ms | 2.15 ms | 2.76 ms |
| BM25 | cold | 1,000 | 28 us | 33 us | 72 us |
| BM25+CatBoost | cold | 1,000 | 406 us | 465 us | 503 us |
| BM25+XGBoost | cold | 1,000 | 592 us | 714 us | 843 us |
| BM25+LightGBM | cold | 1,000 | 430 us | 504 us | 562 us |
| BM25+Blend | cold | 1,000 | 1.24 ms | 1.34 ms | 1.48 ms |
| EASE | cold | 1,000 | 28 us | 29 us | 36 us |
| BM25+EASE RRF | cold | 1,000 | 28 us | 29 us | 36 us |
| BM25+RRF | cold | 1,000 | 317 us | 332 us | 379 us |
| BM25+RRF boosters | cold | 1,000 | 1.16 ms | 1.27 ms | 1.34 ms |
The first 3 cold users by id, and the top 5 of what each pipeline serves them. Bold titles are ones the user rated in the held-out set; held out is how many they rated in all.
| cold user | age, gender, occupation | held out | BM25 | BM25+CatBoost | BM25+XGBoost | BM25+LightGBM | BM25+Blend | EASE | BM25+EASE RRF | BM25+RRF | BM25+RRF boosters |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 2 | 56, M, 16 | 129 | American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Star Wars: Episode VI - Return of the Jedi (1983) Jurassic Park (1993) |
American Beauty (1999) Star Wars: Episode IV - A New Hope (1977) Saving Private Ryan (1998) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode VI - Return of the Jedi (1983) |
American Beauty (1999) Fargo (1996) Jurassic Park (1993) Saving Private Ryan (1998) Star Wars: Episode V - The Empire Strikes Back (1980) |
American Beauty (1999) L.A. Confidential (1997) Fargo (1996) Silence of the Lambs, The (1991) Star Wars: Episode V - The Empire Strikes Back (1980) |
American Beauty (1999) Star Wars: Episode IV - A New Hope (1977) Saving Private Ryan (1998) Star Wars: Episode V - The Empire Strikes Back (1980) Fargo (1996) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Star Wars: Episode VI - Return of the Jedi (1983) Jurassic Park (1993) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Star Wars: Episode VI - Return of the Jedi (1983) Jurassic Park (1993) |
American Beauty (1999) Star Wars: Episode IV - A New Hope (1977) Saving Private Ryan (1998) Jurassic Park (1993) Fargo (1996) |
American Beauty (1999) Fargo (1996) Star Wars: Episode V - The Empire Strikes Back (1980) Jurassic Park (1993) Saving Private Ryan (1998) |
| 13 | 45, M, 1 | 108 | American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Star Wars: Episode VI - Return of the Jedi (1983) Jurassic Park (1993) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Jurassic Park (1993) Star Wars: Episode VI - Return of the Jedi (1983) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Jurassic Park (1993) Star Wars: Episode IV - A New Hope (1977) Fargo (1996) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Jurassic Park (1993) Fargo (1996) Terminator 2: Judgment Day (1991) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Jurassic Park (1993) Star Wars: Episode VI - Return of the Jedi (1983) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Star Wars: Episode VI - Return of the Jedi (1983) Jurassic Park (1993) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Star Wars: Episode VI - Return of the Jedi (1983) Jurassic Park (1993) |
Star Wars: Episode IV - A New Hope (1977) Star Wars: Episode V - The Empire Strikes Back (1980) American Beauty (1999) Jurassic Park (1993) Shakespeare in Love (1998) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Jurassic Park (1993) Star Wars: Episode IV - A New Hope (1977) Fargo (1996) |
| 15 | 25, M, 7 | 201 | American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Star Wars: Episode VI - Return of the Jedi (1983) Jurassic Park (1993) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode VI - Return of the Jedi (1983) Star Wars: Episode IV - A New Hope (1977) Terminator 2: Judgment Day (1991) |
American Beauty (1999) Star Wars: Episode VI - Return of the Jedi (1983) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Matrix, The (1999) |
American Beauty (1999) Star Wars: Episode VI - Return of the Jedi (1983) Star Wars: Episode IV - A New Hope (1977) Star Wars: Episode V - The Empire Strikes Back (1980) Matrix, The (1999) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode VI - Return of the Jedi (1983) Star Wars: Episode IV - A New Hope (1977) Terminator 2: Judgment Day (1991) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Star Wars: Episode VI - Return of the Jedi (1983) Jurassic Park (1993) |
American Beauty (1999) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Star Wars: Episode VI - Return of the Jedi (1983) Jurassic Park (1993) |
Terminator 2: Judgment Day (1991) Star Wars: Episode IV - A New Hope (1977) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode VI - Return of the Jedi (1983) Raiders of the Lost Ark (1981) |
American Beauty (1999) Star Wars: Episode VI - Return of the Jedi (1983) Star Wars: Episode V - The Empire Strikes Back (1980) Star Wars: Episode IV - A New Hope (1977) Matrix, The (1999) |
What the rankers buy¶
On MovieLens 100K, every trained reranker beats the baseline on every segment, and MovieLens 1M
agrees. The paragraphs below follow CatBoost on MovieLens 100K,
the strongest of them. For the 849 warm users, NDCG@10 rises from 0.170 to
0.205, a fifth better, and every other ranking metric moves with it: hit rate from 0.66 to
0.74, MAP from 0.087 to 0.107. It also recommends less of the head. Catalog coverage goes from
a tenth to a quarter and mean pop drops from 256 to 210, because BM25's top 100 holds
plenty of less popular films that the ranker's other signals can lift into the top 10. The
largest single gain in tuning came from RecommenderScores(EASE()). A second model with a
different inductive bias gives the ranker something to weigh BM25's score against. The
demographic columns on their own added little.
The 94 cold users are a harder case. Their baseline is already strong, because MovieLens users rate the popular films, and a cold user's whole history is held out. NDCG@10 still rises from 0.527 to 0.549, MAP from 0.370 to 0.405 and MRR from 0.725 to 0.748. Recall@10 falls slightly, from 0.094 to 0.087, so the reranker puts the hits it finds higher rather than finding more of them. The examples show the mechanism: the popularity baseline serves every cold user the same five films, while the reranker gives the 49-year-old educator The English Patient and Air Force One and the 28-year-old student Scream. The segment features only work because of how the ranker is trained. Trained on warm users, whose candidates have their own history removed, the same features scored below the popularity baseline during development: the ranker learned from candidate lists that no cold user ever gets.
The blend does not beat CatBoost here. It comes second on NDCG@10 in every segment: 0.201
on warm users against CatBoost's 0.205, and 0.541 on cold users against 0.549. It is ahead
of XGBoost (0.199, 0.532) and LightGBM (0.198, 0.535). The spread between the three
boosters is small, and they learn from identical features, so they mostly agree and there is
little for a blender to combine. Other variants did no better during tuning. Z-score
normalization, five folds and a plain average (blender=None) came within 0.003 of CatBoost
on warm users and 0.003–0.011 under it on cold users. Passing the original features to the
logistic blender, or using a gradient-boosted blender, did worse. None of them beat CatBoost
alone. What the blend buys
here is not having to know in advance which booster wins, and it charges for that. Its
request runs all three boosters, so it costs 1.6 ms on a warm user against CatBoost's
0.67 ms. Blending pays when its members disagree, for example when they are trained on
different features or different candidate sources, and this setup does not give them that.
The price of any reranker is latency. With CatBoost, a warm user's request goes from about
20 µs to about 0.67 ms at the median and 1 ms at q99, and a cold user's to about 0.4 ms. BM25 alone answers in 21 µs, so
at least 95% of the reranked request is the second stage: featurizing 100 candidates and
running the tree ensemble over them. That fits comfortably in a request budget, but a service
ranking per request should know where its time goes.
Reciprocal rank fusion¶
Fusing two recommenders gets the better of them without having to know which one it is.
Which of BM25 and EASE is stronger depends on the dataset: EASE on MovieLens 100K (0.187 NDCG@10
on warm users against 0.170), BM25 on MovieLens 1M (0.138 against 0.132). BM25+EASE RRF, which
learns nothing, is never measurably worse than the stronger one, and on MovieLens 1M it beats
both, at 0.142. A paired bootstrap over users (2,000 resamples) puts the gains at:
| Warm users | RRF − BM25 (95% CI) | RRF − EASE (95% CI) |
|---|---|---|
| MovieLens 100K, 849 users | +0.016 [+0.009, +0.022] | −0.001 [−0.007, +0.005] |
| MovieLens 1M, 5,436 users | +0.004 [+0.002, +0.007] | +0.010 [+0.008, +0.013] |
The fusion also recalls more and covers more of the catalog than BM25 (0.18 of it against 0.11 on MovieLens 1M). A request costs about 0.25–0.3 ms, because both members retrieve 100 items. It is still a third of what a booster costs, and there is no training run to schedule. The trained rankers gain more on warm users, 0.157 against 0.142 on MovieLens 1M, because they also learn from the demographics, genres and counts. Fusion is the step before a ranker, or the choice when there is nothing to train one on.
Fusing the three boosters by rank matches stacking them, at a quarter of the fits. On MovieLens
1M, BM25+RRF boosters and BM25+Blend both reach 0.192 NDCG@10 over all users. The fusion is
slightly ahead on warm users (0.158, the best warm row) and behind on cold ones (0.497 against
0.501). MovieLens 100K agrees, within 0.001 in each segment. The blend fits every booster once per
fold and once more for serving, twelve fits per branch, while the fusion fits each booster once.
Neither beats CatBoost alone by a margin that matters, for the reason given above. A request costs
the same either way, because both run all three boosters.
As a cold-start ranker, fusion helps but a trained ranker helps more. Fusing popularity with
segment popularity (BM25+RRF) lifts cold users' NDCG@10 over the popularity baseline on both
datasets, from 0.527 to 0.544 and from 0.483 to 0.486. On MovieLens 100K it even had the best cold
MAP of any row, but that rested on 94 users. On MovieLens 1M's 604, CatBoost's 0.501 is clearly
ahead.