lucene3.0.3中的SpanNearQuery(一)

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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章