2.0 入门案例及详解

2016-02-16 23:53:10 7,984 1

Lucene可以在Java SE程序中使用,也可以在Java EE程序中使用。作为第一个案例,我们采用一个java项目来进行讲解。

假 设我们有这样一个场景:我们开发了一个博客应用,用户可以注册并且发表文章。为了访问者可以高效的搜索博客网站的所有文章,我们打算使用Lucene对文 章建立索引。一个文章可能包含的字段有:id,标题、摘要、关键字、内容、发表时间、作者等信息。我们希望不论哪一个字段包含用户搜索的关键字,都可以搜 索到这片文章。因此在用户发表文章的时候,我们往数据库中存储记录的时候,同时通过Lucene对文章建立索引。

为了简单,在本案例中,我们自己构建文章Article对象实例,并且往其中填写内容。模拟已经获取到的用户输入的内容。查询时,我们自己指定搜索关键字。并且,我们并不真正的将文章数据存入数据库,这个太简单,我们关心的是Lucene索引库的创建与维护。

1、新建maven项目lucene

pom.xml依赖

<dependencies>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>4.10.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>4.10.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>4.10.4</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.9</version>
        </dependency>
        <dependency>
            <groupId>com.thihy</groupId>
            <artifactId>elasticsearch-analysis-paoding</artifactId>
            <version>1.4.2.1</version>
        </dependency>
    </dependencies>

2 新建文章实体Article

注:Article实体表示的是一个文章的信息,我们在创建索引的时候,需要将其转换为Document对象。

public class Article {
    private Integer id;
    private String title;
    private String content;
    private String author;
    
    //-----------constructors----------
     public Article(Integer id, String title, String content, String author) {
        super();
        this.id = id;
        this.title = title;
        this.content = content;
        this.author = author;
    }
    
   
    @Override
    public String toString() {
        return "Artical [id=" + id + ", title=" + title + ", content="
                + content + "]";
    }
    
 //-------------------getters and setters----------------------
    ...

3 创建索引:Indexer.java

Lucene提供了一系列的API来创建索引,在Lucene中,用Document对象来表示索引库中的一条记录,每个Document由很多字段Field组成,可以将Document类比为数据库中的一条记录。而Document最终是通过IndexWriter来创建索引。在后面我们将会详细介绍涉及到的每个API。

/** 建立索引
        发表过文章过后,不仅数据库中有存储记录 索引库中也必须有一条*/
    public static void main(String args[]) throws Exception {

        // 模拟一条数据库中的记录
        Article artical = new Article(1, "Lucene全文检索框架",
                "Lucene如果信息检索系统在用户发出了检索请求后再去网上找答案","田守枝");

        // 建立索引
        // 1、把Article转换为Doucement对象
        Document doc = new Document();
        //根据实际情况,使用不同的Field来对原始内容建立索引, Store.YES表示是否存储字段原始内容
        doc.add(new LongField("id", artical.getId(), Store.YES));
        doc.add(new StringField("author", artical.getAuthor(), Store.YES));
        doc.add(new TextField("title", artical.getTitle(), Store.YES));
        doc.add(new TextField("content", artical.getContent(), Store.NO));

        // 2、建立索引
        // 指定索引库的位置,本例为项目根目录下indexDir
        Directory directory = FSDirectory.open(new File("./indexDir/"));
        // 分词器,不同的分词器有不同的规则
        Analyzer analyzer = new StandardAnalyzer();
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Version.LATEST, analyzer);
        IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
        indexWriter.addDocument(doc);
        indexWriter.close();
    }

运行createIndex()方法,在项目根目录下会出现一些索引文件,如下图所示:

Image.png

indexDir目录下的文件就是Lucene的索引库文件,我们可以通过lukeall工具来查看索引库中的内容,在后面将会介绍。

4 搜索Searcher.java

