HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 系统设计实战

    • 系统设计面试教程
    • 系统设计方法论
    • 01-短链系统设计
    • 02 - 秒杀系统设计
    • 03 - IM 即时通讯系统设计
    • 04 - Feed 流系统设计
    • 05 - 分布式 ID 生成器设计
    • 06 - 限流系统设计
    • 第7章:搜索引擎设计
    • 08 - 推荐系统设计
    • 09 - 支付系统设计
    • 10 - 电商系统设计
    • 11 - 直播系统设计
    • 第12章:缓存系统设计
    • 第13章:消息队列设计
    • 第14章:分布式事务
    • 15 - 监控系统设计

08 - 推荐系统设计

> 面试频率: 需求类型指标
用户规模1 亿日活
物品规模1000 万商品/视频
QPS10 万(推荐请求)
响应时间P99 < 100ms
准确率CTR > 5%(点击率)
实时性行为 → 推荐 < 1 分钟

1.3 面试官可能的追问

Q1: 推荐系统和搜索有什么区别?

A1:

  • 搜索:用户主动输入关键词,有明确意图
  • 推荐:系统主动推送,挖掘潜在需求
  • 搜索:召回 → 排序
  • 推荐:召回 → 粗排 → 精排 → 重排

Q2: 如何解决冷启动问题?

A2:

  • 新用户:热门推荐、基于人口属性(性别、年龄)
  • 新物品:基于内容相似度、少量流量试探
  • AB 测试:逐步放量

2. 容量估算

2.1 场景假设

假设为一个中型电商/短视频平台设计推荐系统:

  • 日活用户(DAU):1 亿
  • 每用户日均推荐请求:50 次
  • 物品总数:1000 万
  • 用户行为:浏览、点击、收藏、购买

2.2 QPS 估算

日推荐请求 = 1 亿 × 50 = 50 亿次

平均 QPS = 50 亿 / 86400 ≈ 58,000 QPS
峰值 QPS = 58,000 × 2 = 116,000 QPS

2.3 存储估算

用户行为数据

单条行为 = 用户ID(8字节) + 物品ID(8字节) + 行为类型(1字节) + 时间戳(8字节) = 25 字节

日行为数 = 1 亿 × 50 = 50 亿条
日存储 = 50 亿 × 25 字节 ≈ 125 GB

月存储 = 125 GB × 30 ≈ 3.75 TB
年存储 ≈ 45 TB

用户画像

用户画像 = 1 亿用户 × 1 KB = 100 GB

物品特征

物品特征 = 1000 万物品 × 2 KB = 20 GB

推荐模型

协同过滤模型:用户-物品矩阵(稀疏)
深度学习模型:Embedding 向量
总存储 ≈ 100 GB

总存储需求:约 50 TB(含历史数据)

2.4 计算资源估算

离线计算

协同过滤:每日全量计算
计算时间:2-4 小时
Spark 集群:100 台(16核 64GB)

在线计算

实时推荐:召回 + 排序
单次推荐耗时 < 100ms
所需服务器:116,000 QPS / 1000 QPS ≈ 116 台

3. API 设计

3.1 核心 API

3.1.1 获取推荐列表

GET /api/v1/recommendations

Request:
{
  "user_id": 12345,
  "scene": "homepage",              // homepage | detail_page | cart
  "count": 20,                      // 推荐数量
  "context": {
    "device": "ios",
    "location": "Beijing",
    "time": "2023-11-13T14:30:00Z"
  },
  "filters": {
    "exclude_item_ids": [101, 102], // 已看过的
    "category": "electronics"
  }
}

Response:
{
  "code": 0,
  "message": "success",
  "data": {
    "items": [
      {
        "item_id": 1001,
        "title": "iPhone 15 Pro",
        "score": 0.95,                // 推荐分数
        "reason": "猜你喜欢",          // 推荐理由
        "tags": ["数码", "热门"],
        "recall_strategy": "user_cf"  // 召回策略
      },
      {
        "item_id": 1002,
        "title": "AirPods Pro",
        "score": 0.88,
        "reason": "看了还看",
        "recall_strategy": "item_cf"
      }
    ],
    "trace_id": "rec_20231113_12345"  // 用于追踪
  }
}

3.1.2 上报用户行为

POST /api/v1/behaviors

Request:
{
  "user_id": 12345,
  "behaviors": [
    {
      "item_id": 1001,
      "behavior_type": "click",     // view | click | collect | purchase
      "timestamp": 1699876200,
      "duration": 30,                // 停留时长(秒)
      "context": {
        "page": "homepage",
        "position": 1,
        "trace_id": "rec_20231113_12345"
      }
    }
  ]
}

Response:
{
  "code": 0,
  "message": "success"
}

3.1.3 相似物品推荐

GET /api/v1/recommendations/similar

Request:
{
  "item_id": 1001,
  "count": 10
}

Response:
{
  "code": 0,
  "message": "success",
  "data": {
    "items": [
      {
        "item_id": 1002,
        "similarity": 0.92,
        "title": "AirPods Pro"
      }
    ]
  }
}

4. 数据模型设计

4.1 用户行为表

