diff --git a/src/matches/season-elo-backfill.service.ts b/src/matches/season-elo-backfill.service.ts index 949e8c26..49900571 100644 --- a/src/matches/season-elo-backfill.service.ts +++ b/src/matches/season-elo-backfill.service.ts @@ -7,6 +7,10 @@ import { e_player_roles_enum, } from "../../generated"; import { PlayerEloRecomputeService } from "./player-elo-recompute.service"; +import { InjectQueue } from "@nestjs/bullmq"; +import { Queue } from "bullmq"; +import { TypesenseQueues } from "../type-sense/enums/TypesenseQueues"; +import { RefreshAllPlayersJob } from "../type-sense/jobs/RefreshAllPlayers"; // Redis-backed so any replica can read progress / request cancellation. Backfill // runs on a concurrency-1 queue, so a single global status blob is sufficient. @@ -40,6 +44,8 @@ export class SeasonEloBackfillService { private readonly cache: CacheService, private readonly notifications: NotificationsService, private readonly eloRecompute: PlayerEloRecomputeService, + @InjectQueue(TypesenseQueues.PlayerReindex) + private readonly reindexQueue: Queue, ) {} public async isRunning(): Promise { @@ -165,12 +171,22 @@ export class SeasonEloBackfillService { status.finished_at = new Date().toISOString(); await this.saveStatus(status, FINAL_TTL_SECONDS); await this.cache.forget(CANCEL_KEY); - // Per-row player_elo search events stay suppressed for the whole run: the - // backfill recomputes ELO only and deliberately does NOT reindex the player - // search index (use the manual "Reindex Search" action when desired). + // Per-row player_elo search events stay suppressed for the whole run, so + // the search index would otherwise keep serving pre-backfill ELO forever. + // One deduped reindex at the end syncs it without the per-row fan-out. await this.eloRecompute.setSuppressEvents(false); await this.cache.forget(LOCK_KEY); + await this.reindexQueue.add( + RefreshAllPlayersJob.name, + {}, + { + jobId: RefreshAllPlayersJob.name, + removeOnComplete: true, + removeOnFail: true, + }, + ); + this.logger.log( `[season-backfill] finished season ${seasonId}: ${status.completed}/${status.total} processed, ${status.failed} failed${status.canceled ? " (canceled)" : ""}`, ); diff --git a/src/type-sense/type-sense.service.ts b/src/type-sense/type-sense.service.ts index fe19d62e..e6b8c789 100644 --- a/src/type-sense/type-sense.service.ts +++ b/src/type-sense/type-sense.service.ts @@ -58,6 +58,23 @@ export class TypeSenseService { }, { name: "steam_id", type: "string", index: true }, { name: "teams", type: "string[]", optional: true }, + // Rank/range on the same number the UI shows (competitive, else wingman, + // else duel). Sorting on elo_competitive alone leaves every wingman/duel + // only player tied as "missing", so asc/desc never reorders them. + { + name: "elo", + type: "int32", + optional: true, + sort: true, + index: true, + }, + { + name: "tournament_elo", + type: "int32", + optional: true, + sort: true, + index: true, + }, { name: "elo_competitive", type: "int32", @@ -105,6 +122,7 @@ export class TypeSenseService { { name: "deaths", type: "int32", optional: true }, { name: "wins", type: "int32", optional: true }, { name: "losses", type: "int32", optional: true }, + { name: "total_matches", type: "int32", optional: true, index: true }, { name: "country", type: "string", optional: true, index: true }, { name: "sanctions", type: "int32", optional: true, index: true }, { name: "is_banned", type: "bool", optional: true, index: true }, @@ -308,6 +326,7 @@ export class TypeSenseService { last_sign_in_at: true, wins: true, losses: true, + total_matches: true, stats: { kills: true, deaths: true, @@ -389,6 +408,19 @@ export class TypeSenseService { Object.assign({}, player, elo, { id: steamId, steam_id: steamId, + elo: TypeSenseService.primaryElo( + elo.elo_competitive, + elo.elo_wingman, + elo.elo_duel, + ), + tournament_elo: TypeSenseService.primaryElo( + elo.tournament_elo_competitive, + elo.tournament_elo_wingman, + elo.tournament_elo_duel, + ), + total_matches: player.total_matches + ? parseInt(String(player.total_matches), 10) + : 0, kills: player.stats?.kills ? parseInt(String(player.stats.kills), 10) : 0, @@ -408,6 +440,11 @@ export class TypeSenseService { ); } + // Mirrors PlayerElo's display order so the sorted value is the one on screen. + private static primaryElo(...values: Array): number | null { + return values.find((value) => value !== null) ?? null; + } + public async removePlayer(steamId: string) { await this.client.collections("players").documents(steamId).delete(); }