在Lucene中,搜索通过IndexSearcher完成。

    // 搜索索引库
    public static void main(String args[]) throws Exception {
        // 搜索条件(不区分大小写)
        String queryString = "lucene";
//        String queryString = "compass";
        

        // 进行搜索得到结果
        // ==============================

        Directory directory = FSDirectory.open(new File("./indexDir/"));// 索引库目录
        Analyzer analyzer = new StandardAnalyzer();

        // 1、把查询字符串转为查询对象(存储的都是二进制文件,普通的String肯定无法查询,因此需要转换)
        QueryParser queryParser = new QueryParser("title",analyzer);// 只在标题里面查询
        Query query = queryParser.parse(queryString);

        // 2、查询,得到中间结果
        IndexReader indexReader=DirectoryReader.open(directory);
        IndexSearcher indexSearcher = new IndexSearcher(indexReader);
        TopDocs topDocs = indexSearcher.search(query, 100);// 根据指定查询条件查询,只返回前n条结果
        int count = topDocs.totalHits;// 总结果数
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;// 按照得分进行排序后的前n条结果的信息

        List<Article> articalList = new ArrayList<Article>();
        // 3、处理中间结果
        for (ScoreDoc scoreDoc : scoreDocs) {
            float score = scoreDoc.score;// 相关度得分
            int docId = scoreDoc.doc; // Document在数据库的内部编号(是唯一的,由lucene自动生成)

            // 根据编号取出真正的Document数据
            Document doc = indexSearcher.doc(docId);

            // 把Document转成Article
            Article artical = new Article(
                    Integer.parseInt(doc.getField("id").stringValue()),//需要转为int型
                    doc.getField("title").stringValue(),
                    null,
                    doc.getField("author").stringValue()
                    );
            
            articalList.add(artical);
        }
        
        indexReader.close();
        // ============查询结束====================
        
    
        // 显示结果
        System.out.println("总结果数量为:" + articalList.size());
        for (Article artical : articalList) {
            System.out.println("id="+artical.getId());
            System.out.println("title="+artical.getTitle());
            System.out.println("content="+artical.getContent());
        }
    }

运行search方法,控制台输出:

总结果数量为:1
id=1
title=Lucene全文检索框架
content=null

注意目前我们搜索的是小写lucene,依然搜索到了结果。读者可以尝试搜索其他的关键字。

5 入门案例详解

5.1 Indexer class

  • IndexWriter

  • Directory

  • Analyzer

  • Document

  • Field

    索引建立过程图示

22.png

IndexWriter类似于数据库的SessionFactory,每次使用完都关闭,是非常浪费资源的,因此,我们应该保证在全局范围内只使用一个IndexWriter

       而每次使用IndexWriter在操作索引库的时候,都会给索引库加上一把锁,当关闭这个IndexWriter时,会把锁释放掉。

而我们开发的web应用是多线程的,这就意味着一个线程在操作索引库的时候,索引库就会被锁住,其他线程无法访问。

此时有两种方法来解决这个问题:

1、  针对每个线程都创建一个IndexWriter,强烈不建议

2、  全局范围内使用一个IndexWriter,但是每次使用完不是close掉,是使用commit方法,当操作提交之后,锁就会被释放掉,别的线程就可以操作索引库了。

在多线程并发访问时,只要保证每个人的结果集不是全局的,就不会出现数据混乱的情况。

Directory directory = FSDirectory.open(new File("./indexDir/"));
// 分词器,不同的分词器有不同的规则
Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Version.LATEST, analyzer);
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);

IndexWriter只有一个构造函数,接受一个Directory和IndexWriterConfig对象。

Directory指的是:索引库的路径,也就是将原始建立索引后,索引的存放位置

IndexWriterConfig是创建索引时的配置信息,例如指定使用的分词器。关于分词我们在后面会详细讲解。

对于大段的文本,IndexWriter并不能针对其建立索引,必须要经过Analyzer进行分词之后。Analyzer只能针对纯文本的文件内容进行分词,如果文件不是纯文本,我们必须先将文件中的文本内容提取出来,Tika框架可以帮助我们将文本内容从不同格式的文件中提取出来。

Analyzer是一个抽象类,Lucene为其提供了一实现类。其中有一些Analyzer是处理停用词stopping word,例如 a、an、the这些词语对于建立索引是没有作用的。一些Analyzer将所有的关键词全部转为小写,这样Lucene就不需要考虑大小写的问题了。等等。

Document对象代表的就是我们建立索引的文档。其内部包含了众多Field,Field的作用是告诉我们需要针对这个文档的哪些内容建立索引,哪些内容需要进行分词。

