文章目录
  1. 1. 前言
  2. 2. 简介
  3. 3. Lucene原理
  4. 4. Lucene模块
  5. 5. 代码示例
前言

公司需要在蓝牙电话的拨号键盘中实现T9搜索功能。第一版是使用字符串模糊匹配和正则表达式来实现该功能的。但用下来发现有点卡顿,体验不好。经过一段时间的优化,效果还是不理想。最后决定寻找新的技术方案。在寻找新的技术方案过程中,发现了Lucune,当利用Lucene实现该功能后,体验非常好,速度很快,无卡顿感。遂写个demo记录一下。

简介

Lucene是Apache基金会的一个开源全文搜索引擎工具包。实现语言是Java。中小型应用可以利用这个工具包,实现高效快速的搜索功能。

Lucene原理

建立索引:Lucene利用Analyzer对输入的源数据A进行分词处理,分词处理后得到关键词列表B,并且建立关键词列表B与源数据A的映射关系。最后将这些信息写入索引目录中。搜索:利用Analyzer对搜索字符串进行分词处理,得到搜索关键词C,然后利用C去索引目录中检索,得到搜索结果。

Lucene模块

Lucene有以下几大模块:index(索引模块)、document(索引单元模块)、analysis(分词模块)、store(存储索引模块)、search(搜索模块)、queryparser(搜索条件解析模块)。

  • index 索引模块的主要功能是建立索引和读取索引。有两个重要的类:IndexWriter和IndexReader,对应的功能分别是建立索引和读取索引。后面demo代码中会详细介绍建立索引和读取索引的步骤。
  • document 索引单元模块 索引的单位是document,一个document就是一条源数据记录。一个document包括多个字段。IndexWriter建立索引时,是写入一条或多条document。搜索时,得到的结果也是document的集合。
  • analysis 分词模块。在此模块中,Analyzer(分析器)是最重要的类。它的功能是将字符串处理并转化成一个个关键词汇。Analyzer在建立索引和搜索时都会用到。Analyzer是一个抽象类,Lucene实现了几个Analyzer:KeywordAnalyzer、SimpleAnalyzer、WhitespaceAnalyzer。开发者也可以通过继承Analyzer定义自己需要的Analyzer。后面demo代码会详细介绍Analyzer分词流程。
  • store 索引存储模块。此模块功能主要是提供索引存储目录。例如RAMDirectory:在内存中创建索引,FSDirectory:在File System中创建索引。
  • search 搜索模块。此模块有两个重要的类:Query和IndexSearch。Query是搜索条件类,IndexSearch是搜索类。IndexSearch从IndexReader获得搜索索引,从Query获得搜索条件,最后调用search()执行搜索。
  • queryparser 搜索条件解析模块。此模块最重要的类是QueryParser。QueryParser将原始的搜索字符串解析成Query对象。
    代码示例
  1. 自定义分词器,下面代码片段是一个自定义的分词器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 /**
  * Lucene4.0实现了三个分词器:KeywordAnalyzer、SimpleAnalyzer、WhitespaceAnalyzer
  * 这四大分词器有一个共同的抽象父类,此类有个方法public final TokenStream tokenStream(),即分词的一个流
  * 假设有这样的文本"are you ok",实际它是以一个java.io.Reader传进分词器中
  * Lucene分词器处理完毕后,会把整个分词转换为TokenStream,这个TokenStream中就保存所有的分词信息
  *
  * @see TokenStream 有两个实现类,分别为Tokenizer和TokenFilter
  * @see Tokenizer---->用于将一组数据划分为独立的语汇单元(即一个一个的单词)
  * @see TokenFilter-->过滤语汇单元
  * @see -----------------------------------------------------------------------
  * <p>
  * 分词流程
  * 1)将一组数据流java.io.Reader交给Tokenizer,由其将数据转换为一个个的语汇单元
  * 2)通过大量的TokenFilter对已经分好词的数据进行过滤操作,最后产生TokenStream
  * 3)通过TokenStream完成索引的存储
  * @see ------------------------------------------------------------------------
  */
 private class CustomAnalyzer extends Analyzer {

     protected final Version matchVersion;

     public CustomAnalyzer(Version version) {
         matchVersion = version;
     }

     @Override
     protected TokenStreamComponents createComponents(String s, Reader reader) {
         //LetterTokenizer是基于文本单词的分词,它会根据非字母符号来分词
         Tokenizer source = new LetterTokenizer(matchVersion, reader);
         //LowerCaseFilter是将数据转换为小写
         TokenStream filter = new LowerCaseFilter(matchVersion, source);
         return new TokenStreamComponents(source, filter);
     }

     /**
      * The default implementation returns <code>reader</code>
      * unchanged.
      *
      * @param fieldName IndexableField name being indexed
      * @param reader    original Reader
      * @return reader, optionally decorated with CharFilter(s)
      */
     protected Reader initReader(String fieldName, Reader reader) {
         return reader;
     }
 }

