热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

lucene源码分析---12

lucene源码分析—PhraseQuery本章开始查看PhraseQuery的源码,PhraseQuery顾名思义是短语查询,先来看PhraseQuery是如何构造的,

lucene源码分析—PhraseQuery

本章开始查看PhraseQuery的源码,PhraseQuery顾名思义是短语查询,先来看PhraseQuery是如何构造的,

        PhraseQuery.Builder queryBuilder = new PhraseQuery.Builder();
queryBuilder.add(new Term("body","hello"));
queryBuilder.add(new Term("body","world"));
queryBuilder.setSlop(100);
PhraseQuery query = queryBuilder.build();

首先创建PhraseQuery.Builder,然后向其添加短语中的每个词Term,PhraseQuery要求Term的数量至少为2个,这很容易理解,例子中的“body”表示域名,“hello”和“world”代表词的内容。然后通过setSlop设置词的最大间距,最后通过build函数创建PhraseQuery 。

创建完PhraseQuery后,就调用IndexSearcher的search函数进行查询,最终会执行到如下代码,

  public void search(Query query, Collector results)
throws IOException {
search(leafContexts, createNormalizedWeight(query, results.needsScores()), results);
}

createNormalizedWeight函数找出短语所在文档的整体信息,然后通过search函数对每个文档进行评分,筛选出最后的文档。

IndexSearcher::createNormalizedWeight

  public Weight createNormalizedWeight(Query query, boolean needsScores) throws IOException {
query = rewrite(query);
Weight weight = createWeight(query, needsScores);

...

return weight;
}

对PhraseQuery而言,rewrite函数什么也不做,即PhraseQuery不需要改写。createNormalizedWeight函数接下来通过createWeight函数创建Weight,封装短语的整体信息。

IndexSearcher::createNormalizedWeight->createWeight

  public Weight createWeight(Query query, boolean needsScores) throws IOException {

...

Weight weight = query.createWeight(this, needsScores);

...

return weight;
}

public Weight createWeight(IndexSearcher searcher, boolean needsScores) throws IOException {
return new PhraseWeight(searcher, needsScores);
}

createWeight函数最终会创建PhraseWeight。

IndexSearcher::createNormalizedWeight->createWeight->PhraseQuery::createWeight->PhraseWeight::PhraseWeight

  public PhraseWeight(IndexSearcher searcher, boolean needsScores)
throws IOException {
final int[] positiOns= PhraseQuery.this.getPositions();
this.needsScores = needsScores;
this.similarity = searcher.getSimilarity(needsScores);
final IndexReaderContext cOntext= searcher.getTopReaderContext();
states = new TermContext[terms.length];
TermStatistics termStats[] = new TermStatistics[terms.length];
for (int i = 0; i final Term term = terms[i];
states[i] = TermContext.build(context, term);
termStats[i] = searcher.termStatistics(term, states[i]);
}
stats = similarity.computeWeight(searcher.collectionStatistics(field), termStats);
}

getPositions函数返回查询的每个词在查询短语中的位置,例如查询“hello world”,则getPositions返回[0,1]。
getSimilarity默认返回BM25Similarity,用于评分,开发者可以定义自己的Similarity并设置进IndexerSearch中,这里就会返回自定义的Similarity。
getTopReaderContext默认返回CompositeReaderContext。再往下,遍历每个查询的词Term,通过TermContext的build函数获取每个词的整体信息,然后通过termStatistics函数将部分信息封装成TermStatistics。
最后通过Similarity的computeWeight函数计算所有词的整体信息保存在stats中。
这些函数在前面的章节已经分析过了,和PhraseQuery并无关系,这里就不往下看了。

回到一开始的search函数中,接下来继续通过search函数进行搜索。

IndexSearcher::search

  protected void search(List leaves, Weight weight, Collector collector)
throws IOException {
for (LeafReaderContext ctx : leaves) {
final LeafCollector leafCollector = collector.getLeafCollector(ctx);
BulkScorer scorer = weight.bulkScorer(ctx);
scorer.score(leafCollector, ctx.reader().getLiveDocs());
}
}

getLeafCollector返回的LeafCollector用来打分并对评分后的文档进行排序。
bulkScorer返回的BulkScorer用来为多个文档打分。
最终通过BulkScorer的scorer函数进行打分并获得最终的结果。

IndexSearcher::search->PhraseWeight::bulkScorer

  public BulkScorer bulkScorer(LeafReaderContext context) throws IOException {
Scorer scorer = scorer(context);
return new DefaultBulkScorer(scorer);
}

scorer函数最终会调用PhraseWeight的scorer函数获得Scorer,然后通过DefaultBulkScorer进行封装和处理。