Document则是Lucene建立索引的最小单位Document中包含了很多Field,其包含了真正的索引数据,每一个Field都有一个独一无二的名称,和对应的值,并包含了一些具体的操作信息,例如是否进行分词。如果两个Field名称相同,内容不会覆盖,后建立的索引的内容会添加在原来的Field的后面。Field的值是在搜索的过程中如果匹配对应的关键字,就可以搜索到内容。而name则指定使用哪一个Fieldvalue进行匹配。例如,我们只想搜索在filename中包含Lucene的文档,就可以指定搜索filenameLucene。这样名称为contentsfieldvalue就不再检索范围内。

建立索引实际上是针对Field的value建立索引,当指定value建立索引后,Lucene会根据value的值计算得到一个Token,这是Lucene使用一些算法得到的。

在 本案例中,我们使用到的Field类型包括:LongField、StringField、TextField。当然还有很多其他类型分Field。需要 注意的是,当我们使用Document对象的add方法添加字段时,只有TextField的值会进行分词,其他类型的Field都不会进行分词。不进行 分词的后果是,用户检索的关键字必须与这个字段的内容完全匹配上,才返回这条记录。在我们案例中,我们是按照如下方式添加Field的:

doc.add(new LongField("id", artical.getId(), Store.YES));
doc.add(new StringField("author", artical.getAuthor(), Store.YES));
doc.add(new TextField("title", artical.getTitle(), Store.YES));
doc.add(new TextField("content", artical.getContent(), Store.NO));

对于文章的作者信息,我们希望用户输入的名字必须完全匹配上,才返回这条记录,因此使用StringField,而不是TextField;而对于文章的标题信息,我们希望用户检索的关键字分词后,只要匹配上一部分就可以搜索到,因此对标题进行分词,类似的我们对文章的内容也进行了分词。

Store.YES和Store.NO的作用是,如果选择了YES,在建立索引后,会把这个字段的原始值也保存在数据库中,而NO则是不保存。这将会影响到搜索的结果的展示。例如我们希望文章的内容content可以进行检索,但是我们不希望将其原始内容保存到索引库中,因为一个文章的内容通常都是很大的。我们搜索的结果只是展示部分信息,而不是展示文章的所有信息,因为我们对文章的id使用Store.YES,所以检索到的结果我们可以获取到文章的id,当检索结果展示在页面上时,只要超链接后面跟上这个id作为参数,在数据库中检索是非常快的,因为有主键索引。

Lucene只处理文本内容,这是由于搜索的时候,用户只会输入文本内容进行搜索。因此对于不同类型的文档,我们在建立索引的时候,会将其的文本内容提取出来,针对图片建立索引没有任何意义。

5.2 searching classes

  • IndexSearcher

  • Term

  • Query

  • TermQuery

  • TopDocs

我 们的案例代码中并没有涉及到Term这个类,Term是索引库中最基本的搜索单元。这是因为,在建立索引的时候,如果指定不分词(如案例中文件名和文件路 径),那么整个内容就是一个Term,而如果指定分词(如案例中的文件内容),内容就会被拆分成多个Term,这些过程是在建立索引的过程中有 Lucene框架自动完成的,因此Term是Lucene中最基本的搜索单元,在建立索引的时候我们不需要考虑Term。
在搜索的时候,我们可以使用Term配合TermQuery进行查询。

Query q = new TermQuery(new Term("contents", "lucene"));
TopDocs hits = searcher.search(q, 10);

Term中的参数对应建立索引时Field中指定的索引的名称,第二个参数是检索的关键字。

TermQuery与Query对象的区别是:Query对象是通过QueryParser解析用户输入的搜索内容解析得到的,会将用户输入的内容分词后再进行搜索。而TermQuery由于搜索的是最小的分词单元,因此不会对搜索关键字再次进行分词。

Query对象是一个抽象类,代表用户的查询,其有很多子类:

TermQuery. 
BooleanQuery, 
PhraseQuery, 
PrefixQuery, 
PhrasePrefixQuery, 
TermRangeQuery,
NumericRangeQuery, 
FilteredQuery, 
SpanQuery.

Query对象本身也实现了一些方法,setBoost(float)方法是一个比较有趣的方法,当我们使用多个子查询对象进行查询的时候,如果某个子查询对象使用了这个方法,可以提高这个子查询对象查询出来的数据的相关度得分。