创新实训(16)——推荐系统实现之基于Lucene3.6的余弦相似度计算与相似文章推荐

设计思路

(1)使用Lucene3.6.0版本,由于之前尝试使用IK分词器,加载到Lucene中,让Lucene自动分词,然后建立索引,但是IK分词器一直报错,所以我选择自己使用HanLP分词之后,在使用Lucene建立倒排索引。

(2)使用建立好的倒排索引,快速获取所有文档的TF-IDF值
词频(term frequency)TF
单个文章的词频,词在文档中出现的词频 词在文档中出现的频度是多少? 频度越高,权重 越高 。 5 次提到同一词的字段比只提到 1 次的更相关。
逆向文档频率(inverse document frequency)IDF
词在这篇文档中出现过次数/词在所有文章出现的次数 词在集合所有文档里出现的频率是多少?频次越高,权重 越低 。 常用词如 and 或 the 对相关度贡献很少,因为它们在多数文档中都会出现,一些不常见词如 elastic 或 hippopotamus 可以帮助我们快速缩小范围找到感兴趣的文档。

(3)以TF-IDF值作为向量,计算两个文本(两个向量)之间的余弦相似度

(4)对每一个文本,计算出与它最相似的的top10的文本,将其存在Redis中, 可以通过articleId取出与此博客文章相似的其他博客文章的id,然后返回。

说明:由于我们的博客是定时抽取的,所以这一步的分词,建立索引,然后计算所有文本的其他相似文本,将其提前存入Redis中,都在抽取之后进行,系统运行过程中的基于相似度的推荐都是直接根据articleId从Redis中读取数据,所以系统的响应还是很快的。

具体实现

(1)maven的有关Lucene的依赖

   <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-core -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>3.6.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-queryparser -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>3.6.0</version>
        </dependency>

(2)在每一次定时抽取完成以后,读取数据库内容,对每一个文本进行分词后,使用Lucene创建索引,提前物化,便于计算文本相似度

  /**
     * 将文本分词后,用" "隔开
     *
     * @param sentence
     * @return
     */
    public String cutWord(String sentence) {
        List<com.hankcs.hanlp.seg.common.Term> termList = HanLP.segment(sentence);
        List<String> wordList = new ArrayList<>();
        //是否应当将这个term纳入计算,词性属于名词、动词、副词、形容词
        for (com.hankcs.hanlp.seg.common.Term term : termList) {
            if (shouldInclude(term)) {
                wordList.add(term.word);
            }
        }
        String result = String.join(" ", wordList);
        return result;
    }
    /**
     * 读取数据库内容,并且创建索引 提前物化
     *
     * @throws IOException
     */
    public void writeFile() throws IOException {
        //删除文件夹下所有文件
        File file = new File(INDEX_DIR);
        deleteFile(file);
        //获取存储器的数据存储位置目录
        Directory directory = getDirectory();
        //创建一个索引创建的类
        IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_36, new WhitespaceAnalyzer(Version.LUCENE_36));
        //创建写入数据的流
        IndexWriter iwriter = new IndexWriter(directory, config);
        //从第几页开始 默认从0开始
        int curPage = 0;
        //每次写入两条
        int size = 20;
        //数据库中的文章总条数
        articleNumbers = articleService.selectArticleCount();
        int totalPage = articleNumbers / size;
        while (curPage <= totalPage) {
            List<Article> articles = articleService.selectArticleList(size, curPage * size);
            for (Article article : articles) {
                //将article对象转换为Document元素,
                Document doc = getDocumentByArticle(article);
                //写入磁盘
                iwriter.addDocument(doc);
            }
            curPage++;
        }
        //关闭写入流
        iwriter.close();
    }

    /**
     * 因为Lucene是根据Document存储的,所以要将Article对象转换为Document元素,
     *
     * @param article
     * @return
     */
    public Document getDocumentByArticle(Article article) {

        //创建一个存储数据的Document
        Document doc = new Document();
        // 文章id
        // 不分词、索引、存储
        Field articleId = new Field("id", new StringReader(String.valueOf(article.getId())), TermVector.YES);
        doc.add(articleId);
        //创建该类的属性,   分词、索引、存储 TextField
        String verbalContent = cutWord(article.getVerbalContent());
        Field content = new Field("verbalContent", new StringReader(verbalContent), Field.TermVector.YES);
        doc.add(content);
        return doc;
    }