IndexSearcher::search->PhraseWeight::bulkScorer->scorer

    public Scorer scorer(LeafReaderContext context) throws IOException {
final LeafReader reader = context.reader();
PostingsAndFreq[] postingsFreqs = new PostingsAndFreq[terms.length];

final Terms fieldTerms = reader.terms(field);
final TermsEnum te = fieldTerms.iterator();
float totalMatchCost = 0;

for (int i = 0; i length; i++) {
final Term t = terms[i];
final TermState state = states[i].get(context.ord);
te.seekExact(t.bytes(), state);
PostingsEnum postingsEnum = te.postings(null, PostingsEnum.POSITIONS);
postingsFreqs[i] = new PostingsAndFreq(postingsEnum, positions[i], t);
totalMatchCost += termPositionsCost(te);
}

return new SloppyPhraseScorer(this, postingsFreqs, slop,
similarity.simScorer(stats, context),
needsScores, totalMatchCost);
}

terms获得FieldReader,并通过其iterator函数创建SegmentTermsEnum,该过程在前面的章节有叙述。
接下来遍历待搜索的词Term,针对每个Term,通过SegmentTermsEnum的postings函数返回倒排列表对应的.doc、.pos和.pay文件的读取信息,并根据.tip文件中的索引数据设置这三个文件的输出流指针。封装成PostingsAndFreq。
假设slop>0,scorer函数最后创建SloppyPhraseScorer并返回,SloppyPhraseScorer用于计算短语查询的最终得分。

IndexSearcher::search->PhraseWeight::bulkScorer->DefaultBulkScorer

    public DefaultBulkScorer(Scorer scorer) {
this.scorer = scorer;
this.iterator = scorer.iterator();
this.twoPhase = scorer.twoPhaseIterator();
}

DefaultBulkScorer构造函数中的iterator和twoPhaseIterator函数都是创建迭代器,用于获取到下一个文档ID,还可以计算词位置登信息。

回到IndexSearcher函数中,接下来通过刚刚创建的DefaultBulkScorer的score函数计算最终的文档得分并排序,DefaultBulkScorer的score函数最终会调用到scoreAll函数。

IndexSearcher::search->DefaultBulkScorer::score->score->scoreAll

    static void scoreAll(LeafCollector collector, DocIdSetIterator iterator, TwoPhaseIterator twoPhase, Bits acceptDocs) throws IOException {
final DocIdSetIterator approximation = twoPhase.approximation();
for (int doc = approximation.nextDoc(); doc != DocIdSetIterator.NO_MORE_DOCS; doc = approximation.nextDoc()) {
if ((acceptDocs == null || acceptDocs.get(doc)) && twoPhase.matches()) {
collector.collect(doc);
}
}
}

approximation是前面在DefaultBulkScorer的构造函数中创建的继承自DocIdSetIterator的ConjunctionDISI,通过其nextDoc函数获取下一个文档ID,然后调用TwoPhaseIterator的matches信息获取词的位置信息,最后通过LeafCollector的collect函数完成最终的打分并排序。

IndexSearcher::search->DefaultBulkScorer::score->score->scoreAll->ConjunctionDISI::nextDoc

  public int nextDoc() throws IOException {
return doNext(lead.nextDoc());
}

这里的变量lead实质上未BlockPostingsEnum,其nextDoc获得.doc文件中对应的倒排列表中的下一个文档ID。然后通过doNext函数查找下一个文档ID,对应的文档包含了查询短语中的所有词,如果不包含,则要继续查找下一个文档。

IndexSearcher::search->DefaultBulkScorer::score->score->scoreAll->ConjunctionDISI::nextDoc->doNext

  private int doNext(int doc) throws IOException {
for(;;) {
advanceHead: for(;;) {
for (DocIdSetIterator other : others) {
if (other.docID() final int next = other.advance(doc);

if (next > doc) {
doc = lead.advance(next);
break advanceHead;
}
}
}
return doc;
}
}
}

others是除了lead外的所有词对应的DocIdSetIterator列表,例如查找“hello world champion”,则假设此时lead为“hello”,others对应的DocIdSetIterator列表则分别包含“world”和“champion”,advance函数从other中找到一个等于或大于当前文档ID的文档,这里有一个默认条件,就是lead中的文档ID是最小的,因此当others中的所有文档ID不大于当前文档ID,就说明该文档ID包含了所有的词,返回当前文档ID。如果某个other的文档ID大于了当前的文档ID,则要重新计算当前文档ID,并继续循环直到找到为止。

获取到文档ID后,就要通过phraseFreq计算词的位置信息了。

