<audio id="audio" title="30 | 推荐系统服务化、存储选型及API设计" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f4/16/f4892bf1542c0b3ee7b703254ce1fd16.mp3"></audio>

在过往的文章中，我讲到了推荐系统方方面面的相关概念。那么说，对于认识一个推荐系统来说，还差最后一个问题需要解决，那就是：万事俱备，如何给用户提供一个真正的在线推荐服务呢？

## 服务化是最后一步

其实一个推荐系统的在线服务，和任何别的在线服务相比，也没有什么本质区别，只是仍然还有一些特殊性。

提供一个在线服务，需要两个关键元素：数据库和API。今天我就来专门说一说推荐系统中大家常常用到的数据库，并会谈谈推荐系统的API应该如何设计。

## 存储

这里注意一下，今天这里讲到的存储，专指近线或者在线部分所用的数据库，并不包括离线分析时所涉及的业务数据库或者日志数据库。

近线和在线的概念我在前面已经讲到过。推荐系统在离线阶段会得到一些关键结果，这些关键结果需要存进数据库，供近线阶段做实时和准实时的更新，最终会在在线阶段直接使用。

首先来看一下，离线阶段会产生哪些数据。按照用途来分，归纳起来一共就有三类。

1. 特征。特征数据会是最多的，所谓用户画像，物品画像，这些都是特征数据，更新并不频繁。
1. 模型。尤其是机器学习模型，这类数据的特点是它们大都是键值对，更新比较频繁。
1. 结果。就是一些推荐方法在离线阶段批量计算出推荐结果后，供最后融合时召回使用。任何一个数据都可以直接做推荐结果，如协同过滤结果。

如果把整个推荐系统笼统地看成一个大模型的话，它依赖的特征是由各种特征工程得到的，这些线下的特征工程和样本数据共同得到模型数据，这些模型在线上使用时，需要让线上的特征和线下的特征一致，因此需要把线下挖掘的特征放到线上去。

特征数据有两种，一种是稀疏的，一种是稠密的，稀疏的特征常见就是文本类特征，用户标签之类的，稠密的特征则是各种隐因子模型的产出参数。

特征数据又常常要以两种形态存在：一种是正排，一种是倒排。

正排就是以用户ID或者物品ID作为主键查询，倒排则是以特征作为主键查询。

两种形态的用途在哪些地方呢？在需要拼凑出样本的特征向量时，如线下从日志中得到曝光和点击样本后，还需要把对应的用户ID和物品ID展开成各自的特征向量，再送入学习算法中得到最终模型，这个时候就需要正排了。

另一种是在需要召回候选集时，如已知用户的个人标签，要用个人标签召回新闻，那么久就需要提前准备好标签对新闻的倒排索引。

这两种形态的特征数据，需要用不同的数据库存储。正排需要用列式数据库存储，倒排索引需要用KV数据库存储。前者最典型的就是HBase和Cassandra，后者就是Redis或Memcached。稍后再介绍这几个数据库。

另外，对于稠密特征向量，例如各种隐因子向量，Embedding向量，可以考虑文件存储，采用内存映射的方式，会更加高效地读取和使用。

模型数据也是一类重要的数据，模型数据又分为机器学习模型和非机器学习模型。

机器学习模型与预测函数紧密相关。模型训练阶段，如果是超大规模的参数数量，业界一般采用分布式参数服务器，对于达到超大规模参数的场景在中小公司不常见，可以不用牛刀。

而是采用更加灵活的PMML文件作为模型的存储方式，PMML是一种模型文件协议，其中定义模型的参数和预测函数，稍后也会介绍。

非机器学习模型，则不太好定义，有一个非常典型的是相似度矩阵，物品相似度，用户相似度，在离线阶段通过用户行为协同矩阵计算得到的。相似度矩阵之所算作模型，因为，它是用来对用户或者物品历史评分加权的，这些历史评分就是特征，所以相似度应该看做模型数据。

