昨天尝试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的文本相似度计算,从而计算出相似博客,并且计算速度还比较快。