luncene
Lucene是apache软件基金会jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,即它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。
Lucene的原作者是Doug Cutting,他是一位资深全文索引/检索专家,曾经是V-Twin搜索引擎的主要开发者,后在Excite担任高级系统架构设计师,目前从事于一些Internet底层架构的研究。早先发布在作者自己的http://www.lucene.com/,后来发布在SourceForge,2001年年底成为apache软件基金会jakarta的一个子项目:http://jakarta.apache.org/lucene/。
1. 初始 luncene
1.1 luncene是什么
Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供。那么什么是全文检索和搜寻呢?
简单的说,搜索就是搜寻、查找,在IT行业中就是指用户输入关键字,通过相应的算法,查询并返回用户所需要的信息。比如我们在百度输入 “你好是什么” 会给我们反馈许多你好的信息。 但是 luncene 像是百度的搜索,但是又不是百度的搜索。 不能把他俩混为一谈。
全文检索 : 计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。
1.2 分词器
第一次听到这个概念的时候你可能有点懵,什么是分词器? 分词器能干什么呢 ?
分词器 :是把一段文本中的词按一定规则进行切分。
小朋友,你是否有许多的问号?
什么是把一段文本中的词按一定规则进行切分。
emmmmm…
在我们上小学的时候 , 常常有这样一道题 。 连词成句 ,请把下面几个词组成一句话。分词器呢,就是把句子分成词。 例如:“什么是全文搜索” 这句话,我们可以把他分成 “什么” 、“是” 、“全文搜索" 或者 ”什么“ 、 ”是" 、“全文” 、“搜索” 等等, 分词的不同就是规则不同 如 “什么是全文搜索” 这个例子 用了两种规则。
分词的规则会影响到搜索的结果,例如上面的 “什么是全文搜索” 按照第二种分词的话,可能会出现 “…的全文” 的搜索结果。
luncene 中自带了许多的分词器,但是他对中文的支持不是很好!!! 但是大佬们已经写好了许多强大的中文分词器,例如 HanLP 、 IKAnalyzer 、结巴 、Anjs 等等。
1.3 搭建 Luncene 环境
-
新建 Maven 项目
-
引入Maven坐标
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- lucene核心库 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>${lunece.version}</version>
</dependency>
<!-- Lucene的查询解析器 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>${lunece.version}</version>
</dependency>
<!-- lucene的默认分词器库 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>${lunece.version}</version>
</dependency>
<!-- lucene的高亮显示 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>${lunece.version}</version>
</dependency>
<!-- IK分词器 -->
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
2 . luncene 的使用
2.1 主要类库说明
首先创建索引用到的类所在的包;
- org.apache.lucene.document : 索引存储时的文档结构管理,类似于关系型数据库的表结构。
- org.apache.lucene.index : 索引管理,包括索引建立、删除等
- org.apache.lucene.analysis : 语言分析器,主要用于的切词,支持中文主要是扩展此类
- org.apache.lucene.queryParser : 查询分析器,实现查询关键词间的运算,如与、或、非等
- org.apache.lucene.search : 检索管理,根据查询条件,检索得到结果
- org.apache.lucene.store : 数据存储管理,主要包括一些底层的I/O操作
- org.apache.lucene.util :一些公用类
2.2 创建索引库
我们引入完成 maven 的坐标后,就可以创建 luncene索引库了 。先上代码
创建索引库
/**
* @Desc 简单的创建创建索引库
* @param library 创建索引库的位置
* @return 是否创建成功
*/
public boolean create(String library) {
try {
//索引目录类,指定索引在硬盘中的位置
Directory directory = FSDirectory.open(new File(library));
//创建分词器
Analyzer analyzer = new StandardAnalyzer();
//索引配置对象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
//创建文档对象 用户来存储索引的数据
Document document = new Document();
// 创建并添加字段信息。参数:字段的名称、字段的值、是否存储,这里选Store.YES代表存储到文档列表。Store.NO代表不存储
document.add(new StringField("id", "1", Field.Store.YES));
// TextField,即创建索引又会被分词。
document.add(new TextField("project", "Hello,Word!", Field.Store.YES));
//创建索引的写出工具类。参数:索引的目录和配置信息
IndexWriter indexWriter = new IndexWriter(directory, conf);
//把文档交给IndexWriter
indexWriter.addDocument(document);
//提交
indexWriter.commit();
//关闭
indexWriter.close();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
测试
package cn.guoke.lunceneCreate;
import org.apache.lucene.queryparser.classic.ParseException;
import org.hamcrest.Matcher;
import org.junit.Test;
import java.io.IOException;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.*;
/**
* @Desc 测试
*/
public class SetUpTest {
private SetUp setUp = new SetUp();
private String path = "luncene\\index\\e";
/**
* @Desc 测试创建是否成功
*/
@Test
public void createLib(){
boolean b = setUp.create(path);
assertThat(b, is(true));
}
}
执行完成后我们可以在 path
路径下看到创建好的索引库。
2.3 创建索引例子说明
为了对文档进行索引,Lucene 提供了五个基础的类,他们分别是 Document, Field, IndexWriter, Analyzer, Directory。
下面我们分别介绍一下这五个类的用途:
-
Document :
Document 是用来描述文档的,这里的文档可以指一个 HTML 页面,一封电子邮件,或者是一个文本文件。一个 Document 对象由多个 Field 对象组成的。可以把一个 Document 对象想象成数据库中的一个记录,而每个 Field 对象就是记录的一个字段。
方法:
- add(Field field) :添加一个字段(Field)到Document中
- String get(String name) :从文档中获得一个字段对应的文本
- Field getField(String name) :由字段名获得字段值
- Field[] getFields(String name) :由字段名获得字段值的集
-
Field :
Field 对象是用来描述一个文档的某个属性的,比如一封电子邮件的标题和内容可以用两个 Field 对象分别描述。
构造函数 :
Field(String name, String string, boolean store, boolean index, boolean token)
- Indexed:如果字段是Indexed的,表示这个字段是可检索的。
- Stored:如果字段是Stored的,表示这个字段的值可以从检索结果中得到。
- Tokenized:如果一个字段是Tokenized的,表示它是有经过Analyzer转变后成为一个tokens序列,在这个转变过程tokenization中,Analyzer提取出需要进行索引的文本,而剔除一些冗余的词句(例如:a,the,they等,详见org.apache.lucene.analysis.StopAnalyzer.ENGLISH_STOP_WORDS和org.apache.lucene.analysis.standard.StandardAnalyzer(String[] stopWords)的API)。Token是索引时候的基本单元,代表一个被索引的词,例如一个英文单词,或者一个汉字。因此,所有包含中文的文本都必须是Tokenized的
-
Analyzer :
在一个文档被索引之前,首先需要对文档内容进行分词处理,这部分工作就是由 Analyzer 来做的。Analyzer 类是一个抽象类,它有多个实现。针对不同的语言和应用需要选择适合的 Analyzer。Analyzer 把分词后的内容交给 IndexWriter 来建立索引。
方法:
- addDocument(Document doc) :索引添加一个文档
- addIndexes(Directory[] dirs) : 将目录中已存在索引添加到这个索引
- addIndexes(IndexReader[] readers) :将提供的索引添加到这个索引
- optimize() :合并索引并优化
- close() :关闭
-
IndexWriter :
IndexWriter 是 Lucene 用来创建索引的一个核心的类,他的作用是把一个个的 Document 对象加到索引中来
-
Directory :
这个类代表了 Lucene 的索引的存储的位置,这是一个抽象类,它目前有两个实现,第一个是 FSDirectory,它表示一个存储在文件系统中的索引的位置。第二个是 RAMDirectory,它表示一个存储在内存当中的索引的位置。
更具文件创建索引:
代码
public boolean createFileIndex(String source,String library){
try {
//创建索引目录
Directory directory = FSDirectory.open(new File(library));
//实例化分词器
Analyzer analyzer = new StandardAnalyzer();
//加载配置
IndexWriterConfig config = new IndexWriterConfig(Version.LATEST,analyzer);
//实例化写入对象
IndexWriter indexWriter = new IndexWriter(directory, config);
//获取数据源
File f = new File(source);
File[] files = f.listFiles();
for (File file : files) {
//初始化数据document对象
Document document = new Document();
//文件名称
String fileName = file.getName();
//根据文件名 创建域
TextField fileNameField = new TextField("fileName", fileName, Field.Store.YES);
//根据文件路径创建域
StoredField filePathField= new StoredField("filePath", file.getPath());
//根据文件大小创建域
long fileSize = FileUtils.sizeOf(file);
Field fileSizeField = new LongField("fileSize", fileSize, Field.Store.YES);
// 文件内容域
String fileContent = FileUtils.readFileToString(file);
Field fileContentField = new TextField("fileContent", fileContent, Field.Store.YES);
//将四种域加入到document对象中
document.add(fileNameField);
document.add(filePathField);
document.add(fileSizeField);
document.add(fileContentField);
//利用indexWriter将文档对象写入索引库中
indexWriter.addDocument(document);
}
indexWriter.close();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
测试
@Test
public void createFileIndex(){
boolean b = setUp.createFileIndex("luncene\\source\\e", path);
assertThat(b, is(true));
}
2.4 查询说明
luncene 查询的方式是什么呢 ?
我们在创建的时候,都是指定了 Field
,每一个 Field
都是一个不同数据,我们可以把他称之为域 ,但是在一个 document 对象中只有一个域的名字是唯一的,那么疑问就来了,既然是域应该是可以存储多个的,那么他们是怎么存储多个的呢。
在 luncene 中一个 docunmet 对象域的名字是唯一的,但是我们可以创建多个 document 对象,这样一个域的信息就可以存储更多。
ps:这里的域是帮助理解的!!!
例子;
代码
//创建文档对象 用户来存储索引的数据
Document document = new Document();
// 创建并添加字段信息。参数:字段的名称、字段的值、是否存储,这里选Store.YES代表存储到文档列表。Store.NO代表不存储
document.add(new StringField("id", "1", Field.Store.YES));
// TextField,即创建索引又会被分词。
document.add(new TextField("project", "Hello,Word!", Field.Store.YES));
//创建文档对象 用户来存储索引的数据
Document document1 = new Document();
// 创建并添加字段信息。参数:字段的名称、字段的值、是否存储,这里选Store.YES代表存储到文档列表。Store.NO代表不存储
document1.add(new StringField("id", "2", Field.Store.YES));
// TextField,即创建索引又会被分词。
document1.add(new TextField("project", "Hello,世界", Field.Store.YES));
//创建索引的写出工具类。参数:索引的目录和配置信息
IndexWriter indexWriter = new IndexWriter(directory, conf);
//把文档交给IndexWriter
indexWriter.addDocument(document);
indexWriter.addDocument(document1);
如果还是不明白的话,可以想象我们的字典 。
luncene 在查询时要指定域和关键字:
代码
//创建查询解析器,两个参数:默认要查询的字段的名称,分词器
QueryParser parser = new QueryParser("project", new StandardAnalyzer());
// 创建查询对象
Query query = parser.parse("Hello");
结果样式
id: 1
project: Hello,Word!
得分: 0.37158427
id: 2
project: Hello,世界
得分: 0.297267
2.5 查询例子
代码
/**
* @Desc 查询索引
* @param path 路径
* @throws IOException io异常
*/
public void queryIndex(String path) throws IOException, ParseException {
//找到索引库的位置
Directory directory = FSDirectory.open(new File(path));
//读取索引
IndexReader reader = DirectoryReader.open(directory);
//索引搜索
IndexSearcher searcher = new IndexSearcher(reader);
//创建查询解析器,两个参数:默认要查询的字段的名称,分词器
QueryParser parser = new QueryParser("project", new StandardAnalyzer());
// 创建查询对象
Query query = parser.parse("Hello");
// 搜索数据,两个参数:查询条件对象要查询的最大结果条数
// 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。
TopDocs topDocs = searcher.search(query, 10);
System.out.println(topDocs.totalHits);
// 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 取出文档编号
int docID = scoreDoc.doc;
// 根据编号去找文档
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
System.out.println("project: " + doc.get("project"));
// 取出文档得分
System.out.println("得分: " + scoreDoc.score);
}
//关闭
reader.close();
}
测试
@Test
public void queryIndex() throws IOException, ParseException {
setUp.queryIndex(path);
}
结果
2
id: 1
project: Hello,Word!
得分: 0.37158427
id: 2
project: Hello,世界
得分: 0.2972674
2.6 说明
- Query : 这是一个抽象类,他有多个实现,比如TermQuery, BooleanQuery, PrefixQuery. 这个类的目的是把用户输入的查询字符串封装成Lucene能够识别的Query。
- Term :Term是搜索的基本单位,一个Term对象有两个String类型的域组成。生成一个Term对象可以有如下一条语句来完成:Term term = new Term(“fieldName”,”queryWord”); 其中第一个参数代表了要在文档的哪一个Field上进行查找,第二个参数代表了要查询的关键词。
- TermQuery :TermQuery是抽象类Query的一个子类,它同时也是Lucene支持的最为基本的一个查询类。生成一个TermQuery对象由如下语句完成: TermQuery termQuery = new TermQuery(new Term(“fieldName”,”queryWord”)); 它的构造函数只接受一个参数,那就是一个Term对象。
- IndexSearcher :IndexSearcher是用来在建立好的索引上进行搜索的。它只能以只读的方式打开一个索引,所以可以有多个IndexSearcher的实例在一个索引上进行操作。
- Hits :Hits是用来保存搜索的结果的。
使用 TermQuery 查询 这个是对应文件创建索引的
Query query = new TermQuery(new Term("fileName", "2"));
结果
2.txt
3. 添加删除
3.1 添加一个文档到索引文
添加
Document document = new Document();
document.add(new Field("content",textReader));
document.add(new Field("path",textFiles[i].getPath(), Field.Store.YES, Field.Index.ANALYZED_NO_NORMS));
indexWriter.addDocument(document);
//最后不要忘记了关闭
indexWriter.close();
首先第一行创建了类 Document 的一个实例,它由一个或者多个的域(Field)组成。你可以把这个类想象成代表了一个实际的文档,比如一个 HTML 页面,一个 PDF 文档,或者一个文本文件。而类 Document 中的域一般就是实际文档的一些属性。比如对于一个 HTML 页面,它的域可能包括标题,内容,URL 等。我们可以用不同类型的 Field 来控制文档的哪些内容应该索引,哪些内容应该存储。如果想获取更多的关于 Lucene 的域的信息,可以参考 Lucene 的帮助文档。代码的第二行和第三行为文档添加了两个域,每个域包含两个属性,分别是域的名字和域的内容。在我们的例子中两个域的名字分别是"content"和"path"。分别存储了我们需要索引的文本文件的内容和路径。最后一行把准备好的文档添加到了索引当中。
3.2 从索引中删除文档
类IndexReader负责从一个已经存在的索引中删除文档。
File indexDir = new File("C:\\luceneIndex");
IndexReader ir = IndexReader.open(indexDir);
ir.delete(1);
ir.delete(new Term("path","C:\\file_to_index\lucene.txt"));
ir.close();
3.3 恢复已删除文档
恢复删除的文档
File indexDir = new File("C:\\luceneIndex");
IndexReader ir = IndexReader.open(indexDir);
ir.undeleteAll();
ir.close();
3.4 物理上删除文档
删除
File indexDir = new File("C:\\luceneIndex");
Analyzer luceneAnalyzer = new StandardAnalyzer();
IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,false);
indexWriter.optimize();
indexWriter.close();
第三行创建了类 IndexWriter 的一个实例,并且打开了一个已经存在的索引。第 4 行对索引进行清理,清理过程中将把所有标记为删除的文档物理删除。
4. luncene 补充
4.1 lunecne 的优点
-
索引文件格式独立于应用平台。Lucene定义了一套以8位字节为基础的索引文件格式,使得兼容系统或者不同平台的应用能够共享建立的索引文件。
在传统全文检索引擎的倒排索引的基础上,实现了分块索引,能够针对新的文件建立小文件索引,提升索引速度。然后通过与原有索引的合并,达到优化的目的。
-
优秀的面向对象的系统架构,使得对于Lucene扩展的学习难度降低,方便扩充新功能。
-
设计了独立于语言和文件格式的文本分析接口,索引器通过接受Token流完成索引文件的创立,用户扩展新的语言和文件格式,只需要实现文本分析的接口。
-
已经默认实现了一套强大的查询引擎,用户无需自己编写代码即使系统可获得强大的查询能力,Lucene的查询实现中默认实现了布尔操作、模糊查询(Fuzzy Search)、分组查询等等。
4.2 提高索引性能
利用 Lucene,在创建索引的工程中你可以充分利用机器的硬件资源来提高索引的效率。当你需要索引大量的文件时,你会注意到索引过程的瓶颈是在往磁盘上写索引文件的过程中。为了解决这个问题, Lucene 在内存中持有一块缓冲区。但我们如何控制 Lucene 的缓冲区呢?幸运的是,Lucene 的类 IndexWriter 提供了三个参数用来调整缓冲区的大小以及往磁盘上写索引文件的频率。
-
合并因子(mergeFactor)
这个参数决定了在 Lucene 的一个索引块中可以存放多少文档以及把磁盘上的索引块合并成一个大的索引块的频率。比如,如果合并因子的值是 10,那么当内存中的文档数达到 10 的时候所有的文档都必须写到磁盘上的一个新的索引块中。并且,如果磁盘上的索引块的隔数达到 10 的话,这 10 个索引块会被合并成一个新的索引块。这个参数的默认值是 10,如果需要索引的文档数非常多的话这个值将是非常不合适的。对批处理的索引来讲,为这个参数赋一个比较大的值会得到比较好的索引效果。
-
最小合并文档数
这个参数也会影响索引的性能。它决定了内存中的文档数至少达到多少才能将它们写回磁盘。这个参数的默认值是10,如果你有足够的内存,那么将这个值尽量设的比较大一些将会显著的提高索引性能。
-
最大合并文档数
这个参数决定了一个索引块中的最大的文档数。它的默认值是 Integer.MAX_VALUE,将这个参数设置为比较大的值可以提高索引效率和检索速度,由于该参数的默认值是整型的最大值,所以我们一般不需要改动这个参数。
4.3 中文分词器
这个说一下Ik分词器 其他的用发都是相同的。
代码
public static void testAnalyer() throws IOException{
Analyzer analyer = new IKAnalyzer(true);
TokenStream tokenStream = analyer.tokenStream("suibianqide", "This is a boy ,and his name is Jack");
CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
tokenStream.reset();
while (tokenStream.incrementToken()){
System.out.println("关键词开始的位置:"+offsetAttribute.startOffset());
System.out.println("该关键词是:"+charTermAttribute);
System.out.println("关键词结束的位置:"+offsetAttribute.endOffset());
}
目前最好的分词器是 HanLP 功能强大且免费
tream tokenStream = analyer.tokenStream(“suibianqide”, “This is a boy ,and his name is Jack”);
CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
tokenStream.reset();
while (tokenStream.incrementToken()){
System.out.println(“关键词开始的位置:”+offsetAttribute.startOffset());
System.out.println(“该关键词是:”+charTermAttribute);
System.out.println(“关键词结束的位置:”+offsetAttribute.endOffset());
}
目前最好的分词器是 HanLP 功能强大且免费