一. Lucene介绍及下载:
Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一款高性能的、可扩展的,纯java语言编写的一个开放源代码的信息检索(IR)工具库。它适合几乎任何需要全文本搜索(特别是跨平台)的应用程序。但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免费开源工具。就其本身而言,Lucene是当前以及最近几年最受欢迎的免费Java信息检索程序库。人们经常提到信息检索程序库,虽然与搜索引擎有关,但不应该将信息检索程序库与搜索引擎相混淆。
下载地址:http://lucene.apache.org/java
旧版本下载地址:http://archive.apache.org/dist/lucene/java/
二. Lucene jar包及说明
- lucene-core-3.1.0.jar 核心包
- lucene-highlighter-3.1.0.jar 高亮包
- lucene-analyzers-3.1.0.jar Lucene自带的分词解析包
- lucene-queries-3.1.0.jar搜索条件包
- IKAnalyzer3.2.3Stable.jar IK中文分词包
三. Lucene原理
lucene是基于关键词索引来做查询的
1. 全文分析:把文本解析为一个个关键字存储到索引文件中。
假设有两篇文章 A 和 B
A文章内容为:Tom lives in Guangzhou,I live in Guangzhou too
B文章内容为:He once lived in Shanghai.
首先我们要获取这两篇文章的关键字,通常我们需要如下处理措施 :
- a. 现有文章,即一个字符串,我们先要找出字符串中的所有单词( 英文单词由空格分隔,比较好处理 )。中文单词间是连在一起的,需要特殊的分词处理。
- b. 文章中的“in”,“once”,“to”等词无实际意义,中文中的“的”,“是”等字通常也无具体含义,这些不代表概念的词可忽略。
- c. 用户通常希望查询“he”时能把含“He”,“HE”的文章也找出来,所以单词需要统一大小写。
- d. 用户通常希望查询“live”时能把含有“lives”,“lived”的文章也找出来,所以都要还原成“live”。
- e. 文章中的标点符号通常不表示某种概念,也可以过滤。
在lucene中以上措施由Analyzer类完成
经过以上处理之后:
文章A的所有关键词为 [tom] [live][guangzhou] [i] [live] [guangzhou]
文章B的所有关键词为 [he] [live] [shanghai]2. 倒排索引(invertedindex):也常被称为反向索引,置入档案或反向档案,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置映射。它是文档检索系统中常用的数据结构。
有了关键后,我们就可以建立倒序索引了。 上面的对应关系是:“文章号”对“文章中所有关键词”。
倒排索引把这个关系倒过来,变成“关键词”对“拥有该关键词的所有文章号” 文章A,B 经过倒排之后变成:
![]()
通常仅知道关键词在哪些文章中出现还不够,我们还需要知道关键词在文章中出现次数和出现的位置,通常有两种位置
a) 字符位置 – 即记录该词是文章中第几个字符(优点是关键词亮显时定位快)
b) 关键词位 – 即记录该词是文章中第几个关键词(优点是节约索引空间、词组(phase)查询快),
lucene中记录的就是这种位置加上“出现频率”和“出现位置”信息后,我们的索引结构变为:
![]()
以live这行为例我们说明一下该结构:live在文章A中出现了2次,文章B中出现了一次,它的出现位置为“2/5/2”这表示什么呢?我们需要结合文章号和出现频率来分析,文章A中出现了2次,那么“2/5”就表示live在文章1中出现的两个位置,文章B中出现了一次,剩下的“2”就表示live是文章B中第2个关键字。
Lucene索引文件结构介绍 :
- Lucene两种索引结构:多文件索引结构和复合文件索引结构。
- 多文件索引结构由多个文件来表示索引。
- 复合索引:将多个索引文件压缩成一个文件(后缀名为cfs)。
– 怎样创建复合索引 ?
– indexwriter.setUseCompoundFile(true);和版本有关(3.6版本已经不建议这样做了)- 索引端:lucene索引由一个或多个段(segments)组成,而每个段由多个索引文件组成
剖析索引文件:
- 段文件(sements_x)段文件保存了所有现有索引段的名称以及相关信息。每次当IndexWriter向索引提交修改之前,段文件的值都会增加。
- 域名(.fnm): .fnm文件存储了段中相关文档所包含的所有域名。每个域名都要一个标志位。
- 项词典(.tis .tii)段中索引的项(域名和域值构成的元组)都保存在tis文件中。
- 项频率(.frq)记录每个域名出现的次数
- 项位置(.prx)
- 域存储(.fdx .fdt) .fdx文件包含了简单的索引信息,该信息用来将该域对应的文档号保存至.fdt文件中对应的位置。.fdt文件存储该域的内容。
- Norms(.nrm).nrm文件包含了索引期间获取的用户加权信息的归一化因子。每个文档都在.nrm文件中占有一个字节的空间。保存的内容为编码后的文档加权,域加权,基于域内容长度的归一化因子等内容的联合体。
- 文档删除:若删除一个文档,则lucene程序产生一个.del格式的文件。名称为_X_N.del(其中X表示段名称,N是一个整数,每进行一次删除,N的值加1)。该文件还为每个删除后的文档设置了一个标志位。
四. 代码实现分词搜索
pom.xml文件:
<dependencies>
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
</dependencies>
定义一个存储路径和分词器
//Lucene索引文件路径
static String dir="E:\\lucence";
//定义分词器
static Analyzer analyzer = new IKAnalyzer();
写入指定的存储路径
/**
* 把文本解析为一个个关键字存储到索引文件中
*/
public static void write(){
try {
//索引库的存储目录
Directory directory = FSDirectory.open(new File(dir));
//关联当前lucence版本和分值器
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_47, analyzer);
//传入目录和分词器
IndexWriter iwriter = new IndexWriter(directory, config);
//document对象
Document doc=new Document();
//一个field就相当于一个属性
Field A=new Field("A","Tom lives in Guangzhou,I live in Guangzhou too",TextField.TYPE_STORED);
doc.add(A);
Field B=new Field("B","He once lived in Shanghai.",TextField.TYPE_STORED);
doc.add(B);
//写入到目录文件中
iwriter.addDocument(doc);
//提交事务
iwriter.commit();
//关闭流
iwriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
搜索测试方法
/**
* 搜索
*/
public static void search(){
try {
//索引库的存储目录
Directory directory = FSDirectory.open(new File(dir));
//读取索引库的存储目录
DirectoryReader ireader = DirectoryReader.open(directory);
//搜索类
IndexSearcher isearcher = new IndexSearcher(ireader);
//lucence查询解析器,用于指定查询的属性名和分词器
QueryParser parser = new QueryParser(Version.LUCENE_47, "A", analyzer);
//搜索
Query query = parser.parse("live");
//获取搜索的结果,指定返回document返回的个数
ScoreDoc[] hits = isearcher.search(query, null, 5).scoreDocs;
//遍历,输出
for (int i = 0; i < hits.length; i++) {
Document hitDoc = isearcher.doc(hits[i].doc);
System.out.println(hitDoc.getField("A").stringValue());
}
ireader.close();
directory.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
调用write()写入到文件路径,调用search()查看搜索结果
public static void main(String[] args) {
write();
search();
}