Node.js从入门到实战(八)Solr的层级

参考: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有以下几种状态:

  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对象。

展开阅读全文

没有更多推荐了,返回首页