IndexSearcher::search->DefaultBulkScorer::score->score->scoreAll->TwoPhaseIterator::matches->phraseFreq

  private float phraseFreq() throws IOException {
if (!initPhrasePositions()) {
return 0.0f;
}
float freq = 0.0f;
numMatches = 0;
PhrasePositions pp = pq.pop();
int matchLength = end - pp.position;
int next = pq.top().position;
while (advancePP(pp)) {
if (pp.position > next) {
if (matchLength <= slop) {
freq += docScorer.computeSlopFactor(matchLength);
numMatches++;
}
pq.add(pp);
pp = pq.pop();
next = pq.top().position;
matchLength = end - pp.position;
} else {
int matchLength2 = end - pp.position;
if (matchLength2 matchLength = matchLength2;
}
}
}
if (matchLength <= slop) {
freq += docScorer.computeSlopFactor(matchLength);
numMatches++;
}
return freq;
}

省略的部分代码用于处理重复的词,例如当查询“hello hello world”时,就会出现两个重复的“hello”。initPhrasePositions用来初始化。matchLength表示计算得到的两个词的距离,当matchLength小于slop时,调用computeSlopFactor计算得分并累加,公式为1/(distance+1)。
这里不逐行逐字地分析这段代码了,举个例子就明白了,假设一篇文档如下
“…hello(5)…world(10)…world(13)…hello(21)…”
查询短语为“hello world”。
则首先找到第一个hello和第一个world,计算得到matchLength为10-5=5,

IndexSearcher::search->DefaultBulkScorer::score->score->scoreAll->TwoPhaseIterator::matches->phraseFreq->initPhrasePositions

  private boolean initPhrasePositions() throws IOException {
end = Integer.MIN_VALUE;
if (!checkedRpts) {
return initFirstTime();
}
if (!hasRpts) {
initSimple();
return true;
}
return initComplex();
}

DefaultBulkScorer::score->score->scoreAll->TwoPhaseIterator::matches->phraseFreq->initPhrasePositions->initFirstTime

  private boolean initFirstTime() throws IOException {
checkedRpts = true;
placeFirstPositions();

LinkedHashMap rptTerms = repeatingTerms();
hasRpts = !rptTerms.isEmpty();

if (hasRpts) {
rptStack = new PhrasePositions[numPostings];
ArrayList> rgs = gatherRptGroups(rptTerms);
sortRptGroups(rgs);
if (!advanceRepeatGroups()) {
return false;
}
}

fillQueue();
return true;
}

checkedRpts设置为true,表示初始化过了。
placeFirstPositions函数初始化位置信息。
repeatingTerms函数获取重复的词。
如果存在重复的词,则此时hasRpts为true,则要进行相应的处理,本章不考虑这种情况。
最后调用fillQueue函数设置end成员变量,end表示当前最大的位置。

DefaultBulkScorer::score->score->scoreAll->TwoPhaseIterator::matches->phraseFreq->initPhrasePositions->initFirstTime->placeFirstPositions

  private void placeFirstPositions() throws IOException {
for (PhrasePositions pp : phrasePositions) {
pp.firstPosition();
}
}

phrasePositions对应每个Term的PhrasePositions,内部封装了倒排列表信息,firstPosition函数初始化位置信息,内部会读取.pos文件。

DefaultBulkScorer::score->score->scoreAll->TwoPhaseIterator::matches->phraseFreq->initPhrasePositions->initFirstTime->placeFirstPositions->PhrasePositions::firstPosition

  final void firstPosition() throws IOException {
count = postings.freq();
nextPosition();
}

final boolean nextPosition() throws IOException {
if (count-- > 0) {
position = postings.nextPosition() - offset;
return true;
} else
return false;
}

freq表示当前有多少可以读取的位置信息。
然后调用nextPosition获得下一个位置信息,进而调用BlockPostingsEnum的nextPosition函数获取下一个位置信息,offset是起始的位移,例如查询“abc def”,当获得abc所在文档的位置时,offset为0,当获得def所在文档的位置时,offset为1。

DefaultBulkScorer::score->score->scoreAll->TwoPhaseIterator::matches->phraseFreq->initPhrasePositions->initFirstTime->placeFirstPositions->PhrasePositions::firstPosition->nextPosition->BlockPostingsEnum::nextPosition

    public int nextPosition() throws IOException {

if (posPendingFP != -1) {
posIn.seek(posPendingFP);
posPendingFP = -1;
posBufferUpto = BLOCK_SIZE;
}

if (posPendingCount > freq) {
skipPositions();
posPendingCount = freq;
}

if (posBufferUpto == BLOCK_SIZE) {
refillPositions();
posBufferUpto = 0;
}
position += posDeltaBuffer[posBufferUpto++];
posPendingCount--;
return position;
}

