Candidate generation benchmark

When is it worth giving Cascade several generators instead of one? A ranker can only reorder what the first stage retrieved, so this measures the first stage alone: how many of the items a user goes on to like are among the N candidates, for single generators and for merges of them, at the same budget of N distinct items. Each merged set is compared with the best of its own members, which is the alternative a merge has to beat. No ranker is trained, so every number is deterministic.

uv run python benchmarks/run.py run candidates   # 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, warm held-out users only, every generator at its default hyper-parameters. Regenerate with python benchmarks/run.py run candidates --dataset movielens-100k-cold.

candidate recall@N is measured at each budget N of distinct items retrieved per user: the share of a user's held-out items among the candidates, averaged over users, which is the most any ranker could recall from them. A merged set's gain over the best of its own members at the same N is in brackets.

Generators candidate recall@10 candidate recall@20 candidate recall@50 candidate recall@100 candidate recall@200
BM25 0.101 0.171 0.305 0.425 0.536
EASE 0.108 0.184 0.326 0.460 0.604
ItemKNN 0.103 0.181 0.341 0.507 0.683
RP3Beta 0.091 0.153 0.300 0.453 0.625
MostPopular 0.060 0.098 0.195 0.316 0.476
BM25+EASE 0.107 (-0.001) 0.184 (+0.000) 0.335 (+0.010) 0.472 (+0.012) 0.626 (+0.022)
EASE+RP3Beta 0.103 (-0.006) 0.176 (-0.008) 0.323 (-0.003) 0.465 (+0.005) 0.637 (+0.012)
EASE+ItemKNN 0.108 (-0.000) 0.189 (+0.006) 0.344 (+0.002) 0.498 (-0.009) 0.670 (-0.013)
BM25+EASE+ItemKNN 0.109 (+0.001) 0.185 (+0.002) 0.341 (+0.000) 0.489 (-0.018) 0.661 (-0.022)
EASE+MostPopular 0.093 (-0.015) 0.156 (-0.028) 0.296 (-0.030) 0.440 (-0.020) 0.612 (+0.009)
BM25+EASE RRF 0.108 (-0.001) 0.186 (+0.002) 0.335 (+0.010) 0.473 (+0.013) 0.627 (+0.023)
EASE+RP3Beta RRF 0.102 (-0.007) 0.175 (-0.009) 0.323 (-0.003) 0.470 (+0.010) 0.639 (+0.014)
EASE+ItemKNN RRF 0.110 (+0.002) 0.192 (+0.008) 0.344 (+0.002) 0.504 (-0.003) 0.673 (-0.010)
BM25+EASE+ItemKNN RRF 0.109 (+0.000) 0.189 (+0.005) 0.346 (+0.005) 0.502 (-0.004) 0.673 (-0.010)
EASE+MostPopular RRF 0.093 (-0.015) 0.156 (-0.028) 0.294 (-0.031) 0.440 (-0.020) 0.613 (+0.009)

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.

median latency@N is the median time to retrieve one user's N candidates, the way Cascade.recommend does before any feature or ranker runs; a merged set's time includes the predict calls that fill in the score of every generator that did not retrieve a candidate. Sampled until 2s or 300 requests per cell, after 10 untimed ones.

Generators median latency@10 median latency@20 median latency@50 median latency@100 median latency@200
BM25 61 us 63 us 62 us 63 us 63 us
EASE 73 us 72 us 78 us 78 us 89 us
ItemKNN 62 us 63 us 66 us 69 us 78 us
RP3Beta 62 us 63 us 66 us 68 us 76 us
MostPopular 69 us 70 us 71 us 75 us 81 us
BM25+EASE 369 us 366 us 378 us 401 us 453 us
EASE+RP3Beta 359 us 371 us 398 us 410 us 464 us
EASE+ItemKNN 362 us 366 us 385 us 406 us 455 us
BM25+EASE+ItemKNN 541 us 550 us 577 us 606 us 683 us
EASE+MostPopular 315 us 312 us 330 us 352 us 394 us
BM25+EASE RRF 327 us 326 us 329 us 342 us 368 us
EASE+RP3Beta RRF 330 us 325 us 337 us 350 us 399 us
EASE+ItemKNN RRF 334 us 343 us 346 us 359 us 379 us
BM25+EASE+ItemKNN RRF 446 us 447 us 467 us 477 us 513 us
EASE+MostPopular RRF 336 us 338 us 353 us 365 us 399 us

