大数据--商品推荐系统项目实现(下)
业务需求
-
某电商网站首页有猜你喜欢推荐位,该推荐位一次能展示6个商品,推荐内容可以更换四次,共需推荐24个商品。
-
需要使用协同过滤算法(user CF & Item CF)及基于物品内容的算法进行混合推荐。
-
一次性展示的6个商品中,从左到右的顺序分别是:
- 第一位:基于物品的实时推荐结果
- 第二位:基于用户的离线推荐结果
- 第三位:基于物品的离线推荐结果
- 第四位:基于内容的实时推荐结果
- 第五位:基于物品的实时推荐结果
- 第六位:基于用户的离线推荐结果
如有业务需要推广产品,可以指定推广产品出现在某一个位置上。如下图,在第一位上硬推某产品。
-
大型网站的推荐位不仅仅只有一个,需要对每个广告位进行编号,比如猜你喜欢的广告位编号是121
-
每个推荐位是一个独立的推荐产品,需要对每个广告位开发独立的推荐模型
每个推荐位需要配置特有的推荐规则和排序规则 -
为了容错,每个推荐位都需要默认的推荐产品,当推荐系统无法计算正常的结果时,使用默认产品进行推荐。
-
各个推荐模型推送的商品可以能重复和下线的商品,需要对商品进行进行去重和过滤处理
-
推荐结果计算完毕之后,将硬推广告放进去。
功能实现分析
-
用户在商城浏览商品,将用户的浏览记录保存到Cookie,随着用户的请求传送给推荐服务接口。推荐服务接收到用户的基本信息和浏览信息。
另一种思路,可以通过消费点击流日志,将用户的行为保存到Redis中,推荐服务通过访问Redis获取用户的行为记录。 -
推荐接口从用户的基本信息中获取到三种推荐结果(离线结果)
基于历史数据,计算的基于用户的协同过滤的推荐结果,推荐数量24。
基于用户上一次行为记录,计算的基于物品的协同过滤推荐结果,推荐数量24。这里根据用户对某一个商品的浏览次数进行加权。
基于用户上一次行为记录,计算的基于内容的推荐结果,推荐数量24。这里根据用户对某一个商品的浏览次数进行加权。 -
推荐接口从用户的浏览信息中获取用户当前会话的的行为记录,并以此计算基于物品和基于内容的实时推荐结果
基于用户本次会话的记录,计算基于物品的推荐结果,推荐数量为24
基于用户本次会话的记录,计算基于内容的推荐结果,推荐数量为24。 -
对以上的反馈的推荐结果进行排序,排序的过程中对商品去重
按照业务需求对结果排序,第一位是基于物品的实时推荐结果,依次类推。在排序的过程汇中,需要对推荐的商品进行排序。
推荐结果生成完毕之后,对整体的推荐结果的产品数量进行补全和删除操作。补全使用该推荐位的默认推荐产品进行补全。 -
设置业务人员强推的商品,根据业务人员指定的商品序号,替换掉推荐结果中对应序号的推荐商品。
整体架构
-
数据平台
在数据平台上,针对每个用户计算好三个推荐结果,基于用户的推荐结果、基于物品的推荐结果、基于内容的推荐结果。基于物品的相似度、基于内容的相似度。 -
Redis数据缓存:
通过独立的Java应用将每个用户的推荐结果和基于物品的相似度与基于内容的相似度信息导入到Redis缓存集群中。 -
获取推荐结果里两种方式;
一种是已经计算好的离线推荐结果,直接获取即可;
另一种是根据用户实时的浏览记录计算新的推荐结果。第二种推荐结果主要依赖三种数据,用户的浏览记录、基于物品的相似度、基于内容的相似度。 -
排序过滤:将推荐的结果按照业务规则进行混合排序及去重等操作。
-
最终推荐结果:基于业务业务规则对业务推荐的产品进行设置。
代码开发(service项目)
本次代码较多,提供下载地址:点击下载-代码
这里的关系数据由我们写代码生成,模拟真实数据,主要使用下面的类。
- pom依赖
<properties>
<junit.version>4.12</junit.version>
<spring.version>4.2.4.RELEASE</spring.version>
<mybatis.version>3.2.8</mybatis.version>
<mybatis.spring.version>1.2.2</mybatis.spring.version>
<mybatis.paginator.version>1.2.15</mybatis.paginator.version>
<mysql.version>5.1.32</mysql.version>
<slf4j.version>1.6.4</slf4j.version>
<jackson.version>2.4.2</jackson.version>
<druid.version>1.0.9</druid.version>
<httpclient.version>4.3.5</httpclient.version>
<jstl.version>1.2</jstl.version>
<servlet-api.version>2.5</servlet-api.version>
<jsp-api.version>2.0</jsp-api.version>
<joda-time.version>2.5</joda-time.version>
<commons-lang3.version>3.3.2</commons-lang3.version>
<commons-io.version>1.3.2</commons-io.version>
<commons-net.version>3.3</commons-net.version>
<pagehelper.version>3.4.2-fix</pagehelper.version>
<jsqlparser.version>0.9.1</jsqlparser.version>
<commons-fileupload.version>1.3.1</commons-fileupload.version>
<jedis.version>2.7.2</jedis.version>
<solrj.version>4.10.3</solrj.version>
<dubbo.version>2.5.3</dubbo.version>
<zookeeper.version>3.4.7</zookeeper.version>
<zkclient.version>0.1</zkclient.version>
<activemq.version>5.11.2</activemq.version>
<freemarker.version>2.3.23</freemarker.version>
<quartz.version>2.2.2</quartz.version>
</properties>
<dependencies>
<!-- 时间操作组件 -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${joda-time.version}</version>
</dependency>
<!-- Apache工具组件 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>${commons-net.version}</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- 日志处理 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis.spring.version}</version>
</dependency>
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- JSP相关 -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>${servlet-api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>${jsp-api.version}</version>
<scope>provided</scope>
</dependency>
<!-- 文件上传组件 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
</dependency>
<!-- Jackson Json处理工具包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.2.6.RELEASE</version>
</dependency>
<!-- 添加Lucene的pom依赖 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>4.10.2</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queries</artifactId>
<version>4.10.2</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-test-framework</artifactId>
<version>4.10.2</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>4.10.2</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>4.10.2</version>
</dependency>
<!-- 这里的ik分词器的版本号没有对应ik真实的版本号。ik2012年就停止更新了 -->
<dependency>
<groupId>org.wltea.ik-analyzer</groupId>
<artifactId>ik-analyzer</artifactId>
<version>4.10.2</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.10.3</version>
</dependency>
</dependencies>
- 生成数据的代码,在util包下有一个DataInit类,运行这个类中所有的测试方法,会将数据存入redis缓存。
PS:记得更改redis的地址。
public class DataInit {
// public static void main(String[] args) {
// initProduct();//初始化所有的商品的信息
// initBaseUserRecommend();//初始化所有的基于用户的信息
// initBaseItemRecommend();//初始化所有用户基于用户的推荐
// initBaseItemResult();//初始化物品与物品之间的关联度
// initBaseContentResult();//初始基于内容的商品关联度
// initUser();//初始化所有的用户信息
// initDefaultRecomend();//初始化广告位的默认推荐信息
// }
@Test
public void getData() {
ShardedJedis jedis = MyShardedJedisPool.getResource();
//获取基于用户的推荐,用户guyong
System.out.println("获取用户 guyong ,基于用户的推荐结果======================");
System.out.println(jedis.hget("recom:guyong", "userCF"));
System.out.println();
System.out.println();
//获取基于物品的推荐,用户guyong
System.out.println("获取用户 guyong ,基于物品的推荐结果(离线物品)======================");
System.out.println(jedis.hget("recom:guyong", "itemCF"));
System.out.println();
System.out.println();
//打印基于物品的物品相似度
System.out.println("打印基于物品的物品相似度======================");
List<Product> productList = JDProduct.getProduct();
for (Product product : productList) {
System.out.println(product.getId() + " " + jedis.get("recom:baseItem:" + product.getId()));
}
System.out.println();
System.out.println();
//打印基于内容的物品相似度
System.out.println("打印基于内容的物品相似度======================");
for (Product product : productList) {
System.out.println(product.getId() + " " + jedis.get("recom:baseContent:" + product.getId()));
}
System.out.println();
System.out.println();
//获取广告位121的默认推荐产品信息
System.out.println("获取广告位121的默认推荐产品信息======================");
System.out.println(jedis.hget("recom:default", "121"));
System.out.println();
System.out.println();
//获取所有的商品信息
System.out.println("获取所有的商品信息======================");
for (Product product : productList) {
System.out.println(product.getId() + " " + jedis.get("recom:prod:" + product.getId()));
}
System.out.println();
System.out.println();
}
@Test
/**
* 设置一些默认的推荐数据 -----导入到redis中
*/
public void initDefaultRecomend() {
//MyShardedJedisPool 将我们的数据保存到redis集群中
ShardedJedis jedis = MyShardedJedisPool.getResource();
List<Product> productList = JDProduct.getProduct(); //通过网络爬虫获取电商网站的商品数据
// productList = productList.subList(20, 40); //截取20个商品作为默认推荐商品列表
String adId = "121";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < productList.size(); i++) {
sb.append(productList.get(i).getId());
if (i != productList.size() - 1) {
sb.append(",");
}
}
jedis.hset("recom:default", adId, sb.toString());
System.out.println("为广告位121设置默认推荐产品成功." + productList.size());
}
/**
* 为用户顾雍初始化基于itemCF的推荐结果
* recom:guyong:itemCF 12121212,12121212,12121212,12121212
* recom:guyong:userCF 12121212,12121212,12121212,12121212
*/
@Test
public void initBaseItemRecommend() {
ShardedJedis jedis = MyShardedJedisPool.getResource();
List<Product> productList = JDProduct.getProduct();
// productList = productList.subList(10, 30);
String user = "guyong";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < productList.size(); i++) {
sb.append(productList.get(i).getId());
if (i != productList.size() - 1) {
sb.append(",");
}
}
//为用户推荐基于用户的推荐产品---根据昨天的记录计算好的
jedis.hset("recom:" + user, "itemCF", sb.toString());
System.out.println("为用户guyong设置基于物品的推荐产品" + productList.size());
}
/**
* 为用户顾雍初始化基于itemCF的推荐结果
* recom:guyong itemCF 12121212,12121212,12121212,12121212,
* recom:guyong userCF 12121212,12121212,12121212,12121212
*/
@Test
public void initBaseUserRecommend() {
ShardedJedis jedis = MyShardedJedisPool.getResource();
List<Product> productList = JDProduct.getProduct();
// productList = productList.subList(0, 20);
String user = "guyong";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < productList.size(); i++) {
sb.append(productList.get(i).getId());
if (i != productList.size() - 1) {
sb.append(",");
}
}
jedis.hset("recom:" + user, "userCF", sb.toString());
System.out.println("为用户guyong设置基于用户的推荐产品" + productList.size());
}
/**
* 保存物品与物品之间的相似度
* recom:baseItem:productId 121212121:4.2,34343434:3.4
*/
@Test
public void initBaseItemResult() {
ShardedJedis jedis = MyShardedJedisPool.getResource();
List<Product> productList = JDProduct.getProduct();
int num = productList.size();
for (int i = 0; i < num; i++) {
Product product = productList.get(i);
StringBuffer sb = new StringBuffer();
for (int j = 0; j < 10; j++) {
sb.append(productList.get(new Random().nextInt(productList.size())).getId());
sb.append(":" + (new Random().nextInt(6) + 1));
if (j != 9) {
sb.append(",");
}
}
jedis.set("recom:baseItem:" + product.getId(), sb.toString());
}
System.out.println("基于物品:初始化物品与物品之间的相似度关系 " + productList.size());
}
/**
* 保存物品与物品之间的相似度
* recom:baseContent:productId 121212121:4.2,34343434:3.4
*
* 商品属性是否相同
*/
@Test
public void initBaseContentResult() {
ShardedJedis jedis = MyShardedJedisPool.getResource();
List<Product> productList = JDProduct.getProduct();
int num = productList.size();
for (int i = 0; i < num; i++) {
Product product = productList.get(i);
StringBuffer sb = new StringBuffer();
for (int j = 0; j < 10; j++) {
sb.append(productList.get(new Random().nextInt(productList.size())).getId());
sb.append(":" + (new Random().nextInt(6) + 1));
if (j != 9) {
sb.append(",");
}
}
jedis.set("recom:baseContent:" + product.getId(), sb.toString());
}
System.out.println("基于内容:初始化物品与物品之间的相似度关系" + productList.size());
}
public void initUser() {
//todo
}
/**
* 初始化商品信息
* recom:prod:productid json,product对象转化的
*/
@Test
public void initProduct() {
ShardedJedis jedis = MyShardedJedisPool.getResource();
List<Product> productList = JDProduct.getProduct();
for (Product product : productList) {
String skuid = product.getId();
jedis.set("recom:prod:" + skuid, new Gson().toJson(product));
}
System.out.println("初始化商品信息." + productList.size());
}
}
网络爬去需要的数据,与推荐商品对应。
代码如下
/**
* Describe: 请补充类描述
*/
public class JDProduct {
public static void main(String[] args) throws Exception {
System.out.println(getProduct());
}
public static List<Product> getProduct() {
//String url = "https://sale.jd.com/act/kmdO8ah57uswxKT2.html";
String url = "https://sale.jd.com/act/iklG3xhS1LRcQqNJ.html";
List<Product> list = getProductsByUrl(url);
return list;
}
private static List<Product> getProductsByUrl(String url) {
List<Product> list = new ArrayList<Product>();
try {
Document doucument = Jsoup.connect(url).get();
Elements elements = doucument.getElementsByClass("jItem");
for (Element element : elements) {
String price = element.select("span.jdNum").text();
price = price.replaceAll(" ", "");
if (StringUtils.isBlank(price)) {
price = new Random().nextInt(1000) + "";
}
String title = element.select("div[title].jDesc").attr("title");
String producturl = element.select("div[title].jDesc > a").attr("href");
String skuid = null;
if (StringUtils.isNotBlank(producturl)) {
Pattern pattern = Pattern.compile("\\d+");
Matcher matcher = pattern.matcher(producturl);
StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
buffer.append(matcher.group());
skuid = buffer.toString();
}
}
String pic = element.select("img[original]").attr("original");
if (StringUtils.isNotBlank(skuid) &&
StringUtils.isNotBlank(title) &&
StringUtils.isNotBlank(price) &&
StringUtils.isNotBlank(pic) &&
StringUtils.isNotBlank(producturl)) {
list.add(new Product(skuid, title, price, producturl, pic));
}
}
} catch (Exception e) {
}
return list;
}
}
运行数据推荐的类,这个类的内部调用较为复杂,需要下载源码梳理,这里中展示运行类的代码
public class RecommendMain {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring/applicationContext-service.xml");
RecommendService recommendService = (RecommendService) ctx.getBean("recommendServiceImpl");
long start = System.currentTimeMillis();
//根据用户guyong的浏览记录,为广告位121,推荐24个商品
// cookie http协议
List<Product> list = recommendService.recomend("121", "guyong", "5265635,7564497,1039798");
for (Product product : list) {
System.out.println(product);
}
System.out.println("推荐耗时:" + (System.currentTimeMillis() - start));
}
}
web项目,用于异步调用推荐的商品
//异步调用的Javascript的代码
<script type="text/javascript">
var cpro_id = "121";
</script>
<script type="text/javascript" src="http://ad.itcast.cn/js/b.js"></script>
启动项目,会展示如下的结果
加载获取的结果如下
id 1739475
name 我的推荐,你的选择
price 199999
img http://img13.360buyimg.com/n6/s488x350_jfs/t2476/214/1387387908/47235/648d8471/5653ca40N964e7ee4.jpg
url http://item.jd.com/1739475.html
status 1
1 {…}
id 7564497
name 威露士多效洗衣液瓶装套装13.08斤(3kg洗衣液x2、300g内衣净x1,60ml消毒液x4)
price 420
img //img12.360buyimg.com/n7/jfs/t18325/89/2493469623/374092/14956afe/5af4f576Nad5f82a9.jpg
url //item.jd.com/7564497.html
status 1
2 {…}
id 3009211
name 妈妈壹选天然餐具净组合套装(餐具净1kgx4+餐具净500gx4+柔顺剂50gx4) 洗洁精
price 329
img //img12.360buyimg.com/n7/jfs/t15895/77/935366215/594994/db0efff5/5a44af1dNc5b9d65d.jpg
url //item.jd.com/3009211.html
status 1
3 {…}
id 1040870
name 威露士(Walch) 衣物家居消毒液 3L 家居衣物除菌液
price 383
img //img12.360buyimg.com/n7/jfs/t5326/335/2442088867/75285/d768d6af/591ad13fN772df84b.jpg
url //item.jd.com/1040870.html
status 1