lucene源码分析---13

lucene源码分析—高亮

本章分析lucene的highlighter高亮部分的代码,例子的代码如下,

    Analyzer analyzer = new StandardAnalyzer();
    QueryScorer scorer = new QueryScorer(query);
    Highlighter highlight = new Highlighter(scorer);
    TokenStream tokenStream = analyzer.tokenStream("fieldName", new StringReader(fieldValue));
    String highlightStr = highlight.getBestFragment(tokenStream, fieldValue);

该例子首先创建相应的类,例如分词器StandardAnalyzer,用于匹配词并打分的QueryScorer,以及高亮的主体Highlighter,然后调用分词器的tokenStream函数进行初始化,最后调用Highlighter的getBestFragment函数获取片段。
这些类的构造函数都相对简单,本章就不往下看了,下面重点看一下Highlighter的getBestFragment函数。

Highlighter::getBestFragment

  public final String getBestFragment(TokenStream tokenStream, String text)
    throws IOException, InvalidTokenOffsetsException
  {
    String[] results = getBestFragments(tokenStream,text, 1);
    return results[0];
  }

  public final String[] getBestFragments(
    TokenStream tokenStream,
    String text,
    int maxNumFragments)
    throws IOException, InvalidTokenOffsetsException
  {
    maxNumFragments = Math.max(1, maxNumFragments);
    TextFragment[] frag =getBestTextFragments(tokenStream,text, true,maxNumFragments);
    ArrayList<String> fragTexts = new ArrayList<>();
    for (int i = 0; i < frag.length; i++)
    {
      if ((frag[i] != null) && (frag[i].getScore() > 0))
      {
        fragTexts.add(frag[i].toString());
      }
    }
    return fragTexts.toArray(new String[0]);
  }

getBestFragment函数进而调用getBestFragments函数,maxNumFragments代表返回的TextFragment的最大数量。getBestTextFragments返回处理得到的TextFragment数组,里面封装了高亮的文本片段。

Highlighter::getBestFragment->getBestFragments->getBestTextFragments

  public final TextFragment[] getBestTextFragments(
    TokenStream tokenStream,
    String text,
    boolean mergeContiguousFragments,
    int maxNumFragments)
    throws IOException, InvalidTokenOffsetsException
  {
    ArrayList<TextFragment> docFrags = new ArrayList<>();
    StringBuilder newText=new StringBuilder();

    CharTermAttribute termAtt = tokenStream.addAttribute(CharTermAttribute.class);
    OffsetAttribute offsetAtt = tokenStream.addAttribute(OffsetAttribute.class);
    TextFragment currentFrag =  new TextFragment(newText,newText.length(), docFrags.size());
    fragmentScorer.setMaxDocCharsToAnalyze(maxDocCharsToAnalyze);  

    TokenStream tokenStream = fragmentScorer.init(tokenStream);   
    fragmentScorer.startFragment(currentFrag);
    docFrags.add(currentFrag);
    FragmentQueue fragQueue = new FragmentQueue(maxNumFragments);

    String tokenText;
    int startOffset;
    int endOffset;
    int lastEndOffset = 0;
    textFragmenter.start(text, tokenStream);

    TokenGroup tokenGroup=new TokenGroup(tokenStream);
    tokenStream.reset();
      for (boolean next = tokenStream.incrementToken(); next && (offsetAtt.startOffset()< maxDocCharsToAnalyze); next = tokenStream.incrementToken())
      {
        if((tokenGroup.getNumTokens() >0)&&(tokenGroup.isDistinct()))
        {
          startOffset = tokenGroup.getStartOffset();
          endOffset = tokenGroup.getEndOffset();
          tokenText = text.substring(startOffset, endOffset);
          String markedUpText=formatter.highlightTerm(encoder.encodeText(tokenText), tokenGroup);
          if (startOffset > lastEndOffset)
            newText.append(encoder.encodeText(text.substring(lastEndOffset, startOffset)));
          newText.append(markedUpText);
          lastEndOffset=Math.max(endOffset, lastEndOffset);
          tokenGroup.clear();
          if(textFragmenter.isNewFragment())
          {
            currentFrag.setScore(fragmentScorer.getFragmentScore());
            currentFrag.textEndPos = newText.length();
            currentFrag =new TextFragment(newText, newText.length(), docFrags.size());
            fragmentScorer.startFragment(currentFrag);
            docFrags.add(currentFrag);
          }
        }

        tokenGroup.addToken(fragmentScorer.getTokenScore());
      }

      ...

      for (Iterator<TextFragment> i = docFrags.iterator(); i.hasNext();){
        currentFrag = i.next();
        fragQueue.insertWithOverflow(currentFrag);
      }

      TextFragment frag[] = new TextFragment[fragQueue.size()];
      for (int i = frag.length - 1; i >= 0; i--){
        frag[i] = fragQueue.pop();
      }

      ArrayList<TextFragment> fragTexts = new ArrayList<>();
      for (int i = 0; i < frag.length; i++){
        fragTexts.add(frag[i]);
      }
      return fragTexts.toArray(new TextFragment[0]);
    }
  }

