最近试用了一下Lucene这个搜索框架,发觉还是蛮不错的。许多现在比较火的搜索服务器(如Elasticsearch)的内核用的就是Lucene。目前手头有一个小的JavaEE项目程序,想着再给它装一个Elasticsearch服务器似乎太浪费资源,所需要的搜索功能其实很简单,不需要太复杂。于是,何不趁机学学它的内核框架Lucene如何使用呢?
使用Lucene有很多种模式,本文就拿最简单的模式讲起,其基本流程如下:
1、用Lucene的包读取所需的数据 --> 创建索引 --> 保存索引文件
2、用Lucene的包读取索引文件 --> 使用搜索服务
了解这两个流程以后,基本上对Lucene的使用就能大体了解,剩下的无非是怎么读要检索的数据源(从数据库、文件、还是其它网络服务器),怎么建立索引(哪个域需要分词和索引、哪些数据不需要分词只需要单独的存储、使用什么分词器、中文还是其它什么语言)
目前项目的需求是为项目内的静态网页提供搜索服务,而网页的内容都是中文,因此还需要经过加工和分词的过程。最终的目标,是做一个类似简单的百度的功能。以下是获取到数据后的索引建立部分代码(INDEXDIR为存储索引的目录):
private static void index(List<String[]> webAndContentList) throws IOException{
Directory dir = FSDirectory.open(Paths.get(INDEXDIR));
Analyzer analyzer;
analyzer = getDefaultAnalyzer();
IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
try (IndexWriter writer = new IndexWriter(dir, iwc)) {
for (String[] webAndContent : webAndContentList) {
Document doc = new Document();
doc.add(new StringField("pagename", webAndContent[0], Field.Store.YES));
doc.add(new TextField("content", webAndContent[1], Field.Store.YES));
doc.add(new StringField("index", webAndContent[2], Field.Store.YES));
doc.add(new StringField("title", webAndContent[3], Field.Store.YES));
doc.add(new StringField("img", webAndContent[4], Field.Store.YES));
System.out.println(Arrays.toString(webAndContent));
writer.addDocument(doc); // 添加文档
}
}
}
此时需要引用(注意,使用nio部分的包在java 9里边可能会遇到读取问题):
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import java.nio.file.Paths;
实际上的搜索效果,很大程度取决于数据源怎么筛选 和 分词器的选择,需要不断地去尝试。因此,推荐实际使用前,先建立一个本地的JavaSE程序,小规模地建立索引,并且把分词的部分做成可拆卸的,测试测试搜索效果。如果是要索引网页,那么就得事先对网页数据源做一些处理,比如去掉script标签、html标签,去掉js部分,css部分的代码,只把网页的主题内容保留下来,毕竟,我们是为了搜索网页的内容,而不是网页的代码部分(有一次忘记去掉标签,结果导致分词器爆炸,几MB网页得出快1GB的索引,全都废掉了)。
使用java的try-with-resource功能可以避免写file.close类似的冗余代码。接下来就是读取索引文件并进行搜索的部分代码:
public static List< SearchResultEntity> search(String q) throws IOException, InvalidTokenOffsetsException, ParseException{
List< SearchResultEntity> m = new ArrayList<>();
Directory dir = FSDirectory.open(Paths.get(INDEXDIR));
try (IndexReader reader = DirectoryReader.open(dir)) {
IndexSearcher is = new IndexSearcher(reader);
Analyzer analyzer;
analyzer = getDefaultAnalyzer();
QueryParser parser = new QueryParser("content", analyzer);//进行分析的字段
Query query = parser.parse(q);
TopDocs hits = is.search(query, 10);//查询结果选择前10个
QueryScorer scorer = new QueryScorer(query);
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
String preTag = "\0";
String postTag = "\1";
SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter(preTag, postTag);//设置查询结果红色字体加粗(可自行修改默认为粗体)
Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);
highlighter.setTextFragmenter(fragmenter);
ScoreDoc[] scoreDoclist = hits.scoreDocs;
for (ScoreDoc scoreDoc : scoreDoclist) {
Document doc = is.doc(scoreDoc.doc);
String desc = doc.get("content");
if (desc != null) {
TokenStream tokenStream = analyzer.tokenStream("content", new StringReader(desc));
String[] onePageResultList = highlighter.getBestFragments(tokenStream, desc, 10);
String website = doc.get("pagename");
String title = doc.get("title");
String img = doc.get("img");
List<String> htmlList = new ArrayList<>();
// 每个页面单独有这么多搜索结果
for(String singleResult:onePageResultList){
htmlList.add(getKeyContent(singleResult, preTag, postTag));
}
SearchResultEntity e = new SearchResultEntity(website, htmlList, title, img);
m.add(e);
}
}// End of For Loop --------------------------------------------
}
return m;
}
其中SearchResultEntity为自己创作的保存结果的类。从代码看,我们可以知道,整个搜索流程包括:
1、读取索引文件
2、获取要分析的字段,并标注要使用的分词(不但对索引的文章分词,对用户的输入也要分词)
3、设定查询结果的评分器(用于评分哪些才是最符合用户搜索要求的结果)
4、设置检索词高亮