最后，是预先计算出来的推荐结果，或者叫候选集，这类数据通常是ID类，召回方式是用户ID和策略算法名称。这种列表类的数据一般也是采用高效的KV数据库存储，如Redis。

另外，还要介绍一个特殊的数据存储工具：ElasticSearch。这原本是一个构建在开源搜索引擎Lucene基础上的分布式搜索引擎，也常用于日志存储和分析，但由于它良好的接口设计，扩展性和尚可的性能，也常常被采用来做推荐系统的简单第一版，直接承担了存储和计算的任务。

下面我逐一介绍刚才提到的这些存储工具。

### 1.列式数据库

所谓列式数据库，是和行式数据库相对应的，这里不讨论数据库的原理，但是可以有一个简单的比喻来理解这两种数据库。

你把数据都想象成为矩阵，行是一条一条的记录，例如一个物品是一行，列是记录的各个字段，例如ID是一列，名称是一列，类似等等。

当我们在说行和列的时候，其实是在大脑中有一个抽象的想象，把数据想象成了二维矩阵，但是实际上，数据在计算机中，管你是行式还是列式，都要以一个一维序列的方式存在内存里或者磁盘上。

那么有意思的就来了，是按照列的方式把数据变成一维呢，还是按照行的方式把数据变成一维呢，这就是列式数据库和行式数据库的区别。

当然实际上数据库比这复杂多了，这只是一个简单形象的说明，有助于你去理解数据的存储方式。

列式数据库有个列族的概念，可以对应于关系型数据库中的表，还有一个键空间的概念，对应于关系型数据库中的数据库。

众所周知，列式数据库适合批量写入和批量查询，因此常常在推荐系统中有广泛应用。列式数据库当推Cassandra和HBase，两者都受Google的BigTable影响，但区别是：Cassandra是一个去中心化的分布式数据库，而HBase则是一个有Master节点的分布式存储。

Cassandra在数据库的CAP理论中可以平滑权衡，而HBase则是强一致性，并且Cassandra读写性能优于HBase，因此Cassandra更适合推荐系统，毕竟推荐系统不是业务逻辑导向的，对强一致性要求不那么强烈，这和我在一开始建议“你要建立起不确定思维”是一脉相承的。

Cassandra的数据模型组织形式如下图所示：

<img src="https://static001.geekbang.org/resource/image/b9/3c/b9b12de5134a6a3c88ba8a97f6a3333c.png" alt="" />

从这个图可以看出来，可以通过行主键及列名就可以访问到数据矩阵的单元格值。

前面也说过，用户和物品的画像数据适合存储在Cassandra中。也适合存储模型数据，如相似度矩阵，还可以存储离线计算的推荐结果。

### 2.键值数据库

除了列式数据库外，还有一种存储模式，就是键值对内存数据库，这当然首推Redis。Redis你可以简单理解成是一个网络版的HashMap，但是它存储的值类型比较丰富，有字符串、列表、有序列表、集合、二进制位。

并且，Redis的数据放在了内存中，所以都是闪电般的速度来读取。

在推荐系统的以下场景中常常见到Redis的身影：

1. 消息队列，List类型的存储可以满足这一需求；
1. 优先队列，比如兴趣排序后的信息流，或者相关物品，对此sorted set类型的存储可以满足这一需求；
1. 模型参数，这是典型的键值对来满足。

另外，Redis被人诟病的就是不太高可用，对此已经有一些集群方案，有官方的和非官方的，可以试着加强下Redis的高可用。

### 3.非数据库

除了数据库外，在推荐系统中还会用到一些非主流但常用的存储方式。第一个就是虚拟内存映射，称为MMAP，这可以看成是一个简陋版的数据库，其原理就是把磁盘上的文件映射到内存中，以解决数据太大不能读入内存，但又想随机读取的矛盾需求。