CREATE TABLE user_behaviors (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    item_id BIGINT NOT NULL,
    
    behavior_type VARCHAR(20) NOT NULL COMMENT 'view|click|collect|purchase',
    
    duration INT COMMENT '停留时长(秒)',
    
    -- 上下文
    scene VARCHAR(50) COMMENT 'homepage|detail_page',
    position INT COMMENT '推荐位置',
    trace_id VARCHAR(64) COMMENT '追踪ID',
    
    -- 设备信息
    device VARCHAR(20),
    os VARCHAR(20),
    
    timestamp BIGINT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_user_time (user_id, timestamp),
    INDEX idx_item_time (item_id, timestamp),
    INDEX idx_trace (trace_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为表';

4.2 用户画像表

CREATE TABLE user_profiles (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL UNIQUE,
    
    -- 基本信息
    gender VARCHAR(10),
    age_range VARCHAR(20),
    city VARCHAR(50),
    
    -- 行为统计
    total_views INT DEFAULT 0,
    total_clicks INT DEFAULT 0,
    total_purchases INT DEFAULT 0,
    
    -- 偏好标签(JSON)
    preference_tags JSON COMMENT '[{"tag": "数码", "weight": 0.8}]',
    
    -- 偏好品牌
    favorite_brands JSON,
    
    -- 价格偏好
    avg_price_range VARCHAR(20),
    
    -- 活跃度
    last_active_time DATETIME,
    active_days INT DEFAULT 0,
    
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户画像表';

4.3 物品特征表

CREATE TABLE item_features (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    item_id BIGINT NOT NULL UNIQUE,
    
    -- 基本信息
    title VARCHAR(256) NOT NULL,
    category_id INT,
    brand_id INT,
    price DECIMAL(10, 2),
    
    -- 标签
    tags JSON COMMENT '["数码", "手机", "5G"]',
    
    -- 统计信息
    total_views INT DEFAULT 0,
    total_clicks INT DEFAULT 0,
    total_purchases INT DEFAULT 0,
    
    -- CTR(点击率)
    ctr DECIMAL(5, 4) DEFAULT 0,
    
    -- 转化率
    cvr DECIMAL(5, 4) DEFAULT 0,
    
    -- 质量分
    quality_score DECIMAL(5, 2) DEFAULT 0,
    
    -- Embedding 向量(存储路径或二进制)
    embedding_vector BLOB,
    
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    INDEX idx_item_id (item_id),
    INDEX idx_category (category_id),
    INDEX idx_ctr (ctr)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品特征表';

4.4 推荐结果表(缓存)

CREATE TABLE recommendation_cache (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    
    scene VARCHAR(50) NOT NULL,
    
    -- 推荐列表(JSON)
    items JSON COMMENT '[{"item_id": 1001, "score": 0.95}]',
    
    -- 生成时间
    generated_at DATETIME NOT NULL,
    
    -- 过期时间
    expires_at DATETIME NOT NULL,
    
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    
    UNIQUE KEY uk_user_scene (user_id, scene),
    INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐结果缓存表';

4.5 相似度矩阵表(物品 CF)

CREATE TABLE item_similarity (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    item_id_a BIGINT NOT NULL,
    item_id_b BIGINT NOT NULL,
    
    similarity DECIMAL(5, 4) NOT NULL,
    
    -- 计算方法
    method VARCHAR(20) COMMENT 'cosine|jaccard|pearson',
    
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    UNIQUE KEY uk_item_pair (item_id_a, item_id_b),
    INDEX idx_item_a (item_id_a, similarity)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品相似度表';

5. 架构设计

5.1 整体架构

┌─────────────────────────────────────────────────────────────┐
│                          用户端                              │
│                 (App / Web / 小程序)                         │
└─────────────────────┬───────────────────────────────────────┘
                      │
                      
┌─────────────────────────────────────────────────────────────┐
│                      API Gateway                            │
│                  (限流、鉴权、路由)                           │
└─────────────┬───────────────────────────────────────────────┘
              │
              
┌─────────────────────────────────────────────────────────────┐
│                    推荐服务层                                │
│                                                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐  │
│  │  召回层  │→ │  粗排层  │→ │  精排层  │→ │  重排层  │  │
│  │ (Recall) │  │(Pre-rank)│  │  (Rank)  │  │(Re-rank) │  │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘  │
│       ↑             ↑             ↑             ↑          │
└───────┼─────────────┼─────────────┼─────────────┼──────────┘
        │             │             │             │
                                               
┌─────────────────────────────────────────────────────────────┐
│                    数据层                                    │
│                                                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐       │
│  │  用户画像   │  │  物品特征   │  │  实时特征   │       │
│  │   (Redis)   │  │   (Redis)   │  │   (Redis)   │       │
│  └─────────────┘  └─────────────┘  └─────────────┘       │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    离线计算层                                │
│                                                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐       │
│  │  协同过滤   │  │  物品相似度 │  │  深度学习   │       │
│  │   (Spark)   │  │   (Spark)   │  │   (TF)      │       │
│  └─────────────┘  └─────────────┘  └─────────────┘       │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    实时计算层                                │
│                                                             │
│  ┌─────────────┐  ┌─────────────┐                         │
│  │  行为流处理 │  │  特征更新   │                         │
│  │  (Flink)    │  │  (Flink)    │                         │
│  └─────────────┘  └─────────────┘                         │
└─────────────────────────────────────────────────────────────┘

5.2 V1: 基于协同过滤的推荐系统(MVP)

适用场景:初创产品、用户量 < 100 万

package main

import (
    "fmt"
    "math"
    "sort"
)

// ==================== 数据结构 ====================

// UserBehavior 用户行为
type UserBehavior struct {
    UserID    int64
    ItemID    int64
    BehaviorType string // "view" | "click" | "purchase"
    Score     float64   // 隐式评分
    Timestamp int64
}

// RecommendationItem 推荐物品
type RecommendationItem struct {
    ItemID int64
    Score  float64
    Reason string
}

// ==================== 协同过滤:User-based CF ====================

// UserBasedCF 基于用户的协同过滤
type UserBasedCF struct {
    // 用户-物品评分矩阵
    userItemMatrix map[int64]map[int64]float64
    
    // 用户相似度矩阵
    userSimilarity map[int64]map[int64]float64
}

// NewUserBasedCF 创建 User-based CF
func NewUserBasedCF() *UserBasedCF {
    return &UserBasedCF{
        userItemMatrix: make(map[int64]map[int64]float64),
        userSimilarity: make(map[int64]map[int64]float64),
    }
}

// AddBehavior 添加用户行为
func (cf *UserBasedCF) AddBehavior(behavior *UserBehavior) {
    if cf.userItemMatrix[behavior.UserID] == nil {
        cf.userItemMatrix[behavior.UserID] = make(map[int64]float64)
    }
    
    // 隐式评分:浏览=1, 点击=3, 购买=5
    score := map[string]float64{
        "view":     1.0,
        "click":    3.0,
        "purchase": 5.0,
    }[behavior.BehaviorType]
    
    // 累加评分
    cf.userItemMatrix[behavior.UserID][behavior.ItemID] += score
}

// CalculateUserSimilarity 计算用户相似度(余弦相似度)
func (cf *UserBasedCF) CalculateUserSimilarity() {
    users := make([]int64, 0)
    for userID := range cf.userItemMatrix {
        users = append(users, userID)
    }
    
    // 计算所有用户对之间的相似度
    for i := 0; i < len(users); i++ {
        userA := users[i]
        if cf.userSimilarity[userA] == nil {
            cf.userSimilarity[userA] = make(map[int64]float64)
        }
        
        for j := i + 1; j < len(users); j++ {
            userB := users[j]
            similarity := cf.cosineSimilarity(userA, userB)
            
            if similarity > 0 {
                cf.userSimilarity[userA][userB] = similarity
                if cf.userSimilarity[userB] == nil {
                    cf.userSimilarity[userB] = make(map[int64]float64)
                }
                cf.userSimilarity[userB][userA] = similarity
            }
        }
    }
}

// cosineSimilarity 余弦相似度
func (cf *UserBasedCF) cosineSimilarity(userA, userB int64) float64 {
    itemsA := cf.userItemMatrix[userA]
    itemsB := cf.userItemMatrix[userB]
    
    // 找出共同物品
    commonItems := make(map[int64]bool)
    for itemID := range itemsA {
        if _, exists := itemsB[itemID]; exists {
            commonItems[itemID] = true
        }
    }
    
    if len(commonItems) == 0 {
        return 0
    }
    
    // 计算余弦相似度
    var dotProduct, normA, normB float64
    
    for itemID := range commonItems {
        dotProduct += itemsA[itemID] * itemsB[itemID]
    }
    
    for _, score := range itemsA {
        normA += score * score
    }
    
    for _, score := range itemsB {
        normB += score * score
    }
    
    if normA == 0 || normB == 0 {
        return 0
    }
    
    return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB))
}

// Recommend 生成推荐列表
func (cf *UserBasedCF) Recommend(userID int64, count int) []*RecommendationItem {
    // 1. 找到相似用户(Top K)
    similarUsers := cf.getTopKSimilarUsers(userID, 50)
    
    // 2. 聚合相似用户喜欢的物品
    candidateItems := make(map[int64]float64)
    userItems := cf.userItemMatrix[userID]
    
    for _, su := range similarUsers {
        similarUserID := su.UserID
        similarity := su.Similarity
        
        for itemID, score := range cf.userItemMatrix[similarUserID] {
            // 过滤用户已交互物品
            if _, exists := userItems[itemID]; exists {
                continue
            }
            
            // 加权累加
            candidateItems[itemID] += score * similarity
        }
    }
    
    // 3. 排序并返回 Top N
    items := make([]*RecommendationItem, 0)
    for itemID, score := range candidateItems {
        items = append(items, &RecommendationItem{
            ItemID: itemID,
            Score:  score,
            Reason: "猜你喜欢",
        })
    }
    
    sort.Slice(items, func(i, j int) bool {
        return items[i].Score > items[j].Score
    })
    
    if len(items) > count {
        items = items[:count]
    }
    
    return items
}

// getTopKSimilarUsers 获取最相似的 K 个用户
func (cf *UserBasedCF) getTopKSimilarUsers(userID int64, k int) []*SimilarUser {
    similarities := cf.userSimilarity[userID]
    
    users := make([]*SimilarUser, 0)
    for otherUserID, similarity := range similarities {
        users = append(users, &SimilarUser{
            UserID:     otherUserID,
            Similarity: similarity,
        })
    }
    
    sort.Slice(users, func(i, j int) bool {
        return users[i].Similarity > users[j].Similarity
    })
    
    if len(users) > k {
        users = users[:k]
    }
    
    return users
}

type SimilarUser struct {
    UserID     int64
    Similarity float64
}

// ==================== 协同过滤:Item-based CF ====================

// ItemBasedCF 基于物品的协同过滤
type ItemBasedCF struct {
    // 用户-物品评分矩阵
    userItemMatrix map[int64]map[int64]float64
    
    // 物品相似度矩阵
    itemSimilarity map[int64]map[int64]float64
}

// NewItemBasedCF 创建 Item-based CF
func NewItemBasedCF() *ItemBasedCF {
    return &ItemBasedCF{
        userItemMatrix: make(map[int64]map[int64]float64),
        itemSimilarity: make(map[int64]map[int64]float64),
    }
}

// AddBehavior 添加用户行为
func (cf *ItemBasedCF) AddBehavior(behavior *UserBehavior) {
    if cf.userItemMatrix[behavior.UserID] == nil {
        cf.userItemMatrix[behavior.UserID] = make(map[int64]float64)
    }
    
    score := map[string]float64{
        "view":     1.0,
        "click":    3.0,
        "purchase": 5.0,
    }[behavior.BehaviorType]
    
    cf.userItemMatrix[behavior.UserID][behavior.ItemID] += score
}

// CalculateItemSimilarity 计算物品相似度
func (cf *ItemBasedCF) CalculateItemSimilarity() {
    // 构建倒排索引:物品 -> 用户列表
    itemUsers := make(map[int64]map[int64]float64)
    
    for userID, items := range cf.userItemMatrix {
        for itemID, score := range items {
            if itemUsers[itemID] == nil {
                itemUsers[itemID] = make(map[int64]float64)
            }
            itemUsers[itemID][userID] = score
        }
    }
    
    // 计算物品相似度
    items := make([]int64, 0)
    for itemID := range itemUsers {
        items = append(items, itemID)
    }
    
    for i := 0; i < len(items); i++ {
        itemA := items[i]
        if cf.itemSimilarity[itemA] == nil {
            cf.itemSimilarity[itemA] = make(map[int64]float64)
        }
        
        for j := i + 1; j < len(items); j++ {
            itemB := items[j]
            
            // 使用 Jaccard 相似度(简化)
            usersA := itemUsers[itemA]
            usersB := itemUsers[itemB]
            
            commonUsers := 0
            for userID := range usersA {
                if _, exists := usersB[userID]; exists {
                    commonUsers++
                }
            }
            
            if commonUsers == 0 {
                continue
            }
            
            totalUsers := len(usersA) + len(usersB) - commonUsers
            similarity := float64(commonUsers) / float64(totalUsers)
            
            cf.itemSimilarity[itemA][itemB] = similarity
            if cf.itemSimilarity[itemB] == nil {
                cf.itemSimilarity[itemB] = make(map[int64]float64)
            }
            cf.itemSimilarity[itemB][itemA] = similarity
        }
    }
}

// Recommend 生成推荐列表
func (cf *ItemBasedCF) Recommend(userID int64, count int) []*RecommendationItem {
    // 1. 获取用户历史交互物品
    userItems := cf.userItemMatrix[userID]
    
    // 2. 找到相似物品
    candidateItems := make(map[int64]float64)
    
    for itemID, userScore := range userItems {
        similarItems := cf.itemSimilarity[itemID]
        
        for similarItemID, similarity := range similarItems {
            // 过滤已交互物品
            if _, exists := userItems[similarItemID]; exists {
                continue
            }
            
            // 加权累加
            candidateItems[similarItemID] += userScore * similarity
        }
    }
    
    // 3. 排序并返回 Top N
    items := make([]*RecommendationItem, 0)
    for itemID, score := range candidateItems {
        items = append(items, &RecommendationItem{
            ItemID: itemID,
            Score:  score,
            Reason: "看了还看",
        })
    }
    
    sort.Slice(items, func(i, j int) bool {
        return items[i].Score > items[j].Score
    })
    
    if len(items) > count {
        items = items[:count]
    }
    
    return items
}

// ==================== 测试代码 ====================

func main() {
    // 创建 User-based CF
    userCF := NewUserBasedCF()
    
    // 添加用户行为
    behaviors := []*UserBehavior{
        {UserID: 1, ItemID: 101, BehaviorType: "purchase"},
        {UserID: 1, ItemID: 102, BehaviorType: "click"},
        {UserID: 1, ItemID: 103, BehaviorType: "view"},
        
        {UserID: 2, ItemID: 101, BehaviorType: "purchase"},
        {UserID: 2, ItemID: 102, BehaviorType: "purchase"},
        {UserID: 2, ItemID: 104, BehaviorType: "click"},
        
        {UserID: 3, ItemID: 102, BehaviorType: "click"},
        {UserID: 3, ItemID: 103, BehaviorType: "purchase"},
        {UserID: 3, ItemID: 105, BehaviorType: "view"},
    }
    
    for _, behavior := range behaviors {
        userCF.AddBehavior(behavior)
    }
    
    // 计算相似度
    userCF.CalculateUserSimilarity()
    
    // 生成推荐
    recommendations := userCF.Recommend(1, 5)
    
    fmt.Println("=== User-based CF 推荐结果 ===")
    for i, item := range recommendations {
        fmt.Printf("%d. ItemID: %d, Score: %.2f, Reason: %s\n",
            i+1, item.ItemID, item.Score, item.Reason)
    }
    
    // 创建 Item-based CF
    itemCF := NewItemBasedCF()
    for _, behavior := range behaviors {
        itemCF.AddBehavior(behavior)
    }
    
    itemCF.CalculateItemSimilarity()
    recommendations = itemCF.Recommend(1, 5)
    
    fmt.Println("\n=== Item-based CF 推荐结果 ===")
    for i, item := range recommendations {
        fmt.Printf("%d. ItemID: %d, Score: %.2f, Reason: %s\n",
            i+1, item.ItemID, item.Score, item.Reason)
    }
}

输出示例:

=== User-based CF 推荐结果 ===
1. ItemID: 104, Score: 13.50, Reason: 猜你喜欢
2. ItemID: 105, Score: 8.20, Reason: 猜你喜欢

=== Item-based CF 推荐结果 ===
1. ItemID: 104, Score: 5.67, Reason: 看了还看
2. ItemID: 105, Score: 3.00, Reason: 看了还看

V1 特点:

  • 实现简单,易于理解
  • 可解释性强(相似用户/物品)
  • 数据稀疏问题
  • 冷启动问题
  • 可扩展性差(矩阵计算)

5.3 V2: 多路召回 + 排序

优化点:

  1. 多路召回策略
  2. 两阶段排序(粗排 + 精排)
  3. 特征工程
  4. 模型融合
// ==================== 多路召回 ====================

package recall

import (
    "sort"
)

// RecallStrategy 召回策略接口
type RecallStrategy interface {
    Recall(userID int64, count int) []*CandidateItem
    Name() string
}

// CandidateItem 候选物品
type CandidateItem struct {
    ItemID         int64
    Score          float64
    RecallStrategy string
    Features       map[string]float64 // 特征
}

// MultiRecallEngine 多路召回引擎
type MultiRecallEngine struct {
    strategies []RecallStrategy
}

// NewMultiRecallEngine 创建多路召回引擎
func NewMultiRecallEngine() *MultiRecallEngine {
    return &MultiRecallEngine{
        strategies: make([]RecallStrategy, 0),
    }
}

// AddStrategy 添加召回策略
func (e *MultiRecallEngine) AddStrategy(strategy RecallStrategy) {
    e.strategies = append(e.strategies, strategy)
}

// Recall 多路召回
func (e *MultiRecallEngine) Recall(userID int64, countPerStrategy int) []*CandidateItem {
    // 1. 并发召回
    resultChan := make(chan []*CandidateItem, len(e.strategies))
    
    for _, strategy := range e.strategies {
        go func(s RecallStrategy) {
            items := s.Recall(userID, countPerStrategy)
            resultChan <- items
        }(strategy)
    }
    
    // 2. 合并结果
    allItems := make([]*CandidateItem, 0)
    for i := 0; i < len(e.strategies); i++ {
        items := <-resultChan
        allItems = append(allItems, items...)
    }
    
    // 3. 去重
    itemMap := make(map[int64]*CandidateItem)
    for _, item := range allItems {
        if existing, exists := itemMap[item.ItemID]; exists {
            // 取最高分
            if item.Score > existing.Score {
                itemMap[item.ItemID] = item
            }
        } else {
            itemMap[item.ItemID] = item
        }
    }
    
    // 4. 转为列表
    uniqueItems := make([]*CandidateItem, 0)
    for _, item := range itemMap {
        uniqueItems = append(uniqueItems, item)
    }
    
    return uniqueItems
}

// ==================== 召回策略实现 ====================

// UserCFRecall 基于用户协同过滤的召回
type UserCFRecall struct {
    cf *UserBasedCF
}

func (r *UserCFRecall) Recall(userID int64, count int) []*CandidateItem {
    recommendations := r.cf.Recommend(userID, count)
    
    items := make([]*CandidateItem, 0)
    for _, rec := range recommendations {
        items = append(items, &CandidateItem{
            ItemID:         rec.ItemID,
            Score:          rec.Score,
            RecallStrategy: "user_cf",
            Features:       make(map[string]float64),
        })
    }
    return items
}

func (r *UserCFRecall) Name() string {
    return "user_cf"
}

// ItemCFRecall 基于物品协同过滤的召回
type ItemCFRecall struct {
    cf *ItemBasedCF
}

func (r *ItemCFRecall) Recall(userID int64, count int) []*CandidateItem {
    recommendations := r.cf.Recommend(userID, count)
    
    items := make([]*CandidateItem, 0)
    for _, rec := range recommendations {
        items = append(items, &CandidateItem{
            ItemID:         rec.ItemID,
            Score:          rec.Score,
            RecallStrategy: "item_cf",
            Features:       make(map[string]float64),
        })
    }
    return items
}

func (r *ItemCFRecall) Name() string {
    return "item_cf"
}

// HotRecall 热门召回
type HotRecall struct {
    hotItems []int64
}

func (r *HotRecall) Recall(userID int64, count int) []*CandidateItem {
    items := make([]*CandidateItem, 0)
    for i, itemID := range r.hotItems {
        if i >= count {
            break
        }
        items = append(items, &CandidateItem{
            ItemID:         itemID,
            Score:          float64(len(r.hotItems) - i), // 热度分
            RecallStrategy: "hot",
            Features:       make(map[string]float64),
        })
    }
    return items
}

func (r *HotRecall) Name() string {
    return "hot"
}

// ==================== 排序层 ====================

package ranking

// RankingModel 排序模型接口
type RankingModel interface {
    Predict(item *CandidateItem, userFeatures map[string]float64) float64
}

// LRModel 逻辑回归模型(简化)
type LRModel struct {
    weights map[string]float64
    bias    float64
}

// Predict 预测 CTR
func (m *LRModel) Predict(item *CandidateItem, userFeatures map[string]float64) float64 {
    // 特征拼接
    features := make(map[string]float64)
    
    // 用户特征
    for k, v := range userFeatures {
        features["user_"+k] = v
    }
    
    // 物品特征
    for k, v := range item.Features {
        features["item_"+k] = v
    }
    
    // 召回分数
    features["recall_score"] = item.Score
    
    // 线性加权
    score := m.bias
    for feature, value := range features {
        if weight, exists := m.weights[feature]; exists {
            score += weight * value
        }
    }
    
    // Sigmoid
    return 1.0 / (1.0 + math.Exp(-score))
}

// RankingService 排序服务
type RankingService struct {
    model RankingModel
}

// Rank 排序
func (s *RankingService) Rank(items []*CandidateItem, userFeatures map[string]float64) []*CandidateItem {
    // 1. 预测 CTR
    for _, item := range items {
        item.Score = s.model.Predict(item, userFeatures)
    }
    
    // 2. 排序
    sort.Slice(items, func(i, j int) bool {
        return items[i].Score > items[j].Score
    })
    
    return items
}

// ==================== 完整推荐流程 ====================

type RecommendationService struct {
    recallEngine  *MultiRecallEngine
    rankingService *RankingService
}

func (s *RecommendationService) Recommend(userID int64, count int) []*CandidateItem {
    // 1. 召回(每路召回 500 个)
    candidates := s.recallEngine.Recall(userID, 500)
    
    // 2. 粗排(筛选 Top 100)
    candidates = s.preRank(candidates, 100)
    
    // 3. 精排
    userFeatures := s.getUserFeatures(userID)
    rankedItems := s.rankingService.Rank(candidates, userFeatures)
    
    // 4. 重排(多样性、打散)
    finalItems := s.rerank(rankedItems, count)
    
    return finalItems
}

func (s *RecommendationService) preRank(items []*CandidateItem, count int) []*CandidateItem {
    // 简单规则排序
    sort.Slice(items, func(i, j int) bool {
        return items[i].Score > items[j].Score
    })
    
    if len(items) > count {
        items = items[:count]
    }
    return items
}

func (s *RecommendationService) rerank(items []*CandidateItem, count int) []*CandidateItem {
    // 去重、打散(避免同类物品连续出现)
    // 这里简化实现
    if len(items) > count {
        items = items[:count]
    }
    return items
}

func (s *RecommendationService) getUserFeatures(userID int64) map[string]float64 {
    // 从 Redis/DB 获取用户特征
    return map[string]float64{
        "age":          25,
        "gender":       1, // 0=女, 1=男
        "active_days":  180,
        "avg_price":    500,
    }
}

V2 特点:

  • 多路召回提高覆盖率
  • 特征工程提升精度
  • 两阶段排序平衡效率和准确率
  • ⚠️ 特征工程需要人工设计

5.4 V3: 深度学习推荐系统

优化点:

  1. Embedding 向量化
  2. 深度学习模型(Wide & Deep)
  3. 实时特征更新
  4. A/B 测试框架
// ==================== Embedding 向量化 ====================

package embedding

import (
    "math"
)

// EmbeddingModel Embedding 模型
type EmbeddingModel struct {
    // 用户 Embedding
    userEmbeddings map[int64][]float64
    
    // 物品 Embedding
    itemEmbeddings map[int64][]float64
    
    // Embedding 维度
    dimension int
}

// NewEmbeddingModel 创建 Embedding 模型
func NewEmbeddingModel(dimension int) *EmbeddingModel {
    return &EmbeddingModel{
        userEmbeddings: make(map[int64][]float64),
        itemEmbeddings: make(map[int64][]float64),
        dimension:      dimension,
    }
}

// GetUserEmbedding 获取用户 Embedding
func (m *EmbeddingModel) GetUserEmbedding(userID int64) []float64 {
    if emb, exists := m.userEmbeddings[userID]; exists {
        return emb
    }
    
    // 新用户:随机初始化或零向量
    return make([]float64, m.dimension)
}

// GetItemEmbedding 获取物品 Embedding
func (m *EmbeddingModel) GetItemEmbedding(itemID int64) []float64 {
    if emb, exists := m.itemEmbeddings[itemID]; exists {
        return emb
    }
    return make([]float64, m.dimension)
}

// CosineSimilarity 计算余弦相似度
func CosineSimilarity(vec1, vec2 []float64) float64 {
    if len(vec1) != len(vec2) {
        return 0
    }
    
    var dotProduct, norm1, norm2 float64
    
    for i := 0; i < len(vec1); i++ {
        dotProduct += vec1[i] * vec2[i]
        norm1 += vec1[i] * vec1[i]
        norm2 += vec2[i] * vec2[i]
    }
    
    if norm1 == 0 || norm2 == 0 {
        return 0
    }
    
    return dotProduct / (math.Sqrt(norm1) * math.Sqrt(norm2))
}

// RecallByEmbedding 基于 Embedding 召回
func (m *EmbeddingModel) RecallByEmbedding(userID int64, count int) []int64 {
    userEmb := m.GetUserEmbedding(userID)
    
    // 计算与所有物品的相似度
    type ItemScore struct {
        ItemID int64
        Score  float64
    }
    
    scores := make([]ItemScore, 0)
    for itemID := range m.itemEmbeddings {
        itemEmb := m.GetItemEmbedding(itemID)
        similarity := CosineSimilarity(userEmb, itemEmb)
        scores = append(scores, ItemScore{ItemID: itemID, Score: similarity})
    }
    
    // 排序
    sort.Slice(scores, func(i, j int) bool {
        return scores[i].Score > scores[j].Score
    })
    
    // 返回 Top K
    result := make([]int64, 0)
    for i := 0; i < count && i < len(scores); i++ {
        result = append(result, scores[i].ItemID)
    }
    
    return result
}

// ==================== Wide & Deep 模型(概念)====================

// WideDeepModel Wide & Deep 模型
type WideDeepModel struct {
    // Wide 部分:线性模型
    wideWeights map[string]float64
    
    // Deep 部分:深度神经网络
    deepLayers [][]float64 // 简化:权重矩阵
}

// Predict 预测 CTR
func (m *WideDeepModel) Predict(features map[string]float64) float64 {
    // Wide 部分
    wideScore := 0.0
    for feature, value := range features {
        if weight, exists := m.wideWeights[feature]; exists {
            wideScore += weight * value
        }
    }
    
    // Deep 部分(简化:多层感知机)
    deepScore := 0.0
    // ... 这里需要实现前向传播 ...
    
    // 融合
    finalScore := 0.5*wideScore + 0.5*deepScore
    
    // Sigmoid
    return 1.0 / (1.0 + math.Exp(-finalScore))
}

// ==================== 实时特征更新 ====================

package realtime

import (
    "context"
    "time"
)

// RealtimeFeatureService 实时特征服务
type RealtimeFeatureService struct {
    redis *redis.Client
}

// UpdateUserBehavior 更新用户行为特征
func (s *RealtimeFeatureService) UpdateUserBehavior(userID int64, behavior *UserBehavior) {
    ctx := context.Background()
    
    // 1. 更新最近浏览列表(保留最近 100 个)
    key := fmt.Sprintf("user:%d:recent_items", userID)
    s.redis.LPush(ctx, key, behavior.ItemID)
    s.redis.LTrim(ctx, key, 0, 99)
    s.redis.Expire(ctx, key, 7*24*time.Hour)
    
    // 2. 更新物品交互次数
    itemKey := fmt.Sprintf("user:%d:item:%d:count", userID, behavior.ItemID)
    s.redis.Incr(ctx, itemKey)
    s.redis.Expire(ctx, itemKey, 30*24*time.Hour)
    
    // 3. 更新用户活跃时段
    hour := time.Now().Hour()
    hourKey := fmt.Sprintf("user:%d:active_hours", userID)
    s.redis.ZIncrBy(ctx, hourKey, 1, fmt.Sprintf("%d", hour))
    
    // 4. 发送到 Kafka 进行异步处理
    // kafka.Produce("user-behavior", behavior)
}

// GetUserRecentItems 获取用户最近浏览
func (s *RealtimeFeatureService) GetUserRecentItems(userID int64, count int) []int64 {
    ctx := context.Background()
    key := fmt.Sprintf("user:%d:recent_items", userID)
    
    items, err := s.redis.LRange(ctx, key, 0, int64(count-1)).Result()
    if err != nil {
        return []int64{}
    }
    
    itemIDs := make([]int64, 0)
    for _, item := range items {
        itemID, _ := strconv.ParseInt(item, 10, 64)
        itemIDs = append(itemIDs, itemID)
    }
    
    return itemIDs
}

V3 特点:

  • 深度学习自动特征提取
  • Embedding 向量降维
  • 实时特征更新
  • 更高的推荐精度
  • ⚠️ 需要大量训练数据
  • ⚠️ 模型训练成本高

6. 核心算法优化

6.1 冷启动解决方案

// ColdStartHandler 冷启动处理器
type ColdStartHandler struct {
    hotItems      []int64                    // 热门物品
    categoryHot   map[string][]int64         // 分类热门
    demographicRec map[string][]int64         // 人口统计推荐
}

// HandleNewUser 处理新用户
func (h *ColdStartHandler) HandleNewUser(userProfile *UserProfile) []*RecommendationItem {
    items := make([]*RecommendationItem, 0)
    
    // 1. 基于人口属性推荐
    demographic := fmt.Sprintf("%s_%s_%s", 
        userProfile.Gender, userProfile.AgeRange, userProfile.City)
    
    if recs, exists := h.demographicRec[demographic]; exists {
        for _, itemID := range recs {
            items = append(items, &RecommendationItem{
                ItemID: itemID,
                Score:  0.8,
                Reason: "为你精选",
            })
        }
    }
    
    // 2. 全站热门
    for i, itemID := range h.hotItems {
        if i >= 10 {
            break
        }
        items = append(items, &RecommendationItem{
            ItemID: itemID,
            Score:  0.9 - float64(i)*0.05,
            Reason: "热门推荐",
        })
    }
    
    return items
}

// HandleNewItem 处理新物品
func (h *ColdStartHandler) HandleNewItem(itemID int64, features *ItemFeatures) {
    // 1. 基于内容相似度推荐
    // 找到相似物品,给相似物品的用户推荐新物品
    
    // 2. 小流量试探
    // 给一定比例的流量展示新物品,根据 CTR 决定是否继续推广
}

6.2 多样性与打散

// DiversityReranker 多样性重排
type DiversityReranker struct {
    categoryService *CategoryService
}

// Rerank 重排(MMR 算法:Maximal Marginal Relevance)
func (r *DiversityReranker) Rerank(items []*RecommendationItem, count int, lambda float64) []*RecommendationItem {
    if len(items) <= count {
        return items
    }
    
    result := make([]*RecommendationItem, 0)
    remaining := make([]*RecommendationItem, len(items))
    copy(remaining, items)
    
    // 第一个:最高分
    result = append(result, remaining[0])
    remaining = remaining[1:]
    
    // 迭代选择
    for len(result) < count && len(remaining) > 0 {
        maxScore := -1.0
        maxIndex := 0
        
        for i, item := range remaining {
            // MMR = λ × Relevance - (1-λ) × Similarity
            relevance := item.Score
            
            // 计算与已选物品的最大相似度
            maxSimilarity := 0.0
            for _, selected := range result {
                similarity := r.calculateSimilarity(item, selected)
                if similarity > maxSimilarity {
                    maxSimilarity = similarity
                }
            }
            
            mmr := lambda*relevance - (1-lambda)*maxSimilarity
            
            if mmr > maxScore {
                maxScore = mmr
                maxIndex = i
            }
        }
        
        result = append(result, remaining[maxIndex])
        remaining = append(remaining[:maxIndex], remaining[maxIndex+1:]...)
    }
    
    return result
}

// calculateSimilarity 计算物品相似度(基于类目)
func (r *DiversityReranker) calculateSimilarity(item1, item2 *RecommendationItem) float64 {
    cat1 := r.categoryService.GetCategory(item1.ItemID)
    cat2 := r.categoryService.GetCategory(item2.ItemID)
    
    if cat1 == cat2 {
        return 1.0
    }
    return 0.0
}

6.3 Exploration vs Exploitation

// EpsilonGreedyStrategy ε-贪心策略
type EpsilonGreedyStrategy struct {
    epsilon float64 // 探索概率
}

// Select 选择推荐策略
func (s *EpsilonGreedyStrategy) Select(items []*RecommendationItem) []*RecommendationItem {
    result := make([]*RecommendationItem, 0)
    
    for _, item := range items {
        random := rand.Float64()
        
        if random < s.epsilon {
            // 探索:随机选择
            result = append(result, s.randomItem())
        } else {
            // 利用:选择高分物品
            result = append(result, item)
        }
    }
    
    return result
}

// UCBStrategy UCB(Upper Confidence Bound)策略
type UCBStrategy struct {
    itemStats map[int64]*ItemStats
}

type ItemStats struct {
    Impressions int
    Clicks      int
    CTR         float64
}

// CalculateUCB 计算 UCB 分数
func (s *UCBStrategy) CalculateUCB(itemID int64, totalImpressions int) float64 {
    stats := s.itemStats[itemID]
    if stats == nil || stats.Impressions == 0 {
        return math.MaxFloat64 // 未曝光物品优先
    }
    
    // UCB = CTR + sqrt(2 * ln(总曝光) / 物品曝光)
    ucb := stats.CTR + math.Sqrt(2*math.Log(float64(totalImpressions))/float64(stats.Impressions))
    
    return ucb
}

7. 性能优化

7.1 召回加速

// ANN (Approximate Nearest Neighbor) 近似最近邻
// 使用 FAISS、Annoy 等库加速向量检索

import "github.com/spotify/annoy-go"

type ANNRecallService struct {
    index *annoy.AnnoyIndex
}

func (s *ANNRecallService) BuildIndex(embeddings map[int64][]float64, dimension int) {
    s.index = annoy.NewAnnoyIndex(dimension, "euclidean")
    
    for itemID, emb := range embeddings {
        s.index.AddItem(int(itemID), emb)
    }
    
    s.index.Build(10) // 10 trees
}

func (s *ANNRecallService) Recall(userEmbedding []float64, count int) []int64 {
    results, _ := s.index.GetNnsByVector(userEmbedding, count, -1)
    
    itemIDs := make([]int64, len(results))
    for i, id := range results {
        itemIDs[i] = int64(id)
    }
    
    return itemIDs
}

7.2 特征缓存

type FeatureCacheService struct {
    redis *redis.Client
}

// GetUserFeatures 获取用户特征(带缓存)
func (s *FeatureCacheService) GetUserFeatures(userID int64) map[string]float64 {
    ctx := context.Background()
    cacheKey := fmt.Sprintf("features:user:%d", userID)
    
    // 1. 查询缓存
    cached, err := s.redis.Get(ctx, cacheKey).Result()
    if err == nil {
        var features map[string]float64
        json.Unmarshal([]byte(cached), &features)
        return features
    }
    
    // 2. 查询数据库
    features := s.loadUserFeaturesFromDB(userID)
    
    // 3. 写入缓存
    data, _ := json.Marshal(features)
    s.redis.Set(ctx, cacheKey, data, 10*time.Minute)
    
    return features
}

7.3 结果预计算

// 离线预计算推荐结果
type OfflineRecommendationJob struct {
    db    *sql.DB
    redis *redis.Client
}

func (job *OfflineRecommendationJob) Run() {
    // 1. 获取所有活跃用户
    users := job.getActiveUsers()
    
    // 2. 批量计算推荐
    for _, userID := range users {
        recommendations := job.calculateRecommendations(userID)
        
        // 3. 存储到 Redis
        key := fmt.Sprintf("rec:user:%d:offline", userID)
        data, _ := json.Marshal(recommendations)
        job.redis.Set(context.Background(), key, data, 24*time.Hour)
    }
}

8. 监控与评估

8.1 推荐指标

// RecommendationMetrics 推荐指标
type RecommendationMetrics struct {
    // 业务指标
    CTR         float64 // 点击率
    CVR         float64 // 转化率
    GMV         float64 // 成交金额
    
    // 用户体验指标
    Diversity   float64 // 多样性
    Coverage    float64 // 覆盖率
    Novelty     float64 // 新颖度
    
    // 系统指标
    LatencyP99  time.Duration // 延迟
    QPS         int
    ErrorRate   float64
}

// CalculateCTR 计算点击率
func CalculateCTR(impressions, clicks int) float64 {
    if impressions == 0 {
        return 0
    }
    return float64(clicks) / float64(impressions)
}

// CalculateCoverage 计算覆盖率
func CalculateCoverage(recommendedItems, totalItems int) float64 {
    return float64(recommendedItems) / float64(totalItems)
}

8.2 A/B 测试

package abtest

type ABTestService struct {
    experiments map[string]*Experiment
}

type Experiment struct {
    Name        string
    Variants    []string  // ["control", "treatment_a", "treatment_b"]
    TrafficSplit map[string]float64 // {"control": 0.5, "treatment_a": 0.3, "treatment_b": 0.2}
}

// GetVariant 获取用户分组
func (s *ABTestService) GetVariant(experimentName string, userID int64) string {
    exp := s.experiments[experimentName]
    if exp == nil {
        return "control"
    }
    
    // 哈希分流
    hash := hashUserID(userID, experimentName)
    ratio := float64(hash%10000) / 10000.0
    
    cumulative := 0.0
    for variant, traffic := range exp.TrafficSplit {
        cumulative += traffic
        if ratio < cumulative {
            return variant
        }
    }
    
    return "control"
}

func hashUserID(userID int64, experimentName string) int {
    h := fnv.New32a()
    h.Write([]byte(fmt.Sprintf("%d_%s", userID, experimentName)))
    return int(h.Sum32())
}

// 使用示例
func (s *RecommendationService) Recommend(userID int64, count int) []*RecommendationItem {
    variant := s.abtestService.GetVariant("rec_algorithm_v2", userID)
    
    if variant == "treatment_a" {
        // 使用新算法
        return s.recommendV2(userID, count)
    } else {
        // 使用旧算法
        return s.recommendV1(userID, count)
    }
}

8.3 Prometheus 监控

var (
    recommendationTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "recommendation_total",
            Help: "Total recommendation requests",
        },
        []string{"scene", "strategy"},
    )
    
    recommendationLatency = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "recommendation_latency_seconds",
            Help:    "Recommendation latency",
            Buckets: []float64{0.01, 0.05, 0.1, 0.5, 1},
        },
        []string{"scene"},
    )
    
    recommendationCTR = prometheus.NewGaugeVec(
        prometheus.GaugeOpts{
            Name: "recommendation_ctr",
            Help: "Recommendation CTR",
        },
        []string{"scene"},
    )
)

