package main import ( "context" "flag" "fmt" "os" "strings" "github.com/gochat/gochat/internal/app" "github.com/gochat/gochat/internal/config" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/search" applogger "github.com/gochat/gochat/pkg/logger" "gorm.io/gorm" ) func main() { var accountID uint var batchSize int var types string var bootstrap bool flag.UintVar(&accountID, "account", 0, "account ID to reindex; 0 means all accounts") flag.IntVar(&batchSize, "batch", 500, "documents per indexing batch") flag.StringVar(&types, "types", "all", "comma-separated types: conversation,message,contact,company,article,help_center,all") flag.BoolVar(&bootstrap, "bootstrap", true, "create/update Meilisearch indexes and settings before indexing") flag.Parse() if batchSize < 1 { fmt.Fprintln(os.Stderr, "batch must be >= 1") os.Exit(1) } cfg, err := config.Load() if err != nil { fmt.Fprintf(os.Stderr, "config load: %v\n", err) os.Exit(1) } if err := validateSearchEngineForReindex(cfg.Search); err != nil { fmt.Fprintf(os.Stderr, "search config: %v\n", err) os.Exit(1) } _ = applogger.Init(applogger.Config{Level: cfg.Log.Level, Format: cfg.Log.Format, Output: "stdout", ErrorOutput: "stderr"}) db, err := app.NewDatabase(&cfg.Database, cfg.Log.Level) if err != nil { fmt.Fprintf(os.Stderr, "database: %v\n", err) os.Exit(1) } searchRepo := repository.NewSearchRepo(db) engine, err := search.NewSearchEngine(search.EngineConfig{ Engine: cfg.Search.Engine, Host: cfg.Search.Host, APIKey: cfg.Search.APIKey, IndexPrefix: cfg.Search.IndexPrefix, TimeoutSeconds: cfg.Search.TimeoutSeconds, }, searchRepo) if err != nil { fmt.Fprintf(os.Stderr, "search engine: %v\n", err) os.Exit(1) } ctx := context.Background() if bootstrap { if err := engine.Bootstrap(ctx); err != nil { fmt.Fprintf(os.Stderr, "bootstrap search indexes: %v\n", err) os.Exit(1) } } selected := parseTypes(types) stats := map[search.SearchResultType]int{} if selected[search.ResultTypeConversation] { stats[search.ResultTypeConversation], err = reindexConversations(ctx, engine, db, accountID, batchSize) fatalIf(err) } if selected[search.ResultTypeMessage] { stats[search.ResultTypeMessage], err = reindexMessages(ctx, engine, db, accountID, batchSize) fatalIf(err) } if selected[search.ResultTypeContact] { stats[search.ResultTypeContact], err = reindexContacts(ctx, engine, db, accountID, batchSize) fatalIf(err) } if selected[search.ResultTypeCompany] { stats[search.ResultTypeCompany], err = reindexCompanies(ctx, engine, db, accountID, batchSize) fatalIf(err) } if selected[search.ResultTypeArticle] || selected[search.ResultTypeHelpCenter] { stats[search.ResultTypeArticle], err = reindexArticles(ctx, engine, db, accountID, batchSize) fatalIf(err) } fmt.Println("Search reindex complete") for docType, count := range stats { fmt.Printf("%s: %d\n", docType, count) } } func validateSearchEngineForReindex(cfg config.SearchConfig) error { if strings.ToLower(strings.TrimSpace(cfg.Engine)) != search.EngineMeilisearch { return fmt.Errorf("reindex_search requires search.engine=meilisearch; db fallback does not index documents") } return nil } func parseTypes(raw string) map[search.SearchResultType]bool { selected := map[search.SearchResultType]bool{} for _, part := range strings.Split(raw, ",") { switch search.SearchResultType(strings.TrimSpace(part)) { case "", "all": selected[search.ResultTypeConversation] = true selected[search.ResultTypeMessage] = true selected[search.ResultTypeContact] = true selected[search.ResultTypeCompany] = true selected[search.ResultTypeArticle] = true selected[search.ResultTypeHelpCenter] = true case search.ResultTypeConversation: selected[search.ResultTypeConversation] = true case search.ResultTypeMessage: selected[search.ResultTypeMessage] = true case search.ResultTypeContact: selected[search.ResultTypeContact] = true case search.ResultTypeCompany: selected[search.ResultTypeCompany] = true case search.ResultTypeArticle: selected[search.ResultTypeArticle] = true case search.ResultTypeHelpCenter: selected[search.ResultTypeHelpCenter] = true } } return selected } func reindexConversations(ctx context.Context, engine search.SearchEngine, db *gorm.DB, accountID uint, batchSize int) (int, error) { var count int var lastID uint for { var rows []model.Conversation q := db.WithContext(ctx).Where("id > ?", lastID).Order("id ASC").Limit(batchSize) if accountID != 0 { q = q.Where("account_id = ?", accountID) } if err := q.Find(&rows).Error; err != nil { return count, err } if len(rows) == 0 { return count, nil } docs := make([]search.SearchDocument, 0, len(rows)) for _, row := range rows { docs = append(docs, search.ConversationDocument(row)) lastID = row.ID } if err := engine.IndexBatch(ctx, docs); err != nil { return count, err } count += len(rows) } } func reindexMessages(ctx context.Context, engine search.SearchEngine, db *gorm.DB, accountID uint, batchSize int) (int, error) { var count int var lastID uint for { var rows []model.Message q := db.WithContext(ctx).Where("id > ?", lastID).Order("id ASC").Limit(batchSize) if accountID != 0 { q = q.Where("account_id = ?", accountID) } if err := q.Find(&rows).Error; err != nil { return count, err } if len(rows) == 0 { return count, nil } docs := make([]search.SearchDocument, 0, len(rows)) for _, row := range rows { docs = append(docs, search.MessageDocument(row)) lastID = row.ID } if err := engine.IndexBatch(ctx, docs); err != nil { return count, err } count += len(rows) } } func reindexContacts(ctx context.Context, engine search.SearchEngine, db *gorm.DB, accountID uint, batchSize int) (int, error) { var count int var lastID uint for { var rows []model.Contact q := db.WithContext(ctx).Where("id > ?", lastID).Order("id ASC").Limit(batchSize) if accountID != 0 { q = q.Where("account_id = ?", accountID) } if err := q.Find(&rows).Error; err != nil { return count, err } if len(rows) == 0 { return count, nil } docs := make([]search.SearchDocument, 0, len(rows)) for _, row := range rows { docs = append(docs, search.ContactDocument(row)) lastID = row.ID } if err := engine.IndexBatch(ctx, docs); err != nil { return count, err } count += len(rows) } } func reindexCompanies(ctx context.Context, engine search.SearchEngine, db *gorm.DB, accountID uint, batchSize int) (int, error) { var count int var lastID uint for { var rows []model.Company q := db.WithContext(ctx).Where("id > ?", lastID).Order("id ASC").Limit(batchSize) if accountID != 0 { q = q.Where("account_id = ?", accountID) } if err := q.Find(&rows).Error; err != nil { return count, err } if len(rows) == 0 { return count, nil } docs := make([]search.SearchDocument, 0, len(rows)) for _, row := range rows { docs = append(docs, search.CompanyDocument(row)) lastID = row.ID } if err := engine.IndexBatch(ctx, docs); err != nil { return count, err } count += len(rows) } } func reindexArticles(ctx context.Context, engine search.SearchEngine, db *gorm.DB, accountID uint, batchSize int) (int, error) { var count int var lastID uint for { var rows []model.Article q := db.WithContext(ctx).Where("id > ?", lastID).Order("id ASC").Limit(batchSize) if accountID != 0 { q = q.Where("account_id = ?", accountID) } if err := q.Find(&rows).Error; err != nil { return count, err } if len(rows) == 0 { return count, nil } docs := make([]search.SearchDocument, 0, len(rows)) for _, row := range rows { docs = append(docs, search.ArticleDocument(row)) lastID = row.ID } if err := engine.IndexBatch(ctx, docs); err != nil { return count, err } count += len(rows) } } func fatalIf(err error) { if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }