【直播全文记录】基于Xapian的垂直搜索引擎的构建分析

此文根据【QCon高可用架构群】分享内容,由群内【编辑组】志愿整理,转发请注明出处。

王晓伟:2009年创办麦图科技,专注于电商行业的垂直搜索, 受到多家天使、Pre-A投资机构的关注。有10+年互联网、游戏、内核安全从业经验。历任软件工程师、高级软件工程师、技术经理和总监。目前主要从事手机游戏发行平台的构建,推进公司DevOps的培养和运维自动化的实施。

以下为王晓伟老师最近在微信群直播分享的全文记录。

垂直搜索的应用场景

场景1:基于拼音搜索联系人,可以从开始处搜索,也可以从中间搜索。

场景2:基于关键字搜索常见政府网站,存在的问题参见截图说明。

搜索业务场景归纳:

搜索应用场景举例:

从应用场景上看,目前在大数据、用户产生数据的背景下,搜索的需求是刚性的。

技术选型

1. 检索引擎选型

彼时选型背景

主要思路:

选型一定要了解业务和业务的价值,垂直搜索的核心价值在于,要对用户输入关键字的搜索意图有充分理解,返回的结果的相关性、权重排序要与用户高度匹配,提高满意度,所以对于检索模型和权重计算要求较高。

比如彼时, Nokia N97很流行, iPhone也很流行,当用户输入N97时,京东,亚马逊,一号店等绝大多数网站搜索出来的前10个几乎都为N97的配件,而我们搜索的结果是N97手机。

这就是全文搜索和垂直搜索在本质上的差异。在这点上跟推荐系统有点像,只不过推荐系统是根据用户的历史足迹和行为推定出来,而垂直搜索是通过关键字。某些情况下, 两者可以相互融合。

最终,我们选择了Xapian。

2. 存储选型

这部分主要从笔者所接触的系统着眼,一些看法从今天来看也许不一定正确,主要分享一下思路。 当时我们选择了Cassandra+MongoDB,而不是HDFS/HBase。

几点原因:

创业公司选型最大的问题是时间问题. 我们不能停下来选型而不做业务. 这是个比较大矛盾. 上面提到的几点经验希望对大家有所借鉴.

垂直搜索的引擎架构

分享一下5000W数据以内的垂直搜索引擎的架构模型,模型中也涉及到了流式计算,当然彼时流式计算并没有如此完整的模型和开源项目的支持。我们可谓是蛮干了一把!此处可以参考王新春老师的实时计算在点评中内容做个比较(当然结论是, 我们还是很山寨的)。

整体架构包含以下部分:

1. 种子发生器

用于入口页面的发布,可以根据需求定义粒度,比如整个 http://www.jd.com 是一个入口,夺宝岛http://auction.jd.com/index.action 也是一个入口。后面会介绍业务场景。
以 http://www.jd.com 为入口,是实时性要求不高的数据。
以 http://auction.jd.com/index.action 为入口,是实时性要求很高的业务。

2. 抓取系统(Cralwer)

单进程单线程异步多工方式抓取,分布部署,容错性,健壮性为主。通用的模块与具体站点和业务无关,只负责抓取,抓取的URL最初由种子发生器发出,后面有Parser页面解析系统分析URL再填充。Crawler是一个不会停止的系统。Crawler的数据key和meta-data存储于MongoDB,页面的 RAW 数据存储于 Cassandra。Crawler除了容错性,健壮性之外,性能其实非常重要。

3. 页面解析器(Parser)

业务相关,从MongoDB 和 Cassandra 获取数据负责对页面进行符合业务需求的分析,根据关键字、URL特征和预置的规则将页面URL和部分数据提取出来,另外实现查重的功能(几亿数据查重,采用 bloomfilter + Redis 实现 )
bloomfilter的及时引入确实很救命,数据检索更新和查重节省了很多时间;不过与redis适配当时有点问题,另外虽然redis很快,但是有条件还可以再优化。

4. 数据分析器(Analyser)

语料分析, 真正的业务核心。将HTML的页面数据提取成结构化的数据,存储到MongoDB。整个架构最精彩的部分也在这里,如何能做到处理几十家不同网站数据提取和规整,并能跟上源站更新的节奏(当时某东, 几乎天天更新),又便于多人并行开发和更新, 低耦合, 互相隔离, 持续部署是一个有趣的问题。

5. 索引器(Indexer)

完成从MongoDB取数据,分词,语法分析,部分语义分析,计算预置权重,完成索引。该模块完成一堆数据到信息的提炼,直接影响了用户查询结果的质量和满意度。比如判断语义,相关性排序等。索引工作从代码层面不难,调用几个函数把数据添加即可,但是其实功夫都花在前面了。索引和检索是垂直搜索引擎的大脑,是思考的部分,性能要好,结果要准。