(3)由于我们要将每一篇文章存入redis,这里使用redis的set结构进行存储,key为文章id,value为与key相似的文章id集合,有关set存取方法的实现

 /**
     * 给指定set添加元素 sadd
     * @param key
     * @param value
     */
    public void setAdd(String key, String value) {
        redisTemplate.opsForSet().add(key, value);
    }
    /**
     * 删除指定set中的值  不需要指定位置  srem
     *
     * @param key
     * @param value
     */
    public void setremove(String key, String value) {
        redisTemplate.opsForSet().remove(key, value);
    }

    /**
     * 判断是否包含  sismember
     *
     * @param key
     * @param value
     */
    public void setContains(String key, String value) {
        redisTemplate.opsForSet().isMember(key, value);
    }
    /**
     * 获取集合中所有的值 smembers
     *
     * @param key
     * @return
     */
    public Set<String> setGetValues(String key) {
        return redisTemplate.opsForSet().members(key);
    }

(5)在建立完索引之后,遍历文章列表,使用Lucene建立好的倒排索引,获得文章的TF-IDF值,然后使用TF-IDF计算文章的余弦相似度,然后将每篇文章最相似的其他文章的id存入redis的set中。

 /**
     * 获取所有文档的tf-idf值
     *
     * @return
     * @throws IOException
     * @throws ParseException
     */
    public HashMap<String, Map<String, Float>> getAllTFIDF() throws IOException, ParseException {
        HashMap<String, Map<String, Float>> scoreMap = new HashMap<String, Map<String, Float>>();
        Directory directory = getDirectory();

        IndexReader re = IndexReader.open(directory);
        articleNumbers = articleService.selectArticleCount();
        for (int k = 0; k < articleNumbers; k++) {
            //每一个文档的tf-idf
            Map<String, Float> wordMap = new HashMap<String, Float>();

            //获取当前文档的内容
            TermFreqVector[] a = re.getTermFreqVectors(k);
            String articleId = a[0].getTerms()[0];
//            TermFreqVector articleId = re.getTermFreqVector(k,"id");
            TermFreqVector articleContent = re.getTermFreqVector(k, "verbalContent");
            if (articleContent == null) {
                continue;
            }
            int[] freq = articleContent.getTermFrequencies();

            String[] terms = articleContent.getTerms();
            int noOfTerms = terms.length;
            DefaultSimilarity simi = new DefaultSimilarity();
            for (int i = 0; i < noOfTerms; i++) {
                int noOfDocsContainTerm = re.docFreq(new Term("verbalContent", terms[i]));
                float tf = simi.tf(freq[i]);
                float idf = simi.idf(noOfDocsContainTerm, articleNumbers);
                wordMap.put(terms[i], (tf * idf));
            }
            scoreMap.put(articleId, wordMap);
        }
        return scoreMap;
    }

    /**
     * 获取查找文本的tf-idf
     *
     * @param termList
     * @return
     * @throws IOException
     */
    public Map<String, Float> getSearchTextTfIdf(List<String> termList) throws IOException {
        //统计每一个词,在文档中的数目
        Map<String, Integer> termFreqMap = new HashMap<>();
        for (String term : termList) {
            if (termFreqMap.get(term) == null) {
                termFreqMap.put(term, 1);
                continue;
            }

            termFreqMap.put(term, termFreqMap.get(term) + 1);
        }

        Map<String, Float> scoreMap = new HashMap<String, Float>();
        Directory directory = getDirectory();
        IndexReader re = IndexReader.open(directory);
        DefaultSimilarity simi = new DefaultSimilarity();
        articleNumbers = articleService.selectArticleCount();
        for (String term : termList) {
            int noOfDocsContainTerm = re.docFreq(new Term("verbalContent", term));
            float tf = simi.tf(termFreqMap.get(term));
            float idf = simi.idf(noOfDocsContainTerm, articleNumbers);
            scoreMap.put(term, (tf * idf));
        }

        return scoreMap;
    }

