package main import ( "context" "flag" "fmt" "time" "apps/backend/internal/config" "github.com/zeromicro/go-zero/core/conf" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) // init creates idempotent operational indexes only. It intentionally never // inserts members or other business records. func main() { configFile := flag.String("f", "etc/gateway.yaml", "config file") flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) c.ApplyEnv() if c.Mongo.URI == "" || c.Mongo.Database == "" { panic("Mongo.URI and Mongo.Database are required") } ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() client, err := mongo.Connect(ctx, options.Client().ApplyURI(c.Mongo.URI)) if err != nil { panic(err) } defer client.Disconnect(context.Background()) db := client.Database(c.Mongo.Database) for collection, models := range indexModels() { if _, err := db.Collection(collection).Indexes().CreateMany(ctx, models); err != nil { panic(fmt.Errorf("create %s indexes: %w", collection, err)) } } fmt.Println("database indexes initialized") } // ownerIndex covers the near-universal "list one member's rows, newest first" shape. The sort key // belongs in the index too, otherwise Mongo fetches every matching row before sorting. func ownerIndex(sortField, name string) mongo.IndexModel { return mongo.IndexModel{ Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: sortField, Value: -1}}, Options: options.Index().SetName(name), } } // indexModels lists every index the running code depends on. Owner-scoped collections were all // unindexed, so each list endpoint was a full collection scan that grew with total rows across // every member rather than with the member's own data. // // These are deliberately all non-unique. Adding a uniqueness constraint to existing data can make // CreateMany fail and take a deploy down, so that belongs in its own migration with a duplicate // pre-check rather than here. func indexModels() map[string][]mongo.IndexModel { return map[string][]mongo.IndexModel{ // members.uid is read on every authenticated request via the auth middleware, and it is a // field lookup rather than _id, so without this it scanned the whole collection whenever // the Redis cache missed. "members": { {Keys: bson.D{{Key: "uid", Value: 1}}, Options: options.Index().SetName("member_uid")}, {Keys: bson.D{{Key: "email", Value: 1}}, Options: options.Index().SetName("member_email")}, {Keys: bson.D{{Key: "phone", Value: 1}}, Options: options.Index().SetName("member_phone")}, }, "identities": { {Keys: bson.D{{Key: "login_id", Value: 1}, {Key: "platform", Value: 1}}, Options: options.Index().SetName("identity_login_platform")}, }, "member_settings": { {Keys: bson.D{{Key: "uid", Value: 1}}, Options: options.Index().SetName("settings_uid")}, }, "jobs": { {Keys: bson.D{{Key: "status", Value: 1}, {Key: "run_after", Value: 1}, {Key: "created_at", Value: 1}}, Options: options.Index().SetName("claim_due_jobs")}, {Keys: bson.D{{Key: "status", Value: 1}, {Key: "completed_at", Value: 1}}, Options: options.Index().SetName("purge_terminal_jobs")}, // The jobs list is polled continuously by the UI and runs a count plus a find per poll. ownerIndex("updated_at", "owner_jobs_updated"), }, // One notification is written per job state change and nothing purges them, so this // collection grows without bound and its scan cost grows with it. "notifications": { ownerIndex("created_at", "owner_notifications_created"), }, "threads_accounts": { ownerIndex("created_at", "owner_accounts_created"), {Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "threads_user_id", Value: 1}}, Options: options.Index().SetName("owner_threads_user")}, }, "studio_outbox": { {Keys: bson.D{{Key: "status", Value: 1}, {Key: "updated_at", Value: 1}}, Options: options.Index().SetName("worker_outbox_status_updated")}, {Keys: bson.D{{Key: "steps.status", Value: 1}, {Key: "steps.scheduled_at", Value: 1}, {Key: "steps.lease_expires_at", Value: 1}}, Options: options.Index().SetName("claim_due_outbox_steps")}, ownerIndex("updated_at", "owner_outbox_updated"), }, "studio_personas": {ownerIndex("updated_at", "owner_personas_updated")}, "studio_plays": {ownerIndex("updated_at", "owner_plays_updated")}, "studio_own_posts": { ownerIndex("published_at", "owner_own_posts_published"), {Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "account_id", Value: 1}, {Key: "published_at", Value: -1}}, Options: options.Index().SetName("owner_account_own_posts")}, }, "studio_mentions": { ownerIndex("created_at", "owner_mentions_created"), {Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "account_id", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("owner_account_mentions")}, }, "scout_brands": {{Keys: bson.D{{Key: "owner_uid", Value: 1}}, Options: options.Index().SetName("owner_brands")}}, "scout_products": {{Keys: bson.D{{Key: "owner_uid", Value: 1}}, Options: options.Index().SetName("owner_products")}}, "scout_posts": { ownerIndex("created_at", "owner_posts_created"), {Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "brand_id", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("owner_brand_posts")}, }, "scout_homework": {{Keys: bson.D{{Key: "owner_uid", Value: 1}}, Options: options.Index().SetName("owner_homework")}}, "inspire_elements": {ownerIndex("updated_at", "owner_elements_updated")}, "inspire_sessions": {ownerIndex("updated_at", "owner_sessions_updated")}, "usage_events": { {Keys: bson.D{{Key: "uid", Value: 1}, {Key: "month_key", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("uid_month_events")}, {Keys: bson.D{{Key: "month_key", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("tenant_month_events")}, {Keys: bson.D{{Key: "created_at", Value: 1}}, Options: options.Index().SetName("events_created_range")}, }, "usage_prefs": { {Keys: bson.D{{Key: "uid", Value: 1}}, Options: options.Index().SetName("prefs_uid")}, // Stripe webhooks arrive knowing only the customer, so this is the reverse lookup // back to a member and it runs on every billing event. {Keys: bson.D{{Key: "stripe_customer_id", Value: 1}}, Options: options.Index().SetName("prefs_stripe_customer")}, }, "usage_purchases": { {Keys: bson.D{{Key: "uid", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("uid_purchases_created")}, {Keys: bson.D{{Key: "stripe_event_id", Value: 1}}, Options: options.Index().SetName("purchase_stripe_event")}, }, "growth_outcomes": { // The observe worker scans by status across all tenants on every tick. {Keys: bson.D{{Key: "status", Value: 1}}, Options: options.Index().SetName("observe_status")}, {Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "source_type", Value: 1}, {Key: "source_id", Value: 1}}, Options: options.Index().SetName("owner_outcome_source")}, ownerIndex("created_at", "owner_outcomes_created"), }, "growth_checkups": {ownerIndex("created_at", "owner_checkups_created")}, "growth_account_health": {{Keys: bson.D{{Key: "owner_uid", Value: 1}}, Options: options.Index().SetName("owner_health")}}, "growth_workspaces": {{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "archived", Value: 1}}, Options: options.Index().SetName("owner_workspaces_archived")}}, "growth_draft_reviews": { {Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "ref_type", Value: 1}, {Key: "ref_id", Value: 1}, {Key: "updated_at", Value: -1}}, Options: options.Index().SetName("owner_review_ref")}, }, "growth_invite_rewards": { {Keys: bson.D{{Key: "inviter_uid", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("inviter_rewards_created")}, {Keys: bson.D{{Key: "invitee_uid", Value: 1}}, Options: options.Index().SetName("invitee_reward")}, }, "growth_utm_links": { // Looked up by code on the unauthenticated public redirect. {Keys: bson.D{{Key: "code", Value: 1}}, Options: options.Index().SetName("utm_code")}, ownerIndex("created_at", "owner_utm_created"), }, "growth_ws_members": { {Keys: bson.D{{Key: "workspace_id", Value: 1}, {Key: "uid", Value: 1}}, Options: options.Index().SetName("workspace_member")}, }, "billing_checkout_attempts": { {Keys: bson.D{{Key: "uid", Value: 1}, {Key: "request_id", Value: 1}}, Options: options.Index().SetName("uid_request")}, {Keys: bson.D{{Key: "session_id", Value: 1}}, Options: options.Index().SetName("checkout_session")}, {Keys: bson.D{{Key: "customer_id", Value: 1}}, Options: options.Index().SetName("checkout_customer")}, }, } }