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,表示片段的生成次序。