自己动手写分布式搜索引擎
上QQ阅读APP看书,第一时间看更新

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有利。

不要高亮显示太长的文本,因为这样会影响搜索速度。