posPendingFP是从.tip文件中读取得到的.pos文件的指针。
通过seek函数移动到读取位置posPendingFP,然后将posPendingFP指针重置。
设置posBufferUpto为BLOCK_SIZE后面会调用refillPositions将.pos文件中的信息读入缓存中。
posPendingCount变量表示待读取的词位置的个数,假设单词“abc”在文档1中的频率为2,在文档2中的频率为3,当读取完文档1中的频率2而没有处理,再读取文档2中的频率3,此时posPendingCount为2+3=5。
当posPendingCount大于当前文档的频率freq时,则调用skipPositions跳过之前读取的信息。
posBufferUpto等于BLOCK_SIZE表示缓存已经读到末尾,或者表示首次读取缓存,此时通过refillPositions函数将.pos文件中的信息读取到缓存中,并将缓存的指针posBufferUpto设置为0。
最后返回的position是从缓存posDeltaBuffer读取到的最终的位置,并将待读取的个数posPendingCount减1。

DefaultBulkScorer::score->score->scoreAll->TwoPhaseIterator::matches->phraseFreq->initPhrasePositions->initFirstTime->placeFirstPositions->PhrasePositions::firstPosition->nextPosition->BlockPostingsEnum::nextPosition->skipPositions

    private void skipPositions() throws IOException {
int toSkip = posPendingCount - freq;

final int leftInBlock = BLOCK_SIZE - posBufferUpto;
if (toSkip posBufferUpto += toSkip;
} else {
toSkip -= leftInBlock;
while(toSkip >= BLOCK_SIZE) {
forUtil.skipBlock(posIn);
toSkip -= BLOCK_SIZE;
}
refillPositions();
posBufferUpto = toSkip;
}

position = 0;
}

toSkip计算需要跳过多少。
leftInBlock表示当前缓存中的剩余空间。
如果toSkip小于leftInBlock则表示缓存中还有富余空间,此时直接更新缓存的指针posBufferUpto即可。
如果toSkip大于等于leftInBlock,则先减去当前缓存的剩余空间,然后判断剩余的toSkip是否大于缓存的最大空间BLOCK_SIZE,如果大于,就循环将文件指针向前移动BLOCK_SIZE,再将toSkip减去BLOCK_SIZE,知道toSkip小于BLOCK_SIZE,此时通过refillPositions函数再向缓存填充BLOCK_SIZE大小的数据,设置文件指针为toSkip。
因为位置信息存储的是差值,因此最后将位置position重置为0。

DefaultBulkScorer::score->score->scoreAll->TwoPhaseIterator::matches->phraseFreq->initPhrasePositions->initFirstTime->repeatingTerms

  private LinkedHashMap<Term,Integer> repeatingTerms() {
LinkedHashMap<Term,Integer> tord = new LinkedHashMap<>();
HashMap<Term,Integer> tcnt = new HashMap<>();
for (PhrasePositions pp : phrasePositions) {
for (Term t : pp.terms) {
Integer cnt0 = tcnt.get(t);
Integer cnt = cnt0==null ? new Integer(1) : new Integer(1+cnt0.intValue());
tcnt.put(t, cnt);
if (cnt==2) {
tord.put(t,tord.size());
}
}
}
return tord;
}

PhrasePositions的terms成员变量保存了该PhrasePositions的词,这里其实是遍历搜索的所有词,将重复的词放入最终返回的HashMap中。

DefaultBulkScorer::score->score->scoreAll->TwoPhaseIterator::matches->phraseFreq->initPhrasePositions->initFirstTime->fillQueue

  private void fillQueue() {
pq.clear();
for (PhrasePositions pp : phrasePositions) {
if (pp.position > end) {
end = pp.position;
}
pq.add(pp);
}
}

fillQueue依次读取每个PhrasePositions,将第一个Term在文档中出现位置的最大值保存在end中。


推荐阅读
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 如何搭建Java开发环境并开发WinCE项目
    本文介绍了如何搭建Java开发环境并开发WinCE项目,包括搭建开发环境的步骤和获取SDK的几种方式。同时还解答了一些关于WinCE开发的常见问题。通过阅读本文,您将了解如何使用Java进行嵌入式开发,并能够顺利开发WinCE应用程序。 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了logistic回归(线性和非线性)相关的知识,包括线性logistic回归的代码和数据集的分布情况。希望对你有一定的参考价值。 ... [详细]
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文讨论了在Windows 8上安装gvim中插件时出现的错误加载问题。作者将EasyMotion插件放在了正确的位置,但加载时却出现了错误。作者提供了下载链接和之前放置插件的位置,并列出了出现的错误信息。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • Ihavethefollowingonhtml我在html上有以下内容<html><head><scriptsrc..3003_Tes ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
author-avatar
简单式赞美
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有