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。
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(); }
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所示:
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 时需要指定所使用的语言分析器。建立索引分为两个类别,第一:不加权索引;第二:加权索引。
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占用内存较少可以进行存储,以方便查询返回。
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的索引为例:
普通的删除(在合并前删除):
writer.deleteDocuments(new Term("id","1")); writer.commit();
彻底的删除(在合并后删除):
writer.deleteDocuments(new Term("id","1")); writer.forceMergeDeletes(); // 强制删除 writer.commit();
对索引的修改原理比较简单,就是在原有索引的基础上实现覆盖,实现代码跟上文的增加索引一样,在此不多做阐述。
4.4 对索引的加权
Lucene默认按照相关度排序,Lucene对Field提供了一个可以设置的Boosting参数,这个参数用来表示记录的重要性,在满足搜索条件是,会优先考虑重要性高的记录,返回结果靠前,如果记录较多,权值低的记录会排到首页之后,因此,对索引的加权操作是影响返回结果满意度的重要因素,在实际设计信息系统的时候,应该有严格的权值计算公式,方便对Field权值的更改,更好的满足用户的需求。
例如搜索引擎将点击率高,链入链出的网页给定较高的权重,在返回的时候排到第一页。实现代码如图4-1所示,不加权和加权结果对比如图4-2所示。
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查询式:
QueryParser parser=new QueryParser("fullPath", analyzer); Query query=parser.parse(q);
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参数指的:是否包含上限和下限在内。
指定项范围:
TermRangeQuery query=new TermRangeQuery("desc", new BytesRef("b".getBytes()), new BytesRef("c".getBytes()), true, true);
指定数字范围:
NumericRangeQuery<Integer> query=NumericRangeQuery.newIntRange("id", 1, 2, true, true);
指定字符串开头:
PrefixQuery query=new PrefixQuery(new Term("city","a"));
多条件查询:
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>标签,显示到网页上为加粗和变红。
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 是一个优秀的开源全文本搜索技术框架,通过对它的深入研究,对其实现机制更加熟悉,在研究它的过程中学习了很多面向对象的编程方法和思想,它良好的系统框架和扩展性值得学习借鉴。