到目前为止,我实现了基于流行度和新鲜度的推荐算法/基于文本相似度的推荐/用户协同过滤推荐/分类(标签)协同过滤推荐。我们需要将其整合到一个Util中,方便调用。
/**
* 文章推荐的接口
* 可以有很多推荐算法分别实现这个接口,以便于我们后面切换推荐算法或者同时使用多个推荐算法
* 接口里面的基本方法在设计中,还没有定型,可以根据需要或者实际情况修改
* 用户id也许可以用用户名,但是看实际情况,id不会重复
* 返回的文章id会简单一点,不过文章内容也行
* 也可以有推荐文章tag的方法,这种可以给用户推荐关注的tag主题等
*/
@Service
public class RecommendedUtil {
@Resource
private FreshPopularRecommended freshPopularRecommended;
@Resource
private SimilarityRecommended similarityRecommended;
@Resource
private CFRecommended cfRecommended;
@Resource
private ArticleTagDao articleTagDao;
@Resource
private ArticleCategoryDao articleCategoryDao;
/**
* 根据流行度和新鲜度获取博客的列表 可以进行分页查询(注意与 数据库的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);
}
/**
*根据文章id 获取相似文章 返回值为articleId列表 应该显示在文章的详细内容最下面
* @param articleId 当前正在浏览的文章id
* @return 返回结果为文章id列表 有10个 是提前物化好 存在redis当中的
*/
public List<Integer> recommendSimilarityArticle(int articleId)
{
return similarityRecommended.getSimilarityArticle(articleId);
}
}
说明:
基于流行度和新鲜度的推荐与基于文本相似度的推荐,直接调用了前几天写好的方法。
/**
* 基于用户id的个性推荐 基于用户浏览博客文章的标签信息
* 通过标签筛选博客 随机返回 对应标签的博客
* 每次请求此接口 返回值都不同
* @param userId
* @return
*/
public List<Integer> recommendArticleByTag(int userId) throws IOException, TasteException {
//基于用户的协同过滤 推荐得到的标签id
List<RecommendedItem> userBasedList = cfRecommended.userBasedRecommendedWithTag(userId,10);
//基于标签id的协同过滤 推荐得到的标签id
List<RecommendedItem> itemBaseList = cfRecommended.itemBaseRecommendedWithTag(userId,10);
Map<Integer,Double> score = new HashMap<>();
for(RecommendedItem recommendedItem :userBasedList)
{
int itemId = (int) recommendedItem.getItemID();
if(!score.containsKey(itemId))
{
score.put(itemId,0.0);
}
score.put(itemId,score.get(itemId)+recommendedItem.getValue());
}
List<Map.Entry<Integer,Double>> list = new ArrayList<>(score.entrySet());
list.sort((o1, o2) -> o2.getValue().compareTo(o1.getValue()));
int index = 0;
List<Integer> tagIdList = new ArrayList<>();
for (Map.Entry<Integer,Double> t : list) {
tagIdList.add(t.getKey());
if (++index > 10) {
break;
}
}
//根据标签 随机选出20篇文章
List<Integer> articleList = articleTagDao.selectArticleByTagList(userId,tagIdList,20);
return articleList;
}
基于文章标签的推荐:将基于用户的协同过滤算法与基于标签的协同过滤算法进行了整合,我通过这两个不同的方法分别推荐出了10个标签,然后将这20个标签通过评分进行排序,选出了综合评分最高的10个标签。然后通过这10个标签,从数据库中查找符合标签并且用户没有浏览过的博客文章。
sql语句的设计:
/**
* 根据标签列表 从文章中选出满足标签信息 且用户没有看过的文章 随即返回size个
* @param userId
* @param tagIdList 标签列表
* @param size 随即返回size个
* @return
*/
/**
* 根据标签列表 从文章中选出满足标签信息 且用户没有看过的文章 随即返回size个
* @param userId
* @param tagIdList 标签列表
* @param size 随即返回size个
* @return
*/
@Select(" ")
List<Integer> selectArticleByTagList(@Param("userId") int userId ,@Param("tagIdList") List<Integer> tagIdList,@Param("size") int size);
写这个sql语句时,由于要筛选的博客标签是一个列表(推荐出的博客标签的列表),所以用到了mybatis提供的foreach方法进行Arraylist的遍历
"<foreach collection='tagIdList' item='item' open='(' separator=',' close=')'>
#{item} </foreach> ORDER BY RAND() LIMIT #{size}"
遍历所有的tag列表。最终与用户浏览记录表做自然连接,返回相应的数据即可。
foreach的用法: foreach元素的属性主要有item,index,collection,open,separator,close。
item:集合中元素迭代时的别名,该参数为必选。
index:在list和数组中,index是元素的序号,在map中,index是元素的key,该参数可选
open:foreach代码的开始符号,一般是(和close=")“合用。常用在in(),values()时。该参数可选
separator:元素之间的分隔符,例如在in()的时候,separator=”,“会自动在元素中间用“,“隔开,避免手动输入逗号导致sql错误,如in(1,2,)这样。该参数可选。
close: foreach代码的关闭符号,一般是)和open=”("合用。常用在in(),values()时。该参数可选。
collection:
要做foreach的对象,作为入参时,List对象默认用"list"代替作为键,数组对象有"array"代替作为键,Map对象没有默认的键。当然在作为入参时可以使用@Param(“keyName”)来设置键,设置keyName后,list,array将会失效。
除了入参这种情况外,还有一种作为参数对象的某个字段的时候。举个例子:如果User有属性List
ids。入参是User对象,那么这个collection = “ids”.如果User有属性Ids
ids;其中Ids是个对象,Ids有个属性List id;入参是User对象,那么collection = “ids.id”
在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有一下3种情况:如果传入的是单参数且参数类型是一个List的时候,collection属性值为list .
如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array .
如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的,map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key.
思路和上面的类似,就不再叙述了
/**
* 基于用户id的个性推荐 基于用户浏览博客文章的分类信息
* 通过分类筛选博客 随机返回 对应分类的博客
* 每次请求此接口 返回值都不同
* @param userId
* @return
* @throws IOException
* @throws TasteException
*/
public List<Integer> recommendArticleByCategory(int userId) throws IOException, TasteException
{
//基于用户的协同过滤 推荐得到的标签id
List<RecommendedItem> userBasedList = cfRecommended.userBasedRecommendedWithCategory(userId,3);
//基于标签id的协同过滤 推荐得到的标签id
List<RecommendedItem> itemBaseList = cfRecommended.itemBaseRecommendedWithCategory(userId,3);
Map<Integer,Double> score = new HashMap<>();
for(RecommendedItem recommendedItem :userBasedList)
{
int itemId = (int) recommendedItem.getItemID();
if(!score.containsKey(itemId))
{
score.put(itemId,0.0);
}
score.put(itemId,score.get(itemId)+recommendedItem.getValue());
}
List<Map.Entry<Integer,Double>> list = new ArrayList<>(score.entrySet());
list.sort((o1, o2) -> o2.getValue().compareTo(o1.getValue()));
int index = 0;
List<Integer> tagIdList = new ArrayList<>();
for (Map.Entry<Integer,Double> t : list) {
tagIdList.add(t.getKey());
if (++index > 4) {
break;
}
}
//根据标签 随机选出20篇文章
List<Integer> articleList = articleCategoryDao.selectArticleByCategoryList(userId,tagIdList,20);
return articleList;
}
sql语句:
/**
* 根据分类的id列表 从文章中选出满足分类且 用户没有看过的文章 随即返回size个
* @param userId
* @param categoryIdList
* @param size
* @return
*/
/**
* 根据分类的id列表 从文章中选出满足分类且 用户没有看过的文章 随即返回size个
* @param userId
* @param categoryIdList
* @param size
* @return
*/
@Select(" ")
List<Integer> selectArticleByCategoryList(@Param("userId") int userId , @Param("categoryIdList") List<Integer> categoryIdList, @Param("size") int size);
至此,基本的推荐算法就设计完毕了。接下来准备写有关文章的相关接口以及有关elasticsearch实现的检索功能。