func (s *RecommendationService) RecordMetrics(scene string, strategy string, latency time.Duration) {
    recommendationTotal.WithLabelValues(scene, strategy).Inc()
    recommendationLatency.WithLabelValues(scene).Observe(latency.Seconds())
}

9. 面试问答(10个高频题)

协同过滤的 User-based 和 Item-based 有什么区别?

答:

对比项User-based CFItem-based CF
原理找相似用户的喜好找相似物品推荐
计算量O(用户数²)O(物品数²)
适用场景用户少,物品多(新闻)物品少,用户多(电商)
实时性差(用户相似度变化快)好(物品相似度稳定)
冷启动新用户困难新物品困难
可解释性"和你相似的人喜欢""看了还看"

实际选择:

  • 电商(淘宝):Item-based(物品稳定)
  • 资讯(今日头条):User-based + 内容推荐

如何解决推荐系统的冷启动问题?

答:

新用户冷启动:

  1. 热门推荐:全站热门物品
  2. 人口属性:基于性别、年龄、地域推荐
  3. 引导注册:让用户选择感兴趣的类目
  4. 基于内容:如果用户有搜索/浏览记录

新物品冷启动:

  1. 基于内容:找相似物品的用户推荐
  2. 小流量试探:给少量用户曝光,根据 CTR 决定
  3. 编辑精选:人工干预推荐
  4. 协同过滤 + 内容:混合推荐

