Lucene研究

来源:互联网 发布:剑灵捏脸数据图怎么用 编辑:程序博客网 时间:2024/05/01 18:40

Lucene研究之一——起源、现状及初步应用

 

作者:陈光(holen@263.net

时间:2004-08-23

 

本文是Lucene研究文集的首篇,主要介绍了Lucene的起源、发展、现状,以及Luence的初步应用,可以作为了解和学习Lucene的入门资料。

 

1. 起源与发展

 

Lucene是一个高性能、纯Java的全文检索引擎,而且免费、开源。Lucene几乎适合于任何需要全文检索的应用,尤其是跨平台的应用。

 

Lucene的作者Doug Cutting是一个资深的全文检索专家,刚开始,Doug CuttingLucene发表在自己的主页上,20003月将其转移到sourceforge,于200110捐献给Apache,作为Jakarta的一个子工程。

 

2.使用现状

 

经过多年的发展,Lucene在全文检索领域已经有了很多的成功案例,并积累了良好的声誉。

 

基于Lucene的全文检索产品(Lucene本身只是一个组件,而非一个完整的应用)和应用Lucene的项目在世界各地已经非常之多,比较知名的有:

l         Eclipse:主流Java开发工具,其帮助文档采用Lucene作为检索引擎

l         Jive:知名论坛系统,其检索功能基于Lucene

l         Ifinder:出自德国的网站检索系统,基于Lucenehttp://ifinder.intrafind.org/

l         MIT DSpace Federation:一个文档管理系统(http://www.dspace.org/

 

国内外采用Lucene作为网站全文检索引擎的也很多,比较知名的有:

l         http://www.blogchina.com/weblucene/

l         http://www.ioffer.com/

l         http://search.soufun.com/

l         http://www.taminn.com/

 

(更多案例,请参见http://wiki.apache.org/jakarta-lucene/PoweredBy

 

在所有这些案例中,开源应用占了很大一部分,但更多的还是商化业产品和网站。毫不夸张的说,Lucene的出现,极大的推动了全文检索技术在各个行业或领域中的深层次应用。

 

3.初步应用

 

前面提到,Lucene本身只是一个组件,而非一个完整的应用,所以若想让Lucene跑起来,还得在Lucene基础上进行必要的二次开发。

 

下载与安装

首先,你需要到Lucene的官方网站http://jakarta.apache.org/lucene/ 去下载一份拷贝,最新版是1.4。下载后将得到一个名为lucene-1.4-final.zip的压缩文件,将其解压,里面有一个名为lucene-1.4-final.jar的文件,这就是Lucene组件包了,若需要在项目使用Lucene,只需要把lucene-1.4-final.jar置于类路径下即可,至于解压后的其他文件都是参考用的。

 

接下来,我用Eclipse建立一个工程,实现基于Lucene的建库、记录加载和记录查询等功能。

 

 

如上图所示,这是开发完成后的工程,其中有三个源文件CreateDataBase.javaInsertRecords.javaQueryRecords.java,分别实现建库、入库、检索的功能。

 

以下是对这三个源文件的分析。

 

建库源码及说明

 

CreateDataBase.java

package com.holen.part1;

 

import java.io.File;

import org.apache.lucene.analysis.standard.StandardAnalyzer;

import org.apache.lucene.index.IndexWriter;

 

/**

 * @author Holen Chen

 * 初始化检索库

 */

public class CreateDataBase {

 

    public CreateDataBase() {  

    }

   

    public int createDataBase(File file){

       int returnValue = 0;

       if(!file.isDirectory()){

           file.mkdirs();

       }

       try{

           IndexWriter indexWriter = new IndexWriter(file,new StandardAnalyzer(),true);

           indexWriter.close();

           returnValue = 1;

       }catch(Exception ex){

           ex.printStackTrace();

       }

       return returnValue;

    }

   

    /**

     * 传入检索库路径,初始化库

     * @param file

     * @return

     */

    public int createDataBase(String file){

       return this.createDataBase(new File(file));  

    }

 

    public static void main(String[] args) {

       CreateDataBase temp = new CreateDataBase();

       if(temp.createDataBase("e://lucene//holendb") == 1){

           System.out.println("db init succ");

       }

    }

}

 

 

说明:这里最关键的语句是IndexWriter indexWriter = new IndexWriter(file,new StandardAnalyzer(),true)

 

第一个参数是库的路径,也就是说你准备把全文检索库保存在哪个位置,比如main方法中设定的“e://lucene//holendb”,Lucene支持多库,且每个库的位置允许不同。

 

第二个参数是分析器,这里采用的是Lucene自带的标准分析器,分析器用于对整篇文章进行分词解析,这里的标准分析器实现对英文(或拉丁文,凡是由字母组成,由空格分开的文字均可)的分词,分析器将把整篇英文按空格切成一个个的单词(在全文检索里这叫切词,切词是全文检索的核心技术之一,Lucene默认只能切英文或其他拉丁文,默认不支持中日韩等双字节文字,关于中文切词技术将在后续章节重点探讨)。

 

第三个参数是是否初始化库,这里我设的是truetrue意味着新建库或覆盖已经存在的库,false意味着追加到已经存在的库。这里新建库,所以肯定需要初始化,初始化后,库目录下只存在一个名为segments的文件,大小为1k。但是当库中存在记录时执行初始化,库中内容将全部丢失,库回复到初始状态,即相当于新建了该库,所以真正做项目时,该方法一定要慎用。

 

加载记录源码及说明

 

InsertRecords.java

package com.holen.part1;

 

import java.io.File;

import java.io.FileReader;

import java.io.Reader;

import org.apache.lucene.analysis.standard.StandardAnalyzer;

import org.apache.lucene.document.Document;

import org.apache.lucene.document.Field;

import org.apache.lucene.index.IndexWriter;

 

/**

 * @author Holen Chen

 * 记录加载

 */

public class InsertRecords {

 

    public InsertRecords() {

    }

   

    public int insertRecords(String dbpath,File file){

       int returnValue = 0;

       try{

           IndexWriter indexWriter

            = new IndexWriter(dbpath,new StandardAnalyzer(),false);

           this.addFiles(indexWriter,file);

           returnValue = 1;

       }catch(Exception ex){

           ex.printStackTrace();

       }

       return returnValue;

    }

   

    /**

     * 传入需加载的文件名

     * @param file

     * @return

     */

    public int insertRecords(String dbpath,String file){

       return this.insertRecords(dbpath,new File(file));

    }

   

    public void addFiles(IndexWriter indexWriter,File file){

       Document doc = new Document();

       try{

           doc.add(Field.Keyword("filename",file.getName()));  

                 

           //以下两句只能取一句,前者是索引不存储,后者是索引且存储

           //doc.add(Field.Text("content",new FileReader(file))); 

           doc.add(Field.Text("content",this.chgFileToString(file)));

          

           indexWriter.addDocument(doc);

           indexWriter.close();

       }catch(Exception ex){

           ex.printStackTrace();

       }

    }

   

    /**

     * 从文本文件中读取内容

     * @param file

     * @return

     */

    public String chgFileToString(File file){

       String returnValue = null;

       StringBuffer sb = new StringBuffer();

       char[] c = new char[4096];

       try{

           Reader reader = new FileReader(file);

           int n = 0;

           while(true){            

              n = reader.read(c);

              if(n > 0){

                  sb.append(c,0,n);

              }else{

                  break;

              }

           }

           reader.close();

       }catch(Exception ex){

           ex.printStackTrace();

       }

       returnValue = sb.toString();

       return returnValue; 

    }

 

    public static void main(String[] args) {

       InsertRecords temp = new InsertRecords();

       String dbpath = "e://lucene//holendb";

       //holen1.txt中包含关键字"holen""java"

       if(temp.insertRecords(dbpath,"e://lucene//holen1.txt") == 1){

           System.out.println("add file1 succ");

       }

       //holen2.txt中包含关键字"holen""chen"

       if(temp.insertRecords(dbpath,"e://lucene//holen2.txt") == 1){

           System.out.println("add file2 succ");

       }  

    }

}

 

 

说明:这个类里面主要有3个方法insertRecords(String dbpath,File file)addFiles(IndexWriter indexWriter,File file)chgFileToString(File file)

 

ChgFileToString方法用于读取文本型文件到一个String变量中。

 

InsertRecords方法用于加载一条记录,这里是将单个文件入全文检索库,第一个参数是库路径,第二个参数是需要入库的文件。

 

InsertRecords需要调用addFilesaddFiles是文件入库的真正执行者。AddFiles里有如下几行重点代码:

doc.add(Field.Keyword("filename",file.getName()));

注意,在Lucene里没有严格意义上表,Lucene的表是通过Field类的方法动态构建的,比如Field.Keyword("filename",file.getName())就相当于在一条记录加了一个字段,字段名为filename,该字段的内容为file.getName()

 

 

常用的Field方法如下:

方法

切词

索引

存储

用途

Field.Text(String name, String value)

Y

Y

Y

标题,文章内容

Field.Text(String name, Reader value)

Y

Y

N

META信息

Field.Keyword(String name, String value)

N

Y

Y

作者

Field.UnIndexed(String name, String value)

N

N

Y

文件路径

Field.UnStored(String name, String value)

Y

Y

N

与第二种类似

 

为了更深入的了解全文检索库,我们可以将全文检索库与通常的关系型数据库(如OracleMysql)作一下对比。

 

全文检索库对关系型数据库对比

对比项

全文检索库(Lucene

关系型数据库(Oracle

核心功能

以文本检索为主,插入(insert)、删除(delete)、修改(update)比较麻烦,适合于大文本块的查询。

插入(insert)、删除(delete)、修改(update)十分方便,有专门的SQL命令,但对于大文本块(如CLOB)类型的检索效率低下。

Oracle类似,都可以建多个库,且各个库的存储位置可以不同。

可以建多个库,每个库一般都有控制文件和数据文件等,比较复杂。

没有严格的表的概念,比如Lucene的表只是由入库时的定义字段松散组成。

有严格的表结构,有主键,有字段类型等。

记录

由于没有严格表的概念,所以记录体现为一个对象,在Lucene里记录对应的类是Document

Record,与表结构对应。

字段

字段类型只有文本和日期两种,字段一般不支持运算,更无函数功能。

Lucene里字段的类是Field,如documentfield1,field2…

字段类型丰富,功能强大。

recordfield1,field2…

查询结果集

Lucene里表示查询结果集的类是Hits,如hitsdoc1,doc2,doc3…

JDBC为例, Resultsetrecord1,record2,record3...

 

两种库对比图如下:

Lucene

doc(field1,field2..),doc(field1,field2..)

入库:  indexer

Hits(doc(field1,field2..),doc(field1,field2..)...)

查询:  seracher

Oracle

record(field1,field2..),doc(field1,field2..)

入库:  insert

rResultset(record(field1,field2..),doc(field1,field2..)

 

查询:  select

 

 

 

 

 

 

 

 

 

 

 

 


检索源码及说明

 

QueryRecords.java

package com.holen.part1;

 

import java.util.ArrayList;

import org.apache.lucene.analysis.standard.StandardAnalyzer;

import org.apache.lucene.document.Document;

import org.apache.lucene.queryParser.QueryParser;

import org.apache.lucene.search.Hits;

import org.apache.lucene.search.IndexSearcher;

import org.apache.lucene.search.Query;

import org.apache.lucene.search.Searcher;

 

/**

 * @author Holen Chen

 * 检索查询

 */

public class QueryRecords {

 

    public QueryRecords() {

    }

   

    /**

     * 检索查询,将结果集返回

     * @param searchkey

     * @param dbpath

     * @param searchfield

     * @return

     */

    public ArrayList queryRecords(String searchkey,String dbpath,String searchfield){

       ArrayList list = null;

       try{

           Searcher searcher = new IndexSearcher(dbpath);

           Query query

            = QueryParser.parse(searchkey,searchfield,new StandardAnalyzer());

           Hits hits = searcher.search(query);

           if(hits != null){

              list = new ArrayList();

              int temp_hitslength = hits.length();

              Document doc = null;

              for(int i = 0;i < temp_hitslength; i++){

                  doc = hits.doc(i);

                  list.add(doc.get("filename"));

              }

           }

       }catch(Exception ex){

           ex.printStackTrace();

       }

       return list;

    }

 

    public static void main(String[] args) {

       QueryRecords temp = new QueryRecords();      

       ArrayList list = null;

       list = temp.queryRecords("holen","e://lucene//holendb","content");

       for(int i=0;i< list.size();i++){

           System.out.println((String)list.get(i));

       }      

    }

}

 

 

说明:该类中Searcher负责查询,并把查询结果以Hits对象集方式返回,Hits好比JDBC中的RecordSetHitsDocument的集合,每个Document相当于一条记录,Document中包含一个或多个字段,可以通过Document.get(“字段名”)方法得到每个字段的内容。

 

通过这三个类,就完成了一个简单的基于Lucene的全文检索应用。

 

4.总结

 

Lucene十分精练纯粹,就一个jar包,引入到你的工程中,调用其接口,就可以为你的应用增添全文检索功能。

 

通过上一节的初步应用会发现,Lucene使用起来很简单,与JDBC有些类似,应用时重点掌握好IndexWriterDocumentFieldSearcher等几个类即可。

 

Lucene的结构很清晰,每个package司职一项,比如org.apache.Lucene.search负责检索,org.apache.Lucene.index索引,org.apache.Lucene.analysis切词等,且Lucene的主要动作都采用了抽象类,扩展起来十分方便。

 

相对于一些商业化全文检索,Lucene的入库速度更快。因为它的存储采取分步合并的方法,先建立小索引,待时机成熟才把小索引合并到大索引树上。因此,我们在操作应用数据时可以同步进行全文检索库的操作而不会(或许很少)影响系统的效能。

 

Lucene性能稳定,使用简单,而且开源免费,有Apache基金在后面做支撑,资金和技术力量都十分雄厚,这两年也一直是稳步更新,每次新版本的推出,业界均争相报导。

Lucene研究之二——系统结构分析初步

 

作者:陈光(holen@263.net

时间:2004-08-26

 

本文主要讨论Lucene的系统结构,希望对其结构的初步分析,更深入的了解Lucene的运作机制,从而实现对Lucene的功能扩展。

 

1. Lucene的包结构

 

如上图所示,Lucene源码中共包括7个子包,每个包完成特定的功能:

 

Lucene包结构功能表

包名

功能

org.apache.lucene.analysis

语言分析器,主要用于的切词,支持中文主要是扩展此类

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

一些公用类

 

2. Lucene的主要逻辑图

 

Lucene功能强大,但从根本上说,主要包括两块:一是文本内容经切词后索引入库;二是根据查询条件返回结果。

以下是上述两大功能的逻辑图:

 

STORAGE

(存储器)

ACCESS INDEX

(访问索引)

SERACHER

(查询器)

ANALYZER

(语言分析器)

QUERY PARSER

(查询分析器)

DOCUMENT

(文档结构)

SEARCHER

(查询)

INDEXER

(入库)

 

FS

 

BDD

 

RAM

Lucene功能逻辑图

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


查询逻辑

 

按先后顺序,查询逻辑可分为如下几步:

 

1.  查询者输入查询条件
条件之间可以通过特定运算符进行运算,比如查询希望查询到与“中国”和“北京”相关的记录,但不希望结果中包括“海淀区中关村”,于是输入条件为“中国+北京-海淀区中关村”;

2.  查询条件被传达到查询分析器中,分析器将将对“中国+北京-海 淀区中关村”进行分析,首先分析器解析字符串的连接符,即这里的加号和减号,然后对每个词进行切词,一般最小的词元是两个汉字,则中国和北京两个词不必再 切分,但对海淀区中关村需要切分,假设根据切词算法,把该词切分为“海淀区”和“中关村”两部分,则最后得到的查询条件可以表示为:“中国” AND “北京” AND NOT(“海淀区” AND “中关村”)

3.  查询器根据这个条件遍历索引树,得到查询结果,并返回结果集,返回的结果集类似于JDBC中的ResultSet

4.  将返回的结果集显示在查询结果页面,当点击某一条内容时,可以链接到原始网页,也可以打开全文检索库中存储的网页内容。

 

这就是查询的逻辑过程,需要说明的是,Lucene默认只支持英文,为了便于说明问题,以上查询过程采用中文举例,事实上,当Lucene被扩充支持中文后就是这么一个查询过程。

 

入库逻辑

 

入库将把内容加载到全文检索库中,按顺序,入库逻辑包括如下过程:

 

1.  入 库者定义到库中文档的结构,比如需要把网站内容加载到全文检索库,让用户通过“站内检索”搜索到相关的网页内容。入库文档结构与关系型数据库中的表结构类 似,每个入库的文档由多个字段构成,假设这里需要入库的网站内容包括如下字段:文章标题、作者、发布时间、原文链接、正文内容(一般作为网页快照)。

2.  包含N个字段的文档(DOCUMENT)在真正入库前需要经过切词(或分词)索引,切词的规则由语言分析器(ANALYZER)完成。

3.  切分后的“单词”被注册到索引树上,供查询时用,另外也需要也其它不需要索引的内容入库,所有这些是文件操作均由STORAGE完成。

 

以上就是记录加载流程,索引树是一种比较复杂的数据存储结构,将在后续章节陆续介绍,这里就不赘述了,需要说明的一点是,Lucene的索引树结构非常优秀,是Lucene的一大特色。

 

接下来将对Lucene的各个子包的结构进行讨论。

 

3. 语言分析包org.apache.lucene.analysis

 

 

Analyzer是一个抽象类,司职对文本内容的切分词规则。

 

切分后返回一个TokenStreamTokenStream中有一个非常重要方法next(),即取到下一个词。简单点说,通过切词规则,把一篇文章从头到尾分成一个个的词,这就是org.apache.lucene.analysis的工作。

 

对英文而言,其分词规则很简单,因为每个单词间都有一个空格,按空格取单词即可,当然为了提高英文检索的准确度,也可以把一些短语作为一个整体,其间不切分,这就需要一个词库,对德文、俄文也是类似,稍有不同。

 

对中文而言,文字之间都是相连的,没有空格,但我们同样可以把字切分,即把每个汉字作为一个词切分,这就是所谓的“切字”,但切字方式方式的索引没有意义,准确率太低,要想提高准确度一般都是切词,这就需要一个词库,词库越大准确度将越高,但入库效率越低。

 

若要支持中文切词,则需要扩展Analyzer类,根据词库中的词把文章切分。

 

简单点说,org.apache.lucene.analysis就是完成将文章切分词的任务。

 

 

4. 文档结构包org.apache.lucene.document

 

document包相对而言比较简单,该包下面就3个类,Document相对于关系型数据库的记录对象,主要负责字段的管理,字段分两种,一是Field,即文本型字段,另一个是日期型字段DateField。这个包中关键需要理解的是Field中字段存储方式的不同,这在上一篇中已列表提到,下面我们可以参见一下其详细的类图:

 

 

5. 索引管理包org.apache.lucene.index

 

 

索引包是整个系统核心,全文检索的的根本就为每个切出来的词建索引,查询时就只需要遍历索引,而不需要去正文中遍历,从而极大的提高检索效率,索引建设的质量关键整个系统的质量。Lucene的索引树是非常优质高效的,具体的索引树细节,将在后续章节中重要探讨。

 

在这个包中,主要学习IndexWriterIndexReader这个类。

 

通过上一篇的初步应用可知,全文检索库的初始化和记录加载均需要通过该类来完成。

 

初始化全文库的语句为:

IndexWriter indexWriter = new IndexWriter(“全文库的目录位置”,new StandardAnalyzer(),true);

 

记录加载的语句为:indexWriter.addDocument(doc);

 

IndexWriter主要用于写库,当需要读取库内容时,就需要用到IndexReader这个类了。

 

6. 查询分析包org.apache.lucene.queryParser和检索包org.apache.lucene.search

 

 

通过查询分析器(queryParser)解析后,将返回一个查询对象(query),根据查询对象就可进行检索了。上图描述了query对象的生成,下图描述了查询结果集(Hits)的生成。

 

7. 存储包org.apache.lucene.store

 

 

一些底层的文件I/O操作。

 

8. 工具包org.apache.lucene.util

 

该包中包括4个工具类。

 

9. 总结

 

通过对Lucene源码包的分析,我们可以初步认识到Lucene的核心类包主要有3个:

 

l         org.apache.lucene.analysis

l         org.apache.lucene.index

l         org.apache.lucene.search

 

其中org.apache.lucene.analysis 主要用于切分词,切分词的工作由Analyzer的扩展类来实现,Lucene自带了StandardAnalyzer类,我们可以参照该写出自己的切词分析器类,如中文分析器等。

 

org.apache.lucene.index主要提供库的读写接口,通过该包可以创建库、添加删除记录及读取记录等。

 

org.apache.lucene.search主要提供了检索接口,通过该包,我们可以输入条件,得到查询结果集,与org.apache.lucene.queryParser包配合还可以自定义的查询规则,像google一样支持查询条件间的与、或、非、属于等复合查询。

 

参考资料

 

1.  http://www-igm.univ-mlv.fr/~dr/XPOSE2003/lucene/node1.html