首先进行初始化工作,获取TokenStream中的CharTermAttribute和OffsetAttribute,分别保存了每个词的byte数组和位置信息。
创建TextFragment,表示一个文档中的一个片段。
newText表示最终被修改的文档,例如在一些关键字上加上标签后的文档。
docFrags是一个TextFragment列表,内部保存了所有处理后的文档片段TextFragment。
fragmentScorer是前面创建的QueryScorer,其setMaxDocCharsToAnalyze函数设置该QueryScorer针对每篇文档能够处理的最大字节数。
接下来调用QueryScorer的init函数进行初始化,该初始化的主要任务是将待处理的文档分词,并保存查询的词。
然后调用fragmentScorer的startFragment函数对第一个待处理的片段进行初始化,并添加第一个片段currentFrag到docFrags列表中,注意这里只是保存了引用,currentFrag还未处理过。
创建FragmentQueue,也用于保存TextFragment,最后会将docFrags中所有处理完的TextFragment再次保存在FragmentQueue中,FragmentQueue继承自PriorityQueue,PriorityQueue会对添加进的元素进行自动排序。maxNumFragments表示该FragmentQueue可以保存的元素个数。
再往下的textFragmenter在Highlighter的构造函数中被初始化为SimpleFragmenter,其作用是用于判断是否需要重新构造一个TextFragment,默认一个TextFragment能够包含的文档字节数为100,即对于一篇文档而言,每处理100个字节,就创建一个新的TextFragment。其start函数获取当前文档处理的位置引用,并将处理的TextFragment总数设置为1。
接下来创建TokenGroup,TokenGroup保存了多组有互相重叠的词,例如“abc”被分词为“ab”和“bc”,则该两个Term的处理结果会保存在一个TokenGroup中。

初始化工作完成后,接下来就要对一个个片段进行处理了,首先通过一个for循环遍历文档的所有分词,TokenGroup的getNumTokens函数返回TokenGroup中已经处理的词的数量,isDistinct表示下一个词和TokenGroup中已经处理的词是否有位置上的重叠,如果重叠则返回true,因此进入if语句,表示要对之前处理的词进行处理。
进入if语句后,首先通过TokenGroup的getStartOffset和getEndOffset函数获取该TokenGroup处理的所有词的起始位置和结束位置,然后根据该位置从text中截取片段tokenText。
接下来的encoder的encodeText对获取到的tokenText进行第一次处理,encoder默认为DefaultEncoder,其encodeText函数默认原样返回tokenText。
formatter默认为SimpleHTMLFormatter,其highlightTerm函数会对tokenText的前后加上默认的标签。
lastEndOffset表示上一次处理词在文档中的结束位置,如果startOffset大于lastEndOffset则表示上一次处理的词到当前处理的词有一段未处理的文本,需要把这段未处理的文本添加到最终的结果newText中。这里举个列子,假设待处理的文本为“abcdddabc”,分词器的字典里只包含“abc”这一个词,则当处理完第二个abc时,startOffset为5,lastEndOffset为2(假设起始位置是0),此时要把中间的“ddd”添加到newText中去。
接下来再添加刚刚SimpleHTMLFormatter修改后的文档片段markedUpText,并重新设置lastEndOffset,然后就要清空TokenGroup,用于存储下一个不重叠的词的处理结果了。
接下来通过isNewFragment函数判断是否要重新创建一个新的TextFragment了,如果需要,则设置当前TextFragment的分数,设置已经处理过的文档的结束位置,也即newText长度(注意前面已经将currentFrag添加进docFrags列表中了),然后重新创建一个新的TextFragment,初始化并添加到docFrags中。
退出if语句,QueryScorer的getTokenScore函数会获取文档中的下一个词,匹配成功后返回该次对应的分数,然后通过TokenGroup的addToken函数记录到当前TokenGroup中。

退出for循环,此时docFrags中保存了所有经过处理后的片段,接下来遍历这些片段,并通过FragmentQueue的insertWithOverflow函数将这些片段插入FragmentQueue中,FragmentQueue默认会对这些插入的片段进行排序,最后通过其pop函数,返回得分最高的片段,封装,并返回。

初始化

Highlighter::getBestFragment->getBestFragments->getBestTextFragments->QueryScore::init

  public TokenStream init(TokenStream tokenStream) throws IOException {
    position = -1;
    termAtt = tokenStream.addAttribute(CharTermAttribute.class);
    posIncAtt = tokenStream.addAttribute(PositionIncrementAttribute.class);
    return initExtractor(tokenStream);
  }

init函数主要获取其中的CharTermAttribute和PositionIncrementAttribute,分别保存了词的byte数组和位置信息,然后调用initExtractor继续初始化。

Highlighter::getBestFragment->getBestFragments->getBestTextFragments->QueryScore::init->initExtractor

  private TokenStream initExtractor(TokenStream tokenStream) throws IOException {
    WeightedSpanTermExtractor qse = newTermExtractor(defaultField);
    qse.setMaxDocCharsToAnalyze(maxCharsToAnalyze);
    qse.setExpandMultiTermQuery(expandMultiTermQuery);
    qse.setWrapIfNotCachingTokenFilter(wrapToCaching);
    qse.setUsePayloads(usePayloads);
    this.fieldWeightedSpanTerms = qse.getWeightedSpanTermsWithScores(query, 1f,
      tokenStream, field, reader);

    return qse.getTokenStream();
  }

newTermExtractor用于创建WeightedSpanTermExtractor,进行相应的设置后,通过WeightedSpanTermExtractor的getWeightedSpanTermsWithScores函数创建的fieldWeightedSpanTerms的结构为Map,其中Key为查询的Term,Value为WeightedSpanTerm对应Term的信息,用于评分。
getWeightedSpanTermsWithScores还会对待处理的文档进行分词,最后结果保存在WeightedSpanTermExtractor的tokenStream成员变量中,最后返回。

Highlighter::getBestFragment->getBestFragments->getBestTextFragments->QueryScore::init->initExtractor->WeightedSpanTermExtractor::getWeightedSpanTerms

  public Map<String,WeightedSpanTerm> getWeightedSpanTerms(Query query, float boost, TokenStream tokenStream, String fieldName) throws IOException {

    Map<String,WeightedSpanTerm> terms = new PositionCheckingMap<>();
    this.tokenStream = tokenStream;
    try {
      extract(query, boost, terms);
    } finally {
      IOUtils.close(internalReader);
    }

    return terms;
  }

getWeightedSpanTerms函数主要通过extract函数,最后将查询语句分词后保存在Map结构terms中并返回。

Highlighter::getBestFragment->getBestFragments->getBestTextFragments->QueryScore::init->initExtractor->WeightedSpanTermExtractor::getWeightedSpanTerms->extract

  protected void extract(Query query, float boost, Map<String,WeightedSpanTerm> terms) throws IOException {

    ...

    else if (query instanceof TermQuery || query instanceof SynonymQuery) {
      extractWeightedTerms(terms, query, boost);
    }

    ...

  }

extract会根据不同的Query类型调用不同的函数进行处理,本章假设Query的类型为TermQuery,因此接下来调用extractWeightedTerms函数。

Highlighter::getBestFragment->getBestFragments->getBestTextFragments->QueryScore::init->initExtractor->WeightedSpanTermExtractor::getWeightedSpanTerms->extract->extractWeightedTerms

  protected void extractWeightedTerms(Map<String,WeightedSpanTerm> terms, Query query, float boost) throws IOException {
    Set<Term> nonWeightedTerms = new HashSet<>();
    final IndexSearcher searcher = new IndexSearcher(getLeafContext());
    searcher.createNormalizedWeight(query, false).extractTerms(nonWeightedTerms);

    for (final Term queryTerm : nonWeightedTerms) {

      if (fieldNameComparator(queryTerm.field())) {
        WeightedSpanTerm weightedSpanTerm = new WeightedSpanTerm(boost, queryTerm.text());
        terms.put(queryTerm.text(), weightedSpanTerm);
      }
    }
  }