代码示例:

func (s *RecommendationService) HandleColdStart(userID int64) []*RecommendationItem {
    // 检查用户行为数
    behaviorCount := s.getUserBehaviorCount(userID)
    
    if behaviorCount < 10 {
        // 新用户:热门 + 人口属性
        return s.coldStartHandler.HandleNewUser(userProfile)
    } else {
        // 老用户:正常推荐
        return s.Recommend(userID, 20)
    }
}

召回和排序有什么区别?为什么要分两阶段?

答:

召回(Recall):

  • 目标:快速筛选候选集
  • 规模:从百万 → 数千
  • 算法:简单、快速(协同过滤、向量检索)
  • 延迟:< 10ms

排序(Ranking):

  • 目标:精准排序
  • 规模:从数千 → 数十
  • 算法:复杂、精准(深度学习)
  • 延迟:< 50ms

为什么分两阶段:

  1. 效率:不可能对所有物品精排(计算量太大)
  2. 分工:召回保证覆盖,排序保证精准
  3. 灵活性:多路召回 + 统一排序

流程:

全量物品(1000万)
    ↓ 召回(多路)
候选集(3000个)
    ↓ 粗排
精选集(200个)
    ↓ 精排
最终推荐(20个)

如何评估推荐系统的效果?

答:

离线评估:

  1. 准确率指标:

    • Precision@K:前 K 个推荐中命中的比例
    • Recall@K:前 K 个推荐覆盖用户兴趣的比例
    • NDCG(Normalized Discounted Cumulative Gain):考虑排序位置
  2. 多样性指标:

    • Coverage:覆盖物品的比例
    • Diversity:推荐列表的类目分散程度

