tercul-backend/linguistics/work_analysis_service.go
Damir Mukimov fa336cacf3
wip
2025-09-01 00:43:59 +02:00

218 lines
6.4 KiB
Go

package linguistics
import (
"context"
"fmt"
"tercul/internal/models"
"time"
"tercul/internal/platform/log"
)
// WorkAnalysisService defines the interface for work-specific analysis operations
type WorkAnalysisService interface {
// AnalyzeWork performs linguistic analysis on a work
AnalyzeWork(ctx context.Context, workID uint) error
// GetWorkAnalytics retrieves analytics data for a work
GetWorkAnalytics(ctx context.Context, workID uint) (*WorkAnalytics, error)
}
// WorkAnalytics contains analytics data for a work
type WorkAnalytics struct {
WorkID uint
ViewCount int64
LikeCount int64
CommentCount int64
BookmarkCount int64
TranslationCount int64
ReadabilityScore float64
SentimentScore float64
TopKeywords []string
PopularTranslations []TranslationAnalytics
}
// TranslationAnalytics contains analytics data for a translation
type TranslationAnalytics struct {
TranslationID uint
Language string
ViewCount int64
LikeCount int64
}
// workAnalysisService implements the WorkAnalysisService interface
type workAnalysisService struct {
textAnalyzer TextAnalyzer
analysisCache AnalysisCache
analysisRepo AnalysisRepository
concurrency int
cacheEnabled bool
}
// NewWorkAnalysisService creates a new WorkAnalysisService
func NewWorkAnalysisService(
textAnalyzer TextAnalyzer,
analysisCache AnalysisCache,
analysisRepo AnalysisRepository,
concurrency int,
cacheEnabled bool,
) WorkAnalysisService {
return &workAnalysisService{
textAnalyzer: textAnalyzer,
analysisCache: analysisCache,
analysisRepo: analysisRepo,
concurrency: concurrency,
cacheEnabled: cacheEnabled,
}
}
// AnalyzeWork performs linguistic analysis on a work and stores the results
func (s *workAnalysisService) AnalyzeWork(ctx context.Context, workID uint) error {
if workID == 0 {
return fmt.Errorf("invalid work ID")
}
// Check cache first if enabled
if s.cacheEnabled && s.analysisCache.IsEnabled() {
cacheKey := fmt.Sprintf("work_analysis:%d", workID)
if result, err := s.analysisCache.Get(ctx, cacheKey); err == nil {
log.LogInfo("Cache hit for work analysis",
log.F("workID", workID))
// Store directly to database
return s.analysisRepo.StoreAnalysisResults(ctx, workID, result)
}
}
// Get work content from database
content, err := s.analysisRepo.GetWorkContent(ctx, workID, "")
if err != nil {
log.LogError("Failed to get work content for analysis",
log.F("workID", workID),
log.F("error", err))
return fmt.Errorf("failed to get work content: %w", err)
}
// Skip analysis if content is empty
if content == "" {
log.LogWarn("Skipping analysis for work with empty content",
log.F("workID", workID))
return nil
}
// Get work to determine language (via repository to avoid leaking GORM)
work, err := s.analysisRepo.GetWorkByID(ctx, workID)
if err != nil {
log.LogError("Failed to fetch work for analysis",
log.F("workID", workID),
log.F("error", err))
return fmt.Errorf("failed to fetch work: %w", err)
}
// Analyze the text
start := time.Now()
log.LogInfo("Analyzing work",
log.F("workID", workID),
log.F("language", work.Language),
log.F("contentLength", len(content)))
var result *AnalysisResult
// Use concurrent processing for large texts
if len(content) > 10000 && s.concurrency > 1 {
result, err = s.textAnalyzer.AnalyzeTextConcurrently(ctx, content, work.Language, s.concurrency)
} else {
result, err = s.textAnalyzer.AnalyzeText(ctx, content, work.Language)
}
if err != nil {
log.LogError("Failed to analyze work text",
log.F("workID", workID),
log.F("error", err))
return fmt.Errorf("failed to analyze work text: %w", err)
}
// Store results in database
if err := s.analysisRepo.StoreAnalysisResults(ctx, workID, result); err != nil {
log.LogError("Failed to store analysis results",
log.F("workID", workID),
log.F("error", err))
return fmt.Errorf("failed to store analysis results: %w", err)
}
// Cache the result if caching is enabled
if s.cacheEnabled && s.analysisCache.IsEnabled() {
cacheKey := fmt.Sprintf("work_analysis:%d", workID)
if err := s.analysisCache.Set(ctx, cacheKey, result); err != nil {
log.LogWarn("Failed to cache work analysis result",
log.F("workID", workID),
log.F("error", err))
}
}
log.LogInfo("Successfully analyzed work",
log.F("workID", workID),
log.F("wordCount", result.WordCount),
log.F("readabilityScore", result.ReadabilityScore),
log.F("sentiment", result.Sentiment),
log.F("durationMs", time.Since(start).Milliseconds()))
return nil
}
// GetWorkAnalytics retrieves analytics data for a work
func (s *workAnalysisService) GetWorkAnalytics(ctx context.Context, workID uint) (*WorkAnalytics, error) {
if workID == 0 {
return nil, fmt.Errorf("invalid work ID")
}
// Get the work to ensure it exists
work, err := s.analysisRepo.GetWorkByID(ctx, workID)
if err != nil {
return nil, fmt.Errorf("work not found: %w", err)
}
// Get analysis results from database
_, readabilityScore, languageAnalysis, _ := s.analysisRepo.GetAnalysisData(ctx, workID)
// Extract keywords from JSONB
var keywords []string
if languageAnalysis.Analysis != nil {
if keywordsData, ok := languageAnalysis.Analysis["keywords"].([]interface{}); ok {
for _, kw := range keywordsData {
if keywordMap, ok := kw.(map[string]interface{}); ok {
if text, ok := keywordMap["text"].(string); ok {
keywords = append(keywords, text)
}
}
}
}
}
// For now, return placeholder analytics with actual analysis data
return &WorkAnalytics{
WorkID: work.ID,
ViewCount: 0, // TODO: Implement view counting
LikeCount: 0, // TODO: Implement like counting
CommentCount: 0, // TODO: Implement comment counting
BookmarkCount: 0, // TODO: Implement bookmark counting
TranslationCount: 0, // TODO: Implement translation counting
ReadabilityScore: readabilityScore.Score,
SentimentScore: extractSentimentFromAnalysis(languageAnalysis.Analysis),
TopKeywords: keywords,
PopularTranslations: []TranslationAnalytics{}, // TODO: Implement translation analytics
}, nil
}
// extractSentimentFromAnalysis extracts sentiment from the Analysis JSONB field
func extractSentimentFromAnalysis(analysis models.JSONB) float64 {
if analysis == nil {
return 0.0
}
if sentiment, ok := analysis["sentiment"].(float64); ok {
return sentiment
}
return 0.0
}