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有以下幾種狀態:
- pending: 初始狀態,既不是成功,也不是失敗狀態。
- fulfilled: 意味著操作成功完成。
- rejected: 意味著操作失敗。
Promise 物件是由關鍵字 new 及其建構函式來建立的。該建構函式會把一個叫做“處理器函式”(executor function)的函式作為它的引數。這個“處理器函式”接受兩個函式——resolve 和 reject ——作為其引數。當非同步任務順利完成且返回結果值時,會呼叫 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物件。