123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465 |
- package dao
- import (
- "context"
- "fmt"
- "sort"
- "strconv"
- "strings"
- "time"
- "github.com/pkg/errors"
- "go-common/app/service/live/recommend/internal/conf"
- "go-common/app/service/live/recommend/recconst"
- relation_api "go-common/app/service/live/relation/api/liverpc"
- room_api "go-common/app/service/live/room/api/liverpc"
- "go-common/library/cache/redis"
- "go-common/library/log"
- "go-common/library/net/rpc/liverpc"
- )
- var _userRecCandidateKey = "rec_candidate_%d"
- var _recommendOffsetKey = "rec_offset_%d"
- // 已经推荐过的池子,用户+日期
- var _recommendedKey = "recommended_%d_%s"
- // RoomAPI room liverpc client
- var RoomAPI *room_api.Client
- // RelationAPI relation liverpc client
- var RelationAPI *relation_api.Client
- // Dao dao
- type Dao struct {
- c *conf.Config
- redis *redis.Pool
- }
- func init() {
- RoomAPI = room_api.New(getConf("room"))
- RelationAPI = relation_api.New(getConf("relation"))
- }
- func getConf(appName string) *liverpc.ClientConfig {
- c := conf.Conf.LiveRpc
- if c != nil {
- return c[appName]
- }
- return nil
- }
- // ClearRecommend 清空该用户相关的推荐缓存
- func (d *Dao) ClearRecommend(ctx context.Context, uid int64) error {
- candidateKey := fmt.Sprintf(_userRecCandidateKey, uid)
- recommendedKey := fmt.Sprintf(_recommendedKey, uid, time.Now().Format("20060102"))
- offsetKey := fmt.Sprintf(_recommendOffsetKey, uid)
- conn := d.redis.Get(ctx)
- defer conn.Close()
- _, err := conn.Do("DEL", candidateKey, recommendedKey, offsetKey)
- return errors.WithStack(err)
- }
- // New init mysql db
- func New(c *conf.Config) (dao *Dao) {
- dao = &Dao{
- c: c,
- redis: redis.NewPool(c.Redis),
- }
- return
- }
- // Close close the resource.
- func (d *Dao) Close() {
- d.redis.Close()
- }
- func (d *Dao) saveOffset(conn redis.Conn, uid int64, offset int) {
- conn.Do("SETEX", fmt.Sprintf(_recommendOffsetKey, uid), 86400, offset)
- }
- func (d *Dao) addToRecommended(conn redis.Conn, uid int64, ids []int64) {
- if len(ids) == 0 {
- return
- }
- day := time.Now().Format("20060102")
- key := fmt.Sprintf(_recommendedKey, uid, day)
- var is []interface{}
- is = append(is, key)
- for _, id := range ids {
- is = append(is, id)
- }
- conn.Send("EXPIRE", key, 86400)
- conn.Send("SADD", is...)
- conn.Flush()
- conn.Receive()
- _, err := conn.Receive()
- if err != nil {
- log.Info("addToRecommended error +%v", err)
- }
- }
- // GetRandomRoomIds 随机获取count个推荐
- // 如果总数量total比count小,则返回total个
- func (d *Dao) GetRandomRoomIds(ctx context.Context, uid int64, reqCount int, existRoomIDs []int64) (ret []int64, err error) {
- if reqCount == 0 {
- return
- }
- var (
- candidateLen int
- )
- r := d.redis.Get(ctx)
- defer r.Close()
- candidateKey := fmt.Sprintf(_userRecCandidateKey, uid)
- exists, err := redis.Int(r.Do("exists", candidateKey))
- if err != nil {
- err = errors.WithStack(err)
- return
- }
- existMap := map[int64]struct{}{}
- for _, id := range existRoomIDs {
- existMap[id] = struct{}{}
- }
- if exists == 0 {
- var candidate []int64
- var currentOffset = 0
- candidate, err = d.generateLrCandidateList(r, uid, candidateKey)
- if err != nil {
- return
- }
- Loop:
- for len(ret) < reqCount && currentOffset < len(candidate) {
- var tmp []int64
- if len(candidate)-currentOffset < int(reqCount) {
- tmp = candidate[currentOffset:]
- } else {
- tmp = candidate[currentOffset : currentOffset+reqCount]
- }
- //去重
- for _, id := range tmp {
- _, ok := existMap[id]
- currentOffset += 1
- if !ok {
- ret = append(ret, id)
- if len(ret) >= int(reqCount) {
- break Loop
- }
- }
- }
- }
- d.addToRecommended(r, uid, ret)
- d.saveOffset(r, uid, currentOffset)
- } else {
- candidateLen, err = redis.Int(r.Do("LLEN", candidateKey))
- if err != nil {
- return
- }
- var offset int
- offset, _ = redis.Int(r.Do("GET", fmt.Sprintf(_recommendOffsetKey, uid)))
- if offset > (candidateLen - 1) {
- return
- }
- var currentOffset = offset
- Loop2:
- for len(ret) < reqCount && currentOffset < candidateLen {
- var ids []int64
- ids, err = redis.Int64s(r.Do("LRANGE", candidateKey, currentOffset, currentOffset+reqCount-1))
- if err != nil {
- err = errors.WithStack(err)
- return
- }
- // 去重
- for _, id := range ids {
- currentOffset++
- _, ok := existMap[id]
- if !ok {
- ret = append(ret, id)
- if len(ret) >= int(reqCount) {
- break Loop2
- }
- }
- }
- if len(ids) == 0 {
- log.Error("Cannot get recommend candidate, key=%s, offset=%d, count=%d", candidateKey, offset, reqCount)
- break
- }
- }
- d.addToRecommended(r, uid, ret)
- d.saveOffset(r, uid, currentOffset)
- }
- return
- }
- // GetLrRecRoomIds 在GetRandomRoomIds的基础上进行LR计算并返回倒排的房间号列表
- // 与GetRandomRoomIds有相同的输入输出结构
- func (d *Dao) GetLrRecRoomIds(r redis.Conn, uid int64, candidateIds []int64) (ret []int64, err error) {
- var areas string
- areaIds := map[int64]struct{}{}
- areas, err = redis.String(r.Do("GET", fmt.Sprintf(recconst.UserAreaKey, uid)))
- if err != nil && err != redis.ErrNil {
- log.Error("redis GET error: %v", err)
- return
- }
- err = nil
- if areas != "" {
- split := strings.Split(areas, ";")
- for _, areaIdStr := range split {
- areaId, _ := strconv.ParseInt(areaIdStr, 10, 64)
- areaIds[areaId] = struct{}{}
- }
- }
- weightVector := makeWeightVec(d.c)
- roomFeatures, ok := roomFeatureValue.Load().(map[int64][]int64)
- if !ok {
- ret = candidateIds
- return
- }
- roomScoreSlice := ScoreSlice{}
- for _, roomId := range candidateIds {
- if fv, ok := roomFeatures[roomId]; ok {
- featureVector := make([]int64, len(fv))
- copy(featureVector, fv)
- areaId := featureVector[0]
- if _, ok := areaIds[areaId]; ok {
- featureVector[0] = 1
- } else {
- featureVector[0] = 0
- }
- counter := Counter{roomId: roomId, score: calcScore(weightVector, featureVector)}
- roomScoreSlice = append(roomScoreSlice, counter)
- }
- }
- sort.Sort(roomScoreSlice)
- for _, counter := range roomScoreSlice {
- ret = append(ret, counter.roomId)
- }
- return
- }
- // generateCandidateList 得到候选集
- func (d *Dao) generateCandidateList(r redis.Conn, uid int64, candidateKey string) (ret []int64, err error) {
- // 第一步 itemcf,优先级最高。
- itemCFKey := fmt.Sprintf(recconst.UserItemCFRecKey, uid)
- var itemCFList []int64
- itemCFList, err = redis.Int64s(r.Do("ZREVRANGE", itemCFKey, 0, -1))
- if err != nil {
- err = errors.WithStack(err)
- return
- }
- itemCFOnlineIds := d.FilterOnlineRoomIds(itemCFList)
- if len(itemCFOnlineIds) == 0 {
- log.Info("No item-cf room online for user, uid=%d, before online filter room ids: %+v", uid, itemCFList)
- }
- // 第二步 取兴趣分区的房间 人气超过100的房间
- var areas string
- areas, err = redis.String(r.Do("GET", fmt.Sprintf(recconst.UserAreaKey, uid)))
- if err != nil && err != redis.ErrNil {
- err = errors.WithStack(err)
- return
- }
- err = nil
- var areaRoomIDs []int64
- if areas != "" {
- split := strings.Split(areas, ";")
- for _, areaIdStr := range split {
- areaId, _ := strconv.ParseInt(areaIdStr, 10, 64)
- var ids = d.getAreaRoomIds(areaId)
- areaRoomIDs = append(areaRoomIDs, ids...)
- }
- }
- // 第三步 取兴趣分区大分区的100个 先不做
- // 第四步 减去已经推荐过的
- day := time.Now().Format("20060102")
- var recommendedList []int64
- edKey := fmt.Sprintf(_recommendedKey, uid, day)
- recommendedList, err = redis.Int64s(r.Do("SMEMBERS", edKey))
- if err != nil {
- err = errors.WithStack(err)
- return
- }
- recommended := map[int64]struct{}{}
- for _, id := range recommendedList {
- recommended[id] = struct{}{}
- }
- var itemCFFinalIDs []int64
- for _, id := range itemCFOnlineIds {
- _, exist := recommended[id]
- if !exist {
- itemCFFinalIDs = append(itemCFFinalIDs, id)
- }
- }
- var areaRoomFinalIDs []int64
- for _, id := range areaRoomIDs {
- _, exist := recommended[id]
- if !exist {
- areaRoomFinalIDs = append(areaRoomFinalIDs, id)
- }
- }
- ret = mergeArr(itemCFFinalIDs, areaRoomFinalIDs)
- log.Info("UserRecommend : uid=%d total=%d, "+
- "itemcf.original=%d, itemcf.online=%d, itemcf.noviewd=%d, "+
- "areaRoom.original=%d, itemcf.noviewd=%d viewed=%d",
- uid, len(ret), len(itemCFList), len(itemCFOnlineIds), len(itemCFFinalIDs),
- len(areaRoomIDs), len(areaRoomFinalIDs), len(recommendedList))
- return
- }
- // generateCandidateList 得到进过LR的候选集
- func (d *Dao) generateLrCandidateList(r redis.Conn, uid int64, candidateKey string) (ret []int64, err error) {
- roomIDs, err := d.generateCandidateList(r, uid, candidateKey)
- if err != nil {
- log.Error("generateLrCandidateList failed 1, error:%v", err)
- return
- }
- if len(ret) > 0 {
- ret, err = d.GetLrRecRoomIds(r, uid, roomIDs)
- if err != nil {
- log.Error("generateLrCandidateList failed 2, error:%v", err)
- return
- }
- }
- // 召回源不足的情况下补足推荐房间数
- if len(ret) < 150 {
- ids, ok := recDefaultRoomIds.Load().([]int64)
- if !ok {
- return
- }
- ret1, err1 := d.GetLrRecRoomIds(r, uid, ids)
- if err1 != nil {
- log.Error("generateLrCandidateList failed 3, error:%v", err1)
- return
- }
- ret = mergeArrWithOrder(ret, ret1, 150) // TODO:当前ret1的结果是没有过滤掉今天看过的房间的, 看后面是否需要优化
- }
- {
- for _, roomID := range ret {
- r.Send("RPUSH", candidateKey, roomID)
- }
- r.Send("EXPIRE", candidateKey, 60*2)
- err = r.Flush()
- if err != nil {
- err = errors.WithStack(err)
- return
- }
- for i := 0; i < len(ret)+1; i++ {
- r.Receive()
- }
- }
- return
- }
- // Ping dao ping
- func (d *Dao) Ping(ctx context.Context) (err error) {
- conn := d.redis.Get(ctx)
- defer conn.Close()
- _, err = conn.Do("ping")
- if err != nil {
- err = errors.Wrap(err, "dao Ping err")
- }
- return err
- }
- // Counter 房间-分数结构体, 用于构建一个可排序的slice
- type Counter struct {
- roomId int64
- score float32
- }
- // ScoreSlice Counter对象的slice
- type ScoreSlice []Counter
- func (s ScoreSlice) Len() int {
- return len(s)
- }
- func (s ScoreSlice) Swap(i, j int) {
- s[i], s[j] = s[j], s[i]
- }
- func (s ScoreSlice) Less(i, j int) bool {
- return s[j].score < s[i].score
- }
- func calcScore(weightVector []float32, featureVector []int64) (score float32) {
- if len(weightVector) != len(featureVector) {
- panic(fmt.Sprintf("权重数量和特征数量不匹配, 请检查配置或逻辑, weight: %+v, feature: %+v", weightVector, featureVector))
- }
- for i := 0; i < min(len(weightVector), len(featureVector)); i++ {
- score += weightVector[i] * float32(featureVector[i])
- }
- return
- }
- func min(x int, y int) int {
- if x < y {
- return x
- }
- return y
- }
- // 合并两个集合
- func mergeArr(x []int64, y []int64) (ret []int64) {
- tmpMap := map[int64]struct{}{}
- for _, id := range x {
- tmpMap[id] = struct{}{}
- }
- for _, id := range y {
- tmpMap[id] = struct{}{}
- }
- for id := range tmpMap {
- ret = append(ret, id)
- }
- return
- }
- // 按x, y的顺序合并两个集合, 当x的长度不小于limit则直接返回
- func mergeArrWithOrder(x []int64, y []int64, limit int) (ret []int64) {
- if len(x) >= limit {
- ret = x
- return
- }
- tmpMap := map[int64]struct{}{}
- ret = append(ret, x...)
- num := len(ret)
- for _, id := range x {
- tmpMap[id] = struct{}{}
- }
- for _, id := range y {
- if _, ok := tmpMap[id]; ok {
- continue
- }
- num += 1
- tmpMap[id] = struct{}{}
- ret = append(ret, id)
- if num >= limit {
- break
- }
- }
- return
- }
- func makeWeightVec(c *conf.Config) (ret []float32) {
- ret = append(ret, c.CommonFeature.UserAreaInterest.Weights...)
- ret = append(ret, c.CommonFeature.FansNum.Weights...)
- ret = append(ret, c.CommonFeature.CornerSign.Weights...)
- ret = append(ret, c.CommonFeature.Online.Weights...)
- return
- }
|