127 lines
3.0 KiB
Go
127 lines
3.0 KiB
Go
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))
|
|
}
|