再入lucene
相信很多人都听过lucene,这样一个用于实现搜索引擎功能的一个工具包。说它是一个工具包,因为它只是提供我们用于索引和查询的工具,并不包含真正一个搜索引擎需要的其他东西——爬虫,抽取等等。
废话不多说,我们直接来看看lucene4的一些简单的例子(lucene4.1已经出了,暂时没来得及看有什么变化,lucene的API经常修改,所以版本间可能会有些不同的,大家需要注意):
还是给下lucene的下载地址,有些朋友喜欢用百度的,但那家伙太坑人了,很多外国网站没的。下载地址如下:http://archive.apache.org/dist/lucene/java/4.0.0/
下载后,我们可以看到有一堆的文件夹,首先demo里面当然就是例子了,这个不多说。看看还是有好处的。
我们经常使用的无非是下面几个:queryparser,analysis,core(核心,显然是必须的了),queries,其他的貌似比较少用到,除非你进行比较深入的定制,可能会用到suggest(建议方面的东西),其他的因为我暂时没接触到,就不在这里误人子弟了,以后有机会再学习。
好,接下来来点干货了。我在例子中用到的包有analysis,queryparser,core,另外的暂没有使用。说到底是一个索引工具包,首先当然是建立索引啦,
比如我现在在ubuntu下,我的eclipse目录是/opt/programs/eclipse,我要索引它里面的所有文件的文件名,放在一个名叫fileName的field里面。
在看代码前,我们先来了解一下lucene索引的结构:
一个索引是由无数多个doc组成的,而doc也是由无数多个field组成的。举个比较符合生活的例子:我们可以把一个索引想像成一本书,而doc则是每一本书的第一篇文章,而field则是每一篇文章里面的词语,词组,我们就是通过词组去找到那篇文章。(当然,说根据词组等去找一篇文章不太可行,我们可以把词语换成标题,每一篇文章的标题,这样肯定可以找到对应的文章的。)
进行lucene检索的情况就是查找field,如果找到对应的,则把该doc取出来,作为我们查询出来的结果。
1)首先我们看看索引情况:
package com.shun.lucene.simple; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.IndexWriterConfig.OpenMode; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.util.Version; public class IndexAllFiles { public static void main(String[] args) { Directory dir = null; IndexWriter indexWriter = null; Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_40); try { dir = FSDirectory.open(new File("allFiles")); IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_40,analyzer); /* * 如果索引存在的情况下,我们采取什么样的策略 * 这里我们使用的是:存在的情况下新增 * 不存在的情况下创建 * 这符合我们一般的使用规则 */ iwc.setOpenMode(OpenMode.CREATE); indexWriter = new IndexWriter(dir,iwc); File file = new File("/opt/programs/eclipse"); System.out.println("Starting analyze fileNames...Please wait..."); List<String> fileNameList = checkAllFile(file); System.out.println("Ending analyze fileNames..."); for (String fileName:fileNameList) { /* * field就是lucene里面的最小单位了,一个索引里面可以有一大堆Document,而一个document里面可以有很多field * 这跟文章类似,一本书里面有很多篇文章,而一篇文章里面可以有很多单词,词组等。 */ Document doc = new Document(); Field field = new Field("fileName",fileName.toString(),TextField.TYPE_STORED); doc.add(field); indexWriter.addDocument(doc); } System.out.println("Writing index to file"); System.out.println("Finish indexing"); } catch (IOException e) { e.printStackTrace(); } finally { try { indexWriter.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * 此方法只是循环列出所传入的文件夹下的所有文件列表 * @param dir * @return */ private static List<String> checkAllFile(File dir) { List<String> fileNameList = new ArrayList<String>(); //不是目录的话里面肯定没文件了,我们在下面直接添加该文件到列表就OK了 if (dir.isDirectory()) { for (File file:dir.listFiles()) { //由于我在linux下,并且当前运行eclipse的用户非root,会有权限的问题,所以我这里加了一个判断是否可读 //正常情况下在windows上不需要,只需要判断是否是目录,然后递归调用即可。 if(file.isDirectory() && file.canRead()) { fileNameList.addAll(checkAllFile(file)); } else { fileNameList.add(file.getName()); } } } else { fileNameList.add(dir.getName()); } return fileNameList; } }相信注释已经够清楚了吧。
这里有个地方需要解释下:
for (String fileName:fileNameList) { /* * field就是lucene里面的最小单位了,一个索引里面可以有一大堆Document,而一个document里面可以有很多field * 这跟文章类似,一本书里面有很多篇文章,而一篇文章里面可以有很多单词,词组等。 */ Document doc = new Document(); Field field = new Field("fileName",fileName.toString(),TextField.TYPE_STORED); doc.add(field); indexWriter.addDocument(doc); }
我们这里是通过多个doc来添加的,注意,我们频繁的addDocument可能效率不高,我们其实可以把所有的fileName放在一个doc里面,doc并不像我们的map,它不会合并相同的名称的field的。我们稍后来看看这种情况。
2)有了索引之后,我们肯定就是需要在索引当中找我们需要的东西了。我们搜索一个eclipse这个单词,当然,我们前面只有一个fileName这个field,没有其他的,也只能查那一个了。
package com.shun.lucene.simple; import java.io.File; import java.io.IOException; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.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 ReadFromIndex { public static void main(String[] args) { Directory dir = null; Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_40); try { //这里打开的当然就是我们之前建立的索引文件夹了 dir = FSDirectory.open(new File("allFiles")); //DirectoryReader这个是lucene的一个工具类,在以前的IndexSearcher里面,貌似不用使用这个 IndexSearcher searcher = new IndexSearcher(DirectoryReader.open(dir)); QueryParser parser = new QueryParser(Version.LUCENE_40,"fileName",analyzer); Query query = parser.parse("eclipse"); TopDocs topDocs = searcher.search(query, 10); System.out.println("找到结果数:"+topDocs.totalHits); for (ScoreDoc scoreDoc:topDocs.scoreDocs) { System.out.println(searcher.doc(scoreDoc.doc).get("fileName")); } } catch (IOException e) { e.printStackTrace(); } catch (ParseException e) { e.printStackTrace(); } } }
我们可以看到,查询很简单,只是指定一个索引所在目录,然后来个IndexSearcher,再结合几个QueryParser和Query,然后就搞定了。
结果就不截图了,大家私下去运行下。我们来看看索引里面的内容,这里又涉及到另外的工具了,luke,这个是用于看索引的工具,可以到这里去下载:http://code.google.com/p/luke/downloads/list。但到了4.0.0alpha就没更新好久了,好在alpha对4.0的正式版还能用。
我们打开我们的索引,可以看到doc的内容:
在这里,我们可以看到doc的数量,2835,还有doc里面的field名称和value,另外的IdfXXX和Norm这个大家有兴趣自己可以去研究,是lucene的一些属性信息,包含是否索引、分词等等。
这里,我们是通过多个doc来添加的,我们上面说了,其他我们完全可以把多个field添加到同一个doc里面,虽然同一个fielName,但无所谓,因为索引并不是map形式的,不会覆盖相应的值。
我们回到上面的创建索引的例子,我们把循环的代码修改如下:
Document doc = new Document(); for (String fileName:fileNameList) { /* * field就是lucene里面的最小单位了,一个索引里面可以有一大堆Document,而一个document里面可以有很多field * 这跟文章类似,一本书里面有很多篇文章,而一篇文章里面可以有很多单词,词组等。 */ Field field = new Field("fileName",fileName.toString(),TextField.TYPE_STORED); doc.add(field); } indexWriter.addDocument(doc);
我们把Document提出去了,并且只在最后的时候来一个addDocument,添加到indexWriter中去。这里,我们可以点击luke的File->Re-open current index来重新打开当前的索引。我们可以看到:
我们可以看到,doc只剩一个了,而里面的fileName却有一大堆了。
当然,我们在一个doc里面添加了多个同样名字的filedName之后,在检索的时候通过searcher.doc(scoreDoc.doc).get("xxx")这样取出来的只能是第一个了,我们可以换成getValues("xxx")这样就可以取出数组,再进行遍历。