设计推荐系统
问题
如何用 Go 设计一个推荐系统?理解召回、排序的核心流程。
答案
推荐系统架构
| 阶段 | 候选数 | 作用 |
|---|---|---|
| 召回 | 万级 → 千级 | 快速筛出候选集 |
| 粗排 | 千级 → 百级 | 轻量模型初筛 |
| 精排 | 百级 → 十级 | 精确打分排序 |
| 重排 | 十级 | 去重、多样性、运营干预 |
基于协同过滤的召回
// 用户行为矩阵:user → item → score
type UserItemMatrix map[string]map[string]float64
// 基于用户的协同过滤
// 找到和目标用户最相似的 K 个用户,推荐他们喜欢的物品
func UserBasedCF(matrix UserItemMatrix, targetUser string, k int) []string {
// 1. 计算用户相似度(余弦相似度)
similarities := make(map[string]float64)
targetItems := matrix[targetUser]
for user, items := range matrix {
if user == targetUser {
continue
}
similarities[user] = cosineSimilarity(targetItems, items)
}
// 2. 取 Top-K 相似用户
topUsers := topK(similarities, k)
// 3. 推荐相似用户喜欢但目标用户没看过的物品
seen := make(map[string]bool)
for item := range targetItems {
seen[item] = true
}
scores := make(map[string]float64)
for _, user := range topUsers {
for item, score := range matrix[user] {
if !seen[item] {
scores[item] += score * similarities[user]
}
}
}
return sortByScore(scores)
}
// 余弦相似度
func cosineSimilarity(a, b map[string]float64) float64 {
var dotProduct, normA, normB float64
for key, va := range a {
if vb, ok := b[key]; ok {
dotProduct += va * vb
}
normA += va * va
}
for _, vb := range b {
normB += vb * vb
}
if normA == 0 || normB == 0 {
return 0
}
return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB))
}
基于内容的推荐
// 物品标签向量
type Item struct {
ID string
Tags map[string]float64 // 标签 → 权重
}
// 根据用户历史偏好推荐相似物品
func ContentBasedRecommend(userHistory []Item, candidates []Item, topN int) []Item {
// 用户偏好 = 历史物品标签的加权平均
userProfile := make(map[string]float64)
for _, item := range userHistory {
for tag, weight := range item.Tags {
userProfile[tag] += weight
}
}
// 计算候选物品与用户偏好的相似度
type scored struct {
item Item
score float64
}
var results []scored
for _, candidate := range candidates {
score := cosineSimilarity(userProfile, candidate.Tags)
results = append(results, scored{candidate, score})
}
sort.Slice(results, func(i, j int) bool {
return results[i].score > results[j].score
})
items := make([]Item, 0, topN)
for i := 0; i < topN && i < len(results); i++ {
items = append(items, results[i].item)
}
return items
}
热门推荐 + Redis
// 基于 Redis ZSet 实现热门排行
func RecordClick(rdb *redis.Client, itemID string) {
// 按小时粒度记录点击量
key := fmt.Sprintf("hot:items:%s", time.Now().Format("2006010215"))
rdb.ZIncrBy(context.Background(), key, 1, itemID)
rdb.Expire(context.Background(), key, 48*time.Hour)
}
func GetHotItems(rdb *redis.Client, topN int) []string {
key := fmt.Sprintf("hot:items:%s", time.Now().Format("2006010215"))
results, _ := rdb.ZRevRangeByScore(context.Background(), key, &redis.ZRangeBy{
Min: "-inf",
Max: "+inf",
Count: int64(topN),
}).Result()
return results
}
推荐 API
func RecommendHandler(c *gin.Context) {
userID := c.Query("user_id")
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
// 多路召回并发执行
var (
cfItems, hotItems, contentItems []string
)
var wg sync.WaitGroup
wg.Add(3)
go func() { defer wg.Done(); cfItems = recallByCF(userID) }()
go func() { defer wg.Done(); hotItems = recallByHot() }()
go func() { defer wg.Done(); contentItems = recallByContent(userID) }()
wg.Wait()
// 合并去重
candidates := mergeAndDedup(cfItems, hotItems, contentItems)
// 排序(简化版:按分数排序)
ranked := rank(candidates, userID)
// 截取 TopN
if len(ranked) > limit {
ranked = ranked[:limit]
}
c.JSON(200, gin.H{"items": ranked})
}
常见面试问题
Q1: 冷启动怎么解决?
答案:
- 新用户冷启动:展示热门/编辑推荐,引导用户选择兴趣标签
- 新物品冷启动:基于内容特征推荐,利用物品标签匹配用户偏好
- 用 Explore-Exploit 策略(如 ε-greedy)平衡探索和利用
Q2: 如何评估推荐效果?
答案:
- 离线指标:准确率、召回率、F1、AUC、NDCG
- 在线指标:CTR(点击率)、CVR(转化率)、用户停留时长
- A/B 测试对比不同策略