Lucene学习笔记
2013-12-02 10:03 hduhans 阅读(543) 评论(0) 收藏 举报Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。它是一个开放源代码的全文检索引擎工具包,即它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎。
lucene-3.5.0.zip下载:http://pan.baidu.com/s/11lKHu
索引和查看必备工具lukeall-3.5.0.jar下载:http://pan.baidu.com/s/1vgjso
一、Lucene的基本使用
1、选项说明
1) 存储域选项Field.Store.YES或Field.Store.NO,表示是否存储原数据信息,一般id存储,文章内容不存储;
2) 索引选项Field.Index:
① Field.Index.ANALYZED: 进行分词和索引,适用于标题、内容等;
② Field.Index.NOT_ANALYZED: 进行索引,但是不进行分词,如果是身份证号,姓名,ID等,适用于精确搜索;
③ Field.Index.ANALYZED_NOT_NORMS: 进行分词但是不存储norms信息,这个norms中包括了创建索引的时间和权值等信息,norms保存了排序的信息;
④ Field.Index.NOT_ANALYZED_NOT_NORMS: 既不进行分词也不存储norms信息
⑤ Field.Index.NO: 不进行索引
2、创建索引常用方法
1) 创建索引:① 创建Directory ② 创建IndexWriter ③ 创建Document对象 ④ 为Document添加Field ⑤ 通过IndexWriter添加文档到索引中 ⑥ 关闭writer
2) 搜索索引:① 创建Directory ② 创建IndexReader ③ 根据IndexReader创建IndexSearcher ④ 创建搜索的Query ⑤ 根据searcher搜索并且返回TopDocs ⑥ 根据TopDocs获取ScoreDoc对象 ⑦ 根据searcher和ScoreDoc获取具体的Document对象 ⑧ 根据Document对象获取需要的值 ⑨ 关闭reader
3) 一个简单示例(示例源码下载),读取文件夹D:\lucene\index01中的文本文档内容并创建索引(需导入包lucene-core-3.5.0.jar)。

package org.itat.test; import java.io.File; import java.io.FileReader; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.queryParser.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.util.Version; public class HelloLucene { /** * 建立索引 */ public void index() { IndexWriter writer=null; try { //1、创建Directory //Directory directory = new RAMDirectory(); //建立在内存中 Directory directory = FSDirectory.open(new File("D:/lucene/index01")); //2、创建IndexWriter IndexWriterConfig iwc =new IndexWriterConfig(Version.LUCENE_35, new StandardAnalyzer(Version.LUCENE_35)); writer = new IndexWriter(directory, iwc); //3、创建Document对象 Document doc = null; //4、为Document添加Field File f=new File("D:/lucene/data"); for(File file:f.listFiles()){ doc = new Document(); doc.add(new Field("content",new FileReader(file))); doc.add(new Field("filename",file.getName(),Field.Store.YES,Field.Index.NOT_ANALYZED)); doc.add(new Field("path",file.getAbsolutePath(),Field.Store.YES,Field.Index.NOT_ANALYZED)); //5、通过IndexWriter添加文档到索引中 writer.addDocument(doc); } } catch (Exception e) { e.printStackTrace(); } finally{ if(writer!=null) try { writer.close(); } catch (Exception e) { e.printStackTrace(); } } } /** * 搜索 */ public void searcher(){ try { //1、创建Directory Directory directory = FSDirectory.open(new File("D:/lucene/index01")); //2、创建IndexReader IndexReader reader=IndexReader.open(directory); //3、根据IndexReader创建IndexSearcher IndexSearcher searcher = new IndexSearcher(reader); //4、创建搜索的Query //创建parser来确定要搜索文件的内容,第二个参数表示搜索的域 QueryParser parser=new QueryParser(Version.LUCENE_35,"content",new StandardAnalyzer(Version.LUCENE_35)); //创建query,表示搜索域为content中包含java的文档 Query query=parser.parse("公开"); //5、根据searcher搜索并且返回TopDocs TopDocs tds = searcher.search(query, 10); //6、根据TopDocs获取ScoreDoc对象 ScoreDoc[] sds = tds.scoreDocs; for(ScoreDoc sd:sds){ //7、根据searcher和ScoreDoc获取具体的Document对象 Document d = searcher.doc(sd.doc); //8、根据Document对象获取需要的值 System.out.println(d.get("filename")+"["+d.get("path")+"]"); } //9、关闭reader reader.close(); } catch (Exception e) { e.printStackTrace(); } } }
4) 删除文档,使用IndexWriter.deleteDocuments(),文档删除后,会存储域回收站中,可用下面的方法进行恢复。

writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_35,new StandardAnalyzer(Version.LUCENE_35))); writer.deleteDocuments(new Term("id", "1")); //可用forceMergeDeletes()方法强制清空回收站 //writer.forceMergeDeletes();
5) 恢复回收站文档,使用IndexReader.undeleteAll(),需设置readonly=false。

