由於SearchHandler過程比較複雜,基本每次用到都需要重新看一眼才能記憶,而且還比較容易記錯。因此索引把寫博文一來方便自己,二來方便別人。
當然,對Solr來講SearchHandler並不複雜,而且十分簡潔和清晰。不過,在SolrCloud下由於需要節點的協助,所以變得有些複雜。同時SolrCloud下的設計非常巧妙,非常值得細讀和參考。
先看一眼Solr下SearchHandler流程。
主幹流程就是這樣,比較清晰簡單。當然後細節依然比較複雜,主要原因Solr在組織代碼的時候並沒有單機與分佈式的邏輯分開來。也就是單機與分佈式的代碼是堆一起的,這只是其一;其次是因爲處理搜索的處理器QueryComponent邏輯也非常複雜。對了解SearchHandler而言,我覺得上面的流程就可以。對於查詢語法的解析,搜索邏輯以及搜索過程都出現QueryComponent中,另找機會介紹這一塊,本文不詳述這塊內容。
SearchHandler#inform(SolrCore core) {
Object declaredComponents = initArgs.get(INIT_COMPONENTS);
List<String> first = (List<String>) initArgs.get(INIT_FIRST_COMPONEMTS);
List<String> last = (List<String>) initArgs.get(INIT_LAST_COMPONENTS);
// TODO 不堆代碼了,主要目的是根據 Handler 的配置,構建 **components** 鏈。
// 順序是 first -> defaultComps -> last 。記住此順序有用的。
}
SearchHandler#handleRequestBody(SolrQueryRequest req, SolrQueryRespones rsp) {
ResponseBuilder rb = new ResponseBuilder(req, rsp, components);
for( SearchComponent c : components) {
c.prepare(rb);
}
for( SearchComponent c : components) {
c.prepare(rb);
}
}
即是SeachComponent正常情況下可以分成三部分,前後兩可以爲空,但中間一定不爲空。而且中間部分大多數情況下都是默認提供的搜索組件。前部分預處理,後部分結果轉換,所以這兩部分都可以爲空。
ResponseBuilder
這東西慣穿整個搜索過程,充當上下文環境的角色,她的意義在於她使得Handler變成一個單例多線程安全。即是相關變量存儲在她身上。
接到外部請求的Shard稱主節點,其它們所有的Shard(含主節點)稱爲從節點
OK, 接下來聊聊分佈式下搜索請求流程。在此之前我一直在想用方式表達呢,時序圖也不太適合。不過,還是用了時序圖了。首先,你得知道SolrCloud搜索是分步進行的,每一步都是一個狀態。然後極簡單的說,當請求到主節點吧。一個請求到主節點之後,主節點分步異步向所有節點(含主節點)發一個子請求,此時同單機搜索請求過程。
主節點跟從節點用的同一份代碼,由各種各樣的狀態碼和標記,走着兩條完全不一樣的路徑而已。對
SearchComponent
而言,從節點永遠永遠只走prepare()
和process()
。
在此之前先看幾個東西,
ShardHandler : 給先安一個高大上的名字吧,就叫分佈式執行引擎。
ShardRequest : 子請求
ShardResponse : 子響應
以上所有命名都是我亂取。
原本想畫個活動圖來說明Master-Slave各自做了些什麼,節點比較多就不畫,簡單敘述即可。
1.主節點跟單機搜索請求,對所有SearchComponent
進行預處理。
2.主節點通過SearchComponent.distributedProcess()
構建一個子請求,並聲明爲進行詞條統計後放到ResponseBuilder。即在請求參數中把請求聲明爲Term_Stats。
3.主節點通過ShardHanlder讓所有的子節點進行任務,當時所有子節點接到的子請求跟主節點的請求基本一致,只是被標記爲某個分步以及非分佈式請求而已,所有子節點的都先走c.prepare()
, 然後走c.process()
。
4.主節點收集所有異步請求的響應結果,並進行處理。同樣通過SearchComponents
進行,這主要考慮的SearchHandler
的可以擴展性,即加一個處理只有修改,或者添加一個SearchComponents
便能輕鬆實現。
5.跟2差不多,構建一個子請求,標記爲 GET_TOP_IDS。
6.跟3雷同,子節點此事進行真正意義上的檢索,然後根據條件取TopK個文檔返回。文檔一般只含兩個字段,ID和Score。
7.此時收集所有結果,對結果集取TopK。
8.跟2相似,構建一個子請求,標記爲 GET_FIELD。此時向TopK的ID每在節點發送請求。
9.主節點發出請求,對應的子節點進行load迴文檔的全部字段返回。
10.收集所有結果,對結果集排序。
11.返回client。
SearchHandler的主要邏輯,其實應該配合QueryComponent的。不過那樣的就更復雜,所以就SearchHandler和QueryComponent分開來看。記住一點,SearchHandler依賴QueryComponent的狀態,也就說是SearchHanlder分步查詢是由QueryComponent(SearchComponent們)決定的。
SearchHandler#handleRequestBody() {
ResponseBuilder rb = new ResponseBuilder();
for( SearchComponent c : components ) {
c.prepare(rb);
}
int nextStage = 0;
do {
for( SearchComponent c : components )
nextStage = c.distributedProcess(rb);
while(rb.outgoing.size() > 0) {
while(rb.outgoing.size() > 0) {
for(String shard : sreq.actualShards)
shardHanlder.submit(sreq, shard, params)
}
while(rb.outgoing.size() > 0) {
rb.finished.add(srsp.getShardRequest());
for( SearchComponent c : components )
c.handleResponses(rb, srsp.getShardRequest());
}
}
for( SearchComponent c : components )
c.finishStage(rb);
} while(nextStage != Integer.MAX_VALUE);
}
一個簡單分佈式搜索請求分三個步走,
1. GET_TERM_STATS : Term統計
2. GET_TOP_IDS : TopK文檔編號以及評分
3. GET_FIELDS : 取文檔編號的所有字段