6. 图片到文字的提取识别(Ocr)

最初测试过 Tesseract OCR ( 由HP开源, 后来由 Google 赞助的一个项目),但是后来发现不实用,样本学习过程繁琐,更新不方便,关键识别太慢。后来我们手动写了一套专门只针对数字的识别服务。OCR处理会在索引之前完成。这一部分很多时候不是必须的,当时主要针对某东和后来被鹅厂收购的某讯,现在某东已经不是图片了。

下面来一张图,大概表示一下这之间的逻辑,分层关系图中表现的不是特别准确。核心中间件是RabbitMQ。

后来有所改进,我们把CWS也单独抽取出来,自己写了一个简单的MQ Agent,是基于Redis实现的,性能比RabbitMQ要好很多。建议有条件可以使劲的Hack。

垂直搜索技术和业务细节

1. 如何提高垂直检索质量和语义识别

其实语义识别本身是很难的,但是一定的关键词集合里是可以做到和优化的。比如前面提到的,搜索”N97 手机”搜索出一堆手机配件的问题,因为手机和手机配件含有相同的关键字。传统的相关度,权重的模型是基于语料库TF/IDF做的。但是商品的名称文字是很短的,基本上都只会出现一次,名称相似度也没有可以参考的,那怎么办呢?

这种情况我们就需要预置权重,我们编写了一套学习的工具。通过分类、品类,建立了词干、词根的树形结构;同时设定每层的权重,那么用户在搜索的时候,匹配从根部开始,那么就避免了搜出树枝部分。这是个非常繁琐细致的工作,要分析整个商品库,人工很难。需要有一套启发式的词根更新方法和工具。

2. 如何应对不同众多异构数据模型

先发一张图, 图中列出了对于不同站点检索的模板文件的管理结构。

这是其下层目录的结构。

前面提到我们是做电商的垂直搜索的,需要从源站获取商品信息:商品名称、价格、图片、规则参数、详细介绍及评论。

显然每个源站都不一样,要适配40几家网站需要一种独立、易更新的、fast-fail的、错误自容的架构。这样能保证开发可以并行,源站更新可以快速适配,出现错误不影响其他模块。
一开始的做法是在代码里做各种if/else,但是这样导致并行开发代码合并问题很大;后来用插件式编程,这样动态加载模块看上不错,但是会污染代码运行环境,运行久了容易出一些莫名其妙的bug。

后来,我们采取了基于模板语言的方法,把业务逻辑封装到一个模板文件,此文件结构本身是HTML,通过、\<script>标签把我们业务代码放到HTML里。这样好处是:业务代码跟要解析的HTML在一起,方便代码提取数据的逻辑调试,同时业务代码运行环境局限在模板本身,即使出了问题也不会污染到其他代码。

代价是开发一套工具用于模板编写调试。

3 .关于算法选择

算法一定不是万能的。现在计算权重,相关性的算法很多。我们尝试过基本的布尔、BM25、SVM、Cosine similarity等等。

其实这些算法直接应用到现实的业务都不灵。那为啥会有这么高大上的算法存在呢?

我们的经验是:想要发挥算法奇效,要靠数据索引前的预处理。所以,踏踏实实做数据处理,分析业务,然后理解数据与算法的结合点;最终才能做到相得益彰,检索出符合用户心理的结果。

Q&A

Q1: 请问使用的中文分词器是什么,自研的还是其它?拼音搜索是如何实现的,是在新建索引时将转换为拼音存起来吗?

1、中文分词器:基于scws, 国人开源之作。自行开发了Ruby和Python的binding。
2、拼音搜索是如何实现的,如你所言要事先索引。
3、补充一点,类似Google的搜索纠错(编辑距离算法),智能补全等都可以通过预置的方法实现。

Q2: 数据分析器,分析HTML代码的时候,如何解决Ajax的问题?

解决Ajax的问题:我们遇到某宝是这样的, 一开始想开发一套基于Webkit的工具处理,后来发现性能太差,而且某宝Ajax请求基本稳定;所以在模板里直接发起HTTP搞定。或者也可以在Parser阶段取出相关URL进行处理。 这里我们没找到通用的解决方案。

Q3: 重新让你选择一次,目前情况下你会做哪些选型上的改进?

1、业务量不增加的情况下,索引和存储层面我可能不太会做更多改变。 2、技术架构上,对于Crawler及其他几个模块的实现要改;之前是自行开发的并行结构,健壮性还行, 但是维护难度高, 适合采用现在流行的并行计算的架构。