IndexReader reader = IndexReader.open(directory,false); reader.undeleteAll(); reader.close();
6) 设置权值,在建立索引时使用Document..setBoost(1.0f),其中数值为浮点型,需跟一个字母f。权值越大搜索结果排序越靠前。
7) 数字和日期进行索引。

//对数字进行索引 doc.add(new NumericField("attach",Field.Store.YES,true).setIntValue(888)); //对日期进行索引 doc.add(new NumericField("attach",Field.Store.YES,true).setLongValue(new java.util.Date().getTime()));
3、IndexReader的实时更新设置。IndexReader每次打开消耗的资源较大,因此在频繁查询的情况下,建议IndexReader使用单例模式,一个项目周期使用一个reader,但因此会带来一个问题,就是当IndexReader打开后,搜索结果不会随着索引的改变而改变,须判断IndexReader是否被改变,使用方法 IndexReader.openIfChanged(),通常写法如下:

//声明全局reader变量,一次打开,多次使用 private IndexReader reader = null; //获取IndexSearch对象,根据方法openIfChanged判断reader是否更新,使得查询的结果总是最新的 public IndexSearcher getSearcher() { try { if (reader == null) { reader = IndexReader.open(directory); } else { IndexReader ir = IndexReader.openIfChanged(reader); if (ir != null){ reader.close(); reader = ir; } } } catch (Exception e) { e.printStackTrace(); } return new IndexSearcher(reader); }
注:有些程序会对IndexWrite也使用单例模式,此时,writer必须手动提交,即调用writer.commit()
4、常用搜索方法(示例源码下载)。
1) TermQuery 精确搜索。例:Query query = new TermQuery(new Term("name", "hans")); --查询name=hans的记录,必须相等,不是包含
2) TermRangeQuery 字符串范围搜索。例:Query query = new TermRangeQuery("name", "a", "h", true, true); --查询name从a开头到h开头的记录,最后两个true表示包含a和h
3) NumericRangeQuery 数字范围搜索。例:Query query = NumericRangeQuery.newIntRange("attach", 1, 5, true, true); --attach建立索引时保存是数字类型,查询1<=attach<=5的记录
4) PrefixQuery 前缀搜索。例:Query query = new PrefixQuery(new Term("name", "han")); --查询name以"han"字符串开头的记录
5) WildcardQuery 通配符搜索。例:Query query = new WildcardQuery(new Term("email","*@pccpa.cn")); --查询email以@pccpa.cn结尾的所有记录
6) BooleanQuery 多条件搜索。本例查询name!=hanlong且content包含pccpa单词且email以"@qq.com"结尾的记录。

//多条件查询 示例:查询name!=hanlong 且 content包含pccpa单词 且 emal以"@qq.com"结尾 的数据 BooleanQuery query = new BooleanQuery(); query.add(new TermQuery(new Term("name", "hanlong")),Occur.MUST_NOT); query.add(new TermQuery(new Term("content", "pccpa")),Occur.MUST); query.add(new WildcardQuery(new Term("email","*@qq.com")),Occur.MUST); //Occur条件说明:1) Occur.MUST 条件必须成立 2) Occur.SHOULD 条件可以成立,也可以不成立 3) Occur.MUST_NOT 条件必须不成立
7) PhraseQuery 短语搜索,可以查询两个间隔一定数目的单词的记录,对中文分词不起作用,性能开销较大,不太建议使用。

//查询content中单词"welcome"和单词"pccpa"之间间隔<=4个单词的记录 PhraseQuery query = new PhraseQuery(); query.setSlop(4); query.add(new Term("content", "welcome")); query.add(new Term("content", "pccpa"));
8) FuzzyQuery 模糊搜索。例:Query query = new FuzzyQuery(new Term("name", "make")); --对name进行模糊搜索
9) QueryParser 强大搜索。QueryParser搜索字符串格式说明如下:
hans | 默认域包含hans |
hans pccpa 或 hans OR pccpa | 默认域包含hans或pccpa |
+name:hanlong +content:pccpa 或 name:hanlong AND content:pccpa | name=hanlong并且content包含pccpa |
name:hanlong | name=hanlong |
content:pccpa -name:hanlong 或 content:pccpa AND NOT name:hanlong | content包含pccpa且name!=hanlong |
(pccpa OR welcome) AND name:hanlong | 默认域包含pccpa或welcome并且name=hanlong |
content:"welcome to pccpa" | 默认域包含字符串"welcome to pccpa" |
content:"welcome pccpa"~3 | content域中单词"welcome"与单词"pccpa"间隔距离小于等于3 |
han* | 默认域是han开头 |
id:[1 TO 3] | ID从1到3 |

