创新实训(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;
    }

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

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
自己动手写搜索引擎 1 第1章 了解搜索引擎 1 1.1 Google神话 1 1.2 体验搜索引擎 1 1.3 你也可以做搜索引擎 4 1.4 本章小结 4 第2章 遍历搜索引擎技术 5 2.1 30分钟实现的搜索引擎 5 2.1.1 准备工作环境(10分钟) 5 2.1.2 编写代码(15分钟) 6 2.1.3 发布运行(5分钟) 9 2.2 搜索引擎基本技术 14 2.2.1 网络蜘蛛 14 2.2.2 全文索引结构 14 2.2.3 Lucene 全文检索引擎 15 2.2.4 Nutch网络搜索软件 15 2.2.5 用户界面 17 2.3 商业搜索引擎技术介绍 17 2.3.1 通用搜索 17 2.3.2 垂直搜索 18 2.3.3 站内搜索 19 2.3.4 桌面搜索 21 2.4 本章小结 21 第3章 获得海量数据 22 3.1 自己的网络蜘蛛 22 3.1.1 BerkeleyDB介绍 27 3.1.2 抓取网页 28 3.1.3 MP3 抓取 29 3.1.4 RSS 抓取 30 3.1.5 图片抓取 33 3.1.6 垂直行业抓取 34 3.2 抓取数据库中的内容 36 3.2.1 建立数据视图 36 3.2.2 JDBC数据库连接 36 3.2.3 增量抓取 40 3.3 抓取本地硬盘上的文件 41 3.3.1 目录遍历 41 3.4 本章小结 42 第4章 提取文档中的文本内容 43 4.1 从HTML文件中提取文本 43 4.1.1 HtmlParser介绍 51 4.1.2 结构化信息提取 54 4.1.3 网页去噪 60 4.1.4 网页结构相似计算 63 4.1.5 正文提取的工具FireBug 64 4.1.6 正文提取的工具NekoHTML 66 4.1.7 正文提取 68 4.2 从非HTML文件中提取文本 73 4.2.1 TEXT文件 73 4.2.2 PDF文件 73 4.2.3 Word文件 82 4.2.4 Rtf文件 82 4.2.5 Excel文件 83 4.2.6 PowerPoint文件 84 4.3 流媒体内容提取 85 4.3.1 音频流内容提取 85 4.3.2 视频流内容提取 87 4.4 抓取限制应对方法 89 4.5 本章小结 90 第5章 自然语言处理 91 5.1 中文分词处理 91 5.1.1 Lucene 中的中文分词 91 5.1.2 Lietu中文分词的使用 92 5.1.3 中文分词的原理 92 5.1.4 查找词典算法 95 5.1.5 最大概率分词方法 98 5.1.6 新词发现 101 5.1.7 隐马尔可夫模型 102 5.2 语法解析树 104 5.3 文档排重 105 5.4 中文关键词提取 106 5.4.1 关键词提取的基本方法 106 5.4.2 关键词提取的设计 107 5.4.3 从网页提取关键词 107 5.5 相关搜索 107 5.6 拼写检查 110 5.6.1 英文拼写检查 110 5.6.2 中文拼写检查 112 5.7 自动摘要 116 5.7.1 自动摘要技术 117 5.7.2 自动摘要的设计 117 5.7.3 Lucene中的动态摘要 124 5.8 自动分类 125 5.8.1 Classifier4J 126 5.8.2 自动分类的接口定义 127 5.8.3 自动分类的SVM方法实现 128 5.8.4 多级分类 128 5.9 自动聚类 131 5.9.1 聚类的定义 131 5.9.2 K均值聚类方法 131 5.9.3 K均值实现 133 5.10 拼音转换 138 5.11 语义搜索 139 5.12 跨语言搜索 143 5.13 本章小结 144 第6章 创建索引库 145 6.1 设计索引库结构 146 6.1.1 理解 Lucene 的索引库结构 146 6.1.2 设计一个简单的索引库 148 6.2 创建和维护索引库 149 6.2.1 创建索引库 149 6.2.2 向索引库中添加索引文档 149 6.2.3 删除索引库中的索引文档 151 6.2.4 更新索引库中的索引文档 151 6.2.5 索引的合并 151 6.2.6 索引的定时更新 152 6.2.7 索引的备份和恢复 153 6.2.8 修复索引 154 6.3 读写并发控制 154 6.4 优化使用 Lucene 155 6.4.1 索引优化 155 6.4.2 查询优化 157 6.4.3 实现时间加权排序 162 6.4.4 实现字词混合索引 163 6.4.5 定制Similarity 170 6.4.6 定制Tokenizer 171 6.5 查询大容量索引 173 6.6 本章小结 174 第7章

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值