大家应该知道广告有一种计算收益的方式叫CPM,简单讲就是通过不断扩大曝光量来提升广告收益的计算方式;同样这种方式也适用于推荐系统;业务在不断提升,而商品的展示位相对又是比较固定的,为了收益最大化,必须要充分利用商品展示栏位,最大化优质商品的曝光。
数据分析
易企秀PC商城90%以上的流量来自搜索推荐,每天PC端H5商品总曝光次数800w次,去重后的商品有16.8w个,占总商品的84%,咋一看这个数字挺可喜的,可进一步分析发现问题所在;首先将商品曝光人数分别限制在2人或5人以上时,那么有效的商品曝光数是9.4w和6w,分别占比47%和30%,曝光占比一下子下滑了50%;我们继续调大人数到100,此时曝光占比跌至2%;一天中PC端活跃人数6w,很明显绝大部分商品只有极小部分人可以看到,长尾问题可见一斑;有很多优质商品得不到展示的机会,没有曝光就不可能有后续的点击和购买动作,那么定义一个好的曝光控制策略将是多么的重要。
架构
-
商品索引库
结合商品属性数据及用户行为特征数据,通过hadoop离线计算平台算出用户商品特征关系存入ES,该数据为商品候选结果集,为推荐系统召回模块提供商品检索、筛选及底层排序支持。
-
策略库
通过管理后台,产品可轻松添加推荐策略;策略库存放推荐系统使用的策略信息,如召回策略、排序策略、打散策略、分流策略、分群策略、配比策略等。
-
标签库
基于业务数据及用户行为数据,通过制定运营规则及算法模型离线计算得到;标签库主要存放商品、作品及用户的标签,如用户属性标签、用户兴趣标签,作品用途标签、商品风格标签及商品行业标签等。
-
CTR模型库
定期通过spark任务提取业务日志与用户行为日志构建商品点击预估模型,CTR模型用来对商品进行二次排序,将用户可能点击的商品优先返回。
-
实时特征库
实时特征库的构建主要为了弥补离线特征在时效性方面的不足;特征库记录用户与商品实时产生的互动数据,如曝光、点击、购买,依据用户未来对商品兴趣程度判断:已点击 > 已曝光 > 已购买 ,参照兴趣程度及数据量分别设置小时、日、周的曝光限制,如用户已购买的商品本周内将不会重复推荐。
用户上下文特征库
重点来了,实时特征库我们选择ES进行存储,理由是便于特征检索及排序;用户动作的上下文环境数据结构如下:
- 创建索引库自动生成模板
PUT _template/uplog_temp
{
"template" : "uplog_*",
"order":1,
"mappings" : {
"dynamic" : "false",
"dynamic_templates" : [
{
"strings" : {
"match_mapping_type" : "string",
"mapping" : {
"doc_values" : false,
"norms" : false,
"type" : "keyword"
}
}
}
],
"date_detection" : false,
"properties" : {
"pid" : { ##动作对象
"type" : "keyword"
},
"platform" : {
"type" : "keyword"
},
"position" : { ## 位置信息
"type" : "text",
"index_options" : "docs",
"analyzer" : "ik_max_word",
"search_analyzer" : "ik_smart"
},
"mark" : { ## 备注
"type" : "text",
"index_options" : "docs",
"analyzer" : "ik_max_word",
"search_analyzer" : "ik_smart"
},
"product" : {
"type" : "keyword"
},
"time" : {
"type" : "date",
"format" : "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},
"feature_type" : {
"type" : "integer" ##用户动作 1:点击;2:曝光;3:购买 ;4:升级会员...
},
"u_i" : { ## 不能为空
"type" : "keyword"
},
"s_i" : {
"type" : "keyword" ## 标记上下文环境
}
}
},
"settings" : {
"index" : {
"codec" : "best_compression",
"search" : {
"slowlog" : {
"level" : "info",
"threshold" : {
"fetch" : {
"info" : "500ms"
},
"query" : {
"info" : "1s"
}
}
}
},
"refresh_interval" : "60s",
"number_of_shards" : "1",
"translog" : {
"flush_threshold_size" : "2024mb",
"sync_interval" : "1200s",
"durability" : "async"
},
"merge" : {
"scheduler" : {
"max_thread_count" : "2"
}
},
"store" : {
"type" : "niofs"
},
"number_of_replicas" : "0"
}
}
}
因为曝光日志较多(千万日志量),所以分库存储只保留1天,其它上下文日志保留7天(每天一个索引库),超过对应天数的日志会执行delete操作,执行删除索引库的动作比delete_by_query性能要高很多
- 通过spark脚本同步离线日志到ES进行测试(上线后切streaming日志)
#!/bin/bash
export SPARK_HOME=/data/work/spark-2.4.4
export PATH=$SPARK_HOME/bin:$PATH
source /etc/profile
exec spark-shell --master yarn --name log2es --num-executors 5 --packages org.elasticsearch:elasticsearch-spark-20_2.11:7.3.0 <<!EOF
import org.elasticsearch.spark.sql._
val df=spark.read.parquet("/data/merge/tracker/202003/31")
df.registerTempTable("log")
#曝光日志
sql("select u_i,s_i,e_d.product_id pid, 'h5' product,os_2 platform,tk_id position,'2' as feature_type, from_unixtime(cast(s_t/1000 as long),'yyyy-MM-dd HH:mm:ss') time from log where e_t='element_view' and length(u_i)>4 and length(e_d.product_id)>4 and e_d.type=2").saveToEs("uplog_view_20200331",Map("es.nodes"->""))
#其它行为上下文日志
sql("select u_i,s_i,e_d.product_id pid, 'h5' product,os_2 platform,tk_id position,'1' as feature_type, from_unixtime(cast(s_t/1000 as long),'yyyy-MM-dd HH:mm:ss') time from log where e_t='element_click' and length(u_i)>4 and length(e_d.product_id)>4 and e_d.type=2").saveToEs("uplog_ctx_20200331",Map("es.nodes"->""))
spark.stop
!EOF
- 数据预览
"_source" : {
"u_i" : "ff80808159905690015991716a1d007e",
"s_i" : "f69604159905001585499443PGXGRB",
"pid" : "1237715",
"product" : "h5",
"platform" : "PC",
"position" : "search-69604-96046-1585499443PGXGRB-687-11",
"feature_type" : "1",
"time" : "2020-03-30 00:52:11"
}
- 例如获取某用户当天推荐曝光次数高的200个商品进行屏蔽
GET uplog_view_20200330/_search
{
"size": 0,
"query": {
"bool": {
"filter": {
"term": {
"u_i": {
"value": "ff80808159905690015991716a1d007e"
}
}
},
"must": [
{
"match": {
"position": "re"
}
}
]
}
},
"aggregations": {
"field1": {
"terms": {
"field": "pid",
"size": 200
}
}
}
}
通过实时的用户上下文日志也可快速区分用户当前兴趣变化及身份变化情况,配合运营规则及时合理调配资源以提升用户活跃及留存
商品曝光控制
商品曝光控制主要分为两种:节假日&兴趣预判
-
节假日
已过节假日及热点商品,理论上热度已经减少,需要在推荐系统召回模块进行降权甚至过滤处理;
通过离线任务每天对昨日新增商品进行标签化处理,标记出有节假日性质的商品,整个过程程序通过算法规则自动完成的无需人工干预。
推荐系统会在数据召回时先获取全年中不在最近一个月的节日类清单,基于此清单在获取ES候选结果集时对商品进行排除或降权处理。
兴趣预判
基于实时特征库的行为日志,结合一定的分析策略来预判用户未来一段时间内对该商品的兴趣程度;首先设计类电商平台有区别于传统电商平台,例如某用户今天购买了一个模板商品,那么很大程度上近期该用户是不会再重复购买类似商品的,所以推荐系统需要在各推荐位自动屏蔽该商品继续曝光给该用户,通过选择其它商品曝光不仅能充分利用推荐位展示更多信息,同时更能拓展用户兴趣需求;
在商品召回模块,程序会根据用户id从实时特征库抽取出当前用户最近触达到的Top商品,基于此清单在获取ES候选结果集时对商品进行排除。
注:
根据不同用户的活跃程度,每个用户日志量有大有小,在获取用户日志信息时需要进行Top限制,防止因数据量太大影响服务性能(建表时已根据type和time进行排序)
该屏蔽策略并不会永久屏蔽商品,根据用户兴趣预判,最长屏蔽1周,最短屏蔽1天
兜底策略
当召回模块没有提取到足够的数据时需要触发兜底策略:
- 热点
基于业务日志及用户行为日志,汇总求出商品近7日销量作为热点候选商品,每天通过hadoop离线数据平台进行计算,将热点score已标签方式更新到商品标签库,更新之前需要先将所有热点商品score致为0。 - 运营干预
运营可根据时下热点及促销模式指定一部分商品,并以标签的形式更新到商品标签库。