Lucene浅析
更新日期:
前言
公司需要在蓝牙电话的拨号键盘中实现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 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(); } } } } } |