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

    技术2022-05-11  74

    作者:陈光(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基金在后面做支撑,资金和技术力量都十分雄厚,这两年也一直是稳步更新,每次新版本的推出,业界均争相报导。

     

    参考资料

     

    1.  Introduction to Text Indexing with Apache Jakarta LuceneOtis Gospodnetic

    2.  Lucene Introduction in Chinese(车东)

    3.  Lucene TutorialSteven J. Owens

     

    最新回复(0)