关于电商搜索中Elasticsearch的正确使用姿势--配置篇

前言

过年放假啦,总算是闲下来了,笔者自从上次文章更新之后经历了许多事情(裁员风波,面试找工作等等),最近总算是安定下来了。

言归正传,笔者在之前接触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)相当于记录中的一个属性。

ESmysql
索引(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个部分,filterchar_filteranalyzerfilter,char_filter表示过滤器,analyzer为分析器(其实还有tokenizer分词器,不过这里没有配置),这三者的关系为analyzer包含filterchar_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": {
        //商品id
        "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"
        },
        //ip名称
        "ipname": {
          "copy_to":[
            "full_name"
          ],
          "analyzer":"ik_syno",
          "type":"text",
          "fields":{
            "pinyin":{
              "analyzer":"origin_pinyin_firstletter",
              "type":"text"
            }
          }
        },
        //品牌id
        "brandid": {
          "type": "integer"
        },
        //品牌名称
        "brandname": {
          "copy_to":[
            "full_name"
          ],
          "type":"keyword",
          "fields":{
            "pinyin":{
              "analyzer":"origin_pinyin_firstletter",
              "type":"text"
            }
          }
        },
        //后台类目id
        "categoryid": {
          "type": "long"
        },
        //前台类目id
        "logic_categoryid": {
          "type": "keyword"
        },
        //评论数
        "commentnum": {
          "type": "long"
        },
        //用户打分
        "qualityscore": {
          "type": "long"
        },
        //订阅数
        "subscribenum": {
          "type": "long"
        },
        // copy_to 字段
        "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"
	    }
	}
}

可以看到,标题作为最常被检索的字段,是很有必要进行分词的,所以类型上我们将其定义为文本textanalyzer使用上之前配置的ik_syno分析器。

copy_to

copy_to是ES提供的一种用于提高检索效率,简化查询语句的功能,他可以将带有 copy_to的字段复制到copy_to所指定的字段上。

观察上面的字段配置,发现有很多字段被copy到了full_namesmart_title字段上,以full_name为例。这样做的好处是我们在查询文档时候,不需要去指定搜索哪个字段,而是只需要去搜索full_name就可以了,这在电商搜索中是很重要的,因为你无法判断用户搜索的是商品的名称,品牌还是类型(也许更多)。

但是这样做会引起另外一个问题,这个我们将会在下一篇文章中提到并解决。

multi-fields

我们还看到字段中配置了field,这是出于我们需要根据不同的目的将同一个字段用不同的方式索引。这就相当于实现了 multi-fields。例如,一个 string 类型字段可以被映射成 text 字段作为 full-text 进行搜索,同时也可以作为 keyword 字段用于排序和聚合。

除此之外,multi-fields的另一个主要作用就是让同一字段使用不同的解析方式,使其能更好的检索。例如在本文中,我们为title字段设置了fields: pinyin,使其能够将不同的词语再分解成拼音,使得我们再检索过程中能够尽量更多的匹配到文档。

结语

笔者以电商索引为例,简单描述了在索引配置中用到的一些实用功能。ES在配置方面为我们提供了灵活丰富的其他功能及拓展,想要将ES的功能发挥到最大,合理的索引配置是必不可少的,笔者也期望和朋友们一起在未来继续探索和挖掘。

更新

检索篇已更新,感兴趣的小伙伴可以看看:传送门
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值