public void searchByQueryParse(Query query,int num) throws Exception { IndexSearcher searcher = getSearcher(); TopDocs tds = searcher.search(query, num); System.out.println("一共查询了:" + tds.totalHits); for(ScoreDoc sd:tds.scoreDocs) { Document doc = searcher.doc(sd.doc); System.out.println("id="+doc.get("id")+",name="+doc.get("name")+",email="+doc.get("email")+",attach="+doc.get("attach")); } searcher.close(); } @Test public void testSearchByQueryparse() throws Exception { //1、创建QueryParse对象,默认搜索域为content QueryParser parser = new QueryParser(Version.LUCENE_35,"content",new StandardAnalyzer(Version.LUCENE_35)); /********************************************************************************/ //可以改变默认空格操作符为AND,默认为OR parser.setDefaultOperator(Operator.AND); //正常搜索 如默认搜索符为OR,则搜索content域包含welcome或包含pccpa的记录,如默认搜索符为AND,则搜索content域既包含welcome又包含pccpa的记录 //当默认为OR时,"welcome pccpa"相当于"welcome OR pccpa" //当默认为AND时,"welcome pccpa"相当于"welcome AND pccpa"或"+welcome +pccpa" Query query = parser.parse("welcome pccpa"); /********************************************************************************/ /********************************************************************************/ //可以通过冒号改变在搜索字符串中改变搜索域 此处搜索name=hanlong的记录 query = parser.parse("name:hanlong"); /********************************************************************************/ /********************************************************************************/ //开启首位为*的通配符匹配,默认关闭,因为首位为*比较消耗资源 parser.setAllowLeadingWildcard(true); //可以使用通配符 使用*和? query = parser.parse("email:*@pccpa.cn"); /********************************************************************************/ /********************************************************************************/ //多条件查找,查找content中包含pccpa,且name中不包含hanlong且id不等于6的记录 query = parser.parse("content:pccpa -name:hanlong -id:6"); //上下两句相等 query = parser.parse("content:pccpa AND NOT name:hanlong AND NOT id:6"); /********************************************************************************/ /********************************************************************************/ //查询默认字段中包含American或China,但一定包含country的记录 query = parser.parse("(American OR China) AND country"); /********************************************************************************/ /********************************************************************************/ //区间范围查找只能匹配字符串 不能匹配数字范围 匹配数字需自己扩展 //区间范围查找,查找id从1到3的记录(包含1和3) 闭区间 TO必须大写 query = parser.parse("id:[1 TO 3]"); //区间范围查找,查找id从1到3的记录(不包含1和3) 开区间 query = parser.parse("id:{1 TO 3}"); /********************************************************************************/ /********************************************************************************/ //查询必须包含"welcome pccpa"字符的记录,注意使用"" query = parser.parse("content:\"welcome pccpa\""); /********************************************************************************/ /********************************************************************************/ //查询content中"welcome"和"pccpa"之间距离小于等于4的记录 query = parser.parse("content:\"welcome pccpa\"~4"); /********************************************************************************/ /********************************************************************************/ //模糊查询 查询content中包含"welcome"的记录 //query = parser.parse("content:welcome~"); /********************************************************************************/ su.searchByQueryParse(query, 10); }
5、分页搜索。
方法一:使用再查找的办法,即每次分页都查询所有数据再从中挑选符合自己页数要求的记录。Lucene官方强调由于搜索速度足够快,因此可以使用再分页查找。
方法二:lucene3.5版本后支持searchAfter,可根据上一页的最后一项查询PageSize项。

/** * 分页搜索 通过再查询 * @param pageIndex * @param pageSize */ public void searcherPaging(int pageIndex,int pageSize){ try { Directory directory = FSDirectory.open(new File("D:/lucene/index01")); IndexReader reader=IndexReader.open(directory); IndexSearcher searcher = new IndexSearcher(reader); QueryParser parser=new QueryParser(Version.LUCENE_35,"content",new StandardAnalyzer(Version.LUCENE_35)); Query query=parser.parse("pccpa"); TopDocs tds = searcher.search(query, pageIndex*pageSize); ScoreDoc[] sds = tds.scoreDocs; int start = (pageIndex-1)*pageSize; int end = Math.min(pageIndex*pageSize,sds.length); for(int i=start;i<end;i++){ Document d = searcher.doc(sds[i].doc); System.out.println(d.get("filename")+"["+d.get("path")+"]"); } reader.close(); } catch (Exception e) { e.printStackTrace(); } } /** * 分页搜索 lucene3.5以后版本支持searchAfter(ScoreDoc after, Query query, int n),其中after为上一个页面的最后一项,第一页则after为null * 关键步骤 * int lastIndex=(pageIndex-1)*pageSize-1; * if(lastIndex+1>tds.totalHits) return; * ScoreDoc lastDoc = lastIndex==-1?null:sds[lastIndex]; * tds = searcher.searchAfter(lastDoc, query, pageSize); * @param pageIndex * @param pageSize */ public void searcherPagingByAfter(int pageIndex,int pageSize){ try { Directory directory = FSDirectory.open(new File("D:/lucene/index01")); IndexReader reader=IndexReader.open(directory); IndexSearcher searcher = new IndexSearcher(reader); QueryParser parser=new QueryParser(Version.LUCENE_35,"content",new StandardAnalyzer(Version.LUCENE_35)); Query query=parser.parse("pccpa"); TopDocs tds = searcher.search(query, pageIndex*pageSize); ScoreDoc[] sds = tds.scoreDocs; int lastIndex=(pageIndex-1)*pageSize-1; //超出下标 返回 if(lastIndex+1>tds.totalHits) return; ScoreDoc after = lastIndex==-1?null:sds[lastIndex]; tds = searcher.searchAfter(after, query, pageSize); for(ScoreDoc sd:tds.scoreDocs){ Document d = searcher.doc(sd.doc); System.out.println(d.get("filename")+"["+d.get("path")+"]"); } reader.close(); } catch (Exception e) { e.printStackTrace(); } }
6、通过TokenStream显示分词结果(使用CharTremAttribute)。每个分词器都要经过两个工序Tokenizer和TokenFilter,其中Tokenizer主要作用将词分开,将数据划分为不同的语汇单元;而,TokenFilter主要作用是过滤没有意义的语汇单元。

//显示所有的分词信息 public static void displayAllToken(String str,Analyzer a){ try { TokenStream stream = a.tokenStream("content", new StringReader(str)); //位置增量的属性,存储语汇单元之间的距离 PositionIncrementAttribute pia = stream.addAttribute(PositionIncrementAttribute.class); //每个语汇单元的位置偏移量 OffsetAttribute oa = stream.addAttribute(OffsetAttribute.class); //存储每一个语汇单元的信息(分词单元信息) CharTermAttribute cta = stream.addAttribute(CharTermAttribute.class); //使用的分词器的类型信息 TypeAttribute ta = stream.addAttribute(TypeAttribute.class); for(;stream.incrementToken();){ System.out.print(pia.getPositionIncrement()+":"); System.out.println(cta+"["+oa.startOffset()+"-"+oa.endOffset()+"] type:"+ta.type()); } } catch (Exception e) { e.printStackTrace(); } } //测试 @Test public void test02(){ Analyzer a1 = new StandardAnalyzer(Version.LUCENE_35); Analyzer a2 = new StopAnalyzer(Version.LUCENE_35); Analyzer a3 = new SimpleAnalyzer(Version.LUCENE_35); Analyzer a4 = new WhitespaceAnalyzer(Version.LUCENE_35); String txt = "how are you thank you"; AnalyzerUtils.displayAllToken(txt, a1); System.out.println("--------------------------------"); AnalyzerUtils.displayAllToken(txt, a2); System.out.println("--------------------------------"); AnalyzerUtils.displayAllToken(txt, a3); System.out.println("--------------------------------"); AnalyzerUtils.displayAllToken(txt, a4); }
7、自定义分词器,一个小例子MyStopAnalyzer

//文件MyStopAnalyzer.java package org.lucene.util; import java.io.Reader; import java.util.Set; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.LetterTokenizer; import org.apache.lucene.analysis.LowerCaseFilter; import org.apache.lucene.analysis.StopAnalyzer; import org.apache.lucene.analysis.StopFilter; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.util.Version; public class MyStopAnalyzer extends Analyzer { private Set stops; public MyStopAnalyzer(String[] sws){ stops = StopFilter.makeStopSet(Version.LUCENE_35, sws,true); //添加默认停用词 stops.addAll(StopAnalyzer.ENGLISH_STOP_WORDS_SET); } public MyStopAnalyzer(){ stops = StopAnalyzer.ENGLISH_STOP_WORDS_SET; } @Override public TokenStream tokenStream(String fieldName, Reader reader) { return new StopFilter(Version.LUCENE_35, new LowerCaseFilter(Version.LUCENE_35, new LetterTokenizer(Version.LUCENE_35, reader)), stops); } }
8、中文分词器。中文分词器最重要最核心的就是中文词库,市面上有很多中文分词器可供使用,主要的有如下几种:
① Paoding:庖丁解牛分词器, 已经停止更新,不太推荐使用;
② mmseg:使用了搜狗的词库(可自定义扩展词库),推荐使用;
1) mmseg4j-1.8.5.zip下载;
2) data文件夹存放中文词库,其中words-my.dic文件可以自定义词库;
3) 使用时有两个包可供选择,区别是一个带中文词库,另一个不带中文词库,需初始化时指定词库路径,具体如下:
a、mmseg4j-all-1.8.5.jar 不带中文词库,初始化时需指定词库路径,如:Analyzer a = new MMSegAnalyzer(new File("D:\\mmseg4j-1.8.5\\data"));
b、mmseg4j-all-1.8.5-with-dic.jar 包中自带中文词库,可直接使用;
9、一个自定义同义词分词器的良好设计方案(示例源码下载,基于MMseg中文分词算法)
① 定义我的同义词类MySameAnalyzer,继承Analyzer

