Leaderboard
High-level overview — competitive collection, MMR ordering, page composition and contextual ranking.
Leaderboard — overview
Note: this document is a high-level summary. The complete deep dive on indexing, contextual ZSETs and rebuilds is in
persistence/leaderboard-ranking(collections, indexes, locking, TTL, warmup).
Sources:
src/main/java/com/safjnest/lol/service/LeaderboardService.java·CompetitiveService.java:104-182·src/main/java/com/safjnest/nosql/MongoDB.java:1559-1705,2159·TierDivisionUtils.java
1. competitive collection — source of truth
competitive is the derived projection from Summoner.ranks + ProfileStatistics:
_id = competitiveId(puuid, queue)— first 16 bytes ofSHA-256(puuid:queue)(MongoDB.competitiveId:3218).- Fields:
puuid,region(LeagueShard),queue(GameQueueTypecanonical),tier(TierDivisionType),mmr(TierDivisionUtils.getMmr),primary(LaneTypemost played),otpChampionId,lastUpdate.
Written only via CompetitiveService:
// CompetitiveService.java:111-119
Rank rank = ranks.get(canonicalQueue);
long mmr = TierDivisionUtils.getMmr(rank.tier(), rank.lp());
if (mmr < 0) return null; // UNRANKED excluded
return new CompetitiveEntry(puuid, shard, canonicalQueue, rank.tier(), mmr, primary, otpChampionId, now);
updateFromRanks/updateFromStatistics— called byRankServiceandProfileServiceafter each refresh.write→MongoDB.upsertCompetitive(:2159replaceOneupsert) +LeaderboardService.updateIndexfor Redis delta.
Indexes (docs/mongo/13-contextual-ranking.md + MongoDB.java:35-48):
db.competitive.createIndex({ queue:1, region:1, tier:1, mmr:-1, _id:-1 }, { name: "competitive_contextual_global" })
db.competitive.createIndex({ queue:1, tier:1, mmr:-1, _id:-1 })
Cover filter queue + region + tier + role/otp with sort mmr DESC, _id DESC.
2. MMR ordering
Leaderboard sort is always mmr DESC, _id DESC (MongoDB.findLeaderboardPage:1559):
Bson filter = competitiveFilter(rank, queue, region, role, otpChampionId);
// rank → MmrRange → { mmr: { $gte: min, $lt: max } } (CHALLENGER: $gte: 30000)
FindIterable<Document> q = competitive().find(filter)
.sort(Sorts.orderBy(Sorts.descending("mmr"), Sorts.descending("_id")))
.skip(offset).limit(limit);
competitiveFilter(MongoDB.java:3545) translatesTierType rankinto an MMR range viaTierDivisionUtils.getMmrRange— sorank=DIAMONDreturns allmmr ∈ [2400,10000).mmris alreadybase + lpso a Master 0 LP (10000) sorts above a Diamond I 100 LP (2800) without special cases.- Tie on identical
mmr→_id DESCensures stable pagination.
3. Page composition
LeaderboardService.getLeaderboard (:52-128):
GET /api/lol/{shard}/leaderboard?queue=RANKED_SOLO_5X5&rank=DIAMOND®ion=EUW1&role=MID&otp=157&page=1&limit=20
→ LolApiParameters validates rank/queue/region/role/otp/page/limit (1..50)
→ Redis LEADERBOARD_PAGE hit? → return
→ Mongo findLeaderboardPage(offset, limit) // competitive
→ Mongo findLeaderboardCount (with cache LEADERBOARD_COUNT + lock)
→ batch ProfileStatistics per puuid/shard (ProfileService.getStatistics, Redis→Mongo→ComputeScheduler)
→ resolveRankings(subjects) → enrich Rank with global/regional ranking
→ map Summoner → SummonerView → SummonerLeaderboard(position = offset+index+1)
→ cache LEADERBOARD_PAGE only if all statistics are present
Pagination: offset = (page-1)*limit, pages = ceil(total/limit), LeaderboardPage(page, limit, total, pages, entries).
Combinable filters:
| Filter | competitive field | Notes |
|---|---|---|
rank | mmr ∈ MmrRange | optional, null = all |
queue | queue | canonical (TEAM_BUILDER_RANKED_SOLO → RANKED_SOLO_5X5) |
region | region | GLOBAL = all |
role | primary | TOP/JUNGLE/MID/BOT/UTILITY |
otp | otpChampionId | champion id >0 |
4. resolveRankings — contextual ranking
LeaderboardService.resolveRankings (:206-263):
public static Map<String, Map<GameQueueType, Rank>> resolveRankings(List<RankedSummoner> subjects) {
for (RankedSummoner s : subjects) {
String gKey = CONTEXTUAL_LEADERBOARD_SEGMENT.of(queue, "GLOBAL", tier.name());
String rKey = CONTEXTUAL_LEADERBOARD_SEGMENT.of(queue, region, tier.name());
keys.add(gKey); members.add(competitiveId(puuid, queue));
keys.add(rKey); members.add(competitiveId(puuid, queue));
}
ensureIndexesAsync(segments); // async rebuild if ZSET missing
List<Long> positions = RedisClient.reverseRanks(keys, members); // ZREVRANK
// for each target:
rank.withRankings(
absoluteRanking(globalCounts, tier, positions[i]),
absoluteRanking(regionalCounts, tier, positions[i+1])
);
}
-
ZSET per segment —
leaderboard:{queue}:{scope}:{tier}(scope=GLOBAL|region), membercompetitiveId(16-byte SHA-256), scoremmr. -
Counts hash —
leaderboard:counts:{queue}:{scope}→tier → count. Populated byMongoDB.findCompetitiveRankCountsand used byabsoluteRanking(:460-468):javastatic Long absoluteRanking(Map<TierDivisionType,Long> counts, TierDivisionType tier, Long withinDivision) { if (counts==null || withinDivision==null || !counts.containsKey(tier)) return null; long higher = 0; for (TierDivisionType d : descendingRankedDivisions()) { if (d==tier) break; higher += counts.getOrDefault(d,0L); } return higher + withinDivision + 1; }Absolute ranking = sum of higher tiers + intra-tier position + 1. Example: Master rank 5 with 100 Challengers → absolute rank 105.
-
ensureIndexesAsync/rebuildIndex(:387-437) — if ZSET missing, enqueues aComputeSchedulerjobPROFILEthat doesforEachCompetitiveRankingSegment(queue,tier,region)→ streaming →buildSortedSetwith temp key →RENAME+SADD CONTEXTUAL_LEADERBOARD_SEGMENTS. Apex (isHighElo) has TTL 0 (persistent), otherwiseCONTEXTUAL_LEADERBOARD_SEGMENT.ttlSeconds(). -
Warmup —
warmupIndex(:277-287) pre-builds all Master+ segments (global + per-region) + counts at boot / afterrebuild.
Deep dive: for details on locking (
LEADERBOARD_COUNT_LOCK), distribution (LeaderboardDistribution),findLeaderboardAggregateCountandindexStatusdiagnostics, seepersistence/leaderboard-ranking.