MovieLens 1M

MovieLens 1M (ml-1m-l200), leave-one-out split, warm held-out users only, every generator at its default hyper-parameters. Regenerate with python benchmarks/run.py run candidates --dataset movielens-1m.

candidate recall@N is measured at each budget N of distinct items retrieved per user: the share of a user's held-out items among the candidates, averaged over users, which is the most any ranker could recall from them. A merged set's gain over the best of its own members at the same N is in brackets.

Generators candidate recall@10 candidate recall@20 candidate recall@50 candidate recall@100 candidate recall@200
BM25 0.064 0.108 0.210 0.321 0.453
EASE 0.089 0.144 0.252 0.367 0.492
ItemKNN 0.071 0.122 0.231 0.353 0.511
RP3Beta 0.064 0.111 0.221 0.343 0.489
MostPopular 0.031 0.056 0.125 0.204 0.316
BM25+EASE 0.080 (-0.009) 0.134 (-0.010) 0.238 (-0.014) 0.362 (-0.006) 0.503 (+0.012)
EASE+RP3Beta 0.081 (-0.008) 0.133 (-0.011) 0.245 (-0.007) 0.367 (+0.000) 0.510 (+0.018)
EASE+ItemKNN 0.082 (-0.007) 0.138 (-0.006) 0.255 (+0.003) 0.376 (+0.009) 0.519 (+0.008)
BM25+EASE+ItemKNN 0.081 (-0.009) 0.132 (-0.012) 0.246 (-0.006) 0.369 (+0.001) 0.517 (+0.006)
EASE+MostPopular 0.063 (-0.026) 0.114 (-0.030) 0.213 (-0.039) 0.328 (-0.040) 0.472 (-0.020)
BM25+EASE RRF 0.080 (-0.009) 0.134 (-0.011) 0.240 (-0.012) 0.365 (-0.002) 0.507 (+0.015)
EASE+RP3Beta RRF 0.079 (-0.010) 0.132 (-0.012) 0.247 (-0.005) 0.371 (+0.004) 0.515 (+0.023)
EASE+ItemKNN RRF 0.082 (-0.007) 0.140 (-0.004) 0.254 (+0.002) 0.378 (+0.011) 0.523 (+0.012)
BM25+EASE+ItemKNN RRF 0.079 (-0.010) 0.133 (-0.011) 0.250 (-0.002) 0.375 (+0.008) 0.523 (+0.012)
EASE+MostPopular RRF 0.062 (-0.027) 0.113 (-0.031) 0.212 (-0.040) 0.328 (-0.039) 0.473 (-0.019)

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.

median latency@N is the median time to retrieve one user's N candidates, the way Cascade.recommend does before any feature or ranker runs; a merged set's time includes the predict calls that fill in the score of every generator that did not retrieve a candidate. Sampled until 2s or 300 requests per cell, after 10 untimed ones.

Generators median latency@10 median latency@20 median latency@50 median latency@100 median latency@200
BM25 67 us 66 us 70 us 73 us 72 us
EASE 128 us 129 us 134 us 134 us 157 us
ItemKNN 72 us 73 us 76 us 83 us 93 us
RP3Beta 69 us 71 us 74 us 80 us 94 us
MostPopular 77 us 78 us 79 us 87 us 103 us
BM25+EASE 439 us 439 us 459 us 501 us 566 us
EASE+RP3Beta 444 us 457 us 482 us 522 us 602 us
EASE+ItemKNN 444 us 465 us 487 us 524 us 610 us
BM25+EASE+ItemKNN 648 us 677 us 700 us 764 us 862 us
EASE+MostPopular 402 us 423 us 420 us 458 us 548 us
BM25+EASE RRF 397 us 394 us 410 us 426 us 460 us
EASE+RP3Beta RRF 436 us 431 us 453 us 471 us 524 us
EASE+ItemKNN RRF 435 us 427 us 453 us 457 us 530 us
BM25+EASE+ItemKNN RRF 567 us 599 us 697 us 626 us 683 us
EASE+MostPopular RRF 420 us 423 us 464 us 494 us 506 us

