0引言
随着万维网的发展和大数据时代的到来,每天都有大量的数字化信息在生产、存储、传递和转化,如何从大量的信息中以一定的方式找到满足自己需求的信息,使之有序化并加以利用成为一大难题。全文检索技术是现如今最普遍的信息查询应用,生活中利用搜索引擎,在博客论坛中查找信息,这些搜索的核心原理就是本文要实现的全文检索技术。随着文档信息数字化的实现,将信息有效存储并及时准确的提取是每一个公司、企业和单位要做好的基础。针对英文的全文检索已经有很多成熟的理论和方法,开放源代码的全文检索引擎lucene 是apache 软件基金会jakarta 项目组的一个子项目,它的目的是为软件开发人员提供一个简单易用的工具包,方便在目标系统中实现全文检索的功能。lucene不支持中文,但是目前已有很多开源的中文分词器可以对中文内容进行索引,本文在研究lucene核心原理的基础上,分别实现了对中英文网页的爬取和检索。
1 lucene介绍
1.1 lucene简介
lucene是一个用java写的全文检索引擎工具包,实现构造了索引和搜索两大核心功能,并且两者相互独立,这使得开发人员可以方便扩展,lucene提供了丰富的api , 可以与存储在索引中的信息方便的交互。需要说明的是它并不是一个完整的全文检索应用, 而是为应用程序提供索引和搜索功能。即若想让lucene 真正起作用, 还需在其基础上做一些必要的二次开发。
lucene的结构设计与数据库的设计较为相似,但lucene的索引与数据库有着极大的不同。数据库和lucene建立索引都是为了查找方便,但是数据库仅仅针对部分字段进行建立,且需要把数据转化为格式化信息,并予以保存。而全文检索是将全部信息按照一定方式进行索引。两种检索的不同和相似如表1-1所示。
表1-1:数据库检索与lucene检索对比
比较项 |
lucene检索 |
数据库检索 |
数据检索 |
从lucene的索引文件中检出 |
由数据库索引检索记录 |
索引结构 |
document(文档) |
record(记录) |
查询结果 |
hit:满足关系的文档组成 |
查询结果集:包含关键字的记录组成 |
全文检索 |
支持 |
不支持 |
模糊查询 |
支持 |
不支持 |
结果排序 |
设置权重,进行相关性排序 |
不能排序 |
1.2 lucene总体结构
lucene软件包的发布形式是一个jar文件,版本更新较快且版本差距较大,本文使用的是5.3.1的版本,主要使用的子包如表1-2所示。
表1-2:子包和功能
包名 |
功能 |
org .apache.lucene .analysis |
分词 |
org .apache.lucene .document |
对索引管理的文档 |
org .apache.lucene .index |
索引操作,包括增加、删除等 |
org .apache.lucene .queryparser |
查询器,构造检索表达式 |
org .apache.lucene .search |
检索管理 |
org .apache.lucene .store |
数据存储管理 |
org .apache.lucene .util |
公共类 |
1.3 lucene架构设计
lucene功能非常强大,但从根本上来说,主要包括两块:一是从文本内容切分词后索引入库;二是根据查询条件返回结果,即建立索引和进行查询两部分。
如图1-1所示,本文抛出外部接口以及信息来源,重点对网页爬取的文本内容进行索引和查询 。
图1-1:lucene的架构设计
2 jdk的安装和环境变量的配置
1.jdk的下载:
在oracle官网下载符合系统版本的压缩包,网址如下。点击安装,根据提示进行安装,在安装过程中会提示是否安装jre,点击是。
http://www.oracle.com/technetwork/java/javase/downloads/index.html
2.设置环境变量:
(1)右键计算机=》属性=》高级系统设置=》环境变量=》系统变量=》新建=》java_home:安装路径
(2)path中新增=》%java_home%\bin
3.测试是否成功:
开始=》运行=》cmd 回车 在弹出的 dos 窗口内
输入:java -version 会出现版本信息,
输入: javac出现 javac 的用法信息
出现如图2-1所示为成功。
图2-1:cmd命令框测试java配置
3 编写java代码实现对网页内容的获取
因为lucene针对不同语言要使用不同的分词器,英文使用标准分词器,中文选择使用smartcn分词器。在获取网页的时候,先获取网页存为html文件,在html中由于标签 的干扰,会对检索效果产生影响,因此需要对html标签进行剔除,并将文本内容转为txt文件进行保存。中英文除了分词器不同,其他基本一致,因此之后的代码和实验结果演 示会选择任一。本文选取五十篇中文故事和英文故事的网页为例。
具体代码设计如下图:url2html.java将输入网址的网页转存为html文件,html2txt.java文件实现html文档标签的去除,转存为txt文档。具体代码如图3-1和3-2。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public void way(string filepath,string url) throws exception{ file dest = new file(filepath); //建立文件 inputstream is; //接收字节输入流 fileoutputstream fos = new fileoutputstream(dest); //字节输出流 url wangzhi = new url(url); //设定网址url is = wangzhi.openstream(); bufferedinputstream bis = new bufferedinputstream(is); //为字节输入流加缓冲 bufferedoutputstream bos = new bufferedoutputstream(fos); //为字节输出流加缓冲 /* * 对字节进行读取 */ int length; byte[] bytes = new byte[1024*20]; while((length = bis.read(bytes, 0, bytes.length)) != -1){ fos.write(bytes, 0, length); } /* * 关闭缓冲流和输入输出流 */ bos.close(); fos.close(); bis.close(); is.close(); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public string getbody(string val){ string zyf = val.replaceall( "</?[^>]+>" , "" ); //剔出<html>的标签 return zyf; } public void writetxt(string str,string writepath) { file writename = new file(writepath); try { writename.createnewfile(); bufferedwriter out = new bufferedwriter( new filewriter(writename)); out.write(str); out.flush(); out.close(); } catch (ioexception e) { e.printstacktrace(); } } |
以童话故事《笨狼上学》的网页为例,文档路径设为”e:\work \lucene \test \data \html”和”e:\work\lucene\test\data\txt”,在每一次读取网页的时候需要设定的两个参数为文件命名filename和获取目标网址url。新建一个main函数,实现对两个方法的调用。具体实现如图3-3所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public static void main(string[] args) { string filename = "jingdizhi" ; //文件名字 string url = "http://www.51test.net/show/8072125.html" ;//需要爬取的网页url string filepath = "e:\\work\\lucene\\test\\data\\html\\" +filename+ ".html" ; //写出html的文件路径+文件名 string writepath = "e:\\work\\lucene\\test\\data\\txt\\" +filename+ ".txt" ; //写出txt的文件路径+文件名 url2html url2html = new url2html(); try { url2html.way(filepath,url); } catch (exception e) { e.printstacktrace(); } html2txt html2txt = new html2txt(); string read=html2txt.readfile(filepath); //读取html文件 string txt = html2txt.getbody(read); //去除html标签 system.out.println(txt); try { html2txt.writetxt(txt,writepath); } catch (exception e) { e.printstacktrace(); } } |
执行程序后,分别在两个文件夹中建立”笨狼上学.html”和”笨狼上学.txt”。
4 建立索引
索引和查询的基本原理如下:
建立索引:搜索引擎的索引其实就是实现“单词-文档矩阵”的具体数据结构。也是进行全文检索的第一步,lucene提供indexwriter类进行索引的管理,主要包括add()、delete()、update()。还有对权值的设定,通过不同索引权值的设定,可以在搜索的时候根据相关性大小进行返回。
进行搜索:原本的直接搜索是针对文档进行顺序检索,在建立索引之后,可以通过对索引的查找以找到索引词在文档中出现的位置,然后返回索引项所对的文档中的位置和词。lucene提供indexsearcher类进行对文档的检索,检索形式主要分为两类,第一类是term,针对单个词项的检索;第二类是parser,可以自定义构造检索表达式,有较多的检索形式,具体的方法会在之后进行实现的演示。
4.1 实验环境
本pc机采用windows 10x64系统,8g内存,256g固态硬盘。开发环境为myeclipse 10,jdk版本为1.8。在实验过程中,因为部分语法的转变,若干class采用1.6版本实现。
4.2 建立索引
建立索引库就是往索引库添加一条条索引记录,lucene为添加一条索引记录提供了接口,添加索引。
主要用到了“写索引器”、“文档”、“域”这3 个类。要建立索引,首先要构造一个document 文档对象,确定document的各个域,这类似于关系型数据库中表结构的建立,document相当于表中的一个记录行,域相当于一行中的列,在lucene 中针对不同域的属性和数据输出的需求,对域还可以选择不同的索引/存储字段规则,在本实验中,文件名filename、文件路径fullpath和文本内容content作为document 的域。
indexwriter 负责接收新加入的文档,并写入索引库中。在创建“写索引器”indexwriter 时需要指定所使用的语言分析器。建立索引分为两个类别,第一:不加权索引;第二:加权索引。
1
2
3
4
5
6
7
|
public indexer(string indexdir) throws exception{ directory dir=fsdirectory.open(paths.get(indexdir)); analyzer analyzer= new standardanalyzer(); // 标准分词器 //smartchineseanalyzer analyzer = new smartchineseanalyzer(); indexwriterconfig iwc= new indexwriterconfig(analyzer); writer= new indexwriter(dir, iwc); } |
设置索引字段,store表示是否对索引内容存储:filename和fullpath占用内存较少可以进行存储,以方便查询返回。
1
2
3
4
5
6
7
|
private document getdocument(file f) throws exception { document doc= new document(); doc.add( new textfield( "contents" , new filereader(f))); doc.add( new textfield( "filename" , f.getname(),store.yes)); doc.add( new textfield( "fullpath" ,f.getcanonicalpath(),store.yes)); //路径索引 return doc; } |
执行主代码后结果如图:设计在索引某个文件的时候返回文件“索引文件:+文件路径”,且计算输出索引全部文件花费的时间。
4.3 对索引的删除和修改
一般对数据库的操作包括crud(增加、删除、更改、查询),增加就是对索引项的选择和建立,查询作为较为核心的功能会在之后展开论述,这里主要记录一下在删除、更新索引时用到的方法。
删除分为两种类型,包括普通的删除和彻底删除,因为索引的删除影响到整个数据库,而且对于大型的系统而言,删除索引意味着对系统的底层进行更改,耗时耗力而且无法返回,前面索引的时候看到建立索引后生成若干小文件,当进行查找的时候会将各个文件进行合并然后查找。普通删除仅仅是对之前建立的索引做个简单的标记,致使无法进行查找返回。彻底删除则是对索引进行销毁,无法撤销。以删除索引项“id”为1的索引为例:
普通的删除(在合并前删除):
1
2
|
writer.deletedocuments( new term( "id" , "1" )); writer.commit(); |
彻底的删除(在合并后删除):
1
2
3
|
writer.deletedocuments( new term( "id" , "1" )); writer.forcemergedeletes(); // 强制删除 writer.commit(); |
对索引的修改原理比较简单,就是在原有索引的基础上实现覆盖,实现代码跟上文的增加索引一样,在此不多做阐述。
4.4 对索引的加权
lucene默认按照相关度排序,lucene对field提供了一个可以设置的boosting参数,这个参数用来表示记录的重要性,在满足搜索条件是,会优先考虑重要性高的记录,返回结果靠前,如果记录较多,权值低的记录会排到首页之后,因此,对索引的加权操作是影响返回结果满意度的重要因素,在实际设计信息系统的时候,应该有严格的权值计算公式,方便对field权值的更改,更好的满足用户的需求。
例如搜索引擎将点击率高,链入链出的网页给定较高的权重,在返回的时候排到第一页。实现代码如图4-1所示,不加权和加权结果对比如图4-2所示。
1
2
3
4
5
|
textfield field = new textfield( "fullpath" , f.getcanonicalpath(), store.yes); if ( "a great grief.txt" .equals(f.getname())){ field.setboost( 2 .0f); //对文件名为secondry story.txt的fullpath路径加权; } //默认权重为1.0,改为1.2即增加权重。 doc.add(field); |
图4-1:索引加权
图4-2:加权之前
图4-2:加权之后
由图4-2结果可以看出,不加权时,按照字典顺序排列返回,因此first在secondry之前,在对secondry命名的文件路径加权后,返回的时候顺序发生变化,实现对权重的测试。
5 进行查询
lucene 的检索接口主要由queryparser、indexsearcher、hits这3 个类构成,queryparser 是查询解析器,负责解析用户提交的查询关键字,在新建一个解析器时需要指定要解析的域和使用什么语言分析器,这里使用的语言分析器必须与索引库建立时使用的解析器相同,否则查询结果不正确。indexsearcher是索引搜索器,在实例化indexsearcher时需要指定索引库所在的目录,indexsearcher有一个search 方法执行索引的检索,这个方法接受query 作为参数,返回hits,hists 是一系列排好序的查询结果的集合,集合的元素是document。通过document的get 方法可以得到与这个文档对应文件的信息,比如:文件名、文件路径、文件内容等。
5.1 基本查询
如图查询主要有两种方式,但是推荐使用第一种构造queryparser表达式,它可以有灵活的组合方式,包括布尔逻辑表达、模糊匹配等,但是第二种term只能针对词汇查询。
1.构造queryparser查询式:
1
2
|
queryparser parser= new queryparser( "fullpath" , analyzer); query query=parser.parse(q); |
2.对特定项的查询:
1
2
|
term t = new term( "filename" , q); query query = new termquery(t); |
查询结果如图5-1所示:以查询文件名filename包含“大”为例。
图5-1:“大”查询结果
5.2 模糊查询
在构造queryparser时,通过对词项q的修改可以实现精确匹配和模糊匹配。模糊匹配通过在“q”之后加“~”进行修改。如图5-2所示:
图5-2:模糊匹配
5.3 限定条件查询
布尔逻辑查询和模糊查询只需要对查询词q进行更改,而限定条件查询需要对query表达式进行设定,主要分为以下几类:
分别为指定项范围搜索、指定数字范围、指定字符串开头和多条件查询,分别列出应用的查询,true参数指的:是否包含上限和下限在内。
指定项范围:
1
|
termrangequery query= new termrangequery( "desc" , new bytesref( "b" .getbytes()), new bytesref( "c" .getbytes()), true , true ); |
指定数字范围:
1
|
numericrangequery<integer> query=numericrangequery.newintrange( "id" , 1 , 2 , true , true ); |
指定字符串开头:
1
|
prefixquery query= new prefixquery( new term( "city" , "a" )); |
多条件查询:
1
2
3
4
5
|
numericrangequery<integer>query1=numericrangequery.newintrange( "id" , 1 , 2 , true , true ); prefixquery query2= new prefixquery( new term( "city" , "a" )); booleanquery.builder booleanquery= new booleanquery.builder(); booleanquery.add(query1,booleanclause.occur.must); booleanquery.add(query2,booleanclause.occur.must); |
5.4 高亮查询
在百度、谷歌等搜索引擎中,进行查询时,返回的网页包含查询关键字的时候会显示为红色,且进行摘要显示,即对包含关键字的部分内容进行截取并返回。高亮查询即为实现对关键字的样式更改,本实验在myeclipse中进行,返回结果并不会有样式的改变,只会对返回内容的关键字添加html标签,如果显示到网页即产生样式的变化。
高亮的设置代码如图5-3所示,结果如图5-4所示,会对南京匹配词添加<b>和<font>标签,显示到网页上为加粗和变红。
1
2
3
4
5
|
queryscorer scorer= new queryscorer(query); fragmenter fragmenter= new simplespanfragmenter(scorer); simplehtmlformatter simplehtmlformatter= new simplehtmlformatter( "<b><font color='red'>" , "</font></b>" ); highlighter highlighter= new highlighter(simplehtmlformatter, scorer); highlighter.settextfragmenter(fragmenter); |
图5-3:高亮设置
图5-4:高亮显示结果
6 实验过程中遇到的问题和不足
lucene版本更新较快,在jdk版本、eclipse版本和lucene版本之间需要一个良好的衔接,否则会造成很多的不兼容,在调试版本以及jdk1.6和jdk1.8的选择上出现很多困难,比如网页抓取中的append方法在1.8版本已经删除,不能使用。但是对文档路劲的读取fsdirectory.open()则需要jdk1.8才支持。
本实验的不足之处主要表现在:
代码的灵活性较低,在爬取网页的时候需要手工进行,且需要对中文和英文分别进行,应该完善代码使得对网页的语言有个判定,然后自动选择执行不同的分词器。
代码的复用性较低,没有较为合理的分类和方法的构建,为了简便,基本在几个核心代码中进行注释和标记而实现效果,有待改进。
代码的可移植性较低,对网页的爬取使用的是jdk1.6的版本,lucene的实现使用的是jdk1.8的版本,在导出到其他机器上,需要对环境稍加修改和配置,无法实现一键式操作。
7 总结
本文从lucene的原理出发,了解了全文检索的思路和方法,并对常用的功能进行了实验和测试。在实验的过程中,了解了搜索引擎的原理,基于信息检索课程的内容上,有了一个更好的实操体验。lucene 是一个优秀的开源全文本搜索技术框架,通过对它的深入研究,对其实现机制更加熟悉,在研究它的过程中学习了很多面向对象的编程方法和思想,它良好的系统框架和扩展性值得学习借鉴。
原文链接:http://www.cnblogs.com/zhaoyufei/archive/2017/12/22/8087138.html