在线评估:

  1. 业务指标:

    • CTR(点击率)
    • CVR(转化率)
    • GMV(成交金额)
    • 人均浏览时长
  2. A/B 测试:

    • 对照组 vs 实验组
    • 统计显著性检验

示例:

// Precision@K
func PrecisionAtK(recommended, actual []int64, k int) float64 {
    if k > len(recommended) {
        k = len(recommended)
    }
    
    actualSet := make(map[int64]bool)
    for _, item := range actual {
        actualSet[item] = true
    }
    
    hit := 0
    for i := 0; i < k; i++ {
        if actualSet[recommended[i]] {
            hit++
        }
    }
    
    return float64(hit) / float64(k)
}

如何处理推荐系统的实时性?

答:

挑战:

  • 用户行为实时变化
  • 推荐结果需要快速更新

解决方案:

1. 实时特征更新:

// 用户点击后立即更新 Redis
func (s *RealtimeService) OnUserClick(userID, itemID int64) {
    // 更新最近点击
    s.redis.LPush(fmt.Sprintf("user:%d:clicks", userID), itemID)
    
    // 更新实时兴趣标签
    tags := s.getItemTags(itemID)
    for _, tag := range tags {
        s.redis.ZIncrBy(fmt.Sprintf("user:%d:interest", userID), 1, tag)
    }
}

