Files
gochat/cmd/reindex_search/main.go
T

278 lines
8.0 KiB
Go

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)
}
}