lucene

来源:互联网 发布:生化危机 剧情 知乎 编辑:程序博客网 时间:2024/06/05 21:55

一、           Introduction

Lucene是apache的一个项目,是一个开源的全文检索引擎工具包,不是完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎。优点是:1,独立于应用平台;2,分块索引,能针对新文件建立小文件索引,提高了速度;3,面向对象的系统架构,易学;4,独立于语言和文件格式的文本分析接口,易扩展;5,一套默认的强大的查询引擎,可以直接使用。

检索技术主要应用于检索数据,根据数据的存在特点,可以分为结构化数据和非结构化数据,结构化数据是具有固定格式和有限长度的数据,如数据库中的数据,非结构化数据指不定长或无固定格式的数据,如文档等。对结构化数据的搜索,如数据库可以用sql,对非结构化数据的搜索,可以使用顺序扫描法和全文检索。顺序检索,相当于一个文档一个文档的打开看,直到扫描完所有文件,在数据量大的情况下,相当慢。全文检索是将非结构化数据中的一部分信息提取出来,重新组织成一定结构,然后搜索这个一定结构的数据,以提高速度,这个提取后的部分就是索引。例如,字典就是这样的全文索引。

全文索引可以应用于数据量大、数据结构不固定的数据查询需求中,比如百度、google等搜索引擎、论坛内搜索、其他网站内搜索。Lucene就是一款全文检索的工具包,可以为应用程序提供api接口去调用,是一套为了实现全文检索的类库。搜索引擎是一个全文检索的系统,是单独的软件。两者是不同的。创建索引的过程很耗时,但是索引一旦建立,可以重复使用,且提高检索速度,是值得创建的。

二、           Lucens实现全文检索的流程

全文检索根据生产和应用,大概分为创建索引和查询索引的过程,创建索引是对原始文档进行索引构建一个索引库,包括获得文档》采集文档》创建文档》索引文档几个过程,获得文档,将原始文档放入内存,创建文档对象,以便使用java分析文档,然后创建索引,将索引和原始文档放入索引库,可以将这个索引库理解为一本字典,创建索引的过程可以理解为编字典的过程;查询索引是用户端的操作,通过搜索界面(一个搜索的接口)》创建查询》执行查询》结果渲染一步步实现需求。

(一)创建索引的过程

1.       获得原始文档

原始文档就是索引和搜索的内容,可以是网页、数据库中的数据或者磁盘上的文件等。一般,网页上的信息是通过爬虫或者蜘蛛,也称为网络机器人,来访问网页,将获取的网页内容存储起来。Lucene当中没有信息采集的类库,需要自己写或者通过开源软件来实现,常见的java爬虫有nutch,jsoup,heritrix等。

2.       创建文档(document)对象

文档对象中包括一个一个的域(field),每个域中存储文档相关的信息内容。文档对象可以一个类的对象,域可以理解为类的属性。Lucene的文档对象有几个特点,一个文档可以有多个域,不同的文档可以有不同的域,同一个文档可有相同的域,同一个文档有相同的域可以理解为一个类有相同的属性,比如一个person类有两个name属性(这在类中是不允许的,但是索引中可以),每个文档都有唯一的编号,即文档id。

3.       分析文档

将原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的词汇单元。比如有一段原始文档的内容,如下:

从小丘西行百二十步。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

……

4.       创建索引

创建索引,就是对所有文档分析得出的语汇单元进行索引,以搜索,这样只搜索被索引的语汇单元从而找到文档。每个语汇单元就是一个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

这种索引结构称为倒排索引结构。就是相比传统的先找文档,然后从文档中找要搜索的内容,这个过程是先找要搜索的内容的的索引,然后根据索引的语汇单元找到文档,看起来是颠倒了传统的查找的过程。

(二)查询索引

也就是搜索的过程,用户输入关键字,从索引中进行搜索的过程。根据关键字搜索索引,然后根据索引找到对应的文档,从而找到需要的内容。

1.       用户查询接口

实际上就是搜索的界面,一般提供关键字输入,搜索完称后显示搜索结果。

2.       创建查询

用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中指定查询要搜索的field文档域、查询关键字等,查询对象生成具体的查询语法。

3.       执行查询

根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表。搜索过程就是在索引上查找域的键对应的值的term,然后根据term找到文档id列表。

4.       渲染结果

以界面将查询结果展示给用户,为了提高用户体验,提供了很多展示的效果,比如关键字高亮,快照等。

三、           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。

四、           编写创建索引类

步骤:

1.       新建java项目,导入相关jar包;

2.       创建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 (Filefile : listFiles) {

         // 3,创建document对象。

         Document document = new Document();

         // 文件名称

         String file_name = file.getName();

         Field fileNameField =new TextField("fileName",file_name, Store.YES);

         // 文件大小

         longfile_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();

   }

 

}

3.       运行测试类,测试。

生成目录文件后,可以使用luke工具查看。

关于field属性的实现类,如下:

Field类

数据类型

Tokenized是否分词

Indexed

是否索引

Stored

是否存储

说明

StringField(FieldName, FieldValue,Store.YES))

字符串

N

Y

YN

这个Field用来构建一个字符串Field,但是不会进行分析,会将整个串存储在索引中,比如(订单号,姓名等)

是否存储在文档中用Store.YES或Store.NO决定

LongField(FieldName, FieldValue,Store.YES)

Long

Y

Y

YN

这个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

YN

如果是一个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(ScoreDocscoreDoc: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(ScoreDocscoreDoc: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>

   <!--用户可以在这里配置自己的扩展字典 -->

   <entrykey="ext_dict">ext.dic;</entry>

  

   <!--用户可以在这里配置自己的扩展停止词字典-->

   <entrykey="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);

   }

1.       删除索引

全删除的例子:

   @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();

   }

2.       修改索引

   @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(newTerm("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(ScoreDocscoreDoc: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的子类查询

1.       查询所有

   @Test

   public voidsearchAllIndex()throws Exception{

      IndexSearcher indexSearcher = getIndexSearcher();

      Query query = new MatchAllDocsQuery();

      printSearcherResult(indexSearcher,query);

      indexSearcher.getIndexReader().close();

   }

2.       精准查询

   @Test

   public voidsearchTermQuery()throwsException{

      IndexSearcher indexSearcher = getIndexSearcher();

      Query query = new TermQuery(new Term("fileName","建文"));

      printSearcherResult(indexSearcher,query);

      indexSearcher.getIndexReader().close();

   }

3.       根据数值范围查询

   @Test

   public voidsearchByNumericRange()throws Exception{

      IndexSearcher indexSearcher = getIndexSearcher();

      Query query = NumericRangeQuery.newLongRange("fileSize",0L, 7000L,true,false);

      printSearcherResult(indexSearcher,query);

      indexSearcher.getIndexReader().close();

   }

4.       组合查询

   @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包。

1.       带默认的域查询所有

   @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();

   }

2.       带默认域的按照默认域的值为条件查询

   @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();

   }

3.       带默认域按照其他指定的域为条件查询

   @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();

   }

4.       使用查询解析的组合查询

   @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表示不要。

5.       带多个默认域的查询解析

   @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();

   }

 

 

 

原创粉丝点击