Lucene实现索引和查询的实例讲解

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所示,本文抛出外部接口以及信息来源,重点对网页爬取的文本内容进行索引和查询 。

Lucene实现索引和查询的实例讲解_第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所示为成功。

Lucene实现索引和查询的实例讲解_第2张图片

图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("]+>", ""); //剔出的标签
		  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;
 }

执行主代码后结果如图:设计在索引某个文件的时候返回文件“索引文件:+文件路径”,且计算输出索引全部文件花费的时间。

Lucene实现索引和查询的实例讲解_第3张图片

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:索引加权

Lucene实现索引和查询的实例讲解_第4张图片

图4-2:加权之前

Lucene实现索引和查询的实例讲解_第5张图片

图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包含“大”为例。

Lucene实现索引和查询的实例讲解_第6张图片

图5-1:“大”查询结果

5.2 模糊查询

在构造QueryParser时,通过对词项q的修改可以实现精确匹配和模糊匹配。模糊匹配通过在“q”之后加“~”进行修改。如图5-2所示:

Lucene实现索引和查询的实例讲解_第7张图片

Lucene实现索引和查询的实例讲解_第8张图片

图5-2:模糊匹配

5.3 限定条件查询

布尔逻辑查询和模糊查询只需要对查询词q进行更改,而限定条件查询需要对query表达式进行设定,主要分为以下几类:

分别为指定项范围搜索、指定数字范围、指定字符串开头和多条件查询,分别列出应用的查询,true参数指的:是否包含上限和下限在内。

指定项范围:

TermRangeQuery query=new TermRangeQuery("desc", new BytesRef("b".getBytes()), new BytesRef("c".getBytes()), true, true);

指定数字范围:

NumericRangeQuery query=NumericRangeQuery.newIntRange("id", 1, 2, true, true);

指定字符串开头:

PrefixQuery query=new PrefixQuery(new Term("city","a"));

多条件查询:

NumericRangeQueryquery1=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所示,会对南京匹配词添加标签,显示到网页上为加粗和变红。

QueryScorer scorer=new QueryScorer(query);
Fragmenter fragmenter=new SimpleSpanFragmenter(scorer);
SimpleHTMLFormatter simpleHTMLFormatter=new SimpleHTMLFormatter("","");
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 是一个优秀的开源全文本搜索技术框架,通过对它的深入研究,对其实现机制更加熟悉,在研究它的过程中学习了很多面向对象的编程方法和思想,它良好的系统框架和扩展性值得学习借鉴。

你可能感兴趣的:(Lucene实现索引和查询的实例讲解)