1. 程式人生 > >Node.js從入門到實戰(八)Solr的層級

Node.js從入門到實戰(八)Solr的層級

參考:Node.js從入門到實戰(七)Solr查詢規則總結

一、Solr的層級

Solr作為關鍵的搜尋元件,在整個系統中的架構如下圖所示:


Solr的索引服務是為了提高搜尋的效率,一般而言Solr需要配合Nosql DB使用,作為與NoSQL DB相互獨立的補充,在能夠享受到NoSQL DB的優勢(如儲存遍歷、速度快等)時,也能夠保持系統較高的索引效率。

Solr一般在使用中被封裝為服務的樣式使用,網上存在的一張架構圖如下(詳見參考):


Solr作為服務時對url中的欄位進行解析並按照Solr的查詢規則進行相應,因此如何將查詢子串構建為Solr規定的樣式就是Solr使用過程中的關鍵。

一般而言如果將Solr作為元件釋出給系統內的其餘子系統使用的話,Solr應當具備將普通查詢轉換為Solr查詢字串的parse內建功能,這樣的轉換對於Solr服務的呼叫者而言降低了使用的複雜度,而對於Solr內部而言也有利於進行引數檢查和控制。

二、Solr非同步包裝

Solr的操作過程是同步阻塞的,這個過程會增加系統的延時和不確定性,實現非同步呼叫是提升穩定性、降低編碼難度的一個有效方式,且在Node.js+React的架構中,使用Promise封裝Solr-Client可以達到非同步的目的。

2.1 Promise

Promise 物件用於表示一個非同步操作的最終狀態(完成或失敗),以及其返回的值。Promise 物件是一個代理物件(代理一個值),被代理的值在Promise物件建立時可能是未知的。它允許你為非同步操作的成功和失敗分別繫結相應的處理方法(handlers)。 這讓非同步方法可以像同步方法那樣返回值,但並不是立即返回最終執行結果,而是一個能代表未來出現的結果

的promise物件
一個 Promise有以下幾種狀態:

  1. pending: 初始狀態,既不是成功,也不是失敗狀態。
  2. fulfilled: 意味著操作成功完成。
  3. rejected: 意味著操作失敗。
pending 狀態的 Promise 物件可能觸發fulfilled 狀態並傳遞一個值給相應的狀態處理方法,也可能觸發失敗狀態(rejected)並傳遞失敗資訊。當其中任一種情況出現時,Promise 物件的 then 方法繫結的處理方法(handlers )就會被呼叫(then方法包含兩個引數:onfulfilledonrejected,它們都是 Function 型別。當Promise狀態為fulfilled時,呼叫 then 的 onfulfilled 方法,當Promise狀態為rejected時,呼叫 then 的 onrejected 方法, 所以在非同步操作的完成和繫結處理方法之間不存在競爭
)。

Promise 物件是由關鍵字 new 及其建構函式來建立的。該建構函式會把一個叫做“處理器函式”(executor function)的函式作為它的引數。這個“處理器函式”接受兩個函式——resolvereject ——作為其引數。當非同步任務順利完成且返回結果值時,會呼叫 resolve 函式;而當非同步任務失敗且返回失敗原因(通常是一個錯誤物件)時,會呼叫reject 函式。想要某個函式?擁有promise功能,只需讓其返回一個promise即可。

function myAsyncFunction(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.onload = () => resolve(xhr.responseText);
    xhr.onerror = () => reject(xhr.statusText);
    xhr.send();
  });
};

2.2 Server端Solr封裝

Server端Solr使用solr-client訪問Solr,solr-client提供的search方法為同步方法,對其進行封裝如下:

import solr from 'solr-client';

const SOLRHOST = "127.0.0.1"
const SOLRPORT = "8393"
export class SolrSearcher {
  client: Object;

  constructor(core: string) {
    this.client = solr.createClient({
      host: SOLRHOST,
      port: SOLRPORT,
      core: core,
    });
  }
  
  search<T>(conditions: Object, mapper: (doc: Object) => T): Promise<AsyncResult<T>> {
    return this.asyncSearch(this.buildUrl(conditions), mapper);
  }
}
在SolrSearcher中對search方法進行了重新封裝,設定引數為map物件,在封裝的search內部對引數進行處理生成查詢使用的url字串後傳入asyncSearch方法中:
asyncSearch<T>(query: any, mapper: (doc: Object) => T): Promise<AsyncResult<T>> {
    return new Promise((resolve, reject) => {
      this.client.search(query, (err, obj) => {
        if (err) {
          reject(err);
        } else {
          if (obj.responseHeader.status === 0) {
            resolve({
              dataArray: obj.response.docs.map(doc => mapper(doc)),
              totalCount: obj.response.numFound,
            });
          } else {
            reject(new Error('AsyncSearch status is not 0.'));
          }
        }
      });
    });
  }
在此封裝中返回的結果AsyncResult中包含了返回的資料map和返回的結果的個數。

如上即完成了服務端對Solr-client的封裝(這裡的buildUrl需要根據傳入的map的格式進行處理並未列出)。

三、Solr提供的服務

完成Server端封裝之後需要考慮的問題就是,SolrSearcher的使用方法是怎麼樣的?