2. 离线 + 在线结合:

  • 离线:每天计算基础推荐(User CF、Item CF)
  • 在线:根据实时行为调整排序

3. 流式计算:

用户行为 → Kafka → Flink → 实时特征更新 → Redis

4. 增量更新:

  • 不是全量重新计算
  • 只更新相关用户/物品

推荐系统如何保证多样性?

答:

问题:

  • 推荐结果都是同类物品
  • "信息茧房"

解决方案:

1. MMR 重排(Maximal Marginal Relevance):

// 选择物品时,同时考虑相关性和多样性
score = λ × relevance - (1-λ) × max_similarity

2. 类目打散:

// 确保连续推荐不是同类目
func (r *DiversityReranker) SpreadCategories(items []*Item) []*Item {
    result := make([]*Item, 0)
    usedCategories := make(map[string]int)
    
    for _, item := range items {
        // 如果这个类目连续出现 2 次,跳过
        if usedCategories[item.Category] >= 2 {
            continue
        }
        
        result = append(result, item)
        usedCategories[item.Category]++
        
        // 重置计数
        if len(result) % 5 == 0 {
            usedCategories = make(map[string]int)
        }
    }
    
    return result
}

3. Exploration(探索):

  • 一定比例推荐新物品
  • ε-贪心、UCB 策略