2.. 建立索引,搜索。在下面代码中,先调用buildIndex()建立索引,然后调用search()便可执行搜索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/**
 * Created by Michael.
 */
public class LuceneDemo {
    private static final String TAG = "LuceneDemo";

    /**
     * 创建索引
     *
     * @see ---------------------------------------------------------------------------------------------------------
     * @see 1、创建Directory-----------------指定索引被保存的位置
     * @see 2、创建IndexWriter---------------通过IndexWriter写索引
     * @see 3、创建Document对象---------------我们索引的有可能是一段文本or数据库中的一张表
     * @see 4、为Document添加Field------------相当于Document的标题、大小、内容、路径等等,二者类似于数据库表中每条记录和字段的关系
     * @see 5、通过IndexWriter添加文档到索引中
     * @see 6、关闭IndexWriter----------------用完IndexWriter之后,必须关闭之
     */
    private void buildIndex() {
        Directory directory = null;
        IndexWriter writer = null;
        Document doc = null;
        Field nameField = null;
        Field numberField = null;
        try {
            // FSDirectory会根据当前的运行环境打开一个合理的基于File的Directory(若在内存中创建索引则new RAMDirectory())
            // 这里是在手机默认存储上创建文件夹index,在文件夹index中创建索引
            directory = FSDirectory.open(new File(Environment.getExternalStorageDirectory(), "index"));
            // IndexWriterConfig()构造方法的Version.LUCENE_40参数值指明索引所匹配的版本号,
            // 并使用自定义的分词器:CustomAnalyzer
            writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_40, new CustomAnalyzer(Version.LUCENE_40)));

            // 把内容添加到索引域中,这里连续添加了两个索引单元的数据
            doc = new Document();
            // Field.Store.YES-----------这里是将文件的全名存储到硬盘中
            nameField = new StringField("name", "michael", Field.Store.YES);
            doc.add(nameField);
            numberField = new StringField("number", "16512", Field.Store.YES);
            doc.add(numberField);
            // 通过IndexWriter添加文档到索引中
            writer.addDocument(doc);

            doc = new Document();
            nameField = new StringField("name", "coco", Field.Store.YES);
            doc.add(nameField);
            numberField = new StringField("number", "15867", Field.Store.YES);
            doc.add(numberField);
            writer.addDocument(doc);

        } catch (Exception e) {
            Log.e(TAG, "build index exception");
            e.printStackTrace();
        } finally {
            if (null != writer) {
                try {
                    writer.close(); // IndexWriter在用完之后一定要关闭
                } catch (IOException ce) {
                    Log.e(TAG, "close IndexWriter exception");
                    ce.printStackTrace();
                }
            }
        }
    }

    /**
     * 搜索
     * <p>
     * 1、创建Directory
     * 2、创建IndexReader
     * 3、根据IndexReader创建IndexSearcher
     * 4、创建搜索的Query
     * 5、根据searcher搜索并返回TopDocs
     * 6、根据TopDocs获取ScoreDoc对象
     * 7、根据searcher和ScoreDoc对象获取具体的Document对象
     * 8、根据Document对象获取需要的值
     * 9、关闭IndexReader
     */
    private void search() {
        IndexReader reader = null;
        try {
            Directory directory = FSDirectory.open(new File(Environment.getExternalStorageDirectory(), "index"));
            reader = DirectoryReader.open(directory);
            IndexSearcher searcher = new IndexSearcher(reader);
            // 创建基于Parser搜索的Query,创建时需指定其"搜索的版本,默认搜索的域,分词器"....这里的域指的是创建索引时Field的名字
            QueryParser parser = new QueryParser(Version.LUCENE_40, "name", new SimpleAnalyzer(Version.LUCENE_40));
            Query query = parser.parse("coco");       // 指定搜索域为name(即上一行代码指定的"name")中包含"coco"的文档
            TopDocs tds = searcher.search(query, 10); // 第二个参数指定搜索后显示的条数,若查到5条则显示为5条,查到15条则只显示10条
            ScoreDoc[] sds = tds.scoreDocs;           // TopDocs中存放的并不是我们的文档,而是文档的ScoreDoc对象
            for (ScoreDoc sd : sds) {                   // ScoreDoc对象相当于每个文档的ID号,我们就可以通过ScoreDoc来遍历文档
                Document doc = searcher.doc(sd.doc);  // sd.doc得到的是文档的序号
                Log.i(TAG, "name:" + doc.get("name") + " number:" + doc.get("number"));
            }
        } catch (Exception e) {
        	Log.e(TAG, "search exception");
            e.printStackTrace();
        } finally {
            if (null != reader) {
                try {
                    reader.close();
                } catch (IOException e) {
                	Log.e(TAG, "close IndexReader exception");
                    e.printStackTrace();
                }
            }
        }
    }
}
文章目录
  1. 1. 前言
  2. 2. 简介
  3. 3. Lucene原理
  4. 4. Lucene模块
  5. 5. 代码示例