getLeafContext函数会对待高亮的片段进行分词,并将分词结果保存在TokenSteam中。
createNormalizedWeight和前面章节的分析类似,用于获得查询词的整体信息。
extractTerms将TermQuery中的词添加到nonWeightedTerms集合中。
最后创建WeightedSpanTerm封装了每个词的信息并添加到terms中。

Highlighter::getBestFragment->getBestFragments->getBestTextFragments->QueryScore::init->initExtractor->WeightedSpanTermExtractor::getWeightedSpanTerms->extract->extractWeightedTerms->getLeafContext

  protected LeafReaderContext getLeafContext() throws IOException {

    ...

    final MemoryIndex indexer = new MemoryIndex(true, usePayloads);
    tokenStream = new CachingTokenFilter(new OffsetLimitTokenFilter(tokenStream, maxDocCharsToAnalyze));
    indexer.addField(DelegatingLeafReader.FIELD_NAME, tokenStream);
    final IndexSearcher searcher = indexer.createSearcher();
    internalReader = ((LeafReaderContext) searcher.getTopReaderContext()).reader();
    this.internalReader = new DelegatingLeafReader(internalReader);


    return internalReader.getContext();
  }

getLeafContext创建MemoryIndex,即将索引信息保存在内存中,而不是硬盘文件中。
接下来创建CachingTokenFilter,通过MemoryIndex的addField对文档进行分词并保存。
IndexSearcher的add函数内部会对文档进行分词,该函数较为复杂,在之前的章节已经分析过了。
最后创建DelegatingLeafReader。

处理过程

Highlighter::getBestFragment->getBestFragments->getBestTextFragments->TokenGroup::isDistinct

  boolean isDistinct() {
    return offsetAtt.startOffset() >= endOffset;
  }

offsetAtt的startOffset函数表示当前处理的词在文档中的起始位置,endOffset则表示之前处理的词在文档中最大的结束位置,如果startOffset大于等于endOffset,则表示当前处理的词和之前处理的词在位置上并不重叠,此时返回true,否则返回false。

Highlighter::getBestFragment->getBestFragments->getBestTextFragments->DefaultEncoder::encodeText

  public String encodeText(String originalText) {
    return originalText;
  }

默认的DefaultEncoder直接原样返回originalText字符串。

Highlighter::getBestFragment->getBestFragments->getBestTextFragments->SimpleHTMLFormatter::highlightTerm

  public String highlightTerm(String originalText, TokenGroup tokenGroup) {
    if (tokenGroup.getTotalScore() <= 0) {
      return originalText;
    }

    StringBuilder returnBuffer = new StringBuilder(preTag.length() + originalText.length() + postTag.length());
    returnBuffer.append(preTag);
    returnBuffer.append(originalText);
    returnBuffer.append(postTag);
    return returnBuffer.toString();
  }

TokenGroup的getTotalScore函数返回内部的成员变量tot,表示是否有打分高于0的文档,假设有,就对originalText的前后分别加上preTag和postTag标签,默认的标签为B标签。

Highlighter::getBestFragment->getBestFragments->getBestTextFragments->SimpleFragmenter::isNewFragment

  public boolean isNewFragment() {
    boolean isNewFrag = offsetAtt.endOffset() >= (fragmentSize * currentNumFrags);
    if (isNewFrag) {
      currentNumFrags++;
    }
    return isNewFrag;
  }

fragmentSize是当前段的大小,默认为100个字符,currentNumFrags表示一共处理的TextFragment的数量,fragmentSize*currentNumFrags当前的TextFragment在文档中最大的结束位置,endOffset表示如果当前处理的词在文档中的位置,如果大于fragmentSize*currentNumFrags,则超过了一个段的大小,此时返回true,并递增currentNumFrags。

Highlighter::getBestFragment->getBestFragments->getBestTextFragments->QueryScorer::getTokenScore

  public float getTokenScore() {
    String termText = termAtt.toString();
    WeightedSpanTerm weightedSpanTerm;
    if ((weightedSpanTerm = fieldWeightedSpanTerms.get(
              termText)) == null) {
      return 0;
    }
    float score = weightedSpanTerm.getWeight();
    if (!foundTerms.contains(termText)) {
      totalScore += score;
      foundTerms.add(termText);
    }
    return score;
  }

