package page import ( "context" "fmt" "github.com/gocql/gocql" "go.uber.org/zap" domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search" ) // SearchPagesUseCase handles page search functionality type SearchPagesUseCase struct { siteRepo domainsite.Repository searchClient *search.Client logger *zap.Logger } // ProvideSearchPagesUseCase creates a new SearchPagesUseCase func ProvideSearchPagesUseCase( siteRepo domainsite.Repository, searchClient *search.Client, logger *zap.Logger, ) *SearchPagesUseCase { return &SearchPagesUseCase{ siteRepo: siteRepo, searchClient: searchClient, logger: logger, } } // SearchPagesInput is the input for searching pages type SearchPagesInput struct { Query string `json:"query"` Limit int64 `json:"limit"` Offset int64 `json:"offset"` Filter string `json:"filter,omitempty"` // e.g., "status = publish AND post_type = post" } // SearchPagesOutput is the output after searching pages type SearchPagesOutput struct { Hits interface{} `json:"hits"` // meilisearch.Hits Query string `json:"query"` ProcessingTimeMs int64 `json:"processing_time_ms"` TotalHits int64 `json:"total_hits"` Limit int64 `json:"limit"` Offset int64 `json:"offset"` } // Execute performs a search on the site's indexed pages func (uc *SearchPagesUseCase) Execute(ctx context.Context, tenantID, siteID gocql.UUID, input *SearchPagesInput) (*SearchPagesOutput, error) { uc.logger.Info("executing search pages use case", zap.String("tenant_id", tenantID.String()), zap.String("site_id", siteID.String()), zap.String("query", input.Query)) // Get site to validate and check quotas site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID) if err != nil { uc.logger.Error("failed to get site", zap.Error(err)) return nil, domainsite.ErrSiteNotFound } // Verify site is verified (skip for test mode) if site.RequiresVerification() && !site.IsVerified { uc.logger.Warn("site not verified", zap.String("site_id", siteID.String())) return nil, domainsite.ErrSiteNotVerified } // No quota checking - usage-based billing (anti-abuse via rate limiting only) // Set default limits if not provided limit := input.Limit if limit <= 0 || limit > 100 { limit = 20 // Default to 20 results } offset := input.Offset if offset < 0 { offset = 0 } // Build search request searchReq := search.SearchRequest{ Query: input.Query, Limit: limit, Offset: offset, Filter: input.Filter, } // If no filter provided, default to only published pages if searchReq.Filter == "" { searchReq.Filter = "status = publish" } // Perform search result, err := uc.searchClient.Search(siteID.String(), searchReq) if err != nil { uc.logger.Error("failed to search pages", zap.Error(err)) return nil, fmt.Errorf("failed to search pages: %w", err) } // Increment search request count (for usage tracking/billing) site.IncrementSearchCount() uc.logger.Info("incremented search count", zap.String("site_id", siteID.String()), zap.Int64("new_count", site.SearchRequestsCount)) if err := uc.siteRepo.UpdateUsage(ctx, site); err != nil { uc.logger.Error("failed to update search usage", zap.Error(err)) // Don't fail the search, just log the error } else { uc.logger.Info("search usage updated successfully", zap.String("site_id", siteID.String()), zap.Int64("search_count", site.SearchRequestsCount)) } uc.logger.Info("search completed successfully", zap.String("site_id", siteID.String()), zap.String("query", input.Query), zap.Int64("total_hits", result.TotalHits), zap.Int64("processing_time_ms", result.ProcessingTimeMs)) return &SearchPagesOutput{ Hits: result.Hits, Query: result.Query, ProcessingTimeMs: result.ProcessingTimeMs, TotalHits: result.TotalHits, Limit: result.Limit, Offset: result.Offset, }, nil }