参考:Node.js从入门到实战(七)Solr查询规则总结
参考: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有以下几种状态:
- 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对象。