When a merge pays

A merge splits a fixed budget between its members. Round-robin gives each generator about the same number of slots, so the merge gains only when the items a second generator brings are worth more than the items of the first that they push out. On these two datasets, that happens under narrow conditions:

  • Only at large budgets. At N of 50 and below, a merge moves recall by about 0.01 either way. On MovieLens 1M, every merge loses at N=10 and N=20: by 0.006 to 0.012, and by up to 0.030 with popularity. Near the top of the list the generators largely agree, so the slots a second generator takes are mostly slots the first would have filled with hits. The gains appear at N=100 and N=200, where each generator's tail reaches items the other misses. They are modest: the best is BM25+EASE at N=200 on MovieLens 100K, 0.604 to 0.626, about 4% relative.
  • Only between generators of similar strength. EASE+MostPopular loses up to 0.040 of recall, because popularity takes half the budget and recalls far less with it. Its one gain is +0.009 at N=200 on MovieLens 100K. The same holds for any weak partner.
  • More members is not better. BM25+EASE+ItemKNN is never more than 0.001 ahead of the best pair, and on MovieLens 100K it trails ItemKNN alone by 0.018 at N=100 and 0.022 at N=200.
  • Compare with the best single generator at your budget, not only with the merge's members. The strongest generator changes with N: EASE leads at small budgets and ItemKNN at large ones. On MovieLens 100K, no merge beats ItemKNN alone at N=100 or N=200. On MovieLens 1M, only the merges holding both EASE and ItemKNN beat every single generator at N=100 and N=200. The better of them, EASE+ItemKNN, reaches 0.376 against EASE's 0.367 at N=100, and 0.519 against ItemKNN's 0.511 at N=200.

The merge also costs time. A request asks every member for the full budget, and then each member scores the candidates it did not retrieve with predict, so the ranker sees every generator's opinion of every candidate. That scoring is about half of a merged request. A two-generator merge costs 0.3–0.6 ms against 60–160 µs for one generator, and a third member adds another 0.2 ms or so.

Reciprocal rank fusion merges slightly better, and usually faster, than round-robin. The rows marked RRF merge the same members with ReciprocalRankFusion, which asks every member for the same N items and keeps the N with the highest fused score, so items several members rank high come first. Against round-robin it wins 29 of the 50 cells, ties 11 and loses 10, none by more than 0.002. It gains most where round-robin does worst: for BM25+EASE+ItemKNN on MovieLens 100K it cuts the deficit to ItemKNN alone from 0.018 to 0.004 at N=100 and from 0.022 to 0.010 at N=200. A fused request fills no missing scores with predict, since the fused score is the only one it keeps, so it is up to a quarter cheaper, and more so for more members. The exception is EASE+MostPopular, where filling in popularity costs almost nothing and fusion runs up to 10% slower. Fusion does not rescue a weak partner either: EASE+MostPopular loses as much recall either way.

In short: merge generators when the ranker reorders a wide list (N of 100 or more) and the members are comparably strong but disagree, and check the merge against the best single generator at the same N. For a narrow list, spend the whole budget on the best single generator. Candidate recall is the ceiling, not the result. Whether a ranker turns a higher ceiling into better top-10 lists is what the reranking benchmark measures. A merge can also be worth its cost for a reason this benchmark does not measure: one Cascade whose list includes MostPopularRecommender also serves cold users (see composing recommenders).