前言
过年放假啦,总算是闲下来了,笔者自从上次文章更新之后经历了许多事情(裁员风波,面试找工作等等),最近总算是安定下来了。
言归正传,笔者在之前接触Elasticsearch很少,在新公司中,接触到了以电商搜索推荐为主的项目,其中就大量运用到了Elasticsearch(以下简称ES),并收获了不少经验。
本篇就来围绕如何在电商搜索中正确高效的使用ES聊聊笔者的一些心得。
什么是Elasticsearch
ES是一款分布式搜索引擎,这里注意点重点:搜索引擎,很显然,ES对于数据检索有非常多的设计和优化,在这方面有着得天独厚的优势。
ES底层基于 Lucene 实现,同时屏蔽了很多Lucene的底层细节,提供了分布式特性,同时对外提供了 Restful API,我们的所有针对ES的操作,可以直接通过发HTTP请求就可以完成了。
ES快在哪里
答案是 全文检索
想象这么一个场景,有1亿篇文章,现在想要通过搜索文章内容找到对应的文章。假设这些数据存储在mysql当中,能够做到吗。
当然是可以的,但是性能上就比较感人了,原因是由于mysql通常的索引底层实现(B+树)无法很好的实现上述匹配度查找,而使用like 语法查找则会导致索引失效,同样也会有性能问题(mysql 5.6之后开始支持全文索引,这些内容不在本文的描述范围之内,不过正如佛瑞德·布鲁克斯所说,软件工程没有银弹,不同场景下采用合适的技术才是解决问题更加有效率的方式)。
而ES基于分词 + 倒排索引,在匹配查询这方面性能上更加优异,操作也更加友好(restful,并且为nosql,数据格式十分灵活),关于ES检索的核心实现机制也不在本文的论述范围当中,兴趣的小伙伴可以自行谷歌学习。
而本文主要描述如何在检索项目(如电商搜索)中灵活使用ES实现对应需求
创建索引
首先需要提一句的是,在ES中,万物皆索引。这正是ES将检索功能发挥到极致的体现。由于是不同的数据库,术语的含义也有一些变化,为便于理解,这里使用mysql作为对比。
在ES中,一个索引(Index)即相当于mysql的一个数据库;一个类型(Type)相当于mysql的一张表(ES7以后舍弃了type的概念,一个索引只能有一个type);一个文档(Document)相当于mysql的一行记录;一个字段(Field)相当于记录中的一个属性。
ES | mysql |
---|
索引(Index) | 数据库 | 类型(Type) | 表 | 文档(Document) | 记录(行) | 字段(Field) | 属性(列) |
正如mysql中检索后出来的是若干条记录,ES检索出来的则是一条条文档,文档在ES中是被检索的最小单位。一般文档就长这样
{
"_index": "mall",
"_type": "_doc",
"_id": "10104",
"_version": 6,
"_score": 1,
"_source": {
"itemid": 10104,
"title": "1500g铁观音赛君王安溪铁观音",
"pubtime": 1576152329,
"ipname": "铁观音",
"brandid": 0,
"brandname": "西湖牌",
"categoryid": 3,
"logic_categoryid": [
"1",
"2",
"3",
],
"commentnum": 2,
"qualityscore": 2,
"subscribenum": 2
}
}
这是通过ES检索后返回的文档,标准的json格式,其中_index, _type 就是我们上述讲到的ES的存储结构;_id 相当于这条文档的主键,如果不主动设置的话ES会默认分配;
_score 是匹配分,ES会对每条查询结果打一个分数,评分机制比较复杂且灵活,只需要记住,分数越高,这条文档的相对于检索条件的匹配度就越高(ES的查询结果默认是按分数进行排序的)。
_version 是这条文档的版本号,每次对文档进行修改,版本号都会增加,版本号的作用在于保持数据的一致性。
最后的_source 就是文档本身的内容了,也就是我们的业务数据。
简单的介绍就到此为止,业务数据是上面那样,如果什么都不做,直接往ES里塞数据的话,ES会根据文档的属性Field 自动进行分词优化以达到快速检索的目的,但是实际上什么都不设置在工程中是几乎不存在的,我们或多或少会根据业务需求对每个字段进行单独设置。
打个比方来说,文档中的title(标题), ipname(ip名称), brandname(品牌名称) 正常来说我们都要进行分词,有时候用户检索不一定是中文,还可能是拼音,我们则需要对应的拼音分词(可以通过增加插件);又或者我们的品牌名称是一个生造词,这种时候通过分词器是识别不出这样的词汇的,我们也需要进行特定的设置。
当然,上述情况只是众多需要我们自定义索引结构的理由之一,下面我将通过一份索引配置来引出ES所提供的更多实用的功能。
索引的基本配置
分片
为了便于理解,我们把一份完整的配置拆分成若干单独的配置逐个查看
{
"mall": {
"settings": {
"index": {
"number_of_shards": "5",
"number_of_replicas": "1",
"analysis": {
"filter": {...},
"char_filter": {...},
"analyzer": {...}
}
}
},
"mappings": {...},
}
}
其中,number_of_shards 表示ES主分片的数量,这里的分片相当于的mysql的分库分表,这里ES自身实现了这个功能。number_of_replicas 表示复制分片的数量,复制分片相当于主分片的备份,上述设置表示为该索引设置5个分片,每个分片有一个备份。这也是ES的默认配置。
深入了解分片功能,可以看看这篇文章,本文不做过多赘述。
分析器
接下来我们看看analysis 中设置哪些内容
...
"analysis": {
"filter": {...},
"char_filter": {...},
"analyzer": {...}
}
...
在上述配置中,主要分位3个部分,filter 、char_filter 、analyzer ,filter ,char_filter 表示过滤器,analyzer 为分析器(其实还有tokenizer 分词器,不过这里没有配置),这三者的关系为analyzer 包含filter 和char_filter ,实际的配置中可能更多,但大致都是analyzer 为最终的组合结果。
我们来看下analysis 部分的完整配置
{
"mall": {
"settings": {
"index": {
"number_of_shards": "5",
"number_of_replicas": "1",
"analysis": {
"filter": {
"my_synonym_filter": {
"type": "synonym",
"synonyms_path": "analysis/synonyms.txt"
},
"origin_and_pinyin": {
"keep_joined_full_pinyin": "true",
"type": "pinyin"
}
},
"char_filter": {
"tsconvert": {
"convert_type": "t2s",
"type": "stconvert",
"keep_both": "false",
"delimiter": "#"
}
},
"analyzer": {...}
}
}
},
"mappings": {}
}
}
上述配置中,我们在filter 中配置了同义词过滤,这使得我们在面对一些生造词的商品也能够使得ES能够正确的进行分词。
my_synonym_filter ,这个名字是我们自己起的,在filter 中的key名字随便起,我们将在接下来对analyzer 中的配置中使用他们,其他配置项也是一样
同理,origin_and_pinyin 使得我们能够识别用户的拼音输入(使用的是medcl的elasticsearch-analysis-pinyin插件)
在char_filter中 ,我们配置了tsconvert ,这使得我们能够识别用户输入的繁体字,并将其匹配对应的简体字。采用的插件为elasticsearch-analysis-stconvert。这里我们不会详细解释每一个配置,有兴趣的同学可以自行谷歌对应的插件学习。
接下来,我们来看下analyzer 的配置,后续我们针对每个Field 实际使用的也是analyzer 中的配置
{
"mall": {
"settings": {
"index": {
...
"analysis": {
"filter": {...},
"char_filter": {...},
"analyzer": {
"ik_syno": {
"filter": [
"my_synonym_filter"
],
"type": "custom",
"tokenizer": "ik_max_word"
},
"smart_syno": {
"filter": [
"my_synonym_filter"
],
"type": "custom",
"tokenizer": "ik_smart"
},
"ik_max_word_t2s": {
"char_filter": [
"tsconvert"
],
"tokenizer": "ik_max_word"
},
"ik_smart_t2s": {
"char_filter": [
"tsconvert"
],
"tokenizer": "ik_smart"
},
"origin_pinyin_firstletter": {
"filter": [
"origin_and_pinyin"
],
"tokenizer": "keyword"
}
}
}
}
},
"mappings": {}
}
}
这里配置了5个最终的分析器 ik_syno smart_syno ik_max_word_t2s ik_smart_t2s origin_pinyin_firstletter 当然,名字也是我们自己取的,我们先来看看ik_syno 中的配置
"ik_syno": {
"filter": [
"my_synonym_filter"
],
"type": "custom",
"tokenizer": "ik_max_word"
}
其中,filter 为过滤器配置(笔者看来更像是修改器),这里我们就用上了上面配置好的my_synonym_filter (如果不记得可以返回上文看看),type 表示分析器的类型,这里是custom 表示自定义类型;tokenizer 是分词器,使用的是大名鼎鼎的ik 分词器(一款针对中文的分词器,不了解的话可以看看这篇文章)
这样,ik_syno 就成为了一个能够进行细粒度中文分词,并且具有自定义同义词过滤修改的分析器。
在看另外一个ik_smart_t2s ,就是一个能够识别繁体中文,并且能够进行智能(粗粒度)分词的分析器
其他几个的组合原理基本类似,这里就不赘述了。
Field分析器应用
好了,分析器配完了,想要让他们生效,我们需要将其应用到对应的Field 上
{
"mall": {
"settings": {
"index": {
"number_of_shards": "5",
"number_of_replicas": "1",
"analysis": {
"filter": {...},
"char_filter": {...},
"analyzer": {...}
}
}
},
"mappings": {
"dynamic": "strict",
"properties": {
"itemid": {
"type": "long"
},
"title": {
"copy_to":[
"full_name",
"smart_title"
],
"analyzer":"ik_syno",
"type":"text",
"fields":{
"pinyin":{
"analyzer":"origin_pinyin_firstletter",
"type":"text"
}
}
},
"pubtime": {
"type": "long"
},
"ipname": {
"copy_to":[
"full_name"
],
"analyzer":"ik_syno",
"type":"text",
"fields":{
"pinyin":{
"analyzer":"origin_pinyin_firstletter",
"type":"text"
}
}
},
"brandid": {
"type": "integer"
},
"brandname": {
"copy_to":[
"full_name"
],
"type":"keyword",
"fields":{
"pinyin":{
"analyzer":"origin_pinyin_firstletter",
"type":"text"
}
}
},
"categoryid": {
"type": "long"
},
"logic_categoryid": {
"type": "keyword"
},
"commentnum": {
"type": "long"
},
"qualityscore": {
"type": "long"
},
"subscribenum": {
"type": "long"
},
"full_name":{
"analyzer":"ik_syno",
"type":"text"
}
"smart_title":{
"analyzer":"smart_syno",
"type":"text"
}
}
}
}
}
字段类型这类属于比较基本的,如果对ES的字段类型不熟悉,可以参考这篇文章。这里字段比较多,我们重点看看最常被用作检索的title 字段是如何配置的
"title": {
"type":"text",
"analyzer":"ik_syno",
"copy_to":[
"full_name",
"smart_title"
],
"fields":{
"pinyin":{
"analyzer":"origin_pinyin_firstletter",
"type":"text"
}
}
}
可以看到,标题作为最常被检索的字段,是很有必要进行分词的,所以类型上我们将其定义为文本text ,analyzer 使用上之前配置的ik_syno 分析器。
copy_to
copy_to 是ES提供的一种用于提高检索效率,简化查询语句的功能,他可以将带有 copy_to 的字段复制到copy_to 所指定的字段上。
观察上面的字段配置,发现有很多字段被copy到了full_name 和smart_title 字段上,以full_name 为例。这样做的好处是我们在查询文档时候,不需要去指定搜索哪个字段,而是只需要去搜索full_name 就可以了,这在电商搜索中是很重要的,因为你无法判断用户搜索的是商品的名称,品牌还是类型(也许更多)。
但是这样做会引起另外一个问题,这个我们将会在下一篇文章中提到并解决。
multi-fields
我们还看到字段中配置了field ,这是出于我们需要根据不同的目的将同一个字段用不同的方式索引。这就相当于实现了 multi-fields 。例如,一个 string 类型字段可以被映射成 text 字段作为 full-text 进行搜索,同时也可以作为 keyword 字段用于排序和聚合。
除此之外,multi-fields 的另一个主要作用就是让同一字段使用不同的解析方式,使其能更好的检索。例如在本文中,我们为title 字段设置了fields: pinyin ,使其能够将不同的词语再分解成拼音,使得我们再检索过程中能够尽量更多的匹配到文档。
结语
笔者以电商索引为例,简单描述了在索引配置中用到的一些实用功能。ES在配置方面为我们提供了灵活丰富的其他功能及拓展,想要将ES的功能发挥到最大,合理的索引配置是必不可少的,笔者也期望和朋友们一起在未来继续探索和挖掘。
|