四、离线推荐服务建设(基于统计的推荐)
目录
4.1 离线推荐服务
离线推荐服务是综合用户所有的历史数据,利用设定的离线统计算法和离线推荐算法周期性的进行结果统计与保存,计算的结果在一定时间周期内是固定不变的,变更的频率取决于算法调度的频率。
离线推荐服务主要计算一些可以预先进行统计和计算的指标,为实时计算和前端业务相应提供数据支撑。
离线推荐服务主要分为统计推荐、基于隐语义模型的协同过滤推荐以及基于内容和基于Item-CF的相似推荐,我这一章主要介绍基于统计的推荐。
4.2 离线统计服务
4.2.1 统计服务主体框架
在recommender下新建子项目StatisticsRecommender,pom.xml文件中只需引入spark、scala和mongodb的相关依赖:
<dependencies>
<!-- Spark的依赖引入 -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
</dependency>
<!-- 引入Scala -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</dependency>
<!-- 加入MongoDB的驱动 -->
<!-- 用于代码方式连接MongoDB -->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>casbah-core_2.11</artifactId>
<version>${casbah.version}</version>
</dependency>
<!-- 用于Spark和MongoDB的对接 -->
<dependency>
<groupId>org.mongodb.spark</groupId>
<artifactId>mongo-spark-connector_2.11</artifactId>
<version>${mongodb-spark.version}</version>
</dependency>
</dependencies>
在resources文件夹下引入log4j.properties,然后在src/main/scala下新建scala 单例对象com.recom.statistics.StatisticsRecommender。
同样,我们应该先建好样例类,在main()方法中定义配置、创建SparkSession并加载数据,最后关闭spark。代码如下:
src/main/scala/com.recom.statistics/StatisticsRecommender.scala
case class Rating(userId: Int, productId: Int, score: Double, timestamp: Int)
case class MongoConfig(uri:String, db:String)
object StatisticsRecommender {
val MONGODB_RATING_COLLECTION = "Rating"
//统计的表的名称
val RATE_MORE_PRODUCTS = "RateMoreProducts"
val RATE_MORE_RECENTLY_PRODUCTS = "RateMoreRecentlyProducts"
val AVERAGE_PRODUCTS = "AverageProducts"
// 入口方法
def main(args: Array[String]): Unit = {
val config = Map(
"spark.cores" -> "local[*]",
"mongo.uri" -> "mongodb://localhost:27017/recommender",
"mongo.db" -> "recommender"
)
//创建SparkConf配置
val sparkConf = new SparkConf().setAppName("StatisticsRecommender").setMaster(config("spark.cores"))
//创建SparkSession
val spark = SparkSession.builder().config(sparkConf).getOrCreate()
val mongoConfig = MongoConfig(config("mongo.uri"),config("mongo.db"))
//加入隐式转换
import spark.implicits._
//数据加载进来
val ratingDF = spark
.read
.option("uri",mongoConfig.uri)
.option("collection",MONGODB_RATING_COLLECTION)
.format("com.mongodb.spark.sql")
.load()
.as[Rating]
.toDF()
//创建一张名叫ratings的表
ratingDF.createOrReplaceTempView("ratings")
//TODO: 不同的统计推荐结果
spark.stop()
}
4.2.2 历史热门商品统计
根据所有历史评分数据,计算历史评分次数最多的商品。
实现思路:
通过Spark SQL读取评分数据集,统计所有评分中评分数最多的商品,然后按照从大到小排序,将最终结果写入MongoDB的RateMoreProducts数据集中。
//统计所有历史数据中每个商品的评分数
//数据结构 -》 productId,count
val rateMoreProductsDF = spark.sql("select productId, count(productId) as count from ratings group by productId ")
rateMoreProductsDF
.write
.option("uri",mongoConfig.uri)
.option("collection",RATE_MORE_PRODUCTS)
.mode("overwrite")
.format("com.mongodb.spark.sql")
.save()
4.2.3 最近热门商品统计
根据评分,按月为单位计算最近时间的月份里面评分数最多的商品集合。
实现思路:
通过Spark SQL读取评分数据集,通过UDF函数将评分的数据时间修改为月,然后统计每月商品的评分数。统计完成之后将数据写入到MongoDB的RateMoreRecentlyProducts数据集中。
//统计以月为单位拟每个商品的评分数
//数据结构 -》 productId,count,time
//创建一个日期格式化工具
val simpleDateFormat = new SimpleDateFormat("yyyyMM")
//注册一个UDF函数,用于将timestamp装换成年月格式 1260759144000 => 201605
spark.udf.register("changeDate",(x:Int) => simpleDateFormat.format(new Date(x * 1000L)).toInt)
// 将原来的Rating数据集中的时间转换成年月的格式
val ratingOfYearMonth = spark.sql("select productId, score, changeDate(timestamp) as yearmonth from ratings")
// 将新的数据集注册成为一张表
ratingOfYearMonth.createOrReplaceTempView("ratingOfMonth")
val rateMoreRecentlyProducts = spark.sql("select productId, count(productId) as count ,yearmonth from ratingOfMonth group by yearmonth,productId order by yearmonth desc, count desc")
rateMoreRecentlyProducts
.write
.option("uri",mongoConfig.uri)
.option("collection",RATE_MORE_RECENTLY_PRODUCTS)
.mode("overwrite")
.format("com.mongodb.spark.sql")
.save()
4.2.4 商品平均得分统计
根据历史数据中所有用户对商品的评分,周期性的计算每个商品的平均得分。
实现思路:
通过Spark SQL读取保存在MongDB中的Rating数据集,通过执行以下SQL语句实现对于商品的平均分统计:
//统计每个商品的平均评分
val averageProductsDF = spark.sql("select productId, avg(score) as avg from ratings group by productId ")
averageProductsDF
.write
.option("uri",mongoConfig.uri)
.option("collection",AVERAGE_PRODUCTS)
.mode("overwrite")
.format("com.mongodb.spark.sql")
.save()
统计完成之后将生成的新的DataFrame写出到MongoDB的AverageProducts集合中。
4.3 附件:完整代码
package com.recom.statics
import java.text.SimpleDateFormat
import java.util.Date
import org.apache.spark.SparkConf
import org.apache.spark.sql.{DataFrame, SparkSession}
case class Rating(userId: Int,productId:Int,score:Double,timestamp:Int)
case class MongoConfig(uri:String,db:String)
object StatisticsRecommender {
//定义monogobd中存储的表名
val MONGODB_RATING_COLLECTION = "Rating"
val RATE_MORE_PRODUCTS="RateMoreProducts"
val RATE_MORE_RECENTLY_PRODUCTS="RateMoreRecentlyProducts"
val AVERAGE_PRODUCTS="AverageProducts"
def main(args: Array[String]): Unit = {
//定义基础配置的集合(可以放入配置文件,通过方法获取属性的值)
val config = Map(
"spark.cores"->"local[*]",
"mongo.uri"->"mongodb://hadoop102:27017/recommender",
"mongo.db"->"recommender"
)
//创建一个spark config
val sparkConf = new SparkConf().setMaster(config("spark.cores")).setAppName("StatisticsRecommender")
//创建一个spark session
val spark = SparkSession.builder().config(sparkConf).getOrCreate()
//导入隐式转换类,在DF和DS转换的过程中会使用到
import spark.implicits._
//通过隐式类的方法创建mongodb连接对象
implicit val mongoConfig = MongoConfig(config("mongo.uri"),config("mongo.db"))
//加载数据
val ratingDF: DataFrame = spark.read
.option("uri", mongoConfig.uri)
.option("collection", MONGODB_RATING_COLLECTION)
.format("com.mongodb.spark.sql")
.load()
.as[Rating]
.toDF()
//创建临时表(视图)供后续使用spark sql统计使用
ratingDF.createOrReplaceTempView("ratings")
//TODO: 用spark sql去做不同的统计推荐
//1.历史热门商品,按照评分个数统计,productId, count
val rateMoreProductsDF: DataFrame = spark.sql(
"""
|select productId,count(productId) count
|from ratings
|group by productId
|order by count desc
|""".stripMargin)
//将数据写入mongodb
storeDFInMongoDB(rateMoreProductsDF,RATE_MORE_PRODUCTS)
//2.近期热门商品,把时间戳字段转换为yyyyMM格式进行评分个数统计,最终得到productId, count, yearmonth
//创建工具类转换日期格式
val simpleDateFormat = new SimpleDateFormat("yyyyMM")
//注册UDF
spark.udf.register("changeDate",(x:Int)=>simpleDateFormat.format(new Date(x*1000L)))
//把原始的rating数据转换成想要的结构productId, count, yearmonth
val ratingOfYearMonth: DataFrame = spark.sql(
"""
|select productId,score,changeDate(timestamp) yearmonth
|from ratings
|""".stripMargin)
ratingOfYearMonth.createOrReplaceTempView("ratingOfMonth")
val ratingOfYearMonthDF: DataFrame = spark.sql(
"""
|select productId,count(productId) count,yearmonth
|from ratingOfMonth
|group by productId,yearmonth
|order by yearmonth desc,count desc
|""".stripMargin)
storeDFInMongoDB(rateMoreProductsDF,RATE_MORE_RECENTLY_PRODUCTS)
//3.优质商品统计,商品的平均评分,productId, avg
val averageProductsDF: DataFrame = spark.sql(
"""
|select productId,avg(score) avg
|from ratings
|group by productId
|order by avg desc
|""".stripMargin)
storeDFInMongoDB(averageProductsDF,AVERAGE_PRODUCTS)
spark.stop()
}
def storeDFInMongoDB(df: DataFrame, collect_name: String)(implicit mongoConfig: MongoConfig): Unit ={
df.write
.option("uri",mongoConfig.uri)
.option("collection",collect_name)
.mode("overwrite")
.format("com.mongodb.spark.sql")
.save()
}
}