这里的热门商品是从点击量的维度来看的,计算各个区域前三大热门商品,并备注上每个商品在主要城市中的分布比例,超过两个城市用其他显示。
例如:
地区 |
商品名称 |
点击次数 |
城市备注 |
华北 |
商品A |
100000 |
北京21.2%,天津13.2%,其他65.6% |
华北 |
商品P |
80200 |
北京63.0%,太原10%,其他27.0% |
华北 |
商品M |
40000 |
北京63.0%,太原10%,其他27.0% |
东北 |
商品J |
92000 |
大连28%,辽宁17.0%,其他 55.0% |
我们这次Spark-sql操作所有的数据均来自 Hive,首先在Hive中创建表,并导入数据。一共有3张表: 1张用户行为表,1张城市表,1 张产品表
CREATE TABLE `user_visit_action`(
`date` string,
`user_id` bigint,
`session_id` string,
`page_id` bigint,
`action_time` string,
`search_keyword` string,
`click_category_id` bigint,
`click_product_id` bigint,
`order_category_ids` string,
`order_product_ids` string,
`pay_category_ids` string,
`pay_product_ids` string,
`city_id` bigint)
row format delimited fields terminated by '\t';
load data local inpath '/opt/dwdatas/sparkdata/user_visit_action.txt' into table user_visit_action;
CREATE TABLE `product_info`(
`product_id` bigint,
`product_name` string,
`extend_info` string)
row format delimited fields terminated by '\t';
load data local inpath '/opt/dwdatas/sparkdata/product_info.txt' into table product_info;
CREATE TABLE `city_info`(
`city_id` bigint,
`city_name` string,
`area` string)
row format delimited fields terminated by '\t';
load data local inpath '/opt/dwdatas/sparkdata/city_info.txt' into table city_info;
import org.apache.spark.SparkConf
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{DataFrame, Encoder, Encoders, SparkSession, functions}
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
object sCityUDF {
/**
*
* user_visit_action
* date string 日期
* user_id bigint 用户id
* session_id string 会话id
* page_id bigint 页面id
* action_time string 活动时间
* search_keyword string 搜索关键词
* click_category_id bigint 点击类别id
* click_product_id bigint 点击产品id
* order_category_ids string 订单类别
* order_product_ids string 订单产品
* pay_category_ids string 完成订单类别ids
* pay_product_ids string 完成订单产品ids
* city_id bigint 城市id
*
*
* product_info
* product_id bigint 产品id
* product_name string 产品名
* extend_info string 产品拓展信息
*
*
* city_info
* city_id bigint 城市id
* city_name string 城市名
* area string 地区名
*
*
* */
/**
* 计算各个区域热门商品Top3,并存储到hive中
* 现在我们需要先把三张表通过相关信息链接起来,然后再进一步查询
* user_visit_info.click_product_id = product_info.product_id
* user_visit_info.city_id = city_info.city_id
*
* */
def main(args: Array[String]): Unit = {
// 配置SparkSession对象的基本信息
val SC = new SparkConf().setMaster("local[*]").setAppName("sCityUDF") // 运行环境本地,工作名称sCityUDF
val spark = SparkSession
.builder() // 创建环境
.enableHiveSupport() // 获取hive支持
.config(SC) // 基本配置信息
.config("hive.metastore.uris", "thrift://192.168.79.132:9083") // 设置hive元数据
.getOrCreate() // 获取或创建一个SparkSession对象。如果已经存在一个SparkSession对象,则返回该对象,否则创建一个新的对象
// 利用SparkSession对象,进入mydb数据库
spark.sql("use mydb")
// 查询基本数据,存储为临时表TV1
// 这里通过inner join on 根据相关信息,连接了三张表
// TV1结构为 地区 产品名 城市名
spark.sql(
"""
|select c.area, p.product_name, c.city_name
| from user_visit_action u inner
| join product_info p on u.click_product_id = p.product_id
| join city_info c on u.city_id = c.city_id
|""".stripMargin).createTempView("TV1")
// //展示TV1
// spark.sql("select * from TV1").show
// 根据地区、商品名进行聚合
// 这段代码是将自定义的聚合函数 CityRemarkUDAF 注册为 Spark 的 UDF (User-Defined Function) ,并命名为 “cityRemark”。
// 通过调用 spark.udf.register() 方法,将该 UDF 注册到 Spark 中,以便在后续的 Spark SQL 查询中使用。
// 这样,我们可以在 SQL 中直接调用 “cityRemark” 函数,对数据进行聚合操作
spark.udf.register("cityRemark", functions.udaf(new CityRemarkUDAF))
spark.sql(
"""
|select area, product_name,count(*) clickCnt, cityRemark(city_name) city_remark from
| TV1 group by area, product_name
| order by count(*) desc
|""".stripMargin).createTempView("TV2")
展示TV2
// spark.sql("select * from TV2").show
// 根据地区排名
spark.sql(
"""
|select area, product_name, clickCnt, city_remark, row_number() over(partition by area order by clickCnt desc) r
| from TV2
|""".stripMargin).createTempView("TV3")
// 取前三名
spark.sql(
"""
|select area, product_name, clickCnt, city_remark from TV3 where r <=3
|""".stripMargin).createTempView("TV4")
// 显示查询
spark.sql("select * from TV4").show(false) // 设置truncate为false防止因超过20个字符导致显示截取
// 转换为DataFrame
val df: DataFrame = spark.sql("select * from TV4")
// 将数据写入hive中,保存的表名为click_product_hot_top3
df.write.format("hive").mode("overwrite").saveAsTable("click_product_hot_top3")
// 关闭资源
spark.stop()
}
// 定义自定义集合,实现城市占比备注功能
/*
* 继承Aggregator,定义泛型
* IN:城市名
* BUF:Buffer => [总点击数量, Map[(city, cnt), (city, cnt), ......]]
* OUT:占比备注
* */
// 单独定义样例类型 总点击数(长整型), Map[(城市名, 点击数), (城市名, 点击数)] mutable是scala中的可变类型
case class Buffer(var total:Long, var cityMap:mutable.Map[String, Long])
// 定义UDF函数类,继承Aggregator聚合类
class CityRemarkUDAF extends Aggregator[String, Buffer, String]{
// 缓冲区初始化
override def zero: Buffer = {
// 设置buffer的初始值为0,可变类型map集合为空
Buffer(0, mutable.Map[String, Long]())
}
// 更新缓冲区数据 将更新的buffer返回 buffer来自参数BUF,city来自参数IN
override def reduce(buffer: Buffer, city: String): Buffer = {
// 每条数据的总点击数量加1
buffer.total += 1
// 在buffer中根据cityMap的key来获取value,如果有值,那就可以得到这个值,如果没有就会得到一个默认值
val newCount: Long = buffer.cityMap.getOrElse(city, 0L) + 1
// 更新buffer中cityMap的值
buffer.cityMap.update(city, newCount)
// 将buffer返回
buffer
}
// 合并缓冲区数据
override def merge(b1: Buffer, b2: Buffer): Buffer = {
//
b1.total += b2.total
val map1: mutable.Map[String, Long] = b1.cityMap
val map2: mutable.Map[String, Long] = b2.cityMap
// // 两个map的合并操作,方法一
// b1.cityMap = map1.foldLeft(map2){
// case (map, (city, cnt)) => {
// val newCount: Long = map.getOrElse(city, 0L) + cnt
// map.update(city, newCount)
// map
// }
// }
// 两个map的合并操作,方法二
// 用map2遍历
map2.foreach{
case (city, cnt) => {
// 如果发现将相同city的点击数量(cnt)相加,如果map1中没有map2中的city,则设置点击量为0,加上map2的cnt
val newCount = map1.getOrElse(city, 0L) + cnt
// 更新map1中的店家量信息
map1.update(city, newCount)
}
}
// 更新b1中的cityMap信息
b1.cityMap = map1
// 将结果返回
b1
}
// 将统计的结果生成字符串信息
override def finish(reduction: Buffer): String = {
val remarkList: ListBuffer[String] = ListBuffer[String]()
val totalCount: Long = reduction.total
val cityMap: mutable.Map[String, Long] = reduction.cityMap
// 转化为list进行排序
val cityCntList: List[(String, Long)] = cityMap.toList.sortWith(
(i, j) => {
i._2 > j._2
}).take(2) // take(2),只取前两个
val hasMore: Boolean = cityMap.size > 2
var rSum = 0L
cityCntList.foreach{
case (city, cnt) => {
val r: Long = cnt * 100 / totalCount
remarkList.append(s"${city} ${r}%")
rSum += r
}
}
if (hasMore){
remarkList.append(s"其它 ${100 - rSum}%")
}
remarkList.mkString(",")
}
// 固定写法
override def bufferEncoder: Encoder[Buffer] = Encoders.product
override def outputEncoder: Encoder[String] = Encoders.STRING
}
}