相信很多人都听过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")这样就可以取出数组,再进行遍历。
final List<String> stopWords = Arrays.asList(
"a", "an", "and", "are", "as", "at", "be", "but", "by",
"for", "if", "in", "into", "is", "it",
"no", "not", "of", "on", "or", "such",
"that", "the", "their", "then", "there", "these",
"they", "this", "to", "was", "will", "with"
);
中文的当然比这个复杂多了,这个大家可以去看看中文分词的处理,实际上使用的时候只是实例化的时候换一个Analyzer而已,使用很简单。