做了三年多的搜索相关的工程建设,从现在网上能够看到的资料来说,搜索工程这一块儿的资料还是相对较少,有的资料也都是比较的泛化,很难依据之进行一个初步的搜索工程的建设。这里结合自己的工作经验尝试对垂直类搜索工程做一些介绍,方便没有做过搜索工程的同学来了解搜索工程的概貌。
所谓垂直类的搜索,一般是指站内搜索,一般不涉及从站外爬取数据,比如你在淘宝里面搜索,搜的都是淘宝里面的东西,想搜索门户,百度,google则是属于大搜,要聚合网络平台的数据来进行搜索,他们的数据源需要通过爬虫从各个网站爬取才能够得到。
搜索工程的三大模块
在搜索工程中,主要有三个部分,搜索引擎,检索服务,数据工程。
- 搜索引擎: 是检索支撑的主要部分,现在一般开源的都是由elasticsearch来做搜索引擎;
- 检索服务: 从搜索引擎中检索出来相关的数据,提供搜索类服务;
- 数据工程: 那么引擎中的数据是怎么来的呢,则主要是由数据工程将数据从mysql,mongo,hdfs等处同步到搜索引擎当中。
整个系统的架构大概是这个样子,针对不同的数据情况可能又会有一些优化。
1. 数据工程
首先聊一下数据工程,搜索使用的数据是存储在es当中的,因为es属于文档型数据库,而且搜索大部分是按照主题进行搜索,比如搜用户,搜帖子,搜评论等在实现中是有多个索引,对不同的索引进行搜索。
每一个索引包含的数据有哪些呢?一般情况下存储在搜索引擎当中的数据都是搜索或者排序的时候需要使用的数据。这里展开说一下,现在高级一些的系统,把召回和排序分开来做,召回一般是通过es来进行文本命中,排序的话则是通过模型来进行排序,来让排序达到更好的效果。如果开发人力资源不够的话可能es会承担召回和排序的两部分的功能。
在es中一般存储的有这样三类数据,我们可以拿用户索引来进行举例:
- 文本类数据:比如用户昵称,简介等等,这种数据也是检索的基础,用来做文本匹配的数据
- 排序相关的数据:这种数据可能很多,要看你的业务使用,比如粉丝数,发帖数等等,一般可以使用es的function_score功能来依据这些数据进行打分。
- 状态过滤字段:搜索的时候有些时候需要进行状态的过滤,比如用户搜索的时候不能搜出来已经删除的用户
这些数据在数据库中可能散落在多个库和表中,那么就需要将这些数据形成一个个以user_id
为标识的文档,索引入es当中。这就需要一个单独的项目来对数据进行组装了,比如同库的可以使用join
查询,不同库的可以在程序里面进行组装。
一般情况下,数据的同步还需要满足两个需求,一个是全量的同步,一个是增量的同步。
- 全量同步:这个主要是为了对索引进行全量的重建工作(索引结构修改,增删字段等)
- 增量同步:为了满足更快的实时性,比如用户修改了名字,新的名字应该很快就能搜索到
1. 全量同步的实现
一般是直接扫表,比如从mysql中读取数据,可以使用多线程的方式来加快速度(mysql如果直接使用limit进行翻页的话也会有很大的性能问题,一定要增加过滤项)。
还有一点需要强调的是,在生产环境中一般都是使用alias
对索引进行操作,比如user
相关的查询,使用的别名是user
,但是实际指向的索引是user_one
, 然后在全量的时候直接新建一个user_two
,等user_two
创建完成后,直接把alias切换到user_two
上面
2. 增量同步实现
使用binlog
来实现,这一块儿为了达到实时性,一般都是通过mysql
,mongo
的binlog
,oplog
来作为触发事件来进行索引的增量更新。架构实现一般是mysql
的binlog
通过canal
发送到kafka
,mongo
的oplog
通过mongo-shake
发送到kafka
,然后java应用通过消费kafka
的数据,解析binlog
的数据,然后再从mysql
中查出doc的整个数据index到es当中。
增量的时候有时候可能会出现写放大的情况,只能增加写入速度,比如采用批处理,采用多线程等。
同时,还会有这样一个问题,我们在做全量同步的时候一直也会有增量的数据产生,那么我们如何控制在做全量的时候产生的新的增量也能够进入到新的索引中呢。有两种实现方式
第一种是增量的话只根据别名写索引,在全量新建的时候增量的数据不会进去,这个时候在全量结束的时候根据全量开始的时间节点去kafka中回放对应的数据,回放完成后进行索引别名的切换。这种其实在索引别名切换之前可能也会有少许的数据误差,因为这个回放的完成很难定义,数据还是在源源不断的产生。
第二种比较好的方式则是,在全量开始的时候,增量使用双写的方式,这样的话,在增量写入没有大的延迟的情况下,数据的一致性还是比较ok的
2. 检索端
搜索端使用的话,主要通过es检索出来一定排序的文档,然后根据其中的user_id去请求完整的用户信息,组装后返回给client端。
这里比较重要的一块儿是设计mapping,主要是text类型的字段的设计,举例用户的昵称字段
一般会使用四种分词设计(结合es的fields功能)
1.使用ik中文分词(实际上用户名使用中文分词意义一般不大,可能用户描述更合适)
2.使用拼音分词,做拼音匹配
3.使用standard分词,主要是为了打散,比如使用的单字检索也能检索出来结果,为了能够提升召回率吧
4.keyword不分词,做完全匹配
类似下面这种
"analysis" : {
"analyzer" : {
"pinyin_analyzer" : {
"tokenizer" : "_pinyin"
}
},
"tokenizer" : {
"_pinyin" : {
"keep_joined_full_pinyin" : "true",
"lowercase" : "true",
"keep_original" : "true",
"remove_duplicated_term" : "true",
"keep_separate_first_letter" : "false",
"type" : "pinyin",
"limit_first_letter_length" : "16",
"keep_full_pinyin" : "true"
}
}
}
"name" : {
"type" : "text",
"analyzer" : "ik_max_word",
"fields" : {
"keyword" : {
"type" : "keyword"
},
"pinyin" : {
"type" : "text",
"analyzer" : "pinyin_analyzer"
},
"single" : {
"type" : "text",
"analyzer" : "standard"
}
}
}