SpanNearQuery和PhraseQuery是差不多的意思,都是表示多個term必須全部存在且距離滿足一定的條件的query,但是SpanTermQuery的用法更多,比如他有一個inorder的參數,可以控制多個term出現的位置是不是要符合指定的順序(phraseQuery就是可以不按照出現的順序的)
構建一個SpanNearQuery需要三個參數,一個是多個SpanQuery,一個是多個spanQuery之間最大的距離slop,第三個是是否要求多個term出現的位置和傳入參數的順序相同,他的構造方法爲:
public SpanNearQuery(SpanQuery[] clauses, int slop, boolean inOrder) {
this(clauses, slop, inOrder, true);
}
我在這個博客中將多個SpanQuery的getSpans方法生成的Span叫做subSpan。看一下這個類的getSpans方法:
public Spans getSpans(final IndexReader reader) throws IOException {
if (clauses.size() == 0) // optimize 0-clause case
return new SpanOrQuery(getClauses()).getSpans(reader);
if (clauses.size() == 1) // optimize 1-clause case
return clauses.get(0).getSpans(reader);
return inOrder ? (Spans) new NearSpansOrdered(this, reader, collectPayloads) : (Spans) new NearSpansUnordered(this, reader);
}
我們忽略clauses==0和1的情況(因爲這兩個沒有意義),直接看最後一種,他會根據各個term在某個doc中出現的順序要不要符合傳入的各個subQuery(也就是clause中的query)的順序返回不同的Span,如果是inorder,則只有存在所有的term且各個term出現的位置是按照sunQuery的順序且他們之間的距離的和小於指定的slop的doc才能被召回,如果不是inorder,則只要全部出現且各個term之間的距離的和小於指定的slop的doc就能被召回。
我們看看NearSpansOrdered的實現,一切還是從next方法入手
/**和termSpan一樣*/
@Override
public boolean next() throws IOException {
if (firstTime) {//初次調用,將每一個sunSpan都調用next方法,即讀取第一個位置
firstTime = false;
for (int i = 0; i < subSpans.length; i++) {
if (!subSpans[i].next()) {
more = false;
return false;
}
}
more = true;
}
if (collectPayloads) {//這個是爲了收集payload設置的,如果是收集payload的話每一個位置都要收集所以把之前的payload清空,collectPayloads是一個list<byte[]>,用於收集所有的subSpan的payload,
matchPayload.clear();
}
return advanceAfterOrdered();//關鍵是這個方法,他會將所有的subSpan都讀取到同一個doc上,然後判斷的當前的doc是否滿足需求。
}
private boolean advanceAfterOrdered() throws IOException {
//因爲所有的span都必須滿足,所以必須調到相等的doc上,即調用toSameDoc方法。toSameDoc方法和booleanQuery在and的情況下生成的ConjunctionSumScorer中將所有的子query調整到同一個doc上的算法是一樣的,這裏不再重複了,都是使用的循環數組
while (more && (inSameDoc || toSameDoc())) {
if (stretchToOrder() && shrinkToAfterShortestMatch()) {
return true;
}
}
return false; // no more matches
}
指執行完toSameDoc之後所有的subSpan都停留在同一個doc上,接下來要判斷下當前doc上各個term出現的順序是不是符合置頂的subQuery的順序,這個是通過stretchToOrder方法實現的
private boolean stretchToOrder() throws IOException {
matchDoc = subSpans[0].doc();
for (int i = 1; inSameDoc && (i < subSpans.length); i++) {//每兩個進行對比,i從1開始。
while (!docSpansOrdered(subSpans[i - 1], subSpans[i])) {//docSpansOrdered用於判斷當前兩個位置符合不符合要求(即不能重疊且按照順序出現)。如果不符合順序要求,則讀取當前的span(也就是第i個span)在當前doc上的下一個位置。
//進入while表示當前term的當前位置是不符合順序的,則要讀取下一個位置(當前term在當前的doc上可能出現了多次)
if (!subSpans[i].next()) {//如果當前的span(裏面封裝了termPosition)已經讀取玩了,也就是所有的位置都讀取完了,則返回false。
inSameDoc = false;
more = false;
break;
} else if (matchDoc != subSpans[i].doc()) {//讀取下一個位置時已經到下一個doc了,表示當前的doc上的所有的位置已經讀取玩了,則返回false。
inSameDoc = false;
break;
}
}
}
return inSameDoc;
}
經過上面的stretchToOrder方法,如果返回是true的話表示當前的doc是符合順序的,接下來判斷各個term的距離的和是不是小於指定的值,用 shrinkToAfterShortestMatch()方法來完成
private boolean shrinkToAfterShortestMatch() throws IOException {
matchStart = subSpans[subSpans.length - 1].start();
matchEnd = subSpans[subSpans.length - 1].end();
Set<byte[]> possibleMatchPayloads = new HashSet<byte[]>();//paylaod最後的結果
if (subSpans[subSpans.length - 1].isPayloadAvailable()) {//這裏添加了最有一個span的payload,因爲現在選擇的最後一個span一定是正確的,距離之和最小的所有的位置一定是現在的最後一個位置,不會再移動,所以下面的for循環使用的是subSpans.length - 2,也就是說從後面向前計算。
possibleMatchPayloads.addAll(subSpans[subSpans.length - 1].getPayload());
}
//這個叫做possible,是因爲他可能是一個合格的payload,也可能不是
Collection<byte[]> possiblePayload = null;
int matchSlop = 0;
int lastStart = matchStart;
int lastEnd = matchEnd;
//for循環的思路是確定了最後一個,然後再向前計算。
for (int i = subSpans.length - 2; i >= 0; i--) {
Spans prevSpans = subSpans[i];
if (collectPayloads && prevSpans.isPayloadAvailable()) {//這裏並沒有更新payload,因爲當前的位置可能並不是最合適的,可能後面還有一個位置更合適呢。
Collection<byte[]> payload = prevSpans.getPayload();
possiblePayload = new ArrayList<byte[]>(payload.size());
possiblePayload.addAll(payload);
}
int prevStart = prevSpans.start();
int prevEnd = prevSpans.end();
//他的目的是計算最小的slop
while (true) { // Advance prevSpans until after (lastStart, lastEnd)
if (!prevSpans.next()) {//當前的span已經窮盡
inSameDoc = false;
more = false;
break; // Check remaining subSpans for final match.
} else if (matchDoc != prevSpans.doc()) {//當前的span沒有窮盡doc,但是下一個doc已經不是當前的doc
inSameDoc = false; // The last subSpans is not advanced here.
break; // Check remaining subSpans for last match in this
// document.
} else {//出現了多次,並且當前不是最後一次。
int ppStart = prevSpans.start();//新的位置的開始
//新的位置的結束
int ppEnd = prevSpans.end(); // Cannot avoid invoking .end()
//判斷新位置和下一個span的位置是不是符合順序,新位置只會比剛纔的位置更靠後,所以不用和前面的對比只需要和後面的對比即可。
if (!docSpansOrdered(ppStart, ppEnd, lastStart, lastEnd)) {//不符合順序,不用繼續向後找
break; // Check remaining subSpans.
} else { // prevSpans still before (lastStart, lastEnd) 仍然符合順序,則更新當前的span的匹配位置,使其更加靠後,從這裏可以發現,他是優先使用最小的距離來計算slop。繼續循環,因爲可能後面還有出現的位置
prevStart = ppStart;
prevEnd = ppEnd;
if (collectPayloads && prevSpans.isPayloadAvailable()) {//當前的位置比上一個位置更靠後,則重新讀取此位置的payload。
Collection<byte[]> payload = prevSpans.getPayload();
possiblePayload = new ArrayList<byte[]>(payload.size());
possiblePayload.addAll(payload);
}
}
}
}
//添加最後確定的payload到最後的結果中
if (collectPayloads && possiblePayload != null) {
possibleMatchPayloads.addAll(possiblePayload);
}
assert prevStart <= matchStart;
if (matchStart > prevEnd) {// Only non overlapping spans add to slop. 對於緊鄰的term,是不算入slop的,因爲matchStart-prevEnd=0,緊鄰的意思是matchStart==prevEnd
matchSlop += (matchStart - prevEnd);
}
/* Do not break on (matchSlop > allowedSlop) here to make sure that subSpans[0] is advanced after the match, if any. */
matchStart = prevStart;
lastStart = prevStart;
lastEnd = prevEnd;
}
boolean match = matchSlop <= allowedSlop;
if (collectPayloads && match && possibleMatchPayloads.size() > 0) {
matchPayload.addAll(possibleMatchPayloads);
}
return match; // ordered and allowed slop
}
這樣按照順序的SpanNearQuery就完成了,他的思路是第一步把所有的span都指向到同一個doc上,然後找到最前面的符合順序的一組,這樣就定死了最後的一個span(即順序是spans.size - 1的那個),然後按照逆序挨個移動其前面的span(即先移動第span.size-2個),移動到超過下一個span的位置,然後記錄在移動的過程中出現的在下一個span的位置之前的最靠近的位置,這樣挨個移動,就可以計算出距離最小的一組了。然後再移動到下一組,直到某個span在這個doc上已經沒有匹配的位置了位置。
這個方法在我看來特別耗cpu資源,因爲他的操作太多了,如果某個doc上的符合要求的term特別多,就更慢了,因爲會每個位置都會讀取一次匹配一次,尤其是當使用的sunQuery比較多或者是當某個域比較大的時候對cpu的更大,所以謹慎使用這個query。下一篇博客中我將寫一下不按照順序的SpanNearQuery。