Q4: 基于Redis实现了一个MQ Agent,是基于Redis的队列实现的吗?为什么没有直接用Redis的Pub/Sub? 还有这个有做集群实现吗?

1、自己开发是将一些业务放进了Agent,便于集中优化。 2、未做集群, 做了主备。

Q5: 拼音库是怎么实现的?有支持模糊音匹配吗?

1、拼音库有现成的,如果觉得不可靠可以通过Character Map提供的数据自己做一份。
2、模糊音匹配我的理解应该就是纠错,根据编辑距离,前提是你有把握识别出这是用户输入错误。看下图:

Q6: 用户的意图你们是怎样预期及理解的?

我们是靠语料的数量,我们直接能假定用户不是调戏我们的。然后再通过一些算法,比如纠错、拼音、引导提示和补全等猜测。还有最重要的是:
1、数据权重计算要理解源站数据组织的用意,比如首页的数据显然很重要。
2、预置权重,比如现在搜索iPhone,我们应该排第一位的是iPhone 6,而不是iPhone 5或者4。
3、昨天王新春老师提到的根据用户行为,这个只会体现给当前用户,其他用户不能简单的应用。

Q7: 请再稍微具体展开下权重计算和检索模型这块儿?N97手机的例子,怎样做到排除干扰词影响的?如N97手机和N97手机壳.

原理如下:
1、手机是一个分类,手机壳是一个分类,两个在一起权重下降( 算法不在这里体现)。
2、如果只是输入N97,我们数据是可以启发认知为 N97 手机
3、手机和手机壳有自身的权重,手机高于手机壳。
4、手机壳可以排除手机, 但是手机排除不了手机壳。

Q8: MongoDB的库级锁对吞吐量的影响是怎样优化或规避的?

1、这个我无法从MongoDB本身回答,我的经验是,mongodb driver for ruby(理论上其他的也是)是异步写的,所以写不会阻塞。
2、读是泛读,所以不会引起读到不存在的数据的情况。

Q9: 基于模版的业务逻辑放到HTML的Script里面,业务开发人员在这个框架如何进行代码调试?

1、最重要的是我们API是详尽和傻瓜的, 用法明确, 完全满足数据操作的要求, 杜绝了开发人员用错API或者没有API可以用。
2、我们开发了console用于测试。这个测试主要测试数据是否正确, 并非要调试。当然也支持调试, 这个是Ruby提供的debugger特性.

Q10: 爬虫获取的URL反垃圾怎么做的?比如URL陷阱,无效参数之类的?

1、我的回答可能会有点遗憾,垂直搜索对于URL的粒度的把控是很细的,所以我们从业务上就可以杜绝。
2、我们尽可能让系统多做事,基于fast-fail,我们工程师只等待系统fail的通知,所以如果出错了我们会有人跟进。
3、如果不是infinite-loop我们也不是特别在乎。

Q11: 单进程单线程设计是基于什么考虑的?

1、我纠正我的说法,我想说的是一个进程只有一个线程,部署其实是多进程的。
2、我是Windows程序员出身,写了6年多的Windows程序,写过很多多线程的程序。我发现一旦陷入到多线程,程序员就开始不关心业务,开始跟多线程斗争(多数人对于操作系统的一些原理没学好)。所以我7年前转到Linux之后,程序全部采用单线程+异步的模型。
3、单线程模型简单,类库好写,数据读写易管理,不易出错,出错后好排查。

Q12: 从HTML中提取结构化数据是你们是用正则还是基于css or dom?

基于dom。css/xpath 正则建议不要用于复杂数据,变动性强的数据和大规模数据,不易调试和维护。

Q13: 排序会用到用户日志的学习及训练吗?

当时的设计是通过朋友收藏,社交上connection + 搜索权重训练。从日志学习没有,主要是没有想到好的防止作弊的方法。

Q14: 最近搜索遇到一个需要存储时间维度的需求,而且是几十天,能通过建索引解决吗?

我不是很明白问题的需求,我觉得应该可以。

Q15: 如果现在你选,你还是用RabbitMQ吗,有没有考虑Kaffka? 自己实现基于MQ Agent,是否类似于Github开源的resque?MongoDB写入不可靠的问题有碰到吗,有用Shard吗?

1、参照尽量Hack的理念,可能会换。
2、自己实现目的只有两个,保证数据处理序列的情况下可以批量处理。跟resque理念不同,细节有点遗忘,主要是把业务放到MQ Agent上了。订阅者可以跟Agent在业务层面上做约定。
3、MongoDB我们是全架构的, Shard+ replication。

官方出版

QCon高可用架构群官方博客 +