Xapian全文检索引擎的使用

  之前的坐席答案推荐项目的功能被开放给越来越多的客户使用了,能够为人工坐席提供越来越有用的帮助让自己倍感欣慰,但是原先C语言并采用Libmicrohttpd、Apache Lucy的架构应对当前的流量也越来越吃力,目前线上频频爆出之前冷宫状态没有显露的Bug,让自己焦灼不堪。原先的构架让自己已经没有更改的动力了,刚好感觉自己在这么一段时间中也成长了不少,于是这个功能就有了重写的打算。
  检索部分算是这个功能的核心部件,之前用的是Apache Lucy工具,虽然目前情况感觉用起来也还可以,但是毕竟没入各个发行版的仓储说明还是极为小众的,部署起来也较为不便,况且其底层是基于那个什么cfish模块的,让人也没有办法深入研究下去。查阅发现现在开源检索引擎是相当之多,被大家总体归类如下:
  (1) Apache Lucene系,包括其衍生变种的Apache Solr和ElasticSearch,算是当前开源检索引擎的第一梯队,名门望族派系华丽,因而被很多公司采纳用作企业信息检索的解决方案;
  (2) C++的Sphinx之一大强项就是可以很好的同MySQL数据库进行结合,大家知道虽然MySQL数据库支持全文索引,但是总被大家认为能力有限且不方便修改和扩展,因而大多用Sphinx这种外挂支持全文检索,将MySQL做他专长的数据存储就好了;
  (3) 其他类别,包括本文的Xapian。
  在选型上,个人最终选择了Xapian,主要是Lucene系是Java实现的,虽然不可隐瞒C++程序员对于Java有那么一丢丢的鄙视,而且至少《程序员的鄙视链》也赫然在目了,主要是公司没人用Java的话出了问题不好甩锅的;如果是简单的数据检索并且和数据库集成的话Sphinx算是最好的候选者了,但是Sphinx不支持中文分词,而之前人们推荐的Coreseek中文解决方案也长草许久没有更新了;Xapian可以原汁原味的使用C++接口,而且利用任何分词工具处理后用空格分割就可以丢给他了,对我们这种做自然语言的专业性应用极为重要。此外中文的迅搜的底层也采用了Xapian,我想也一定有其可取之处吧!一开始还记不全这个名字,但是把他想着“瞎编”的拼音,估计你就忘不掉了。

一、Xapian全文检索引擎使用

1.1 建立索引

  Xapian的数据索引是自己管理并放置到文件系统中的,其被称为Database,多个索引通过文件系统路径的方式打开引用。建立索引的时候需要写操作,需要创建Xapian::WritableDatabase类型打开,而检索作为只读操作可以通过Xapian::Database类型打开。
  Xapian::Document对象对应着每一条记录,通过调用set_data()可以设置其值,并且其值可以是任何的数据,因为Xapian不会使用该数据,只有在后续检索命中到该条目的时候,就可以通过get_data()函数原样取出。我使用该字段存储中文文本原文,因为默认中文是不能建立索引的,原文可以保存在这里。

1
2
3
Xapian::WritableDatabase db(doc_path, Xapian::DB_CREATE_OR_OPEN);
Xapian::Document doc;
doc.set_data(request->docu());

  通过Xapian::TermGenerator类型建立的indexer对象,是和建立索引密切相关的,通过调用set_document()和上面的Xapian::Document相关联,然后使用index_text()对正文建立索引。由于中文的独特性需要分词,这里可以使用第三方的分词器将分词后的结果使用空格分割后,作为参数传递给该函数就可以了,而真正的原文被事先调用set_data()保存了。

1
2
3
4
5
Xapian::TermGenerator indexer;
indexer.set_document(doc);
indexer.index_text(request->term_docu());
db.add_document(doc);
db.commit();

1.2 查询文档

  查询文档是个只读操作,用普通方式打开数据库,文档检索本身就是个技术活,不然当初上大学就不会开信息检索这门课程了。我这边比较的简单,直接切分原始检索语句后,进行停用词等预处理,就可以交给Xapian::Enquire引擎去搜索结果了,而搜索语法中的AND、NOT、OR、~等高级特性用起来也十分的简单,拼接好检索语句交于Xapian::QueryParser对象解析就可以了。

