Add player search read model

This commit is contained in:
devRaGonSa
2026-06-09 10:31:21 +02:00
parent 3834662b23
commit 73a6c1afe1
6 changed files with 1144 additions and 3 deletions

View File

@@ -0,0 +1,126 @@
# Player Search Read Model
## Objective
Define the first dedicated read model for player search so `/api/stats/players/search` does not aggregate large RCON historical tables on every public request.
## Table
Primary table:
- `player_search_index`
Operational scope:
- PostgreSQL is the default operational store
- SQLite remains available only as explicit local compatibility for validation and isolated maintenance runs
Read-model nature:
- this table is regenerable
- it is not the canonical source of truth
- canonical historical data remains in `rcon_materialized_matches` and `rcon_match_player_stats`
## Stored Fields
Each row stores one player projection for one public search scope:
- `server_id`
- `player_id`
- `player_name`
- `normalized_player_name`
- `first_seen_at`
- `last_seen_at`
- `servers_seen`
- `matches_current_year`
- `kills_current_year`
- `deaths_current_year`
- `teamkills_current_year`
- `updated_at`
Current scope rows:
- `all-servers`
- `comunidad-hispana-01`
- `comunidad-hispana-02`
Why `server_id` exists:
- the public search endpoint already supports `server_id`
- keeping one row per scope preserves the existing frontend contract without recalculating large runtime aggregates for server-filtered searches
## Refresh Command
Manual command:
```bash
python -m app.rcon_historical_player_stats refresh-player-search-index
```
SQLite-only local override:
```bash
python -m app.rcon_historical_player_stats refresh-player-search-index --sqlite-path backend/data/hll_vietnam_dev.sqlite3
```
Refresh policy:
- rebuild from materialized RCON/AdminLog tables
- replace rows scope by scope
- aggregate only the current UTC year
- keep the latest current-year player name
- store accent-insensitive normalized names in Python
## Public Read Path
Priority for `/api/stats/players/search`:
1. use `player_search_index` when the requested scope has rows
2. return read-model results directly, including empty query results when the index is populated but the query does not match
3. fall back to runtime aggregation only when:
- the read model table is unavailable
- the requested scope has no rows yet
- a controlled read error occurs
Returned compatibility:
- the payload still returns `player_id`
- the payload still returns `player_name`
- the payload still returns `matches_considered`
- the payload still returns `last_seen_at`
- the payload still returns `servers_seen`
`matches_considered` remains compatible by mapping from `matches_current_year`.
## PostgreSQL Notes
No extra PostgreSQL extensions are required.
Specifically:
- no `pg_trgm`
- no custom text-search extension
Search tolerance is implemented with:
- normalized lowercase names
- accent stripping in Python
- indexed scope + normalized-name reads
- runtime fallback preserved as a safety net
## Current Limitations
- this read model is focused on player search only, not personal profile totals
- counts are current-year only by design
- historical players with no activity in the current UTC year are not intentionally prioritized in this first model
- profile and personal stats still use runtime aggregation over materialized tables
## Production Validation
Recommended checks after refresh:
- run `python -m app.rcon_historical_player_stats refresh-player-search-index`
- confirm the command reports rows for `all-servers`, `comunidad-hispana-01` and `comunidad-hispana-02`
- call `/api/stats/players/search?q=<known-player>&limit=5`
- verify response metadata reports `read_model=player-search-index`
- verify fallback metadata only appears when the read model is empty or unavailable