创新实训(15)——爬坑Lucene搜索引擎工具包(续)

昨天尝试Lucene的各种分词器,未果,之后找了一个有人实现好的分词器,想把它集成到我们的项目中,来试试效果。他用的Lucene版本6.6.5版本
学习的项目地址为:

https://github.com/suxiongwei/lucene

Lucene相关度打分

Lucene是在用户进行检索时实时根据搜索的关键字计算出来的,分两步:

1)计算出词(Term)的权重
2)根据词的权重值,采用空间向量模型算法计算文档相关度得分
这个文档的相关度采用的是TF——IDF,由于lucene已经对所有文本进行分词之后,建立了倒排索引,所以可以计算文档的相关程度

在这里插入图片描述

Lucene索引过程的核心类

(1)IndexWriter
索引过程的核心组件。这个类负责创建新索引或者打开已有索引,以及向索引中添加、删除或更新被索引文档的信息。可以把IndexWriter看作这样一个对象:它为你提供针对索引文件的写入操作,但不能用于读取或搜索索引。IndexWriter需要开辟一定空间来存储索引,该功能可以由Directory完成。

(2)Directory
该类描述了Lucene索引的存放位置。它是一个抽象类,它的子类负责具体指定索引的存储路径。用FSDirectory.open方法来获取真实文件在文件系统的存储路径,然后将它们一次传递给IndexWriter类构造方法。IndexWriter不能直接索引文本,这需要先由Analyzer将文本分割成独立的单词才行。

(3)Analyzer
文本文件在被索引之前,需要经过Analyzer(分析器)处理。Analyzer是由IndexWriter的构造方法来指定的,它负责从被索引文本文件中提取语汇单元,并提出剩下的无用信息。如果被索引内容不是纯文本文件,那就需要先将其转换为文本文档。对于要将Lucene集成到应用程序的开发人员来说,选择什么样Analyzer是程序设计中非常关键的一步。分析器的分析对象为文档,该文档包含一些分离的能被索引的域。

(4)Document
Document对象代表一些域(Field)的集合。文档的域代表文档或者文档相关的一些元数据。元数据(如作者、标题、主题和修改日期等)都作为文档的不同域单独存储并被索引。Document对象的结构比较简单,为一个包含多个Filed对象容器;Field是指包含能被索引的文本内容的类。

(5)Field
索引中的每个文档都包含一个或多个不同命名的域,这些域包含在Field类中。每个域都有一个域名和对应的域值,以及一组选项来精确控制Lucene索引操作各个域值。

Field是文档中的域,包括Field名和Field值两部分,一个文档可以包括多个Field,Document只是Field的一个承载体,Field值即为要索引的内容,也是要搜索的内容。

Lucene对于索引的建立以及相似度的计算

(1)建立索引
这里我将文章的一部分先物化到了本地,然后使用Lucene建立索引

/**
	 * 将数据库中的内容读取到磁盘,并且创建索引
	 * 注意:在这里读取数据库中的数据写入磁盘时要注意,
	 * 应该去批量写入,否则当数据库中的数据过大时,一次性将过多的值读入到内存中,有可能会内存溢出
	 * @throws IOException
	 */
	public void writeFile() throws IOException{
		//获取存储器的数据存储位置目录
		Directory directory = getDirectory();
		//创建一个索引创建的类
		IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_47, analyzer);
		//创建写入数据的流
		IndexWriter iwriter = new IndexWriter(directory, config);
		//从第几页开始 默认从0开始
		int curPage=0;
		//每次写入两条
		int size=2;
		//获取总页数 总页数的算法是总条数除以每次读入的条数
		long totalPage = newsManager.getTotalPage(size);
		//设置每次读取一页,因为在使用了 PagingAndSortingRepository 接口映射,所以他的第一页是0 ,如果是1则我们就需要设置循环条件为 <=
		while(curPage<totalPage){
			//获取某一页的几条数据
			List<News> list = newsManager.getNewsByPageRequest(curPage, size);
			for (News news : list) {
				//将News对象转化为Document对象
				Document doc = getDocumentByNes(news);
				//写入磁盘
				iwriter.addDocument(doc);
			}
			curPage++;
		}
		
		//关闭写入流
		iwriter.close();
	}
	/**
	 *因为Lucene是根据Document存储的,所以要将news对象转换为Document元素,
	 * @param news
	 * @return
	 */
	public Document getDocumentByNes(News news){
		//创建一个存储数据的Document  (类似于java类,也可以理解为行)
		Document doc = new Document();
		//创建该类的属性,也可以理解为列
		Field title = new Field("title",news.getTitle(),TextField.TYPE_STORED);
		doc.add(title);
		//创建该类的属性,也可以理解为列  如果以太多的内容建议只针对前两百个字为创建索引的内容,节省带宽
		String text = news.getContent();
		if(text.length()>200){
			text = text.substring(0, 200);
		}
		Field content = new Field("content",text,TextField.TYPE_STORED);
		doc.add(content);
		return doc;
	}