1
2
3
4
5
6
Xapian::Database db(doc_path);
Xapian::Enquire enquire(db);
Xapian::QueryParser qp;
Xapian::Query query = qp.parse_query(request->term_docu());
enquire.set_query(query);
Xapian::MSet result = enquire.get_mset(0, request->count());

  检索的结果存放在Xapian::MSet结构中,可以使用Xapian::MSetIterator迭代器进行访问,包括data域、排名、相似度等信息。

1
2
3
4
for(Xapian::MSetIterator itr = result.begin(); itr!=result.end(); itr++) {
Xapian::Document doc = itr.get_document();
// doc.get_data(); doc.get_rank(); doc.get_percent(); ...
}

二、Xapian同gRPC进行服务化集成

  Google出品之大名鼎鼎的gRPC之前介绍过,但是一直没有被实际使用到过。gRPC的通信是双方面的事情,于是在一个巴掌拍不响的情况下和已有项目集成必定涉及到大规模的改造工程。个人这个项目具有全文检索的需求,考虑到可以将其拆分出来作为独立服务,这样一方面体验RPC的解耦功能,同时服务拆分对今后的维护、伸缩也会很有好处。

2.1 定义proto文件,生成辅助代码

  编写proto文件是gRPC中最初也最重要的步骤,通过该文件既可以定义客户端-服务端之间交换消息的定义,同时作为IDL接口描述语言也约定了服务端提供的调用接口。
  接下来通过使用protoc工具,生成proto文件描述的且针对特定语言(这里是C++)访问的辅助代码,两个源文件是帮助操作消息的,另外两个源文件定义了RPC服务的调用框架。

2.2 实现业务逻辑

  上面生成的.h和.cc代码是通过protoc工具自动产生的,你的业务代码不应当直接修改上面的文件,否则万一你需要更新proto文件的话,那么新生成的文件就会覆盖掉你之前的所有修改。通过官方的example,你的业务代码可以通过派生Service类产生实现类来编写,其实敏锐的C++开发者看见辅助生成代码中类成员函数都是虚函数,就可以推断出来最终需要在你的派生实现类中override这些服务接口就行了,这些接口函数的参数和之前在proto文件中定义的一致,返回类型都是grpc::Status类型。
  同时为了客户端的方便,RPC的客户端还会做对应的操作封装。首先连接远程服务端建立一个Channel,并形成维持一个远程调用stub环境;而对于上层来说,客户端最终暴露出来的服务接口不必和RPC中所定义的接口相同或者是一一对应的关系,完全从使用者的角度来封装就行了,只是在需要远程服务端提供服务的时候,使用事先初始化的stub调用远程服务,并判断返回值做出对应操作就可以了。

2.3 服务端和客户端测试集成

  服务端作为一个守候进程,使用默认的参数启动既可。
  因为RPC是在内网可信环境使用,所以可以不用SSL加密方式通信;gRPC默认通过线程池的方式启动,在一个4Core处理器的服务器上,该RPC框架服务端自动产生了四个线程侦听客户端的请求。
  通过封装的客户端也极为好用,对业务层看来,真的就像是在调用一个本地函数一样进行远程服务的调用!

  好吧,一切顺利,gRPC确实为我省去了之前服务端架构、网络通信、数据包封拆等各项操作,寥寥没几行代码就形成了客户端-服务器通信模型。可能自己用的比较简单浅显,还没遇到什么坑,不知道是福是祸……
  不过我相信gRPC不会是没有缺陷的,要不然的话也不会有那么多的互联网公司重复造轮子,搞自己的RPC框架出来了,当然gRPC发布的时间也比较晚,其他公司业务需求在先也可能是一个原因。

  下面只是一个简单的样例,真正的行业分词、停用词、行业关键词、行业同义词等逻辑也会被封装到RPC的服务端去实现,否则这个RPC就真的没啥意思了。相关内容就不方便在此展示了。

参考文献