3.3.13 关键词高亮显示
因为搜索出来的文档内容可能比较长,所以不仅要检索出命中的文本,还要提供查询词在文本中出现的位置,方便用户直接看到想要找的信息。最终高亮显示的是一个片段(包含高亮词),而不是一个完整的列值。将来的浏览器应该可以支持显示全文的同时,先定位到查询词所在的位置。
在搜索结果中一般都有和用户搜索关键词相关的摘要。关键词一般都会高亮显示。从实现上说,就是把要突出显示的关键词前加上<B>标签,关键词后加上</B>标签。Lucene的highlighter包可以做到这一点。
Lucene有两个高亮显示的实现:一个是org.apache.lucene.search.highlight;还有一个是org.apache.lucene.search.vectorhighlight。下面是使用org.apache.lucene.search.highlight的例子:
doSearching("汽车"); //使用一个查询初始化Highlighter对象 Highlighter highlighter = new Highlighter(new QueryScorer(query)); //设置分段显示的文本长度 highlighter.setTextFragmenter(new SimpleFragmenter(40)); //设置最多显示的段落数量 int maxNumFragmentsRequired = 2; for (int i = 0; i < hits.length(); i++) { //取得索引库中存储的原始文本 String text = hits.doc(i).get(FIELD_NAME); TokenStream tokenStream=analyzer.tokenStream(FIELD_NAME, new StringReader(text)); //取得关键词加亮后的结果 String result = highlighter.getBestFragments(tokenStream, text, maxNumFragmentsRequired, "..."); System.out.println("\t" + result); }
QueryScorer()设置查询的query,这里还可以加上对字段列的限制,比如只对body条件的term高亮显示,可以使用new QueryScorer(query, “body”)。对于模糊匹配,需要先找出要高亮显示的词。可以使用SpanScorer和SimpleSpanFragmenter,或者使用QueryScorer和SimpleFragmenter。
使用SpanScorer和SimpleSpanFragmenter生成高亮段落的代码如下:
TokenStream stream = TokenSources.getTokenStream(fieldName, fieldContents, analyzer); SpanScorer scorer = new SpanScorer(query, fieldName, new CachingTokenFilter(stream)); Fragmenter fragmenter = new SimpleSpanFragmenter(scorer, 100); Highlighter highlighter = new Highlighter(scorer); highlighter.setTextFragmenter(fragmenter); String[] fragments = highlighter.getBestFragments(stream, fieldContents, 5);
为了实现关键词高亮显示,必须知道关键词在文本中的位置。对英文来说,可以在搜索的时候实时切分出位置。但是中文分词的速度一般来说相对慢很多。在Lucene1.4.3以后的版本中,TermVector支持保存Token.getPositionIncrement()和Token.startOffset() 以及Token.endOffset() 信息。利用Lucene中新增加的Token信息保存结果以后,就不需要为了高亮显示而在运行时解析每篇文档。为了实现一列的高亮显示,索引的时候通过Field对象保存该位置信息。
//增加文档时保存term位置信息 private void addDoc(IndexWriter writer, String text) throws IOException{ Document d = new Document(); Field f = new Field(FIELD_NAME, text , Field.Store.YES, Field.Index.TOKENIZED, Field.TermVector.WITH_POSITIONS_OFFSETS); d.add(f); writer.addDocument(d); } //利用term位置信息节省Highlight时间 void doStandardHighlights() throws Exception{ Highlighter highlighter =new Highlighter(this, new QueryScorer(query)); highlighter.setTextFragmenter(new SimpleFragmenter(20)); for (int i = 0; i < hits.length(); i++) { String text = hits.doc(i).get(FIELD_NAME); int maxNumFragmentsRequired = 2; String fragmentSeparator = "..."; TermPositionVector tpv = (TermPositionVector)reader.getTermFreqVector(hits.id(i), FIELD_NAME); TokenStream tokenStream=TokenSources.getTokenStream(tpv); String result = highlighter.getBestFragments( tokenStream, text, maxNumFragmentsRequired, fragmentSeparator); System.out.println("\t" + result); } }
最后把highlight包中的一个额外的判断去掉。对于中文来说没有明显的单词界限,所以下面这个判断是错误的:
tokenGroup.isDistinct(token)
注意上面代码中的highlighter.setTextFragmenter(new SimpleFragmenter(20)), SimpleFragmenter是一个最简单的段落分割器,它把文章按20个字分成一个段落。这种方式简单易行,但显得比较初步。有时会有一些没意义的符号出现在摘要的起始部分,例如逗号出现在摘要的开始位置。
RegexFragmenter是一个改进版本的段落分割器。它通过一个正则表达式匹配可能的热点区域。但它是为英文定制的。我们可以让它认识中文的字符段。
protected static final Pattern textRE = Pattern.compile("[\\w\u4e00-\u9fa5]+");
这样使用highlighter就变成了:
highlighter.setTextFragmenter(new RegexFragmenter(descLenth));
比如“我的妈妈”, Google搜索是这样:“<em>我的</em> <em>妈妈</em>”。实际貌似Lucene都会变成“<em>我</em><em>的</em><em>妈妈</em>”,这样对SEO(搜索引擎优化)很不好。<em>标签算是权重很高的标签,这样分使得页面会降很低,因为词都是分开的。另外,合并到一起,也省流量,对SEO有利。
不要高亮显示太长的文本,因为这样会影响搜索速度。