thread-master/backend/internal/logic/scan_post/lookalike_logic.go

127 lines
3.0 KiB
Go
Raw Normal View History

package scan_post
import (
"context"
"math"
"sort"
"haixun-backend/internal/library/authctx"
app "haixun-backend/internal/library/errors"
"haixun-backend/internal/library/errors/code"
scanpostusecase "haixun-backend/internal/model/scan_post/domain/usecase"
"haixun-backend/internal/svc"
"haixun-backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type LookalikeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewLookalikeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LookalikeLogic {
return &LookalikeLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *LookalikeLogic) Lookalike(req *types.LookalikeReq) (*types.LookalikeData, error) {
actor, ok := authctx.ActorFromContext(l.ctx)
if !ok {
return nil, app.For(code.Auth).AuthUnauthorized("missing actor")
}
source, err := l.svcCtx.ScanPost.Get(l.ctx, actor.TenantID, actor.UID, req.BrandID, req.SourcePostID)
if err != nil {
return nil, err
}
if len(source.Embedding) == 0 {
return &types.LookalikeData{List: nil}, nil
}
brandID := req.BrandID
if brandID == "" {
brandID = source.BrandID
}
limit := req.Limit
if limit < 1 {
limit = 10
}
if limit > 50 {
limit = 50
}
candidates, err := l.svcCtx.ScanPost.List(l.ctx, scanpostusecase.ListRequest{
TenantID: actor.TenantID,
OwnerUID: actor.UID,
BrandID: brandID,
Limit: 200,
})
if err != nil {
return nil, err
}
type scored struct {
post scanpostusecase.ScanPostSummary
sim float64
}
var scoredList []scored
for _, c := range candidates {
if c.ID == req.SourcePostID {
continue
}
if len(c.Embedding) == 0 {
continue
}
sim := cosineSimilarity(source.Embedding, c.Embedding)
if sim < 0.3 {
continue
}
scoredList = append(scoredList, scored{post: c, sim: sim})
}
sort.Slice(scoredList, func(i, j int) bool {
return scoredList[i].sim > scoredList[j].sim
})
if len(scoredList) > limit {
scoredList = scoredList[:limit]
}
out := make([]types.LookalikeItemData, 0, len(scoredList))
for _, s := range scoredList {
out = append(out, types.LookalikeItemData{
PostID: s.post.ID,
AuthorID: s.post.AuthorID,
AuthorName: s.post.Author,
AuthorAvatar: s.post.AuthorAvatar,
Permalink: s.post.Permalink,
Text: s.post.Text,
PlacementScore: s.post.PlacementScore,
SemanticScore: s.post.SemanticScore,
EngagementPredicted: s.post.EngagementPredicted,
AudienceQuality: s.post.AudienceQualityScore,
Similarity: math.Round(s.sim*1000) / 1000,
})
}
return &types.LookalikeData{List: out}, nil
}
func cosineSimilarity(a, b []float32) float64 {
if len(a) == 0 || len(b) == 0 || len(a) != len(b) {
return 0
}
var dot, na, nb float64
for i := range a {
dot += float64(a[i]) * float64(b[i])
na += float64(a[i]) * float64(a[i])
nb += float64(b[i]) * float64(b[i])
}
if na == 0 || nb == 0 {
return 0
}
return dot / (math.Sqrt(na) * math.Sqrt(nb))
}