Solr作為搜尋服務,在複雜和高可用要求的場景下其以Solr-Cloud叢集的方式提供搜尋服務,而對Solr-Client的封裝屬於應用層面的封裝,基於微服務的概念應該將其視作是一種服務,架構於Solr-Cloud之上,在對Solr-cloud進行遮蔽和封裝的同時作為後端的資料查詢介面使用。此處還需要引入一個查詢標準Graphql,用於定義查詢和返回結果的格式。(見Node.js從入門到實戰(九)Graphql與Solr的整合)。

根據業務切分,假設有Score和Rank兩個使用場景下需要用到Solr-Client,基於DDD可以建立如下結構:

src
|---config/		#放置系統配置:如IP、埠號等
|---domains/		#DDD主資料夾
    |---score/		#score下的
        |---index.js      #提供export宣告
        |---model.js      #模型定義
        |---repository.js #提供公用service模組
        |---service.js    #定義專用service
    |---rank/ 
        |---index.js      #提供export宣告
        |---model.js      #模型定義
        |---repository.js #提供公用service模組
        |---service.js    #定義專用service
|---lib/                  
    |---solr/
        |---SolrSearcher  #非同步SolrClient
index.js                  #主要入口,建立express app和graphql rule,繫結schema
schema.js                 #定義graphql使用的定義(包括全部的model/repository/service),繫結query和resolver
query.js                  #繫結所有的model,並對service進行宣告,繫結
resolver.js               #繫結所有的service
在第九篇Graphql與Solr的結合中再進行細緻分析。在上表中可以看到,SolrSearcher作為MicroService的一個內部元件,提供了到Solr的查詢介面。

四、Solr查詢語句格式化

根據第七篇Solr的查詢規則可知,作為Solr之上一層的微服務,其傳入字串也應該匹配Solr的規則。經過封裝後的非同步SolrSearch的呼叫介面如下:

  search<T>(conditions: Object, mapper: (doc: Object) => T): Promise<AsyncResult<T>> {
    return this.asyncSearch(this.buildUrl(conditions), mapper);
  }
即在search介面中第一個入參為JSON物件conditions,使用buildUrl函式進行處理後轉變為Solr可以識別的字串,

第二個入參為函式體mapper,該函式的作用提供一種將JSON字串(或者XML文件,這些都是Solr的返回值)構造成search函式的返回值型別的方法,用於協助AsyncSearch函式構造返回值,其入參為(doc:Object),返回值為查詢希望的返回值<T>。

4.1 buildUrl

buildUrl方法用於處理輸入的JSON物件,生成Solr可識別的URL字串,一個例子如下:

buildUrl(conditions: Object) {
    const q = conditions.q || '*:*';

    this.queryEscape(q);           /* 處理solr Escape Special Characters */
    const query = this.client
      .createQuery()                
      .q(q)                        /* 主要查詢引數 */
      .sort(conditions.sort)       /* 引入排序方式 */
      .start(conditions.start)     /* 引入開始序號 */
      .rows(conditions.rows)       /* 引入查詢總數 */
      .edismax()                   /* 引入權重排序 */
      .qf(conditions.qf);          /* 引入查詢field */

    const fq = conditions.fq || {};  /* 引入過濾條件 */
    this._queryEscape(fq);           /* 處理solr Escape Special Characters */
    for (let k in fq) {              /* 逐個引入fq */
      query.matchFilter(k, fq[k]);
    }
    logger.info(query.build());
    return query;
  }
其中queryEscape用於處理特殊字元(包括:+ - && || ! ( ) { } [ ] ^ " ~ * ? : /),過濾的方法很簡單,用 \ 進行轉義
  queryEscape(q: Object | string): void {
    if (typeof q !== 'string') {
      for (let key of Object.keys(q)) {
        if (typeof q[key] === 'string') {
          q[key] = `"${q[key]}"`;
        }
      }
    }
  }
使用模板字串可以方便得對特殊字元進行轉義操作。

4.2 mapper

mapper函式用於處理返回結果,對其的呼叫位於resolve函式中:

if (obj.responseHeader.status === 0) {
  resolve({
    dataArray: obj.response.docs.map(doc => mapper(doc)),
    totalCount: obj.response.numFound,
  });
} else {
  logger.error(obj);
  reject(new Error('Response status is not 0.'));
}
mapper函式需要根據返回物件的結構而變化,主要內容是將JSON字串填充到物件位置中,對於如下的定義:
{
  id: 10
  name: zenhobby
  {
      subjectId:001
      subjectName:SubjectA
      score: 90
  }
  school: SchoolA
  teacher: TeacherA
  linechart: {}
}

可以寫出如下的mapper函式:

scoreMapper(doc: Object): Score {
  if (doc.linchart) {
    doc.linechart = doc.linechart.map(image => JSON.parse(image));
  }
  const teacher = JSON.parse(doc.teacher);
  const subjectscore = JSON.parse(doc.score || '{}');
  const subjectId = subjectscore.subjectId || {};
  const subjectName = subjectscore.subjectName || {};
  const score = subjectscore.score || {};
  const scoreinstance = { countryId: country.id, stateId: state.id, cityId: city.id };
  return new Score(scoreinstance);
}

將此Mapper傳入,即可將查詢獲取的JSON Object轉換為Score物件。