package org.lucene.util; import java.io.Reader; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import com.chenlb.mmseg4j.Dictionary; import com.chenlb.mmseg4j.MaxWordSeg; import com.chenlb.mmseg4j.analysis.MMSegTokenizer; //自定义我的同义词MySameAnalyzer,必须继承Analyer public class MySameAnalyzer extends Analyzer { private SamewordContext samewordContext; public MySameAnalyzer(SamewordContext swc) { samewordContext = swc; } //继承并实现获取分词流的方法,新增定义我的同义词处理类MySameTokenFilter,将经过MMseg中文分词得到的语汇单元再次进行加工处理 @Override public TokenStream tokenStream(String fieldName, Reader reader) { Dictionary dic = Dictionary.getInstance(); return new MySameTokenFilter( new MMSegTokenizer( new MaxWordSeg(dic), reader),samewordContext); } }
② 定义我的同义词处理类MySameTokenFilter

package org.lucene.util; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Stack; import org.apache.lucene.analysis.TokenFilter; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute; import org.apache.lucene.util.AttributeSource; public class MySameTokenFilter extends TokenFilter { private CharTermAttribute cta = null; private PositionIncrementAttribute pia = null; private AttributeSource.State current; private Stack<String> sames = null; private SamewordContext samewordContext; protected MySameTokenFilter(TokenStream input,SamewordContext samewordContext) { super(input); cta=this.addAttribute(CharTermAttribute.class); pia = this.addAttribute(PositionIncrementAttribute.class); sames = new Stack<String>(); this.samewordContext = samewordContext; } //这里的实现思路是,对于每一个语汇单元先匹配是否存在同义词,若存在,则保存当前的语汇单元状态,等处理跳转到下一个语汇单元的时候,然后将同义词插入语汇单元 //并返回上一个语汇单元处理时状态 @Override public boolean incrementToken() throws IOException { if(sames.size()>0){ //元素出栈,并获取同义词 String str = sames.pop(); restoreState(current); //还原状态 cta.setEmpty(); cta.append(str); //设置位置0 pia.setPositionIncrement(0); return true; } if(!input.incrementToken()) return false; //获取同义词并进行处理 if(getSameWords(cta.toString())){ //如果找到同义词,先保存当前状态 current = captureState(); } return true; } //这里自定义了一个同义词获取接口类SamewordContext private boolean getSameWords(String name) { String[] sws = samewordContext.getSameWords(name); if(sws!=null){ //找到同义词,则将同义词压入栈中 for (String s : sws) { sames.push(s); } return true; } return false; } }
③ 定义同义词接口类SamewordContext,可供实现不同的同义词处理

package org.lucene.util; //自定义接口,想扩展自己的分词器只需实现该接口 public interface SamewordContext { //获取同义词的方法 public String[] getSameWords(String name); }
④ 定义同义词接口实现类SimpleSameWordContext

package org.lucene.util; import java.util.HashMap; import java.util.Map; public class SimpleSameWordContext implements SamewordContext { private Map<String, String[]> maps = new HashMap<String, String[]>(); public SimpleSameWordContext() { maps.put("中国", new String[]{"天朝","大陆"}); maps.put("学生", new String[]{"读书人"}); } @Override public String[] getSameWords(String name) { return maps.get(name); } }