深度学习推荐和传统推荐有什么区别?

答:

对比项传统推荐深度学习推荐
特征工程手工设计自动学习
非线性弱强(深度网络)
数据需求少大量数据
训练成本低高(GPU)
可解释性强弱
实时性好差(模型推理)

常见深度学习模型:

  1. Wide & Deep:结合线性和深度
  2. DeepFM:自动学习特征交叉
  3. DIN(Deep Interest Network):注意力机制
  4. DSSM(双塔模型):用户塔 + 物品塔

选择建议:

  • 小公司、数据少:传统协同过滤
  • 大公司、数据多:深度学习

如何设计推荐系统的 A/B 测试?

答:

步骤:

1. 定义实验:

experiment := &Experiment{
    Name: "rec_algorithm_v2",
    Variants: []string{"control", "treatment"},
    TrafficSplit: map[string]float64{
        "control":   0.9,  // 90% 旧算法
        "treatment": 0.1,  // 10% 新算法
    },
}

2. 流量分配:

// 基于 UserID 哈希分流(保证同一用户始终在同一组)
func GetVariant(userID int64) string {
    hash := hashUserID(userID) % 100
    if hash < 10 {
        return "treatment"
    }
    return "control"
}

3. 指标统计:

// 记录每个分组的指标
func RecordMetrics(userID int64, variant string, metrics *Metrics) {
    // 存储到数据仓库
    dataWarehouse.Insert(&ABTestMetrics{
        ExperimentName: "rec_algorithm_v2",
        Variant:        variant,
        UserID:         userID,
        CTR:            metrics.CTR,
        CVR:            metrics.CVR,
        Timestamp:      time.Now(),
    })
}

4. 显著性检验:

# t-检验
from scipy import stats

control_ctr = [0.05, 0.06, 0.05, ...]
treatment_ctr = [0.07, 0.08, 0.07, ...]

t_stat, p_value = stats.ttest_ind(control_ctr, treatment_ctr)

if p_value < 0.05:
    print("显著提升")

推荐系统如何处理马太效应(热门物品越来越热)?

答:

问题:

  • 热门物品曝光多 → CTR 高 → 更多曝光
  • 长尾物品无法曝光

解决方案:

1. 降权热门物品:

func (s *RecommendationService) AdjustScore(item *Item) float64 {
    score := item.RawScore
    
    // 热门物品降权
    if item.TotalViews > 100000 {
        penalty := math.Log(float64(item.TotalViews) / 100000)
        score = score / (1 + penalty*0.1)
    }
    
    return score
}

2. 时间衰减:

// 老物品降权
func TimeDecay(item *Item) float64 {
    days := time.Since(item.CreatedAt).Hours() / 24
    decay := math.Exp(-0.01 * days)
    return item.Score * decay
}

3. 探索机制:

  • 一定比例推荐长尾物品
  • 监控长尾物品的 CTR

4. 分层推荐:

20% 热门 + 50% 中等热度 + 30% 长尾

如何设计大规模推荐系统的架构?

答:

挑战:

  • 亿级用户
  • 千万级物品
  • 10 万 QPS

架构设计:

1. 分层架构:

召回层(并行)→ 粗排层 → 精排层 → 重排层

2. 存储方案:

  • MySQL:用户画像、物品特征
  • Redis:实时特征、推荐缓存
  • HBase:用户行为历史
  • Elasticsearch:基于内容召回

3. 计算方案:

  • 离线:Spark 计算协同过滤(每日)
  • 实时:Flink 处理用户行为流
  • 在线:Go 服务召回+排序

4. 缓存策略:

// 三级缓存
1. 本地缓存(Caffeine):热点用户
2. Redis 缓存:推荐结果(10分钟)
3. 预计算:离线推荐(每日更新)

5. 降级方案:

if 精排服务故障 {
    return 粗排结果
}

if 召回服务故障 {
    return 热门推荐
}

6. 监控告警:

  • QPS、延迟、错误率
  • CTR、CVR 实时监控
  • 模型效果 A/B 对比

10. 总结

核心要点

  1. 推荐算法

    • 协同过滤:User-based、Item-based
    • 内容推荐:基于物品特征
    • 深度学习:Embedding、Wide & Deep
    • 混合推荐:多路召回 + 融合
  2. 系统架构

    • 召回 → 粗排 → 精排 → 重排
    • 离线计算 + 实时计算
    • 多级缓存
  3. 核心问题

    • 冷启动:热门推荐、人口属性、小流量试探
    • 多样性:MMR、类目打散、探索机制
    • 实时性:流式计算、增量更新
  4. 效果评估

    • 离线:Precision、Recall、NDCG
    • 在线:CTR、CVR、GMV
    • A/B 测试

架构演进

V1: 协同过滤(User CF / Item CF)
    ↓ (数据量增大)
V2: 多路召回 + 特征工程 + LR/GBDT
    ↓ (深度学习)
V3: Embedding + 深度学习 + 实时特征

面试建议

  1. 算法原理:协同过滤、矩阵分解要能讲清楚
  2. 工程实现:召回、排序的分工和优化
  3. 实际问题:冷启动、多样性、实时性
  4. 业务理解:CTR、CVR、用户体验
  5. 系统设计:大规模架构、缓存、降级

本章完,祝面试顺利!

Prev
第7章:搜索引擎设计
Next
09 - 支付系统设计