后台可以记录每篇博客的阅读次数,直接简单粗暴的推荐比较流行的博客即可,可能热门的博客会被很多人所喜欢。
问题:可能产生长尾效应,阅读数多的博客可能就有限的几篇,而大量的博客无人问津。
解决方案:将阅读次数与新鲜度结合,找到一个比较适合的排名方式,一篇刚发布的博客,有较大的新鲜度,可以将其排到前面。
算法的设计
(1)在抽取博客时,会抽取到文章发布的时间,我们可以将这个发布时间转化为时间戳,这个时间戳就可以作为一篇文章基础的评分,时间戳是随着时间不断增加的,新发布的博客会有比较大的时间戳,这样就保证了新来的博客的分值比较大。
(2)在时间戳的基础上,我们可以引入点击量,作为博客评分的另一个部分,我们给每一个点击量增加一个权重K,当博客被点击一次后,评分会+K(后期可以引入收藏,多一个收藏,评分还可以继续增加)。这样可以保证热度比较高的博客的分数也比较高。
(3)为了提高系统的响应时间,我考虑使用了redis的zset来进行数据的缓存,zset是一个根据score进行排序的集合,我们正好可以根据文章的评分让zset对博客的顺序自动的进行排序,我们取数据时,是需要顺序读取即可。
具体zset的操作
当每篇文章进入系统时,根据时间戳算出他的基础分数,加入zset中,当用户点击文章时,修改这篇文章的分数,将分数加K,当删除一篇文章时,将这篇文章从zset中删除
(4)当需要获取博客列表时,只需要根据range顺序的从redis中取数据即可,zset提供了方法可以取到排名从start到end的博客数据,所以很简单的就可以实现博客的分页查询。
具体实现
- 配置redis环境,在springboot项目中实现对redis的存取。
配置文件:
spring:
redis:
host: r-bp1943b4a5673ef4pd.redis.rds.aliyuncs.com
port: 6379
password: xxxx
有关redis存取方法的实现:
spring data for redis 提供了RedisTemplate类,实现对redis数据库的快捷存取
/**
* redis zset添加元素 如果元素存在则会用新的score替换原来的
* @param key
* @param value
* @param score
* @return
*/
public boolean zsetAdd(String key,String value,double score)
{
boolean result = false;
try {
redisTemplate.opsForZSet().add(key, value,score);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 删除元素 zset
*
* @param key
* @param value
*/
public void zsetRemove(String key, String value) {
redisTemplate.opsForZSet().remove(key, value);
}
/**
* 值增加score
* @param key
* @param value
* @param score
* @return
*/
public Double zsetIncrScore(String key, String value, double score) {
return redisTemplate.opsForZSet().incrementScore(key, value, score);
}
/**
* 获取value对应的score
* 这个需要注意的是,当value在集合中时,返回其score;如果不在,则返回null
* @param key
* @param value
* @return
*/
public Double score(String key, String value) {
return redisTemplate.opsForZSet().score(key, value);
}
/**
* 查询集合中指定顺序的值 zrevrange 返回有序的集合中,score大的在前面
* @param key
* @param start
* @param end
* @return
*/
public Set<String> zsetRevRange(String key, int start, int end) {
return redisTemplate.opsForZSet().reverseRange(key, start, end);
}
2.推荐算法的设计
具体的解释都在代码里完整的注释了出来
@Service
public class FreshPopularRecommended {
@Resource
private RedisUtils redisUtils;
@Resource
private ArticleDao articleDao;
//一次点击率可以增加的得分
private static int K = 2000;
private static String ZSET_KEY = "article";
/**
* 根据流行度和新鲜度获取博客的列表 注意返回值为articleId组成的list
* @param start 开始位置
* @param end 结束位置 注意与数据库的limit offset不同
* @return
*/
public List<Integer> getArticleByFreshPopular(int start,int end)
{
Set<String> a = redisUtils.zsetRevRange(ZSET_KEY,start,end);
List<Integer> result = new ArrayList<>();
for(String str : a)
{
result.add(Integer.valueOf(str));
}
return result;
}
/**
* 结合博客更新时间的时间戳和博客的点击量给 给每一个博客一个分数
* key为固定的 article value为每个文章的id score为文章的得分
* @param article 新插入的博客
* @throws ParseException
*/
public void setScore(Article article) throws ParseException {
String updateTime = article.getUpdateTime();
int ratingCount = article.getRatingCount();
SimpleDateFormat formatter = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss");
Date date = formatter.parse(updateTime);
//updateTime的时间戳 时间戳是得分的基础
double timeStamp = date.getTime();
//给某个article设置一个得分
redisUtils.zsetAdd(ZSET_KEY,String.valueOf(article.getId()),timeStamp + ratingCount *K);
}
/**
*当一个博客被点击后 点击量会+1 得分会增加K
* @param articleId
*/
public void addScore(int articleId)
{
redisUtils.zsetIncrScore(ZSET_KEY,String.valueOf(articleId),K);
}
/**
* 当一个文章 被删除时 需要在redis删除它对应的分数
* @param articleId
*/
public void deleteArticle(int articleId)
{
redisUtils.zsetRemove(ZSET_KEY,String.valueOf(articleId));
}
}
getArticleByFreshPopular方法可以根据范围,出去博客的id,最终返回到界面上。
3.给负责后端的同学一个方便调用的接口
他是需要调用这个方法,输入参数,就可以返回对应的articleId的列表了
/**
* 根据流行度和新鲜度获取博客的列表 可以进行分页查询(注意与 数据库的limit和offset不同 需要转化一下)
* @param start 开始位置 start = offset
* @param end 结束位置 end = offset+limit
* @return 返回值为articleId组成的list
*/
public List<Integer> recommendArticleByFreshPopular(int start,int end)
{
return freshPopularRecommended.getArticleByFreshPopular(start,end);
}
总结
基于流行度和新鲜度的推荐是比较基础的显示方法,所有的用户看到的内容都是相同的,接下来可能会个性化推荐的内容