导语| Elasticsearch (ES)是一个分布式搜索和分析引擎,它能为我们提供全文搜索等各种丰富的功能,You know, for search (and analysis)。此前关于 Elasticsearch 大多都是调优分享、分布式相关,关于基础的文档基本是简单介绍,本文是从文档搜索实践出发介绍如何搭建一个全文搜索平台。本文不做 ES 的介绍,因此看文章需要了解 ES 相关基础知识。本文作者:allencao,腾讯应用开发工程师。
前言
最开始接到过一个需求,将部门内的研究报告与文档管理起来,利于他们查找与阅读。我毫不费劲的直接使用了 mysql 来管理列表,`where title like “%query%” `来实现了搜索。在使用之初大概只有几百份文档,并且这些文档标题管理规范,报告也没有被打上标签,更没有摘要之类的信息,因此这个 “like” 工作的比网盘好用多了。
但是网站慢慢迭代,文档也增长到几千份,并且运营同学都打上了标签写上了摘要,这时 mysql 的文本匹配就完全不能满足需求了,使用 ES 也是自然而然的事情。ES 全文搜索解决方案已经非常成熟,应用起来也比较方便,但也有很多细节需要关注,这样搜索功能才会更完善。 下面将介绍一下用研云使用 ES 搭建全文搜索的实践经验。
一、搭建步骤
整体搭建步骤如下:
需求分析阶段我们需要讨论清楚搜索功能给哪些人使用,有什么样的功能,有多少数据,数据是哪些信息,还有搜索的整体交互逻辑,这些不仅影响我们的搜索架构,还影响搜索的优化。中间的步骤下面会详细介绍。
二、准备数据
首先我们需要准备以下三个内容:
- 被搜索数据
- 中文分词停止词
- 自定义分词词库
被搜索数据
被搜索数据一定要认真处理,数据质量越高,搜索结果就越准确,被搜索字段越多,搜索结果越丰富。这里的高质量可以指,被搜索字段的文字长短控制在一定范围,较少的符号,中文尽量使用标准的表达,英文单词使用空格隔开。尤其是像标题这种权重较高的字段,可以人工运营的方式进行修改。
报告被搜索的字段主要是标题、摘要、标签、报告内容。其中标题与标签完全是人工运营方式修改的,质量非常高,在搜索中也是高权重字段。摘要有部分人工撰写,部分自动生成(质量很低)。报告内容使用 OCR 解析出高质量的文本,然后进行清洗,将无效字符、符号、数字等非中文过滤掉,这里需要注意,其实报告内容文本在业务逻辑中是无法被用户查看的,这里只能被搜索。所以在 ES 里报告内容数据可以是无符号,不完整句子的纯文本,这样搜索时结果会更加丰富,但是因为文本质量低内容混杂,所以权重很低。
分词停止词
中文分词停止词在搜索中是一个双刃剑,比较齐全的停止词,有利于减少文档有效内容的噪音,减少索引容量,但不利于搜索的精确命中和高亮。停止词主要是出现频率高,但实际意义不大的词,例如介词“在”,“的”等。
分词时停止词的应用可以灵活一些,在标题和标签的分词中不使用停止词,这样不至于搜“我的世界”时结果只高亮了“我”和“世界”,文档内容文本的分词上应用停止词,使用 ES 的 IK 插件这里比较难实现,可以改用其他分词方式。
分词词库
自定义分词词库是很有必要的,如果是专业领域分词的准确率会大大下降,这里推荐去找一些专业领域词库,也可以使用项目中积累的专家词库,例如“00后”这个词就在我们的自定义词库中。
三、数据结构
众所周知 ES 存储的是文档型数据,mysql存储的是关系型数据,我们要把关系型数据放进ES里搜索需要改变一下数据结构,这里有三点:
反范式设计
不按照关系型数据库的设计范式来设计数据结构,也就是我们通常说的拉平数据,把一条主表记录与其关联记录取出,作为一个文档。举个例子:
关联关系字段的设计
在关系型数据中,主记录的所有关联关系可以被我们筛选,例如标签筛选器。
如果在搜索时也需要支持筛选过滤,这里设计时需要把被筛选字段的id也放进 ES 中,例如标签字段,标签title的字段类型为 text(需要搜索,会被分词),但是 id 作为数字被 ES 存储,数字的筛选效率比 text 高很多。
字段的取舍
数据字段可以被分为几种,ID,搜索字段,内容字段,功能字段。
ID字段建议 mysql 与 ES 的一样,这样节省一个字段的索引还更加方便管理。不过增加记录时没有 ES auto id 快,因为自定义 ID 需要做一次重复检测。
搜索字段指的是需要被全文搜索的字段,例如标题,摘要,内容,标签名等。
内容字段会根据结果的处理方式有所不同,搜索结果处理有两种方式,如下图:
左图的方式在查询到结果后,通过 id 在 mysql 中重新获取数据,返回到前端,右图是直接将搜索结果返回到前端。如果我们还需要查询一次 mysql 才返回到前端,我们应该舍去纯内容字段来精简 ES 索引,因为这些内容可以从 mysql 获取。如果结果是直接返回到前端,那应该按照实际需求来放前端需要的完整内容。采用哪个方式还是要具体需求,通过评估索引大小与数据库的压力来选择。
功能字段包括刚才提到的被筛选字段,权限过滤字段,还有搜索优化要用到的,报告时间字段,热度评分字段,运营评分字段。采用第一种方式,具体的字段如下图(图中代码仅为示例):
四、ES索引
ES 索引的 Mappings 配置时只有两点需要注意
使用 text 数据类型
需要被搜索的字段,字段类型要设置为 text,这样字段才会被分析器处理。但是应用了 text 将不能被 term 过滤器筛选,如果需要过滤可以使用 string 数据类型,他将会自动把字段处理成 text keyword。
Analyzer 要灵活设置
分析器分为两种,一个是写入数据时的使用 `analyzer` 关键字配置,还有一个是搜索时用来分析搜索关键词的使用 `search_analyzer` 来配置。这里我们可以直接使用 ik 插件中的分析器 ik_smart 和 ik_max_word。
索引时,为了提供索引的覆盖范围,通常会采用 ik_max_word 分析器,会以最细粒度分词索引,搜索时为了提高索引的准确度,会采用ik_smart分析器,会以粗粒度分词,示例如下:
有个技巧,当某些字段是高质量并且严谨的词语或者短语时,比如标签字段,可以两个都使用ik_smart分析器,例如有如下文档:
- 搜索“微信”、“小程序”、“微信小程序”等,肯定被搜到,因为标题命中了
- 搜索“小”,这个时候应该被搜到,因为标题有这个字,这样跟关键字匹配是一致的
- 搜索“艾”,虽然可以匹配到“艾瑞咨询”,但这个时候还应该被搜到吗?“艾瑞咨询”这个标签是一个专业术语,而且标签的意义就是要完整表达含义,这个时候只需要分为“艾瑞”和“咨询”,甚至不分直接使用“艾瑞咨询”。(艾瑞咨询这个case我们使用了自定义词典,因此只做为一整个词能被搜到)
因此将 tags 的 analyzer 都设置为 ik_smart:
五、数据同步
Mysql 与 ES 数据同步有很多种方案,大多数基于 binlog 同步或者中间件同步的方案,无法灵活的转换数据结构。不过数据同步并不是一件难事,只要在业务代码中捕捉到 ORM 模型中某些字段的修改事件,事件发生时,再通过队列往 ES 中推送数据即可
这里有几个地方需要注意:
- 并不是所有字段更新都要推送新的数据,只有直接影响搜索行为的字段才更新。
- 更新关联模型的信息时,可能要批量更新数据,这里无法避免。要注意队列的压力以及ES更新的压力,搜索功能较重的平台,ES 查询压力较大,应减小写入压力。如果关联模型更新频繁,需要从业务逻辑上避免此类情况。
- 某些更新频繁,但是影响较小的数据可以定时推送,例如热度评分,评分实时在更新,可以用一个限速的循环 worker 来更新此类数据,虽然会有不小的延迟,但基本不影响搜索体验。
这种伪实时的更新可以满足数据修改时的需求,但是无法满足修改索引 mappings 或者全量创建(更新)效率的要求,因此还需要一个全量创建索引的方案。使用场景主要有两个,a.修改索引 mappings、b.初始化数据时。这里有几个要注意的点:
- 传输数据应该使用 bulk 接口,这个接口的效率会非常高
- 请使用 alias 来关联实际的索引名称,并在调用 ES 接口时使用 alias
- 在存储量充足的情况下,可以全量创建一个新的索引,然后切换 alias 来达到平滑重建索引
六、搜索 DSL
本节内容我直接放在了《搜索排名优化篇》里。
Elasticsearch Query DSL 比较复杂并且有一些学习成本,针对不同场景也没有通用设置,经常一开始用的时候毫无头绪,就算搜索可以跑起来了,可结果跟自己想的完全不一样,所以需要大量的时间来优化。
七、丰富搜索功能
报告搜索上线以后,同事们确实能很快的找到想要的报告,不过有同事提出了更近一步的搜索,在搜索之后直接访问相关的报告页面,如果能够根据报告 ID 聚合在一起就更好了。
这里给我提了个难题,如果把每一页的内容当做一个文档,需要先搜索出所有的内容再根据报告ID聚合在一起,首先无法做分页,并且资源也不是无限制,这种方案明显不可取。
这时我了解到了 ES 的 nested 和 join(也称 Parent join) 数据类型,通过 join query 便可以实现以上功能,先来介绍一下 nested 和 join。
文档型数据库设计一般是没有考虑关联关系的,因为其存储方式不同,需要把数据扁平化。但是 ES 提供了这样的功能,不过根据使用场景的不同,实现了两种方案 nested object 和 parent join。
Nested object 把关联对象和父对象放在同一份文档中,这样查询速度快,但子对象更新必须更新整个文档。
Paraent join 把关联对象和父对象放在不同文档中存储,这个就很像关系型数据库中的结构,这样维护方便,但是涉及要父子关系查询时无法一个请求完成,查询速度也慢了一个数量级。
我们可以通过一个表格来查看区别:
因为这些区别,所以 nested 适应查询频繁,更新较少的场景,join 适合更新频繁,查询较少的场景。
看完文档以后,发现 nested 是非常适合我们的,因此给报告文档的索引中加入 nested object:
查询时的 DSL 片段:
因为 OCR 解析出的报告页文本质量太差,搜索的效果并不是很好,之后又通过简单的模式识别的方式从比较规范的报告文档中解析出标题和关键词等字段用来搜索。
ES 的功能非常丰富,在搜索上能做出很多有意思的东西,不过中文文档资料较少,可以直接看官方最新的英文文档来了解更多的功能。
总结
总体来说 Elasticsearch 的使用没有想象中复杂,搭建一个搜索功能的步骤非常明确,但是想要做出一个比较完善的搜索还是有一定的难度,按照本文介绍的实践经验基本可以搭建出一个满足需求的文档搜索平台。
点击文末「阅读原文」,了解腾讯云Elasticsearch Service更多信息~
腾讯云大数据
长按二维码 关注我们