(2)然后实现搜索功能

/**
	 * 根据传入的值,返回一个查询到的信息集合返回
	 * @throws IOException 
	 * @throws ParseException 
	 * @throws InvalidTokenOffsetsException 
	 */
	public List<News> search(String title) throws IOException, ParseException, InvalidTokenOffsetsException{
		//创建一个目录位置,供读写流读取
		Directory directory = getDirectory();
		//指定读取的流
		DirectoryReader ireader = DirectoryReader.open(directory);
		//根据指定的流搜索的搜索工具
		IndexSearcher isearcher = new IndexSearcher(ireader);
		//构建查询解析器
		QueryParser parser = new QueryParser(Version.LUCENE_47,"title", analyzer);
		//通过解析器将输入的查询内容进行分词,并判断得分,并封装到一个Query对象中
		Query query = parser.parse(title);
		//使用搜索器,将符合query的doc返回到一个ScoreDoc数组中
		ScoreDoc[] hits = isearcher.search(query,1000).scoreDocs;
		//创建高亮代码的前缀和后缀默认是加粗<b><b/>
		SimpleHTMLFormatter htmlFormatter = new SimpleHTMLFormatter("<font color='red'>","</font>");
		
		//高亮分析器,指定分词后的 Query对象
		Highlighter highlighter = new Highlighter(htmlFormatter, new QueryScorer(query));

		List<News> list = new ArrayList<News>();
		for(int i = 0; i < hits.length;i++){
			//获取第i个元素的id
			int id = hits[i].doc;
			//把第i个document取出来
			Document document = isearcher.doc(id);
			//获取title中的值并且获取的Title的长度用于
			String dTitle = document.get("title");
			//将搜寻的结果分词
			TokenStream titleTs = TokenSources.getAnyTokenStream(isearcher.getIndexReader(), id, "title", analyzer); 
			//通过高亮分析器将,把要高亮的部分进行加工
			TextFragment[] titleFrag = highlighter.getBestTextFragments(titleTs,dTitle,false,dTitle.length());
			for (int j = 0;j<titleFrag.length;j++) {
				if ((titleFrag[j] != null) && (titleFrag[j].getScore() > 0)) {
					//获取加入高亮后的值
					dTitle = titleFrag[j].toString();
			     }
			}
			
			//获取content中的值
			String dContent = document.get("content");
			//将搜寻的结果分词
			TokenStream contentTs = TokenSources.getAnyTokenStream(isearcher.getIndexReader(), id, "content", analyzer); 
			//通过高亮分析器将,把要高亮的部分进行加工
			TextFragment[] contentFrag = highlighter.getBestTextFragments(contentTs,dContent,false,dContent.length());
			for (int j = 0;j<contentFrag.length;j++) {
				if ((contentFrag[j] != null) && (contentFrag[j].getScore() > 0)) {
					dContent = contentFrag[j].toString();
			     }
			}
			//将要查询的结果封装成一个对象
			News news = new News(null,dTitle,dContent);
			//将对象添加到list集合中最后返回个页面
			list.add(news);
		}
		return list;
		
	}

传入文章的内容,根据内容搜索相似的文章。

(3)坑的事情发生了,由于我们要根据一篇完整的博客来,用Lucene自带的索引,以及相似度的检索,检索出相似的文章,然后给出文章相似度的排序,这样我们就可以选择相似度前几的文章,来给用户进行推荐了。
结果输入一篇博客,测试之后发现,他返回值只有他自己,而不会返回其他相似的博客。当我们输入一个标题或者一句话时,他就会输出多个相似的内容,但是这不是我想要的效果。我想要的是,输入一篇博客,可以借助Lucene建立倒排索引,然后我根据倒排索引可以计算相应的TF-IDF,最终计算出和他相似的博客,返回一个博客列表。

然后继续寻找资料,发现十年前的Lucene3.x,还没有自带分词器,需要人工手动分词后,Lucene帮助建立倒排索引,然后我们可以遍历到所有的索引内容,来计算TF-IDF,进而计算文本的相似度。

但是发现Lucene的版本割裂太严重了,每个版本存在的类,以及方法都大不相同,基本上换一个版本就必须重写代码,越高的版本的集成度越高,系统自动帮忙分词,建立索引,我们根本没有办法访问索引,只能根据Lucene提供的query解析器,然后搜索到需要的内容。

并且Lucene的版本迭代非常快,一年甚至会有好几次大的更新,往往是旧版本还没有被研究透彻,又出来新版本,而且接口变得大不一样,找到相应的教程太难了。
在这里插入图片描述

这就是Lucene爬坑的全部经过,下篇博客介绍使用老版本的Lucene3.6,终于实现了基于TF-IDF的文本相似度计算,从而计算出相似博客,并且计算速度还比较快。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值