OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 127 LINES
 
[ HISTORY ]  [ UP ]
 

package generator

import (
	"database/sql"
	"fmt"
	"ookstats/internal/writer"
	"path/filepath"
	"time"
)

// SearchEntry represents a player in the search index
type SearchEntry struct {
	ID            int64  `json:"id"`
	Name          string `json:"name"`
	Region        string `json:"region"`
	RealmSlug     string `json:"realm_slug"`
	RealmName     string `json:"realm_name"`
	ClassName     string `json:"class_name,omitempty"`
	GlobalRanking *int   `json:"global_ranking,omitempty"`
	GlobalBracket string `json:"global_ranking_bracket,omitempty"`
}

// GenerateSearchIndex generates sharded JSON files for the player search index
func GenerateSearchIndex(db *sql.DB, out string, shardSize int) error {
	if shardSize <= 0 {
		shardSize = 5000
	}
	if err := writer.EnsureDir(out); err != nil {
		return err
	}

	// count first so the shard loop never holds a row iterator across a second query
	var totalPlayers int
	if err := db.QueryRow(`
      SELECT COUNT(*)
      FROM players p
      JOIN (
          SELECT player_id, MAX(season_id) as max_season_id
          FROM player_profiles
          WHERE has_complete_coverage = 1
          GROUP BY player_id
      ) latest ON p.id = latest.player_id
      JOIN player_profiles pp ON pp.player_id = latest.player_id
                              AND pp.season_id = latest.max_season_id
      JOIN realms r ON p.realm_id = r.id
    `).Scan(&totalPlayers); err != nil {
		return err
	}

	rows, err := db.Query(`
        SELECT p.id, p.name, r.region, r.slug, r.name,
               COALESCE(pp.class_name, ''), pp.global_ranking, COALESCE(pp.global_
ranking_bracket,'')
        FROM players p
        JOIN (
            SELECT player_id, MAX(season_id) as max_season_id
            FROM player_profiles
            WHERE has_complete_coverage = 1
            GROUP BY player_id
        ) latest ON p.id = latest.player_id
        JOIN player_profiles pp ON pp.player_id = latest.player_id
                                AND pp.season_id = latest.max_season_id
        JOIN realms r ON p.realm_id = r.id
        ORDER BY pp.global_ranking ASC, p.name ASC
    `)
	if err != nil {
		return err
	}
	defer rows.Close()

	shard := 0
	count := 0
	buf := []SearchEntry{}

	flush := func() error {
		if len(buf) == 0 {
			return nil
		}
		path := filepath.Join(out, fmt.Sprintf("players-%03d.json", shard))
		meta := map[string]any{
			"total_players":    totalPlayers,
			"returned_players": len(buf),
			"offset":           shard * shardSize,
			"limit":            shardSize,
			"last_updated":     time.Now().Format(time.RFC3339),
		}
		if err := writer.WriteJSONFileCompact(path, map[string]any{"players": buf, "meta
data": meta}); err != nil {
			return err
		}
		shard++
		buf = buf[:0]
		return nil
	}

	for rows.Next() {
		var e SearchEntry
		var className sql.NullString
		var gr sql.NullInt64
		var gb string
		if err := rows.Scan(&e.ID, &e.Name, &e.Region, &e.RealmSlug, &e.RealmName, &clas
sName, &gr, &gb); err != nil {
			return err
		}
		e.ClassName = className.String
		e.GlobalBracket = gb
		if gr.Valid {
			v := int(gr.Int64)
			e.GlobalRanking = &v
		}
		buf = append(buf, e)
		count++
		if len(buf) >= shardSize {
			if err := flush(); err != nil {
				return err
			}
		}
	}

	if err := rows.Err(); err != nil {
		return err
	}
	if err := flush(); err != nil {
		return err
	}

	fmt.Printf("[OK] Generated search index: %d players in %d shards\n", count, shard
)
	return nil
}

package generator

import (
	"database/sql"
	"fmt"
	"ookstats/internal/writer"
	"path/filepath"
	"time"
)

// SearchEntry represents a player in the se
arch index
type SearchEntry struct {
	ID            int64  `json:"id"`
	Name          string `json:"name"`
	Region        string `json:"region"`
	RealmSlug     string `json:"realm_slug"`
	RealmName     string `json:"realm_name"`
	ClassName     string `json:"class_name,omit
empty"`
	GlobalRanking *int   `json:"global_ranking,
omitempty"`
	GlobalBracket string `json:"global_ranking_
bracket,omitempty"`
}

// GenerateSearchIndex generates sharded JSO
N files for the player search index
func GenerateSearchIndex(db *sql.DB, out str
ing, shardSize int) error {
	if shardSize <= 0 {
		shardSize = 5000
	}
	if err := writer.EnsureDir(out); err != nil
 {
		return err
	}

	// count first so the shard loop never hold
s a row iterator across a second query
	var totalPlayers int
	if err := db.QueryRow(`
      SELECT COUNT(*)
      FROM players p
      JOIN (
          SELECT player_id, MAX(season_id) a
s max_season_id
          FROM player_profiles
          WHERE has_complete_coverage = 1
          GROUP BY player_id
      ) latest ON p.id = latest.player_id
      JOIN player_profiles pp ON pp.player_i
d = latest.player_id
                              AND pp.season_
id = latest.max_season_id
      JOIN realms r ON p.realm_id = r.id
    `).Scan(&totalPlayers); err != nil {
		return err
	}

	rows, err := db.Query(`
        SELECT p.id, p.name, r.region, r.slu
g, r.name,
               COALESCE(pp.class_name, ''), 
pp.global_ranking, COALESCE(pp.global_rankin
g_bracket,'')
        FROM players p
        JOIN (
            SELECT player_id, MAX(season_id)
 as max_season_id
            FROM player_profiles
            WHERE has_complete_coverage = 1
            GROUP BY player_id
        ) latest ON p.id = latest.player_id
        JOIN player_profiles pp ON pp.player
_id = latest.player_id
                                AND pp.seaso
n_id = latest.max_season_id
        JOIN realms r ON p.realm_id = r.id
        ORDER BY pp.global_ranking ASC, p.na
me ASC
    `)
	if err != nil {
		return err
	}
	defer rows.Close()

	shard := 0
	count := 0
	buf := []SearchEntry{}

	flush := func() error {
		if len(buf) == 0 {
			return nil
		}
		path := filepath.Join(out, fmt.Sprintf("pl
ayers-%03d.json", shard))
		meta := map[string]any{
			"total_players":    totalPlayers,
			"returned_players": len(buf),
			"offset":           shard * shardSize,
			"limit":            shardSize,
			"last_updated":     time.Now().Format(tim
e.RFC3339),
		}
		if err := writer.WriteJSONFileCompact(path
, map[string]any{"players": buf, "metadata":
 meta}); err != nil {
			return err
		}
		shard++
		buf = buf[:0]
		return nil
	}

	for rows.Next() {
		var e SearchEntry
		var className sql.NullString
		var gr sql.NullInt64
		var gb string
		if err := rows.Scan(&e.ID, &e.Name, &e.Reg
ion, &e.RealmSlug, &e.RealmName, &className,
 &gr, &gb); err != nil {
			return err
		}
		e.ClassName = className.String
		e.GlobalBracket = gb
		if gr.Valid {
			v := int(gr.Int64)
			e.GlobalRanking = &v
		}
		buf = append(buf, e)
		count++
		if len(buf) >= shardSize {
			if err := flush(); err != nil {
				return err
			}
		}
	}

	if err := rows.Err(); err != nil {
		return err
	}
	if err := flush(); err != nil {
		return err
	}

	fmt.Printf("[OK] Generated search index: %d
 players in %d shards\n", count, shard)
	return nil
}
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET