跳转到内容

Module 10: 搜索与推荐

📖 深度参考手册 — 本模块属于理论参考,非主线必读。 主线学习路径见 README.md。 当你在项目实战中遇到相关问题时,回来查阅。

来源:DDIA Ch3 (Storage and Retrieval — indexing), “Acing the System Design Interview” (Search, Typeahead, Recommendation)

搜索和推荐是用户获取信息的两种基本方式:搜索是”用户主动找内容”,推荐是”系统主动推内容”。搜索像书店里的索引目录——你知道要什么,按目录找;推荐像一个了解你口味的店员——你不知道要什么,他帮你挑。这个模块帮你理解搜索与推荐的核心原理、关键数据结构,以及它们如何在真实系统中协作。


定义:倒排索引(Inverted Index)是搜索引擎最核心的数据结构。普通索引是”文档→包含哪些词”,倒排索引反过来——“词→出现在哪些文档中”。它由两部分组成:词典(Term Dictionary)——所有不重复的词的有序集合;倒排列表(Posting List)——每个词对应的文档ID列表,通常还附带词频、位置等信息。

为什么重要:没有倒排索引,搜索就只能暴力遍历每个文档检查是否包含关键词——对于十亿级文档完全不可行。倒排索引把搜索的时间复杂度从 O(N) 降到了近似 O(1) 的查字典操作。这是所有现代搜索引擎(Google、Elasticsearch、Lucene)的基石。

生活类比:倒排索引就像一本书后面的”关键词索引”。你想找”分布式”相关内容,不用翻遍全书,直接查索引:分布式 → 第3章p45, 第7章p182, 第9章p301

案例

Search Engine — 倒排索引的经典应用

假设搜索引擎收录了3个网页:
doc1: "北京酒店预订优惠"
doc2: "上海酒店价格查询"
doc3: "北京旅游景点推荐"
经过分词后,构建倒排索引:
北京 → [doc1, doc3]
酒店 → [doc1, doc2]
预订 → [doc1]
优惠 → [doc1]
上海 → [doc2]
价格 → [doc2]
查询 → [doc2]
旅游 → [doc3]
景点 → [doc3]
推荐 → [doc3]
用户搜索"北京 酒店":
倒排索引[北京] ∩ 倒排索引[酒店] = {doc1, doc3} ∩ {doc1, doc2} = {doc1}
返回 doc1: "北京酒店预订优惠"

Hotel Reservation — 酒店搜索依赖倒排索引

倒排索引不仅支持文本搜索,还可以支持结构化字段:
城市:北京 → [hotel_1, hotel_5, hotel_12, ...]
星级:5星 → [hotel_1, hotel_8, hotel_23, ...]
设施:游泳池 → [hotel_5, hotel_8, hotel_12, ...]
用户搜索"北京 五星 有游泳池":
城市:北京 ∩ 星级:5星 ∩ 设施:游泳池 = {hotel_5}

YouTube — 视频搜索:对视频标题、描述、标签、字幕文本建立倒排索引。搜索”Python教程”时,查倒排索引快速定位匹配视频,再按相关性排序返回。

关键权衡

维度选择A选择B
索引粒度只存文档ID(省空间)存文档ID+词频+位置(支持短语搜索和排序)
更新策略实时更新索引(低延迟,高写入开销)批量重建索引(高延迟,高吞吐)
存储方式内存索引(快,贵)磁盘索引(慢,便宜,用SSD折中)

先想一想 🤔 倒排列表中文档ID通常是有序的。为什么要有序?这和 Module 2 中学的存储引擎(如 SSTable 的有序性)有什么相似之处?

点击查看解析

有序的倒排列表带来两个关键好处:

  1. 高效交集运算:搜索”北京 AND 酒店”需要对两个倒排列表取交集。有序列表可以用双指针法在 O(n+m) 时间内完成,无序列表则需要 O(n×m)。

  2. 高效压缩:有序的文档ID可以用差值编码(delta encoding)——只存相邻ID的差值。例如 [1, 5, 8, 102] 编码为 [1, 4, 3, 94],差值通常很小,用变长编码压缩率极高。

这和 Module 2 的 SSTable 思路一致——SSTable 中的 key 也是有序存储的,利用有序性实现高效查找(二分搜索)和高效合并(归并排序)。“排序是数据结构设计中最强大的武器之一”,无论是存储引擎还是搜索引擎都在反复利用这一点。


定义:全文检索(Full-Text Search)不仅要找出”包含关键词的文档”,更要回答”哪些文档最相关”。这需要相关性评分算法。两个经典算法:TF-IDF——词频(Term Frequency)越高、文档频率(Document Frequency)越低的词越重要;BM25——TF-IDF的改进版,加入了文档长度归一化和词频饱和度,是现代搜索引擎的默认算法。

为什么重要:用户搜索”酒店”时,可能有上百万个文档包含这个词。如果不排序,用户根本无法找到最想要的结果。好的评分算法是搜索引擎质量的核心——它决定了用户是在第一页就找到答案,还是要翻到第十页。

生活类比:TF-IDF 的直觉很简单——如果你在一篇论文里反复看到”量子纠缠”这个词(TF高),而这个词在其他论文里很少出现(IDF高),那这篇论文很可能就是关于量子纠缠的。但如果一个词在所有论文里都出现(如”研究”、“方法”),它的IDF很低,说明这个词没什么区分度。

案例

Search Engine — BM25 评分示例

用户搜索: "北京 酒店 推荐"
TF-IDF 评分思路:
doc1: "北京酒店预订优惠"
→ "北京"出现1次, "酒店"出现1次, "推荐"出现0次
→ 匹配2/3个搜索词
doc3: "北京旅游景点推荐,推荐北京五大必去景点"
→ "北京"出现2次, "酒店"出现0次, "推荐"出现2次
→ 匹配2/3个搜索词,但"推荐"重复多次
BM25 改进:
- 词频饱和: "推荐"出现2次 vs 1次,分数提升不是2倍而是约1.3倍
(第一次出现很重要,之后边际递减)
- 文档长度归一化: 长文档天然包含更多词,要对长文档做惩罚
(100字的文档提到1次"酒店" vs 10000字的文档提到1次"酒店",
前者的匹配信号更强)

Hotel Reservation — 多因子排序

搜索"北京五星酒店"的排序不能只看文本相关性:
综合得分 = 0.3 × BM25文本相关性
+ 0.2 × 用户评分(4.8/5.0)
+ 0.2 × 价格匹配度
+ 0.15 × 距离
+ 0.15 × 转化率(历史预订率)
这就是为什么搜索系统通常分两步:
1. 召回(Recall): 用倒排索引快速找出候选集(数千个)
2. 排序(Ranking): 用多因子模型精细排序(返回Top 20)

News Feed — 内容质量评分:在 News Feed 中,“搜索”变成了隐式的——系统替用户搜索”最值得看的内容”。评分因子包括:内容新鲜度、作者权威度、互动率(点赞/评论/分享)、与用户兴趣的匹配度。

先想一想 🤔 BM25 只看文本匹配。但 Google 搜索显然不只看文本——搜索”Python”时,python.org 排在前面不是因为它提到”Python”次数最多。Google 还用了什么信号?

点击查看解析

Google 的排序是数百个信号的综合,文本相关性只是其中之一:

  • PageRank:网页被其他高质量网页链接越多,权重越高。python.org 被无数网站引用。
  • 用户行为信号:点击率、停留时间、回退率。如果用户点了结果后又回来继续搜索(pogo-sticking),说明那个结果不好。
  • 页面质量:加载速度、移动端适配、HTTPS、Core Web Vitals。
  • 新鲜度:对于新闻类查询,更新的内容排更前。
  • 实体理解:Google 知道”Python”既是编程语言也是蛇,通过搜索上下文消歧。

核心思想是:文本匹配解决”相关不相关”的问题,其他信号解决”多相关”和”多可信”的问题


定义:分词(Tokenization)是将原始文本拆分为有意义的最小单元(Token)的过程。分析器(Analyzer)是一个处理管道,通常包含:字符过滤器(Char Filter)分词器(Tokenizer)Token过滤器(Token Filter)。英文分词相对简单(按空格和标点拆分),中文分词是一个经典难题——因为中文没有空格分隔,“乒乓球拍卖完了”可以切分为”乒乓球/拍卖/完了”或”乒乓/球拍/卖完/了”。

为什么重要:分词质量直接决定搜索质量。如果分词错误,倒排索引就建错了——用户搜”球拍”却搜不到包含”乒乓球拍”的文档。对于中文系统,分词器的选择是搜索架构中最关键的决策之一。

案例

Search Engine — 分析器管道

原始文本: "Google's Search Engine is AMAZING!!!"
1. 字符过滤器 (Char Filter):
→ 去除HTML标签、特殊字符处理
→ "Google's Search Engine is AMAZING"
2. 分词器 (Tokenizer):
→ 按空格和标点拆分
→ ["Google's", "Search", "Engine", "is", "AMAZING"]
3. Token过滤器 (Token Filter):
→ 小写化: ["google's", "search", "engine", "is", "amazing"]
→ 去停用词: ["google's", "search", "engine", "amazing"]
→ 词干提取(stemming): ["googl", "search", "engin", "amaz"]
→ 处理所有格: ["google", "search", "engin", "amaz"]
最终进入倒排索引的token: ["google", "search", "engin", "amaz"]

中文分词的挑战

输入: "南京市长江大桥"
可能的分词结果:
分词方案A: 南京市 / 长江大桥 ← 正确(南京市的长江大桥)
分词方案B: 南京 / 市长 / 江大桥 ← 错误但语法合法
常用中文分词器:
- jieba: 开源、轻量、基于前缀词典+隐马尔可夫模型
- IK Analyzer: Elasticsearch中文分词插件,支持自定义词典
- HanLP: 支持命名实体识别、依存句法分析
实际系统中的做法 — 细粒度+粗粒度双索引:
索引时: "南京市长江大桥" → [南京, 南京市, 市长, 长江, 长江大桥, 大桥]
搜索时: 根据上下文选择最优切分
这样无论用户搜"长江大桥"还是"南京市"都能匹配到

Google Maps / Proximity Service — 地名分词

地名分词有特殊挑战:
"星巴克(国贸CBD店)" → [星巴克, 国贸, CBD, 店]
"麦当劳北京西站店" → [麦当劳, 北京西站, 店]
需要维护地名词典(POI词库)确保地名不被错切

先想一想 🤔 如果用户搜索”wifi”但文档里写的是”Wi-Fi”或”WIFI”或”wi-fi”,怎么保证能搜到?

点击查看解析

这正是分析器的工作。解决方案是索引时和查询时使用相同的分析器管道

  • 小写化:“Wi-Fi”、“WIFI”、“wifi”都变成”wifi”
  • 字符过滤器:去掉连字符,“wi-fi”变成”wifi”
  • 同义词扩展:建立同义词词典 wifi, wi-fi, wireless → wifi

关键原则:索引时对文档做的文本处理,查询时对搜索词也要做同样的处理。如果索引时用了词干提取,查询时也要做词干提取,否则索引里存的是”amaz”但查询的是”amazing”,匹配不上。


定义:搜索架构描述了从数据源到用户拿到搜索结果的完整系统链路。核心组件包括:数据采集管道(从数据库、爬虫等收集原始数据)→ 索引管道(分析、分词、构建倒排索引)→ 搜索服务(接收查询、检索索引、排序返回)。Elasticsearch 是最流行的开源搜索引擎,基于 Apache Lucene 构建,提供了分布式索引和搜索能力。

为什么重要:搜索不是一个”功能”,而是一个完整的”系统”。它涉及数据同步(主数据库和搜索索引的一致性)、索引构建(批量还是实时)、查询优化(缓存、分片路由)、可用性(索引损坏怎么办)等工程问题。

案例

Search Engine — 完整搜索架构

数据源 索引管道 搜索服务
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Web │ │ │ │ │
│ Crawler │────────→│ 文档处理 │ │ 查询解析 │
└──────────┘ │ (清洗/分词) │ │ (分词/纠错) │
│ │ │ │ │ │
┌──────────┐ │ ▼ │ │ ▼ │
│ 数据库 │────────→│ 索引构建 │──写入──→ │ 索引检索 │
│ (结构化) │ CDC │ (倒排索引) │ ES集群 │ (倒排查询) │
└──────────┘ │ │ │ │ │ │
│ ▼ │ │ ▼ │
│ 索引分片 │ │ 结果排序 │
│ (Sharding) │ │ (BM25+业务) │
└──────────────┘ └──────┬───────┘
用户看到结果

数据同步是搜索架构的关键难题

问题: 主数据库(PostgreSQL)是数据源头,Elasticsearch是搜索副本。
如何保证两者一致?
方案1: 双写 (应用同时写数据库和ES)
优点: 简单直接
缺点: 不是原子操作——如果写DB成功但写ES失败,数据不一致
方案2: CDC (Change Data Capture)
数据库 → Binlog/WAL → Kafka → ES索引服务
优点: 可靠、解耦、可重放
缺点: 有延迟(秒级)
这和 Module 9 中的流处理管道直接相关——
CDC本质上就是把数据库变更作为事件流来处理
方案3: 定时全量重建
每天凌晨从数据库全量导出 → 重建ES索引
优点: 最简单、一致性有保证
缺点: 延迟高(天级)、资源消耗大

Hotel Reservation — Elasticsearch 分片策略

索引策略:
- 按城市分片: 北京的酒店数据在一个分片,上海在另一个
- 搜索"北京酒店"时只查一个分片,避免广播所有分片
副本策略:
- 每个分片2个副本,读请求负载均衡到副本
- 热门城市(北京/上海)可以增加副本数应对高查询量
这和 Module 3 (复制) 的思路一致——读副本分担查询压力

先想一想 🤔 如果 Elasticsearch 集群挂了,搜索功能完全不可用。有什么降级方案?

点击查看解析

常见的降级策略:

  1. 降级到数据库查询:关键搜索场景(如酒店按城市搜索)在数据库中也有索引,降级时走数据库的 LIKEtsvector(PostgreSQL全文检索)查询。功能受限但不至于完全不可用。

  2. 缓存兜底:热门搜索词的结果缓存在 Redis 中(Module 4 缓存),ES 挂了可以从缓存返回。虽然结果可能不是最新的,但用户体验远好于报错。

  3. 多集群部署:搜索服务部署多个 ES 集群,一个挂了切换到另一个。

核心原则:搜索是可以容忍一定程度降级的——返回不太精确的结果比返回错误页面好得多。


定义:自动补全(Autocomplete)是用户输入搜索词的过程中,系统实时给出补全建议的功能。核心数据结构是 Trie(前缀树)——一种树形结构,每个节点代表一个字符(或一个词),从根到叶的路径组成一个完整的搜索建议。每个节点还可以存储该前缀的热度/权重,用于返回最热门的补全结果。

为什么重要:自动补全极大提升了搜索效率和用户体验——用户不需要打完整个搜索词,就能选择正确的查询。Google 搜索框的自动补全据估计减少了 50% 以上的打字量。它也是引导用户搜索行为的重要工具。

案例

Search Engine — Trie 实现自动补全

假设有以下热门搜索词及搜索量:
"hotel" - 100万次
"hotel booking" - 80万次
"holiday" - 60万次
"home" - 50万次
构建 Trie:
(root)
h
o ──────── l
│ │
┌────┴────┐ i
m t │
│ │ d
e e │
│ │ a
(home (hotel │
50万) │ y
100万) (holiday
│ 60万)
(空格)
b
o
o
k
i
n
g
(hotel booking
80万)
用户输入"ho" → 遍历"h→o"子树 → 返回:
1. hotel (100万)
2. hotel booking (80万)
3. holiday (60万)
4. home (50万)

YouTube — 视频搜索建议

自动补全不仅基于全局热度,还要个性化:
全局热门: "python" → ["python tutorial", "python crash course", "python for beginners"]
针对一个经常看烹饪视频的用户:
"python" → ["python 蛇类纪录片", "python tutorial", "python 蟒蛇"]
实现:
全局Trie(所有用户搜索聚合) + 用户历史(最近搜索/观看) → 加权混合

Google Maps — 地点搜索建议

地图搜索建议除了文本前缀匹配,还结合地理位置:
用户在北京,输入"星巴":
→ 星巴克(国贸CBD店) - 距离2km
→ 星巴克(三里屯店) - 距离3km
→ 星巴克(五道口店) - 距离5km
排序 = f(文本匹配度, 距离, 热度, 营业状态)

性能要求与架构

自动补全要求极低延迟(<100ms),因为每次按键都要触发:
优化手段:
1. 结果缓存: 热门前缀"ho"的结果缓存在内存/CDN(Module 4)
2. 前端防抖: 用户快速输入时不是每个字符都发请求,
等停顿50ms后再发
3. Trie放内存: 热门搜索词的Trie通常可以放进内存
(1亿个搜索词,平均20字节 ≈ 2GB)
4. 分层: 先从本地缓存查 → 再从CDN查 → 最后查后端

先想一想 🤔 Trie 需要定期更新(新的热门搜索词要加入,过时的要移除)。如何利用 Module 9 的批处理/流处理来维护 Trie?

点击查看解析

典型的混合方案:

  • 批处理(每日):MapReduce/Spark 从搜索日志中统计所有搜索词的频率,全量重建 Trie。这是”基线”,保证数据完整准确。

  • 流处理(实时):Kafka + Flink 实时消费搜索事件流,用滑动窗口统计最近1小时的热门搜索词。如果某个新词(如突发新闻中的关键词)突然飙升,实时更新到 Trie 中。

搜索日志 → Kafka → Flink(实时热词统计) → 增量更新Trie
搜索日志 → HDFS → Spark(每日全量统计) → 全量重建Trie

这是 Module 9 中 Lambda 架构的一个典型应用——批处理层保证准确性,流处理层保证时效性。


定义:搜索相关性调优(Search Relevance Tuning)是在基础的 BM25 文本相关性之上,通过多种手段让搜索结果更符合用户预期的过程。核心技术包括:Boosting(权重提升)——对特定字段或条件加权(标题匹配比正文匹配重要);Filtering(过滤)——用结构化条件缩小候选集(价格范围、日期区间);Faceted Search(分面搜索)——在搜索结果旁展示各维度的聚合统计,帮助用户逐步缩小范围。

为什么重要:默认的全文检索算法对大多数业务场景来说不够用。电商搜索”苹果”不能只返回提到”苹果”最多次的文档,而是要结合用户意图(要手机还是水果?)、商品质量、库存状态等因素。相关性调优直接影响用户满意度和业务转化率。

案例

Hotel Reservation — 分面搜索

用户搜索"北京酒店",搜索结果页不仅返回酒店列表,
还在侧边栏展示分面统计:
星级: 价格区间: 设施:
☐ 五星 (45) ☐ <300元 (120) ☐ 游泳池 (35)
☐ 四星 (89) ☐ 300-600 (95) ☐ 健身房 (67)
☐ 三星 (120) ☐ 600-1000 (45) ☐ 免费WiFi (200)
☐ 二星 (56) ☐ >1000元 (50) ☐ 停车场 (89)
括号里的数字是当前搜索结果中每个分面的文档数量。
用户点击"五星"后,结果缩小到45家,其他分面数字随之更新。
技术实现 (Elasticsearch):
{
"query": { "match": { "city": "北京" } },
"aggs": {
"star_rating": { "terms": { "field": "stars" } },
"price_range": { "range": { "field": "price",
"ranges": [{"to": 300}, {"from": 300, "to": 600}, ...] } },
"facilities": { "terms": { "field": "facilities" } }
}
}

Search Engine — 字段权重 Boosting

搜索"酒店预订"时,标题里包含关键词应该比正文更重要:
Elasticsearch 查询:
{
"query": {
"multi_match": {
"query": "酒店预订",
"fields": ["title^3", "description^2", "body^1"]
// title权重3倍, description权重2倍, body权重1倍
}
}
}
效果:
标题为"酒店预订指南"的文档 得分 >> 正文中提了一次"酒店预订"的文档

Gaming Leaderboard — 搜索 + 排名结合

搜索玩家时,结果排序不只看名字匹配度:
综合得分 = 0.5 × 名字匹配度
+ 0.3 × 玩家等级/排名
+ 0.2 × 活跃度
搜索"dragon" →
1. DragonSlayer (排名#3, 活跃) — 而不是 dragon123 (排名#50000, 不活跃)

先想一想 🤔 分面搜索(Faceted Search)需要对每个分面做聚合统计。如果搜索结果有100万条,计算每个分面的数量会不会很慢?

点击查看解析

这确实是性能挑战,Elasticsearch 的解决方案:

  1. Doc Values:Elasticsearch 为每个字段维护了列式存储(doc values),专门用于聚合和排序。列式存储对聚合操作极为高效——只需读取需要聚合的那一列,而不是整个文档。

  2. 近似计算:对于高基数字段(如”城市”有上千个值),可以使用近似聚合算法,牺牲一点精度换取极大的性能提升。

  3. 缓存:热门查询(如”北京酒店”)的分面结果可以缓存(Module 4)。分面统计不需要100%实时——延迟几秒更新完全可以接受。

  4. 分片并行:Elasticsearch 在每个分片上并行计算局部聚合,然后在协调节点合并。5个分片就是5倍并行。


定义:推荐系统(Recommendation System)通过分析用户行为和物品特征,预测用户可能感兴趣的内容并主动推送。两种基本方法:协同过滤(Collaborative Filtering)——基于”相似用户喜欢相似物品”的假设,利用群体行为数据推荐;基于内容(Content-Based)——基于”用户会喜欢和之前喜欢的物品相似的东西”的假设,利用物品特征推荐。

为什么重要:推荐系统是互联网核心商业引擎之一。YouTube 70% 的观看时间来自推荐,Netflix 80% 的观看来自推荐,Amazon 35% 的销售来自推荐。好的推荐系统直接等于收入增长。

生活类比:协同过滤像”你的朋友们都在看这部电影,你可能也喜欢”;基于内容像”你看了三部科幻片,这里还有一部科幻片你可能喜欢”。

案例

YouTube — 推荐驱动的平台

协同过滤:
用户A 看了: [视频1, 视频2, 视频3]
用户B 看了: [视频1, 视频2, 视频4]
A和B有相似的观看历史(都看了视频1和2)
→ 推荐给A: 视频4(因为相似用户B看了)
→ 推荐给B: 视频3(因为相似用户A看了)
基于内容:
用户C 看了很多"Python教程"类视频
系统分析这些视频的标签: [编程, Python, 教程, 初学者]
→ 推荐标签相似的视频: "Python项目实战"、"Django教程"

News Feed — 个性化信息流

News Feed 本质上就是一个推荐系统:
候选池: 关注的人发的所有帖子 + 平台推荐的热门内容
排序信号:
- 社交关系强度: 亲密好友的帖子优先
- 内容类型偏好: 你经常看视频 → 视频排前面
- 互动预测: 预测你点赞/评论/分享的概率
- 时效性: 新帖子优先
最终每个帖子得到一个分数,按分数排序后呈现给用户

Hotel Reservation — “看了又看”推荐

基于内容:
你搜索的酒店特征: [北京, 五星, 商务, 近国贸]
→ 推荐特征相似的酒店
协同过滤:
"预订了A酒店的用户还看了B酒店"
"浏览了A酒店的用户最终预订了C酒店"
方法优点缺点适用场景
协同过滤不需要理解内容,能发现意外惊喜冷启动问题,数据稀疏用户行为数据丰富时
基于内容不需要其他用户数据,可解释性强推荐趋于同质化,难以跨领域物品特征明确时
混合方法取两者之长系统复杂度高大多数生产系统

先想一想 🤔 协同过滤和 Module 3 中的”复制”有什么概念上的相似之处?

点击查看解析

有趣的类比:协同过滤的核心假设是”相似用户的行为可以互相’复制’“——用户A的偏好可以作为预测用户B偏好的信号。这和数据库复制的核心思想有相似之处——一个节点的数据可以推断出另一个节点应该有的数据。

更深层的相似是:两者都在利用冗余信息。复制利用数据冗余提高可用性,协同过滤利用用户行为的冗余(相似用户的重叠行为)来预测缺失的信息。


定义:协同过滤(Collaborative Filtering)是推荐系统中最经典的方法,分为三种主要形式:User-Based CF(基于用户)——找到与目标用户最相似的用户群,推荐他们喜欢但目标用户还没看过的物品;Item-Based CF(基于物品)——找到与用户已喜欢的物品最相似的物品推荐;矩阵分解(Matrix Factorization)——将稀疏的用户-物品交互矩阵分解为低维的用户特征矩阵和物品特征矩阵,通过它们的乘积预测缺失评分。

为什么重要:协同过滤是推荐系统的基石。Netflix Prize 竞赛中获胜的方案核心就是矩阵分解。它的强大之处在于不需要理解物品内容——纯粹基于行为数据就能发现深层次的兴趣关联。

案例

YouTube — User-Based vs Item-Based CF

User-Based CF:
用户-视频 观看矩阵:
视频A 视频B 视频C 视频D
用户Alice: ✓ ✓ ✓ ?
用户Bob: ✓ ✓ ✓ ✓
用户Carol: ✓ ✓ ✓
Alice和Bob相似度最高(3/3匹配)
→ 推荐Bob看过但Alice没看的: 视频D
问题: 用户数量巨大(数十亿),计算两两相似度不现实
Item-Based CF:
反过来看——哪些视频经常被同时观看:
视频A和视频C: 被3个用户同时看过 → 高度相似
视频C和视频D: 被2个用户同时看过 → 中度相似
Alice看了视频C → 推荐和视频C相似的视频D
优势: 物品数量远少于用户数量,物品相似度可以离线预计算并缓存

矩阵分解

用户-物品交互矩阵 R (大部分是空的——用户只看过极少数视频):
视频1 视频2 视频3 视频4 视频5
用户1: 5 3 ? 1 ?
用户2: 4 ? ? 1 ?
用户3: 1 1 ? 5 ?
用户4: ? ? 5 4 4
矩阵分解: R ≈ U × V^T
U (用户特征矩阵, n×k): V (物品特征矩阵, m×k):
每个用户用k维向量表示 每个物品用k维向量表示
用户1 → [0.9, 0.1] (偏好维度1) 视频1 → [0.9, 0.2]
用户4 → [0.1, 0.9] (偏好维度2) 视频4 → [0.2, 0.8]
预测用户1对视频5的评分: U[1] · V[5]^T
k维向量是"隐因子(latent factor)"——系统自动学习出来的,
可能对应"动作片偏好"、"文艺片偏好"等隐含维度

关键权衡

方法计算复杂度可扩展性效果
User-Based CFO(用户数²)差(用户数太多)好,但不可扩展
Item-Based CFO(物品数²)中(物品数可控)好,Amazon采用
矩阵分解O(k × 非零元素数)最好,Netflix采用

先想一想 🤔 矩阵分解需要处理巨大的用户-物品矩阵(YouTube 有20亿用户 × 数亿视频)。如何利用 Module 9 的批处理来训练这个模型?

点击查看解析

矩阵分解本质上是一个优化问题(最小化预测评分和实际评分的误差),可以用分布式批处理来解决:

  1. 数据并行:将用户-物品矩阵按用户分片到不同机器上,每台机器负责一部分用户的梯度计算。

  2. ALS(交替最小二乘法):固定用户矩阵U,用MapReduce并行更新物品矩阵V;然后固定V,并行更新U。交替迭代直到收敛。Spark MLlib 就实现了分布式ALS。

  3. 增量更新:不需要每次全量训练——新的用户行为可以通过流处理实时更新用户向量,物品向量每天批量更新一次。

每日: 行为日志 → Spark(ALS矩阵分解) → 更新U和V矩阵
实时: 新行为 → Flink → 微调用户向量U[i]

定义:个性化排序(Personalized Ranking)是推荐系统的”最后一公里”——从候选物品中按用户的个人偏好排出最优顺序。现代推荐系统通常分为召回(Recall)粗排(Pre-Ranking)精排(Ranking)重排(Re-Ranking)四个阶段,逐步缩小候选集、提高排序精度。精排阶段的核心是CTR(点击率)预测模型——预测用户点击每个候选物品的概率,按概率排序。

为什么重要:候选物品可能有成千上万个,但用户只看前几个。排在第1位和第10位的点击率可能差10倍以上。个性化排序决定了用户看到什么、不看到什么——这直接影响用户满意度和平台收入。

案例

YouTube — 推荐漏斗

视频库: 数亿个视频
┌──────▼──────┐
│ 召回层 │ 从数亿视频中快速筛选~1000个候选
│ (多路召回) │ - 协同过滤召回: 相似用户看过的
│ │ - 内容召回: 和你看过的视频相似的
│ │ - 热门召回: 全站/分类热门
│ │ - 关注召回: 你订阅的频道新视频
└──────┬──────┘
│ ~1000个候选
┌──────▼──────┐
│ 粗排层 │ 用简单模型快速打分,筛选到~200个
│ (轻量模型) │ 特征少、模型小、速度快
└──────┬──────┘
│ ~200个候选
┌──────▼──────┐
│ 精排层 │ 用复杂模型精细打分
│ (CTR预测) │ 特征丰富: 用户画像×视频特征×上下文
│ │ 模型复杂: 深度学习(Wide&Deep, DIN等)
└──────┬──────┘
│ ~50个候选,按分数排序
┌──────▼──────┐
│ 重排层 │ 业务规则调整
│ (策略调整) │ - 去重: 不连续推同一作者
│ │ - 多样性: 穿插不同类型
│ │ - 运营: 插入广告、推广内容
└──────┬──────┘
最终推荐列表(~20个)

特征工程是精排的关键

CTR预测模型的输入特征:
用户特征: 物品特征: 上下文特征:
- 年龄/性别 - 视频时长 - 当前时间(工作日/周末)
- 历史观看类别分布 - 上传时间 - 设备(手机/电脑)
- 平均观看时长 - 频道粉丝数 - 网络状态(WiFi/4G)
- 最近7天行为序列 - 历史CTR - 页面位置
- 标签/类别
交叉特征:
- 用户偏好类别 × 视频类别(用户喜欢科技,这是科技视频 → 高分)
- 用户活跃时段 × 当前时间(用户习惯晚上看视频,现在是晚上 → 高分)

News Feed — 个性化信息流排序

Facebook/微博的信息流排序也遵循类似的漏斗:
关注的人发的帖子(候选池)
→ 召回: 最近24小时内的帖子
→ 精排: 预测你对每条帖子的互动概率
P(点赞) × w1 + P(评论) × w2 + P(分享) × w3 + P(阅读>10s) × w4
→ 重排: 时间衰减、去重、插入广告

先想一想 🤔 精排模型每次推荐请求都要对200个候选做预测,延迟要求<100ms。如何在这个延迟约束下运行复杂的深度学习模型?

点击查看解析

常见的优化手段:

  1. 特征缓存:用户特征和物品特征提前计算好存在 Redis 中(Module 4),请求时直接查,不用实时计算。只有交叉特征需要实时计算。

  2. 模型蒸馏:用复杂的”教师模型”离线训练,然后蒸馏成轻量的”学生模型”在线服务。精度略降,但推理速度快10倍。

  3. GPU/TPU推理:深度学习模型用 GPU 批量推理,200个候选打包成一个 batch 一次前向传播。

  4. 分层打分:粗排用简单模型(逻辑回归,<1ms),只有粗排高分的才进精排(深度学习,~10ms/候选)。

核心思想:离线能做的不在线做,能预计算的不实时算


定义:冷启动(Cold Start)是推荐系统面对的经典难题——当系统缺乏数据时无法做出好的推荐。分为三种:用户冷启动——新用户没有历史行为,不知道他喜欢什么;物品冷启动——新物品没有被人交互过,无法通过协同过滤推出去;系统冷启动——新系统几乎没有数据,推荐完全无从下手。

为什么重要:每个平台都在持续获取新用户和新内容。如果新用户的首次体验很差(推荐不相关),他可能立刻流失。如果新内容永远得不到曝光(因为没有历史数据),创作者会离开平台。冷启动问题处理得好不好,直接影响平台的增长飞轮。

案例

YouTube — 新用户冷启动

一个新注册的YouTube用户,没有任何观看历史:
阶段1 (注册时 — 零数据):
→ 推荐全站热门视频
→ 根据注册信息粗略个性化(国家、语言、年龄段)
→ 展示类别选择页:"你对哪些话题感兴趣?"
☐ 科技 ☐ 游戏 ☐ 美食 ☐ 音乐 ☐ 体育 ...
阶段2 (看了3-5个视频后 — 少量数据):
→ 基于内容推荐: 你看了Python教程 → 推荐更多编程视频
→ Exploration(探索): 故意推荐不同类别,测试用户兴趣
阶段3 (看了50+视频后 — 数据充足):
→ 协同过滤开始起作用
→ 个性化模型逐渐精准
→ Exploration比例降低,Exploitation(利用已知偏好)为主

News Feed — 新内容冷启动

一个新发布的帖子,没有任何互动数据:
策略:
1. 初始流量分配: 先推给一小批用户(如1000人)
2. 快速收集信号: 这1000人的点击率、停留时间、互动率
3. 根据早期信号调整:
- 如果早期CTR高 → 扩大曝光(可能是好内容)
- 如果早期CTR低 → 减少曝光(可能质量差)
这就是"Explore-Exploit"策略的实际应用:
- Explore: 把新内容推给一些人,收集反馈
- Exploit: 根据反馈决定是否大规模推广
类似多臂老虎机(Multi-Armed Bandit)问题:
每个新内容是一个"臂",拉一次(曝光)获得回报(互动),
目标是用最少的探索找到回报最高的臂

Hotel Reservation — 新酒店冷启动

一家新开业的酒店加入平台,没有历史评分和预订数据:
解决方案:
1. 基于内容属性: 城市、星级、价格、设施 →
找特征相似的已有酒店,参考它们的表现
2. 初始加权: 给新酒店一定的曝光保证(如前2周保底曝光量)
3. 利用外部信号: 其他平台的评分、社交媒体口碑
4. 图片质量评分: 酒店图片质量好 → 提升初始排名

先想一想 🤔 “Explore-Exploit”策略也出现在其他系统设计场景中。Module 4 中的缓存淘汰和这里的推荐探索有什么共同的决策困境?

点击查看解析

两者都面临一个根本决策困境:利用已知信息 vs 探索未知可能

  • 缓存:是保留已知热门数据(exploit),还是给新数据缓存的机会看看它是否会变热门(explore)?LFU 策略偏向 exploit(频率高的留着),LRU 策略隐含了一定的 explore(最近访问的优先,给新数据机会)。

  • 推荐:是推荐已知用户喜欢的内容(exploit),还是推荐不确定用户是否喜欢的新内容来收集信息(explore)?

最优策略都是在两者间找平衡。纯 exploit 会陷入局部最优(缓存都是老热点、推荐都是同类内容),纯 explore 浪费资源(缓存命中率低、推荐不精准)。


定义:推荐系统的计算管道分为两种模式:离线推荐(Offline/Batch)——定期(如每天)用批处理计算所有用户的推荐列表,存储起来,请求时直接查表返回;实时推荐(Online/Real-time)——请求时根据用户最新行为实时计算推荐结果。大多数生产系统采用**近线(Near-line)**方案——结合批处理和流处理的混合架构。

为什么重要:纯离线推荐无法捕捉用户的实时兴趣变化(你刚搜了”滑雪”,推荐还是昨天算好的”编程教程”),但实时推荐计算复杂度高、延迟大。如何在时效性和计算成本之间取平衡,是推荐系统架构的核心挑战。

案例

YouTube — 三层推荐架构

┌─────────────────────────────────────────────────────────────┐
│ 离线层 (Batch) │
│ 每天运行: │
│ - Spark 训练推荐模型(矩阵分解、深度学习模型) │
│ - 为每个用户预计算 Top 1000 候选视频列表 │
│ - 计算物品相似度矩阵 │
│ - 更新用户画像和物品画像 │
│ → 结果存入 HDFS / Hive │
└─────────────────────┬───────────────────────────────────────┘
┌─────────────────────▼───────────────────────────────────────┐
│ 近线层 (Near-line) │
│ 准实时(分钟级): │
│ - Flink 消费用户行为事件流 │
│ - 实时更新用户兴趣向量(你刚看了一个烹饪视频 → 兴趣偏移) │
│ - 实时更新物品热度统计 │
│ - 触发增量模型更新 │
│ → 结果写入 Redis / Feature Store │
└─────────────────────┬───────────────────────────────────────┘
┌─────────────────────▼───────────────────────────────────────┐
│ 在线层 (Online) │
│ 实时(毫秒级): │
│ - 接收推荐请求 │
│ - 从 Redis 读取用户最新特征 + 候选列表 │
│ - 实时精排: CTR预测模型推理 │
│ - 重排: 多样性、去重、业务规则 │
│ → 返回最终推荐结果给用户 │
└─────────────────────────────────────────────────────────────┘

这与 Module 9 的 Lambda 架构直接对应

Lambda 架构:
批处理层(Batch Layer) → 推荐系统的离线层
速度层(Speed Layer) → 推荐系统的近线层
服务层(Serving Layer) → 推荐系统的在线层
批处理层保证准确性(全量数据训练模型)
速度层保证时效性(实时更新用户兴趣)
服务层合并两者的结果,返回给用户

News Feed — 实时性的极端需求

场景: 你刚发了一条关于世界杯的帖子,立刻打开信息流
如果是纯离线推荐:
→ 你看到的推荐完全没有世界杯内容(昨晚算的)
→ 体验很差
实时调整方案:
1. 离线: 每天预计算基础推荐列表
2. 实时信号注入:
- 你刚发了世界杯帖子 → 兴趣实时偏移
- 世界杯话题全站热度飙升 → 热点实时注入
3. 在线重排: 将世界杯相关内容提权
最终效果: 你刷新信息流,看到了世界杯相关内容

Google Maps / Proximity Service — 位置感知的实时推荐

你打开地图App,推荐附近餐厅:
离线: 每家餐厅的评分、特征、菜系分类 → 预计算
实时: 你的当前位置、当前时间、餐厅是否营业、实时等位情况
在线: 综合离线特征 + 实时信号 → 排序返回
关键实时信号:
- 地理位置: 你移动了 → 推荐列表完全变化
- 时间: 早上推荐早餐店,中午推荐午餐
- 实时状态: 某餐厅刚关门 → 从列表移除

先想一想 🤔 离线预计算的推荐列表可以缓存(Module 4),但实时推荐每次都要计算。如何设计缓存策略来兼顾时效性和性能?

点击查看解析

分层缓存策略:

  1. 离线结果缓存(长TTL):每天批处理计算的用户推荐列表存入 Redis,TTL = 24小时。这是”兜底”——如果实时计算来不及,至少有昨天的推荐可用。

  2. 实时特征缓存(短TTL):用户最新兴趣向量、物品实时热度等存入 Redis,TTL = 5分钟。Flink 流处理持续更新。

  3. 请求级缓存(极短TTL):同一用户短时间内多次刷新,返回相同结果。TTL = 30秒。避免每次刷新都重新排序。

  4. 降级策略:实时推荐服务超时 → 降级到离线推荐列表 → 再降级到全局热门列表。

核心原则和 Module 4 一致:越热门的数据缓存越积极,越个性化的数据缓存越保守。全局热门列表可以缓存很久(所有用户看的一样),个性化推荐列表缓存短一些(每个用户不同,且需要更新)。


在真实系统中,搜索和推荐往往不是独立的,而是融合在一起的:

┌──────────────────────────────────────────────────────┐
│ 用户界面 │
│ │
│ 搜索框 [输入关键词] → 搜索结果 + 推荐补充 │
│ │
│ 推荐信息流 → 推荐内容 + 搜索引导 │
│ │
│ 搜索建议 → 热门搜索 + 个性化建议 │
└──────────────────────────────────────────────────────┘
典型融合场景:
1. YouTube: 搜索"Python" → 搜索结果(排序融合了个性化推荐)
→ 侧边栏"推荐视频"(基于搜索词+用户画像)
2. Hotel Reservation: 搜索"北京酒店"
→ 搜索结果(倒排索引+排序)
→ "猜你喜欢"(协同过滤)
→ "看了又看"(Item-Based CF)
3. News Feed: 没有显式搜索,但底层是一个推荐系统
→ 搜索技术用于内容理解(分词、实体识别)
→ 推荐技术用于排序(个性化、CTR预测)

主题核心概念关键技术代表案例
倒排索引词→文档映射Lucene, Posting ListSearch Engine
全文检索相关性评分TF-IDF, BM25Search Engine, Hotel
分词文本→Tokenjieba, IK, 分析器管道Search Engine, Maps
搜索架构索引管道+搜索服务Elasticsearch, CDCHotel Reservation
自动补全前缀匹配Trie, 前缀树Search Engine, Maps
相关性调优多因子排序Boosting, Faceted SearchHotel, Leaderboard
推荐基础协同过滤+基于内容CF, Content-BasedYouTube, News Feed
协同过滤矩阵分解ALS, SVDYouTube, Hotel
个性化排序召回→排序漏斗CTR预测, Wide&DeepYouTube, News Feed
冷启动新用户/新物品Explore-Exploit, BanditYouTube, Hotel
实时vs离线混合推荐管道Lambda架构, Flink+SparkYouTube, News Feed

与其他模块的关联

  • Module 2 (数据存储):倒排索引的存储结构、列式存储用于聚合
  • Module 4 (缓存):搜索结果缓存、推荐列表缓存、特征缓存
  • Module 9 (批处理与流处理):索引构建管道、推荐模型训练、实时特征更新
  • Module 3 (复制):搜索索引副本、推荐服务多副本
  • Module 5 (消息系统):CDC事件流、用户行为事件流