lucene
一、 Introduction
Lucene是apache的一个项目,是一个开源的全文检索引擎工具包,不是完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎。优点是:1,独立于应用平台;2,分块索引,能针对新文件建立小文件索引,提高了速度;3,面向对象的系统架构,易学;4,独立于语言和文件格式的文本分析接口,易扩展;5,一套默认的强大的查询引擎,可以直接使用。
检索技术主要应用于检索数据,根据数据的存在特点,可以分为结构化数据和非结构化数据,结构化数据是具有固定格式和有限长度的数据,如数据库中的数据,非结构化数据指不定长或无固定格式的数据,如文档等。对结构化数据的搜索,如数据库可以用sql,对非结构化数据的搜索,可以使用顺序扫描法和全文检索。顺序检索,相当于一个文档一个文档的打开看,直到扫描完所有文件,在数据量大的情况下,相当慢。全文检索是将非结构化数据中的一部分信息提取出来,重新组织成一定结构,然后搜索这个一定结构的数据,以提高速度,这个提取后的部分就是索引。例如,字典就是这样的全文索引。
全文索引可以应用于数据量大、数据结构不固定的数据查询需求中,比如百度、google等搜索引擎、论坛内搜索、其他网站内搜索。Lucene就是一款全文检索的工具包,可以为应用程序提供api接口去调用,是一套为了实现全文检索的类库。搜索引擎是一个全文检索的系统,是单独的软件。两者是不同的。创建索引的过程很耗时,但是索引一旦建立,可以重复使用,且提高检索速度,是值得创建的。
二、 Lucens实现全文检索的流程
全文检索根据生产和应用,大概分为创建索引和查询索引的过程,创建索引是对原始文档进行索引构建一个索引库,包括获得文档》采集文档》创建文档》索引文档几个过程,获得文档,将原始文档放入内存,创建文档对象,以便使用java分析文档,然后创建索引,将索引和原始文档放入索引库,可以将这个索引库理解为一本字典,创建索引的过程可以理解为编字典的过程;查询索引是用户端的操作,通过搜索界面(一个搜索的接口)》创建查询》执行查询》结果渲染一步步实现需求。
(一)创建索引的过程
- 获得原始文档
原始文档就是索引和搜索的内容,可以是网页、数据库中的数据或者磁盘上的文件等。一般,网页上的信息是通过爬虫或者蜘蛛,也称为网络机器人,来访问网页,将获取的网页内容存储起来。Lucene当中没有信息采集的类库,需要自己写或者通过开源软件来实现,常见的java爬虫有nutch,jsoup,heritrix等。
- 创建文档(document)对象
文档对象中包括一个一个的域(field),每个域中存储文档相关的信息内容。文档对象可以一个类的对象,域可以理解为类的属性。Lucene的文档对象有几个特点,一个文档可以有多个域,不同的文档可以有不同的域,同一个文档可有相同的域,同一个文档有相同的域可以理解为一个类有相同的属性,比如一个person类有两个name属性(这在类中是不允许的,但是索引中可以),每个文档都有唯一的编号,即文档id。
- 分析文档
将原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的词汇单元。比如有一段原始文档的内容,如下:
从小丘西行百二十步。It is a good place,隔篁竹,闻水声,如鸣珮环,心乐之。伐竹取道,下见小潭,水尤清冽。全石以为底,近岸,卷石底以出,为坻,为屿,为嵁,为岩。青树翠蔓,蒙络摇缀,参差披拂。
经过分析后得到的词汇单元:小丘、西行、百二十、步、it、place……
分析后,每个单词叫做一个term,不同的域中拆分的相同的单词是不同的term,term中包含两部分,一部分是文档的域名(相当于key),一部分是单词的内容(相当于值)。
比如:
Term file_content 小丘
Term file_ content 西行
Term file_ content 百二十
Term file_ content 步
Term file_ content it
……
- 创建索引
创建索引,就是对所有文档分析得出的语汇单元进行索引,以搜索,这样只搜索被索引的语汇单元从而找到文档。每个语汇单元就是一个term。
比如索引库中:
索引部分
Term file_content 小丘 2 11,12..
Term file_ content 西行 4 13,14,15,17
Term file_ content 百二十
Term file_ content 步
Term file_ content it
上面的2是含有这个索引的文档的个数,后面的11,12是文档的id。
原始文档部分-文档对象
作品1
作品2
作品3
…
这种索引结构称为倒排索引结构。就是相比传统的先找文档,然后从文档中找要搜索的内容,这个过程是先找要搜索的内容的的索引,然后根据索引的语汇单元找到文档,看起来是颠倒了传统的查找的过程。
(二)查询索引
也就是搜索的过程,用户输入关键字,从索引中进行搜索的过程。根据关键字搜索索引,然后根据索引找到对应的文档,从而找到需要的内容。
- 用户查询接口
实际上就是搜索的界面,一般提供关键字输入,搜索完称后显示搜索结果。
- 创建查询
用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中指定查询要搜索的field文档域、查询关键字等,查询对象生成具体的查询语法。
- 执行查询
根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表。搜索过程就是在索引上查找域的键对应的值的term,然后根据term找到文档id列表。
- 渲染结果
以界面将查询结果展示给用户,为了提高用户体验,提供了很多展示的效果,比如关键字高亮,快照等。
三、 Lucene的安装和包结构
从lucene的官网下载,然后安装,注意对于jdk的要求。Lucene4以上版本请使用jdk1.7及以上。
下载后解压,很不同的文件目录中有jar包。
总体上lucene的包结构如下:
包名 | 功能 |
org.apache.lucene.analysis | 语言分析器,主要用于的切词 Lucene提供的分析器实现类在: lucene-analyzers-common-4.10.3.jar |
org.apache.lucene.document | 索引存储时的文档结构管理,类似于关系型数据库的表结构 |
org.apache.lucene.index | 索引管理,包括索引建立、删除等 |
org.apache.lucene.queryParser | 查询分析器,实现查询关键词间的运算,如与、或、非等, 生成查询表达式, |
org.apache.lucene.search | 检索管理,根据查询条件,检索得到结果 |
org.apache.lucene.store | 数据存储管理,包括一些I/O操作 |
org.apache.lucene.util | 公用类 |
开发需要时候用的jar包,core,analysis-common,queryparser,commons-io,junit-4.9。
四、 编写创建索引类
步骤:
新建java项目,导入相关jar包;
创建lucene类;
public class Lucene1 {
// 创建索引
@Test
public voidtestIndex() throws Exception {
// 1,创建一个indexwriter对象。
Directory directory = FSDirectory.open(new File("F:\\LuceneTemp\\index"));
//Directory directory = newRAMDirectory()//保存索引到内存中(内存索引库)
Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer);
IndexWriter indexWriter = new IndexWriter(directory, config);
// 1)指定索引库的存放位置Directory对象
// 2)指定一个分析器,对文档内容进行分析。
// 2,创建field对象,将field添加到document对象中。
File f = new File("F:\\LuceneTemp\\resource");
File[] listFiles = f.listFiles();
for (File file : listFiles) {
// 3,创建document对象。
Document document = new Document();
// 文件名称
String file_name = file.getName();
Field fileNameField = new TextField("fileName", file_name, Store.YES);
// 文件大小
long file_size = FileUtils.sizeOf(file);
Field fileSizeField = new LongField("fileSize", file_size, Store.YES);
// 文件路径
String file_path = file.getPath();
Field filePathField = new StoredField("filePath", file_path);
// 文件内容
String file_content = FileUtils.readFileToString(file);
Field fileContentField = new TextField("fileContent",file_content,Store.NO);
document.add(fileNameField);
document.add(fileSizeField);
document.add(filePathField);
document.add(fileContentField);
// 4,使用indexwriter对象将document对象写入索引库,此过程进行索引创建。并将索引和document对象写入索引库。
indexWriter.addDocument(document);
}
// 5,关闭IndexWriter对象。
indexWriter.close();
}
}
- 运行测试类,测试。
生成目录文件后,可以使用luke工具查看。
关于field属性的实现类,如下:
Field类 | 数据类型 | Tokenized是否分词 | Indexed 是否索引 | Stored 是否存储 | 说明 |
StringField(FieldName, FieldValue,Store.YES)) | 字符串 | N | Y | Y或N | 这个Field用来构建一个字符串Field,但是不会进行分析,会将整个串存储在索引中,比如(订单号,姓名等) 是否存储在文档中用Store.YES或Store.NO决定 |
LongField(FieldName, FieldValue,Store.YES) | Long型 | Y | Y | Y或N | 这个Field用来构建一个Long数字型Field,进行分析和索引,比如(价格) 是否存储在文档中用Store.YES或Store.NO决定 |
StoredField(FieldName, FieldValue) | 重载方法,支持多种类型 | N | N | Y | 这个Field用来构建不同类型Field 不分析,不索引,但要Field存储在文档中 |
TextField(FieldName, FieldValue, Store.NO) 或 TextField(FieldName, reader) | 字符串 或 流 | Y | Y | Y或N | 如果是一个Reader, lucene猜测内容比较多,会采用Unstored的策略. |
五、 编写查询索引方法
例子:
@Test
public voidtestSearch()throwsException{
//1,创建一个Directory对象,就是索引库存放的位置。
Directory directory = FSDirectory.open(new File("F:\\LuceneTemp\\index"));
//2,创建一个indexReader对象,需要指定directory对象
IndexReader indexReader = DirectoryReader.open(directory);
//3,创建一个indexSearch对象,需要指定indexReader对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//4,创建一个termQuery对象,指定查询的域和查询的关键词
Query query = new TermQuery(new Term("fileName","说"));
//5,执行查询
TopDocs topDocs = indexSearcher.search(query,5);
//6,返回查询结果,遍历结果输出
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for(ScoreDoc scoreDoc:scoreDocs){
int doc = scoreDoc.doc;
Document document = indexSearcher.doc(doc);
String fileName = document.get("fileName");
System.out.println(fileName);
String fileContent = document.get("fileContent");
System.out.println(fileContent);
String fileSize = document.get("fileSize");
System.out.println(fileSize);
String filePath = document.get("filePath");
System.out.println(filePath);
System.out.println("...........");
}
indexReader.close();
}
例子2:
使用解析查询的例子
@Test
public voidtestSearchByIKAnalyzer()throws Exception{
//1,创建分词器对象
Analyzer analyzer = new IKAnalyzer();
//2,创建查询解析器对象,第一个参数是默认搜索的域
QueryParser queryParser = new QueryParser("fileContent",analyzer);
//3,创建搜索语法
Query query = queryParser.parse("fileContent:正版");
//4,创建索引目录流对象
Directory directory = FSDirectory.open(new File("F:\\LuceneTemp\\index"));
//5,读取索引对象
IndexReader indexReader = DirectoryReader.open(directory);
//6,创建搜索对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//7,执行搜索,取出符合记录的前10条
TopDocs topDocs = indexSearcher.search(query,100);
//8,返回结果,遍历输出
//6,返回查询结果,遍历结果输出
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for(ScoreDoc scoreDoc:scoreDocs){
int doc = scoreDoc.doc;
Document document = indexSearcher.doc(doc);
String fileName = document.get("fileName");
System.out.println(fileName);
String fileContent = document.get("fileContent");
System.out.println(fileContent);
String fileSize = document.get("fileSize");
System.out.println(fileSize);
String filePath = document.get("filePath");
System.out.println(filePath);
System.out.println("...........");
}
//9,关闭流
indexReader.close();
}
六、 使用支持中文分词的分析器
Lucene原始使用的分词是Analyzer,这是个抽象类,它提供了标准分词器用,对分析文本进行分词、大写转成小写、去除停用词、去除标点符号等操作过程。
中文的词汇语意结构不同于英文,英文一个词就是一个意思,但是中文确是有组合的,这是电脑不能区分的,比如你吃饭了吗,正常的分词应该是你 吃饭 了 吗,但是电脑可能会分成你吃 饭了 吗。所以,有必要使用中文文词器,优化分词。所以需要中文分词器。
Lucene自带的中文分词器StandardAnalyzer不好用,就是按照中文一个字一个字的分,比如,你吃饭了吗,分词后是你 吃 饭 了 吗。这是不满足需要的。
CJKAnalyzer,二分发分词,也是lucene自带的分词器,就是按两个字进行切分。如,你吃饭了吗,分词后是你吃 吃饭 饭了 了吗。这也是不能满足需要的。
Smartchineseanalyzer,不是自带的,是提供给lucene的一个分词器,对中文支持较好,但扩展性查,扩展词库,禁用词库和同义词库等不好处理。
一般使用第三方的中文分词器,比如ik-analyzer,mmseg4j,imdict-chinese-analyzer,ictclas4j,庖丁解牛分词。除了ik analyzer,其他的三方分词器都渐渐的不再更新了。
Ik analyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。支持用户自定义扩展。使用ik analyzer,先导入jar包,然后加载核心配置文件。
测试ik analyzer的例子:
@Test
public voidtestTokenStream() throws Exception {
// 创建一个标准分析器对象
// Analyzer analyzer = newStandardAnalyzer();
// Analyzer analyzer = newCJKAnalyzer();
// Analyzer analyzer = newSmartChineseAnalyzer();
Analyzer analyzer = new IKAnalyzer();
// 获得tokenStream对象
// 第一个参数:域名,可以随便给一个
// 第二个参数:要分析的文本内容
// TokenStream tokenStream =analyzer.tokenStream("test",
// "The SpringFramework provides a comprehensive programming and configuration model.");
TokenStream tokenStream = analyzer.tokenStream("test",
"从小丘西行百二十步。It is a good place,隔篁竹,闻水声,如鸣珮环,心乐之。伐竹取道,下见小潭,水尤清冽。全石以为底,近岸,卷石底以出,为坻,为屿,为嵁,为岩。青树翠蔓,蒙络摇缀,参差披拂。");
// 添加一个引用,可以获得每个关键词
CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
// 添加一个偏移量的引用,记录了关键词的开始位置以及结束位置
OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
// 将指针调整到列表的头部
tokenStream.reset();
// 遍历关键词列表,通过incrementToken方法判断列表是否结束
while (tokenStream.incrementToken()) {
// 关键词的起始位置
System.out.println("start->" + offsetAttribute.startOffset());
// 取关键词
System.out.println(charTermAttribute);
// 结束位置
System.out.println("end->" + offsetAttribute.endOffset());
}
tokenStream.close();
}
Ik analyzer最大的好处就是可以自定义维护,通过IKanalyzer.cfg.xml配置文件,配置扩展字典和扩展停止字典。
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">ext.dic;</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">stopword.dic;</entry>
</properties>
在src下新建ext.dic和stopword.dic文件,分别设置扩展词和停用词。
注意:创建索引和搜索查询的时候使用同一个分词器。
七、 索引库的维护
创建一个公共方法得到索引流,以方便操作
//得到索引流
public IndexWriter getIndexWriter()throws Exception{
// 1,创建一个indexwriter对象。
Directory directory = FSDirectory.open(new File("F:\\LuceneTemp\\index"));
Analyzer analyzer = new IKAnalyzer();
IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer);
return new IndexWriter(directory, config);
}
- 删除索引
全删除的例子:
@Test
public voiddeleteAllIndext()throwsException{
IndexWriter indexWriter = getIndexWriter();
indexWriter.deleteAll();
indexWriter.close();
}
根据条件删除的例子:
@Test
public voiddeleteIndextByQuery()throws Exception{
IndexWriter indexWriter = getIndexWriter();
Query query = new TermQuery(new Term("fileName","说明"));
indexWriter.deleteDocuments(query);
indexWriter.close();
}
修改索引
@Test
public voidupdateIndex()throwsException{
IndexWriter indexWriter = getIndexWriter();
Document doc = new Document();
doc.add(newTextField(“fileName”,”文件修改”,Store.YES));
doc.add(newTextField(“fileContent”,”文件修改内容”,Store.YES));
indexWriter.updateDocument(new Term(“fileName”,”软件”), doc,newIKAnalyzer());
indexWriter.close();
}
八、 索引库的查询详解
封装一个获取indexSearcher对象的公共方法
public IndexSearcher getIndexSearcher()throws Exception{
//1,创建一个Directory对象,就是索引库存放的位置。
Directory directory = FSDirectory.open(new File("F:\\LuceneTemp\\index"));
//2,创建一个indexReader对象,需要指定directory对象
IndexReader indexReader = DirectoryReader.open(directory);
//3,创建一个indexSearch对象,需要指定indexReader对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
return indexSearcher;
}
封装一个打印结果的公共方法
public voidprintSearcherResult(IndexSearcher indexSearcher,Query query)throws Exception{
TopDocs topDocs = indexSearcher.search(query,40);
ScoreDoc[] scoreDocs= topDocs.scoreDocs;
for(ScoreDoc scoreDoc:scoreDocs){
int doc = scoreDoc.doc;
Document document = indexSearcher.doc(doc);
String fileName = document.get("fileName");
System.out.println(fileName);
String fileContent = document.get("fileContent");
System.out.println(fileContent);
String fileSize = document.get("fileSize");
System.out.println(fileSize);
String filePath = document.get("filePath");
System.out.println(filePath);
System.out.println("...........");
}
}
(一)使用query的子类查询
查询所有
@Test
public voidsearchAllIndex()throws Exception{
IndexSearcher indexSearcher = getIndexSearcher();
Query query = new MatchAllDocsQuery();
printSearcherResult(indexSearcher, query);
indexSearcher.getIndexReader().close();
}
精准查询
@Test
public voidsearchTermQuery()throwsException{
IndexSearcher indexSearcher = getIndexSearcher();
Query query = new TermQuery(new Term(“fileName”,”建文”));
printSearcherResult(indexSearcher, query);
indexSearcher.getIndexReader().close();
}
根据数值范围查询
@Test
public voidsearchByNumericRange()throws Exception{
IndexSearcher indexSearcher = getIndexSearcher();
Query query = NumericRangeQuery.newLongRange(“fileSize”,0L, 7000L, true,false);
printSearcherResult(indexSearcher, query);
indexSearcher.getIndexReader().close();
}
组合查询
@Test
public voidsearchByBooleanQuery()throws Exception{
IndexSearcher indexSearcher = getIndexSearcher();
BooleanQuery query= newBooleanQuery();
Query query1 = new TermQuery(new Term(“fileName”,”新建”));
Query query2 = new TermQuery(new Term(“fileName”,”46”));
query.add(query1,Occur.MUST);
query.add(query2, Occur.MUST_NOT);
printSearcherResult(indexSearcher, query);
indexSearcher.getIndexReader().close();
}
(二)使用queryparser查询
使用queryparser解析查询和使用query的子查询可以得到相同的效果,不同的是,queryparser使用的词汇语法为查询的判断条件。
使用queryparser查询,注意导入lucene-queryparser-x.x.x的jar包。
带默认的域查询所有
@Test
public voidsearchByQueryParser() throws Exception {
IndexSearcher indexSearcher = getIndexSearcher();
Analyzer analyzer = new IKAnalyzer();
//参数1默认查询的域
QueryParser queryparser= newQueryParser(“fileName”, analyzer);
Query query = queryparser.parse(“:“);
printSearcherResult(indexSearcher, query);
indexSearcher.getIndexReader().close();
}
带默认域的按照默认域的值为条件查询
@Test
public voidsearchByQueryParser2()throwsException {
IndexSearcher indexSearcher = getIndexSearcher();
Analyzer analyzer = new IKAnalyzer();
//参数1默认查询的域
QueryParser queryparser = new QueryParser(“fileName”, analyzer);
Query query = queryparser.parse(“账户软件新建”);
//分词器会将上述一句话分词后查询
printSearcherResult(indexSearcher, query);
indexSearcher.getIndexReader().close();
}
带默认域按照其他指定的域为条件查询
@Test
public voidsearchByQueryParser3() throws Exception {
IndexSearcher indexSearcher = getIndexSearcher();
Analyzer analyzer = new IKAnalyzer();
//参数1默认查询的域
QueryParser queryparser = new QueryParser(“fileName”, analyzer);
Query query = queryparser.parse(“fileContect:软件”);
printSearcherResult(indexSearcher, query);
indexSearcher.getIndexReader().close();
}
使用查询解析的组合查询
@Test
public voidsearchByQueryParser4()throwsException {
IndexSearcher indexSearcher = getIndexSearcher();
Analyzer analyzer = new IKAnalyzer();
//参数1默认查询的域
QueryParser queryparser = new QueryParser(“fileName”, analyzer);
//组合查询语法中+表示必需,-表示必需不要,没有符号表示should。
Query query = queryparser.parse(“+fileName:新建 -fileName:账户”);
// Query query = queryparser.parse(“fileName:新建 AND fileName:账户”);
printSearcherResult(indexSearcher, query);
indexSearcher.getIndexReader().close();
}
组合条件的语法:
+域的键:域的值 -域的键:域的值
+表示必需满足,-表示必需不要满足,没有符号表示都可以。
还可以使用OR AND NOT表示。比如:域的键:域的值 OR 域的键:域的值。Or表示或,and表示且,not表示不要。
带多个默认域的查询解析
@Test
public voidsearchByQueryParser5() throws Exception {
IndexSearcher indexSearcher = getIndexSearcher();
Analyzer analyzer = new IKAnalyzer();
//参数1默认查询的域
String[] fields = {“fileName”,”fileContect”};
MultiFieldQueryParser queryparser = new MultiFieldQueryParser(fields, analyzer);
//组合查询语法中+表示必需,-表示必需不要,没有符号表示should。
Query query = queryparser.parse(“+fileName:新建 -fileName:账户”);
printSearcherResult(indexSearcher, query);
indexSearcher.getIndexReader().close();
}
还没有评论,来说两句吧...