哪些地方可以用到呢？比如你训练的词嵌入向量，或者隐因子模型，当特别大时，可以二进制存在文件中，然后采用虚拟内存映射方式读取。

另外一个就是PMML文件，专门用于保存数据挖掘和部分机器学习模型参数及决策函数的。当模型参数还不足以称之为海量时，PMML是一个很好的部署方法，可以让线上服务在做预测时并不依赖离线时的编程语言，以PMML协议保存离线训练结果就好。

## API

除了存储，推荐系统作为一个服务，应该以良好的接口和上有服务之间交互，因此要设计良好的API。

API有两大类，一类数据录入，另一类是推荐服务。数据录入API，可以用于数据采集的埋点，或者其他数据录入。

<img src="https://static001.geekbang.org/resource/image/6e/15/6e88ceb14c4e28e8e113393f147de615.png" alt="" />

推荐服务的API按照推荐场景来设计，则是一种比较常见的方式，下面分别简单说一下API的样子。

### 1.猜你喜欢

**接口：**

/Recommend

**输入：**

* UserID – 个性化推荐的前提<br />
 * PageID – 推荐的页面ID，关系到一些业务策略<br />
 * FromPage – 从什么页面来<br />
 * PositionID – 页面中的推荐位ID<br />
 * Size – 请求的推荐数量<br />
 * Offset – 偏移量，这是用于翻页的

**输出：**

* Items – 推荐列表，通常是数组形式，每一个物品除了有ID，还有展示所需的各类元素<br />
 * Recommend_id – 唯一ID标识每一次调用，也叫做曝光ID，标识每一次曝光，用于推荐后追踪推荐效果的，很重要<br />
 * Size – 本次推荐数量<br />
 * Page —— 用于翻页的

### 2.相关推荐

**接口：**

/Relative

**输入：**

* UserID – 个性化推荐的前提<br />
 * PageID – 推荐的页面ID，关系到一些业务策略<br />
 * FromPage – 从什么页面来<br />
 * PositionID – 页面中的推荐位ID<br />
 * ItemID – 需要知道正在浏览哪个物品导致推荐相关物品<br />
 * Size – 请求的推荐数量<br />
 * Offset – 偏移量，这是用于翻页的

**输出：**

* Items – 推荐列表，通常是数组形式，每一个物品除了有ID，还有展示所需的各类元素<br />
 * Recommend_ID – 唯一ID标识每一次调用，也叫做曝光ID，标识每一次曝光，用于推荐后追踪推荐效果的，很重要<br />
 * Size – 本次推荐数量<br />
 * Page —— 用于翻页的

### 3.热门排行榜

**接口：**

/Relative

**输入：**

* UserID – 个性化推荐的前提<br />
 * PageID – 推荐的页面ID，关系到一些业务策略<br />
 * FromPage – 从什么页面来<br />
 * PositionID – 页面中的推荐位ID<br />
 * Size – 请求的推荐数量<br />
 * Offset – 偏移量，这是用于翻页的

**输出：**

* Items – 推荐列表，通常是数组形式，每一个物品除了有ID，还有展示所需的各类元素<br />
 * Recommend_id – 唯一ID标识每一次调用，也叫做曝光ID，标识每一次曝光，用于推荐后追踪推荐效果的，很重要<br />
 * Size – 本次推荐的数量<br />
 * Page —— 用于翻页的

相信你看到了吧，实际上这些接口都很类似。

## 总结

今天我主要讲解了推荐系统上线的两大问题，一个是线上数据存储，另一个是推荐系统的API有哪些。

虽然实际情况肯定不是只有这点问题，但是这些也足以构建出一个简单的推荐系统线上版了。

你还记得在前几篇专栏中，我提到统一考虑搜索和推荐的问题吗？那么说，如果要把推荐和搜索统一考虑的话，API该如何设计呢？欢迎留言一起讨论。感谢你的收听，我们下期再见。