解释:这里在获取文章TF-IDF的时候,使用了Lucene的IndexReader类来对索引进行读取。而这个IndexReader类在新版本Lucene中已经被取消了,所以我只能选用老版本的Lucene3.6来实现我想要的功能

 /**
     * 每次抽取完成,并且建立索引之后,都计算每篇文章10个相似的文章,存入redis,方便取用
     */
    public void init() throws IOException, org.apache.lucene.queryParser.standard.parser.ParseException {
        //从第几页开始 默认从0开始
        int curPage=0;
        //每次写入两条
        int size=20;
        //数据库中的文章总条数
        int articleNumbers = articleDao.selectArticleCount();
        int totalPage = articleNumbers/size;

        while (curPage<=totalPage)
        {
            List<Article> articles = articleDao.selectArticleList(size,curPage*size);
            //计算文章的相似文章 存入redis
            for(Article article :articles)
            {
                int articleId = article.getId();
                List<Integer> similarityArticleList = luceneUtil.searchSimilarity(articleId,SIZE);

                //加入redis的set中
                for (int i :similarityArticleList)
                {
                    System.out.println(i);
                    if(i != articleId)
                    {
                        redisUtils.setAdd(SET_KEY+String.valueOf(articleId) , String.valueOf(i));
                    }

                }
            }
            curPage++;
        }

    }

(6)在进行相似文章推荐时,只需要调用响应的接口,然后从redis中获取数据即可

    /**
     *  寻找与此文章相似的size个文章     通过文本的相似度进行推荐
     * @param articleId   文章id
     * @param size   推荐的相似文章个数
     * @return  返回相似文章的id列表
     * @throws IOException
     */
    public   List<Integer> searchSimilarity(int articleId,int size) throws IOException, ParseException
    {
        Article article = articleService.getArticleById(articleId);
        String searchText = article.getVerbalContent();

        //去除标点符号,特殊字符
        String content = searchText.replaceAll("[\\p{P}+~$`^=|<>~`$^+=|<>¥×]", "");
        content = content.replaceAll("\\t|\\r|\\n","");
        content = content.replaceAll(" ","");

//        JiebaSegmenter segmenter = new JiebaSegmenter();
        List<String> strings =cutWord(content);

        Map<String, Float> searchTextTfIdfMap = luceneManager.getSearchTextTfIdf(strings);
        HashMap<String, Map<String, Float>> allTfIdfMap = luceneManager.getAllTFIDF();

        //利用余弦相似度求出与所有文档的相似值
        Map<String, Double> docSimMap = cosineSimilarity(searchTextTfIdfMap, allTfIdfMap);


        //找出最相似的size个

        List<Map.Entry<String, Double>> list = new ArrayList<Map.Entry<String, Double>>(docSimMap.entrySet());
        Collections.sort(list, new Comparator<Map.Entry<String, Double>>() {
            @Override
            public int compare(Map.Entry<String, Double> o1, Map.Entry<String, Double> o2) {
                if (o2.getValue() > o1.getValue()) { return 1; } else if (o2.getValue() < o1.getValue()) {
                    return -1; } else {
                    return 0; }
            }
        });
        int index = 0;
        List<Integer> result = new ArrayList<>();
        for (Map.Entry<String, Double> t : list) {
            result.add(Integer.valueOf(t.getKey()));
            if (++index > size-1) {
                break;
            }
        }
        return result;
    }

本地索引的结果
在这里插入图片描述
基于文本相似度的推荐,完成!

猜你喜欢

转载自blog.csdn.net/baidu_41871794/article/details/106753130