首先获取当前文档中待处理的词至termText中,然后从fieldWeightedSpanTerms中查看该词是否为查询的某个词,fieldWeightedSpanTerms是一个Map结构,前面分析过了,里面Key值保存了查询短语经过分词后的所有词,其Value值为WeightedSpanTerm,保存了该词的信息,如果get返回null,则说明当前文档中的词termText并不在查询短语中,直接返回0,如果不为null,则获取该词对应的WeightedSpanTerm结构,并通过getWeight函数获取该词的得分。再往下计算总得分,并把已经匹配到的词添加到foundTerms后,返回当前得分。

Highlighter::getBestFragment->getBestFragments->getBestTextFragments->TokenGroup::addToken

  void addToken(float score) {
    if (numTokens < MAX_NUM_TOKENS_PER_GROUP) {
      final int termStartOffset = offsetAtt.startOffset();
      final int termEndOffset = offsetAtt.endOffset();
      if (numTokens == 0) {
        startOffset = matchStartOffset = termStartOffset;
        endOffset = matchEndOffset = termEndOffset;
        tot += score;
      } else {
        startOffset = Math.min(startOffset, termStartOffset);
        endOffset = Math.max(endOffset, termEndOffset);
        if (score > 0) {
          if (tot == 0) {
            matchStartOffset = termStartOffset;
            matchEndOffset = termEndOffset;
          } else {
            matchStartOffset = Math.min(matchStartOffset, termStartOffset);
            matchEndOffset = Math.max(matchEndOffset, termEndOffset);
          }
          tot += score;
        }
      }
      Token token = new Token();
      token.setOffset(termStartOffset, termEndOffset);
      token.setEmpty().append(termAtt);
      tokens[numTokens] = token;
      scores[numTokens] = score;
      numTokens++;
    }
  }

MAX_NUM_TOKENS_PER_GROUP表示每个TokenGroup能够存储词的最大数量。如果不超过这个值,则首先获取当前词的起始位置termStartOffset和结束位置termEndOffset。
numTokens为0表示当前TokenGroup还没有记录任何词,此时简单的赋值就行,如果不为0,则要重新计算TokenGroup包含所有词的最小位置startOffset和最大位置endOffset,以及有效词(假设有些词的评分为0,则跳过该词)的最小位置matchStartOffset和最大位置matchEndOffset。
最后创建Token,保存相应的信息,并存储到TokenGroup的成员变量数组tokens和scores中。

最后看一下FragmentQueue是如何对插入的TextFragment进行排序的,FragmentQueue继承自PriorityQueue,重载了lessThan函数,每当插入一个元素时,都会调用lessThan函数进行判断。

FragmentQueue::lessThan

  public final boolean lessThan(TextFragment fragA, TextFragment fragB){
    if (fragA.getScore() == fragB.getScore())
      return fragA.fragNum > fragB.fragNum;
    else
      return fragA.getScore() < fragB.getScore();
  }

lessThan函数重点比较片段TextFragment的得分,在得分相等的情况下,比较fragNum,表示片段的生成次序。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Lucene是一个开源的全文搜索引擎工具包,它提供了丰富的API和工具,可以用于创建和管理全文索引。Lucene源码是以Java编写的,其主要目的是为了提供高效的文本搜索和索引功能。 在Lucene源码中,最核心的组件是索引和搜索。索引是指将文本数据分解为若干个文档(Document),然后对每个文档建立一种倒排索引结构。所谓倒排索引,是指通过某个关键词直接找到包含该关键词的文档,而不是通过文档去找关键词。这种倒排索引结构能够提供快速的搜索和检索功能。 Lucene源码还包含一些高级的搜索功能,例如搜索结果的排序和评分。排序是指根据某种规则,将搜索结果按相关性或其他因素进行排序,以便更好地展示给用户。评分是指根据某种算法,为搜索结果打分,以衡量其与查询的相关性。这些高级搜索功能可以根据用户的需求进行定制和扩展。 此外,Lucene源码还包含了一些辅助功能,例如分词器(Tokenizer)和过滤器(Filter)。分词器用于将输入的文本数据拆分为词组,并去除无关的符号和停用词。过滤器则用于对已经拆分的词组进行处理,例如大小写转换、同义词替换等。 总体来说,Lucene源码是非常庞大和复杂的,其中包含了大量的算法和数据结构。研究和理解Lucene源码需要对Java编程和搜索引擎原理有一定的了解。通过对源码的阅读和分析,可以更好地理解Lucene的工作机制,并能够根据自己的需求进行二次开发和定制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值