Elasticsearch初步

Elasticsearch初步

1.ES是什么?

1.1.简介

==ElasticSearch 是一个基于 Lucene 的搜索服务器。==它提供了一个分布式多能力的全文搜索引擎,基于 RESTful web 接口。Elasticsearch 是用 Java 语言开发的,并作为 Apache 许可条款下的开放源码发布,是一种流行的企业级搜索引擎。

ElasticSearch 用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

1.2.特性

  • 分布式的文档存储引擎
  • 分布式的搜索引擎和分析引擎
  • 分布式,支持PB级数据

1.3.使用场景

  • 搜索领域: 如~~百度、谷歌,~~全文检索等。
  • 门户网站: 访问统计、文章点赞、留言评论等。
  • 广告推广: 记录员工行为数据、消费趋势、员工群体进行定制推广等。
  • 信息采集: 记录应用的埋点数据、访问日志数据等,方便大数据进行分析。

缺点

  • 不支持联表查询
  • 不支持事务

1.4.生活中的数据

我们生活中的数据总体分为两种:结构化数据非结构化数据

  • 结构化数据:也称作行数据,是由==二维表结构来逻辑表达和实现的数据,严格地遵循数据格式与长度规范,==主要通过关系型数据库进行存储和管理。指具有固定格式或有限长度的数据,如数据库,元数据等。

  • 非结构化数据:又可称为全文数据,不定长或无固定格式,不适于由数据库二维表来表现,包括所有格式的办公文档、XML、HTML、word文档,邮件,各类报表、图片和咅频、视频信息等。

根据两种数据分类,==搜索也相应的分为两种==:结构化数据搜索非结构化数据搜索

对于结构化数据,因为它们具有特定的结构,所以我们一般都是可以通过关系型数据库(mysql,oracle等)的 二维表(table)的方式存储和搜索,也可以建立索引。

对于非结构化数据,也即对全文数据的搜索主要有两种方法:顺序扫描法全文检索

顺序扫描:通过文字名称也可了解到它的大概搜索方式,即按照顺序扫描的方式查询特定的关键字。例如给你一张报纸,让你找到该报纸中“平安”的文字在哪些地方出现过。你肯定需要从头到尾把报纸阅读扫描一遍然后标记出关键字在哪些版块出现过以及它的出现位置。

这种方式无疑是最耗时的最低效的,如果报纸排版字体小,而且版块较多甚至有多份报纸,等你扫描完你的眼睛也差不多了。

全文搜索:对非结构化数据顺序扫描很慢,我们是否可以进行优化?把我们的非结构化数据想办法弄得有一定结构不就行了吗?==将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。==

这种方式就构成了全文检索的基本思路。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引。这种方式的主要工作量在前期索引的创建,但是对于后期搜索却是快速高效的。

1.5.Lucene

目前开放源代码的最好全文检索引擎工具包就属于 apache 的 Lucene了。

但是 Lucene 只是一个工具包,它不是一个完整的全文检索引擎。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。

目前以 Lucene 为基础建立的开源可用全文搜索引擎主要是 Solr 和 Elasticsearch。

不管是 Solr 还是 Elasticsearch 底层都是依赖于 Lucene,而 Lucene 能实现全文搜索主要是因为它实现了倒排索引的查询结构。

核心:==提出关键词和建立倒排索引==

倒排索引描述:==为了创建倒排索引,我们通过分词器将每个文档的内容域拆分成单独的词(我们称它为词条或 Term),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。==结果如下所示:

  1. Term Doc_1 Doc_2 Doc_3
  2. -------------------------------------
  3. Java | X | |
  4. is | X | X | X
  5. the | X | X | X
  6. best | X | X | X
  7. programming | x | X | X
  8. language | X | X | X
  9. PHP | | X |
  10. Javascript | | | X
  11. -------------------------------------

这种结构由文档中所有不重复词的列表构成,对于其中每个词都有一个文档列表与之关联。这种由属性值来确定记录的位置的结构就是倒排索引。带有倒排索引的文件我们称为倒排文件。

我们将上面的内容转换为图的形式来说明倒排索引的结构信息,如下图所示,

img

其中主要有如下几个核心术语需要理解:

  • 词条(Term):索引里面最小的存储和查询单元,对于英文来说是一个单词,对于中文来说一般指分词后的一个词。
  • 词典(Term Dictionary):或字典,是词条Term的集合。搜索引擎的通常索引单位是单词,单词词典是由文档集合中出现过的所有单词构成的字符串集合,==单词词典内每条索引项记载单词本身的一些信息以及指向“倒排列表”的指针。==
  • 倒排表(Post list):一个文档通常由多个词组成,倒排表记录的是某个词在哪些文档里出现过以及出现的位置。每条记录称为一个倒排项(Posting)。倒排表记录的不单是文档编号,还存储了词频等信息。
  • 倒排文件(Inverted File):所有单词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件被称之为倒排文件,倒排文件是存储倒排索引的物理文件。

2.互联网案例

2.1.京东到家订单中心 Elasticsearch 演进历程

​ ==京东到家订单中心系统业务中==,无论是外部商家的订单生产,或是内部上下游系统的依赖,订单查询的调用量都非常大,造成了订单数据读多写少的情况。京东到家的订单数据存储在MySQL中,但显然只通过DB来支撑大量的查询是不可取的,同时对于一些==复杂的查询,Mysql支持得不够友好==,所以订单中心系统使用了Elasticsearch来承载订单查询的主要压力。

​ Elasticsearch 做为一款功能强大的分布式搜索引擎,支持近实时的存储、搜索数据,在京东到家订单系统中发挥着巨大作用,目前订单中心ES集群存储数据量达到==10亿个文档,日均查询量达到5亿。==随着京东到家近几年业务的快速发展,订单中心ES架设方案也不断演进,发展至今ES集群架设是一套实时互备方案,很好的保障了ES集群读写的稳定性。

2.2.携程Elasticsearch应用案例

目前,我们最大的日志单集群有120个data node,运行于70台物理服务器上。数据规模如下:

  • 单日索引数据条数600亿,新增索引文件25TB (含一个复制片则为50TB)
  • 业务高峰期峰值索引速率维持在百万条/秒
  • 历史数据保留时长根据业务需求制定,从10天 - 90天不等
  • 集群共3441个索引、17000个分片、数据总量约9300亿, 磁盘总消耗1PB

2.3.去哪儿:订单中心基于elasticsearch 的解决方案

​ 15年去哪儿网酒店日均订单量达到30w+,随着多平台订单的聚合日均订单能达到100w左右。原来采用的热表分库方式,即将最近6个月的订单的放置在一张表中,将历史订单放在在history表中。history表存储全量的数据,当用户查询的下单时间跨度超过6个月即查询历史订单表,此分表方式热表的数据量为4000w左右,当时能解决的问题。但是显然不能满足携程艺龙订单接入的需求。如果继续按照热表方式,数据量将超过1亿条。全量数据表保存2年的可能就超过4亿的数据量。所以寻找有效途径解决此问题迫在眉睫。==由于对这预计4亿的数据量还需按照预定日期、入住日期、离店日期、订单号、联系人姓名、电话、酒店名称、订单状态……等多个条件查询。==所以简单按照某一个维度进行分表操作没有意义。Elasticsearch分布式搜索储存集群的引入,就是为了解决订单数据的存储与搜索的问题。

对订单模型进行抽象和分类,将常用搜索字段和基础属性字段剥离。DB做分库分表,存储订单详情;Elasticsearch存储搜素字段。

订单复杂查询直接走Elasticsearch,基于OrderNo的简单查询走DB,如下图所示。

img

系统伸缩性:Elasticsearch 中索引设置了8个分片,目前ES单个索引的文档达到1.4亿,合计达到2亿条数据占磁盘大小64G,集群机器磁盘容量240G。

2.4.滴滴Elasticsearch多集群架构实践

​ 滴滴 2016 年初开始构建 Elasticsearch 平台,如今已经发展到超过 3500+ Elasticsearch 实例,超过 5PB 的数据存储,峰值写入 tps 超过了 2000w/s 的超大规模。Elasticsearch 在滴滴有着非常丰富的使用场景,例如线上核心的打车地图搜索,客服、运营的多维度查询,滴滴日志服务等近千个平台用户。

3.ElasticSearch 基础概念

3.1.ElaticSearch 和 DB 的关系

在 Elasticsearch 中,文档归属于一种类型 ==type==,而这些类型存在于索引 index 中,我们可以列一些简单的不同点,来类比传统关系型数据库:

  • Relational DB -> Databases -> Tables -> Rows -> Columns
  • Elasticsearch -> Indices -> ==Types== -> Documents -> Fields

Elasticsearch 集群可以包含多个索引 indices,每一个索引可以包含多个类型 types,每一个类型包含多个文档 documents,然后每个文档包含多个字段 Fields。而在 DB 中可以有多个数据库 Databases,每个库中可以有多张表 Tables,没个表中又包含多行Rows,每行包含多列Columns。

3.2.索引

  • **索引基本概念(indices):**索引是含义相同属性的文档集合,是 ElasticSearch 的一个逻辑存储,可以理解为关系型数据库中的数据库,ElasticSearch 可以把索引数据存放到一台服务器上,也可以 sharding 后存到多台服务器上,每个索引有一个或多个分片,每个分片可以有多个副本。

  • **索引类型(index_type):**索引可以定义一个或多个类型,文档必须属于一个类型。在 ElasticSearch 中,一个索引对象可以存储多个不同用途的对象,通过索引类型可以区分单个索引中的不同对象,可以理解为关系型数据库中的表。每个索引类型可以有不同的结构,但是不同的索引类型不能为相同的属性设置不同的类型。

3.3.文档

  • **文档(document):**文档是可以被索引的基本数据单位。存储在 ElasticSearch 中的主要实体叫文档 document,可以理解为关系型数据库中表的一行记录。每个文档由多个字段构成,ElasticSearch 是一个非结构化的数据库,每个文档可以有不同的字段,并且有一个唯一的标识符。

  • 映射(mapping): ElasticSearch 的 Mapping 非常类似于静态语言中的数据类型:声明一个变量为 int 类型的变量,以后这个变量都只能存储 int 类型的数据。同样的,一个 number 类型的 mapping 字段只能存储 number 类型的数据。同语言的数据类型相比,Mapping 还有一些其他的含义,Mapping 不仅告诉 ElasticSearch 一个 Field 中是什么类型的值, 它还告诉 ElasticSearch 如何索引数据以及数据是否能被搜索到。

3.4.集群节点、分片和副本

  • 分片(Shards):ES支持PB级全文搜索,当索引上的数据量太大的时候,ES通过水平拆分的方式将一个索引上的数据拆分出来分配到不同的数据块上,拆分出来的数据库块称之为一个分片。这==类似于MySql的分库分表,==只不过Mysql分库分表需要借助第三方组件而ES内部自身实现了此功能。
  • 副本(Replicas):副本就是对分片的Copy,每个主分片都有一个或多个副本分片,当主分片异常时,副本可以提供数据的查询等操作。主分片和对应的副本分片是不会在同一个节点上的,所以副本分片数的最大值是 n -1(其中n为节点数)。

​ ==对文档的新建、索引和删除请求都是写操作,必须在主分片上面完成之后才能被复制到相关的副本分片,==ES为了提高写入的能力这个过程是并发写的,同时为了解决并发写的过程中数据冲突的问题,ES通过乐观锁的方式控制,每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。一旦所有的副本分片都报告写成功才会向协调节点报告成功,协调节点向客户端报告成功。

img

从上图可以看出为了达到高可用,Master节点会避免将主分片和副本分片放在同一个节点上。

假设这时节点Node1服务宕机了或者网络不可用了,那么主节点上主分片S0也就不可用了。幸运的是还存在另外两个节点能正常工作,这时ES会重新选举新的主节点,而且这两个节点上存在我们的所需要的S0的所有数据,我们会将S0的副本分片提升为主分片,这个提升主分片的过程是瞬间发生的。此时集群的状态将会为 yellow。

为什么我们集群状态是 yellow 而不是 green 呢?虽然我们拥有所有的2个主分片,但是同时设置了每个主分片需要对应两份副本分片,而此时只存在一份副本分片。所以集群不能为 green 的状态。如果我们同样关闭了 Node2 ,我们的程序依然可以保持在不丢任何数据的情况下运行,因为Node3 为每一个分片都保留着一份副本。

如果我们重新启动Node1 ,集群可以将缺失的副本分片再次进行分配,那么集群的状态又将恢复到原来的正常状态。如果Node1依然拥有着之前的分片,它将尝试去重用它们,只不过这时Node1节点上的分片不再是主分片而是副本分片了,如果期间有更改的数据只需要从主分片上复制修改的数据文件即可。

3.4.1.物理拓扑结构

img

3.4.2.逻辑拓扑结构

img

3.4.3.存储目录结构图

img

3.4.4.节点的角色

  • 数据节点:负责数据的存储和相关的操作,==例如对数据进行增、删、改、查和聚合等操作,所以数据节点(data节点)对机器配置要求比较高,对CPU、内存和I/O的消耗很大。==通常随着集群的扩大,需要增加更多的数据节点来提高性能和可用性。
  • 主节点(候选) :可以被选举为主节点(master节点),==集群中只有候选主节点才有选举权和被选举权,其他节点不参与选举的工作。主节点负责创建索引、删除索引、跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点、追踪集群中节点的状态等,稳定的主节点对集群的健康是非常重要的。==
  • 协调节点:虽然对节点做了角色区分,但是用户的请求可以发往任何一个节点,并由该节点负责分发请求、收集结果等操作,而不需要主节点转发,这种节点可称之为协调节点,协调节点是不需要指定和配置的,集群中的任何节点都可以充当协调节点的角色。

img

3.5.ES的机制原理

3.5.1.写索引原理

下图描述了3个节点的集群,共拥有12个分片,其中有4个主分片(S0、S1、S2、S3)和8个副本分片(R0、R1、R2、R3),每个主分片对应两个副本分片,节点1是主节点(Master节点)负责整个集群的状态。

img

第一步: 节点路由选择

概念 ===>>> 写索引是只能写在主分片上,然后同步到副本分片。

首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:

shard = hash(routing) % number_of_primary_shards

routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到余数 。这个在 0 到 numberofprimary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。

img

  1. 客户端向ES1节点(协调节点)发送写请求,通过路由计算公式得到值为0,则当前数据应被写到主分片S0上。
  2. ES1节点将请求转发到S0主分片所在的节点ES3,ES3接受请求并写入到磁盘。
  3. 并发将数据复制到两个副本分片R0上,其中通过乐观并发控制数据的冲突。一旦所有的副本分片都报告成功,则节点ES3将向协调节点报告成功,协调节点向客户端报告成功。

3.5.2.Update和Delete实现原理

删除和更新操作也是写操作。但是,Elasticsearch中的文档是不可变的(immutable),因此不能删除或修改。

如何删除/更新文档呢?

删除:磁盘上的每个分段(segment)都有一个.del文件与它相关联。当发送删除请求时,该文档未被真正删除,而是在.del文件中标记为已删除。此文档可能仍然能被搜索到,但会从结果中过滤掉。当分段合并时,在.del文件中标记为已删除的文档不会被包括在新的合并段中。

更新:创建新文档时,Elasticsearch将为该文档分配一个版本号。对文档的每次更改都会产生一个新的版本号。当执行更新时,旧版本在.del文件中被标记为已删除,并且新版本在新的分段中编入索引。其实更新相当于是删除和新增这两个动作组成。会将旧的文档在 .del文件中标记删除,然后文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就会被移除。

3.5.3.Read的实现原理

读操作由两个阶段组成:查询阶段(query)和获取(fetch)阶段。

查询阶段

在此阶段,协调节点将搜索请求路由到索引(index)中的所有分片(shards)(包括:主分片或副本分片)。分片独立执行搜索,并根据相关性分数创建一个优先级排序结果(稍后我们将介绍相关性分数)。所有分片将匹配的文档和相关分数的文档ID返回给协调节点。协调节点创建一个新的优先级队列,并对全局结果进行排序。可以有很多文档匹配结果,但默认情况下,每个分片将前10个结果发送到协调节点,协调创建优先级队列,从所有分片中分选结果并返回前10个匹配。

获取阶段

在协调节点对所有结果进行排序,生成全局排序的文档列表后,它将向所有分片请求原始文档。所有的分片都会丰富文档并将其返回到协调节点。

img

3.5.4.存储原理

上面介绍了在ES内部索引的写处理流程,这个流程是在ES的内存中执行的,数据被分配到特定的分片和副本上之后,最终是存储到磁盘上的,这样在断电的时候就不会丢失数据。

3.5.4.1.分段存储

索引文档以段的形式存储在磁盘上,何为?==索引文件被拆分为多个子文件,则每个子文件叫作, 每一个段本身都是一个倒排索引,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改。在底层采用了分段的存储模式,使它在读写时几乎完全避免了锁的出现,大大提升了读写性能。==

段被写入到磁盘后会生成一个提交点,提交点是一个用来记录所有提交后段信息的文件。一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限。相反,当段在内存中时,就只有写的权限,而不具备读数据的权限,意味着不能被检索。

的概念提出主要是因为:在早期全文检索中为整个文档集合建立了一个很大的倒排索引,并将其写入磁盘中。如果索引有更新,就需要重新全量创建一个索引来替换原来的索引。这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,所以对数据的更新不能过于频繁,也就不能保证时效性。

段被设定为不可修改具有一定的优势也有一定的缺点,优势主要表现在:

  • 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
  • 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
  • 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
  • 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。

段的不变性的缺点如下:

  • 当对旧数据进行删除时,旧数据不会马上被删除,而是在 .del文件中被标记为删除。而旧数据只能等到段更新时才能被移除,这样会造成大量的空间浪费。
  • 若有一条数据频繁的更新,每次更新都是新增新的标记旧的,则会有大量的空间浪费。
  • 每次新增数据时都需要新增一个段来存储数据。当段的数量太多时,对服务器的资源例如文件句柄的消耗会非常大。
  • 在查询的结果中包含所有的结果集,需要排除被标记删除的旧数据,这增加了查询的负担。

3.5.4.2.延迟写策略

介绍完了存储的形式,那么索引是写入到磁盘的过程是这怎样的?是否是直接调 fsync 物理性地写入磁盘?

答案是显而易见的,如果是直接写入到磁盘上,磁盘的I/O消耗上会严重影响性能,那么当写数据量大的时候会造成ES停顿卡死,查询也无法做到快速响应。如果真是这样ES也就不会称之为近实时全文搜索引擎了。

为了提升写的性能,ES并没有每新增一条数据就增加一个段到磁盘上,而是采用延迟写的策略。

每当有新增的数据时,就将其先写入到内存中,在内存和磁盘之间是文件系统缓存,当达到默认的时间(1秒钟)或者内存的数据达到一定量时,会触发一次刷新(Refresh),将内存中的数据生成到一个新的段上并缓存到文件缓存系统 上,稍后再被刷新到磁盘中并生成提交点

这里的内存使用的是ES的JVM内存,而文件缓存系统使用的是操作系统的内存。新的数据会继续的被写入内存,但内存中的数据并不是以段的形式存储的,因此不能提供检索功能。由内存刷新到文件缓存系统的时候会生成了新的段,并将段打开以供搜索使用,而不需要等到被刷新到磁盘。

在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做 refresh (即内存刷新到文件缓存系统)。默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说 Elasticsearch 是近实时搜索,因为文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。我们也可以手动触发 refresh, POST/_refresh 刷新所有索引, POST/nba/_refresh刷新指定的索引。

Tips:尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候, 手动刷新很有用,但是不要在生产> 环境下每次索引一个文档都去手动刷新。而且并不是所有的情况都需要每秒刷新。可能你正在使用 Elasticsearch 索引大量的日志文件, 你可能想优化索引速度而不是> 近实时搜索, 这时可以在创建索引时在 settings中通过调大 refresh_interval="30s" 的值 , 降低每个索引的刷新频率,设值时需要注意后面带上时间单位,否则默认是毫秒。当 refresh_interval=-1时表示关闭索引的自动刷新。

3.5.4.3.事务日志(Translog)

为了避免丢失数据,Elasticsearch添加了事务日志(Translog),事务日志记录了所有还没有持久化到磁盘的数据。添加了事务日志后整个写索引的流程如下图所示。

img

  • 一个新文档被索引之后,先被写入到内存中,但是为了防止数据的丢失,会追加一份数据到事务日志中。不断有新的文档被写入到内存,同时也都会记录到事务日志中。这时新数据还不能被检索和查询。
  • 当达到默认的刷新时间或内存中的数据达到一定量后,会触发一次 refresh,将内存中的数据以一个新段形式刷新到文件缓存系统中并清空内存。这时虽然新段未被提交到磁盘,==但是可以提供文档的检索功能且不能被修改。==
  • ==随着新文档索引不断被写入,当日志数据大小超过512M或者时间超过30分钟时,会触发一次 flush。内存中的数据被写入到一个新段同时被写入到文件缓存系统,文件系统缓存中数据通过 fsync 刷新到磁盘中,生成提交点,日志文件被删除,创建一个空的新日志。==

img

translog其实也是先写入os cache的,默认每隔5秒刷一次到磁盘中去,所以默认情况下,可能有5秒的数据会仅仅停留在buffer或者translog文件的os cache中,如果此时机器挂了,会丢失5秒钟的数据。但是这样性能比较好,最多丢5秒的数据。也可以将translog设置成每次写操作必须是直接fsync到磁盘,但是性能会差很多。

3.5.5.段合并

由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段然后合并查询结果,所以段越多,搜索也就越慢。

Elasticsearch通过在后台定期进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档不会被拷贝到新的大段中。合并的过程中不会中断索引和搜索。

img

段合并在进行索引和搜索时会自动进行,合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中,这些段既可以是未提交的也可以是已提交的。合并结束后老的段会被删除,新的段被 flush 到磁盘,同时写入一个包含新段且排除旧的和较小的段的新提交点,新的段被打开可以用来搜索。

段合并的计算量庞大, 而且还要吃掉大量磁盘 I/O,段合并会拖累写入速率,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好地执行。

3.6.Elasticsearch检索分类详解

这里写图片描述

3.6.1.结构化检索

针对字段类型: 日期、时间、数字类型,以及精确的文本匹配。

结构化检索特点:

  1. 结构化查询,我们得到的结果 总是 非是即否,要么存于集合之中,要么存在集合之外。
  2. 结构化查询不关心文件的相关度或评分;它简单的对文档包括或排除处理。

3.6.2.精确值查找

3.6.2.1.单个精确值查找(term query)

term 查询会查找我们指定的精确值。term 查询是简单的,它接受一个字段名以及我们希望查找的数值。

想要类似mysql中如下sql语句的查询操作:

SELECT document FROM products WHERE price = 20;

DSL写法

GET /my_store/products/_search
{
  "query" : {
  "term" : {
  "price" : 20
  }
  }
}

==当进行精确值查找时, 我们会使用过滤器(filters)。过滤器很重要,因为它们执行速度非常快,不会计算相关度(直接跳过了整个评分阶段)而且很容易被缓存==。如下: 使用 constant_score 查询以非评分模式来执行 term 查询并以一作为统一评分。

GET /my_store/products/_search
{
  "query" : {
  	"constant_score" : {
  		"filter" : {
  			"term" : {
  				"price" : 20
  			}
  		}
  	}
  }
}

注意:5.xES以上,对于字符串类型,要进行精确值匹配。需要讲类型设置为text和keyword两种类型。mapping设置如下:

POST testindex/testtype/_mapping
{
  "testtype ":{
  	"properties":{
  		"title":{
  			"type":"text",
  			"analyzer":"ik_max_word",
  			"search_analyzer":"ik_max_word",
  			"fields":{
  				"keyword":{
  					"type":"keyword"
  				}
  			}
  		}
	}
}

精确值java api jest使用方法:
searchSourceBuilder.query(QueryBuilders.termQuery(“text.keyword”, “来自新华社的报道”));

3.6.2.2. 布尔过滤器

一个 bool 过滤器由三部分组成:

{
   "bool" : {
      "must" :     [],
      "should" :   [],
      "must_not" : [],
      "filter":    []
   }
}
  • must ——所有的语句都 必须(must) 匹配,与 AND 等价。
  • must_not ——所有的语句都 不能(must not) 匹配,与 NOT 等价。
  • should ——至少有一个语句要匹配,与 OR 等价。
  • filter——必须匹配,运行在非评分&过滤模式。
GET /my_store/products/_search
{
  "query" : {
  	"filtered" : {
  		"filter" : {
  			"bool" : {
  				"should" : [
  					{ "term" : {"price" : 20}},
  					{ "term" : {"productID" : "XHDK-A-1293-#fJ3"}}
                ],
  				"must_not" : {
  					"term" : {"price" : 30}
  				}
  			}
  		}
  	}
  }
}

3.6.2.3多个值精确查找(terms query)

{
  "terms" : {
  "price" : [20, 30]
  }
}

如上,terms是包含的意思,包含20或者包含30。

如下实现严格意义的精确值检索, tag_count代表必须匹配的次数为1。

GET /my_index/my_type/_search
{
  "query": {
  	"constant_score" : {
  		"filter" : {
  			"bool" : {
  				"must" : [
  					{ "term" : { "tags" : "search" } },
  					{ "term" : { "tag_count" : 1 } }
  				]
  			}
  		}
  	}
  }
}

3.6.2.4.范围检索(range query)

range 查询可同时提供包含(inclusive)和不包含(exclusive)这两种范围表达式,可供组合的选项如下:

gt: > 大于(greater than)
lt: < 小于(less than)
gte: >= 大于或等于(greater than or equal to)
lte: <= 小于或等于(less than or equal to)

类似Mysql中的范围查询:
SELECT document FROM products WHERE price BETWEEN 20 AND 40

ES中对应的DSL如下:

GET /my_store/products/_search
{
  "query" : {
  	"constant_score" : {
  		"filter" : {
  			"range" : {
  				"price" : {
  					"gte" : 20,
  					"lt" : 40
  				}
  			}
  		}
  	}
  }
}

3.6.2.5.存在与否检索(exist query)

mysql中,有如下sql:

SELECT tags FROM posts WHERE tags IS NOT NULL;

ES中,exist查询某个字段是否存在:

GET /my_index/posts/_search
{
    "query" : {
        "constant_score" : {
            "filter" : {
                "exists" : { "field" : "tags" }
            }
        }
    }
}

若想要exist查询能匹配null类型,需要设置mapping:

"user": {
  "type": "keyword",
  "null_value": "_null_"
  }

missing查询在5.x版本已经不存在,改成如下的判定形式:

GET /_search
{
    "query": {
        "bool": {
            "must_not": {
                "exists": {
                    "field": "user"
                }
            }
        }
    }
}

3.6.2.6.前缀检索( Prefix Query )

匹配包含 not analyzed 的前缀字符:

GET /_search
{ "query": {
  "prefix" : { "user" : "ki" }
  }
}

3.6.2.7.通配符检索( wildcard query)

匹配具有匹配通配符表达式 (not analyzed )的字段的文档。 支持的通配符:

  • *,它匹配任何字符序列(包括空字符序列);
  • ?,它匹配任何单个字符。

请注意,此查询可能很慢,因为它需要遍历多个术语。

为了防止非常慢的通配符查询,通配符不能以任何一个通配符*或?开头。

举例:

GET /_search
{
    "query": {
        "wildcard" : { "user" : "ki*y" }
    }
}

3.6.2.8.正则表达式检索(regexp query)

正则表达式查询允许您使用正则表达式术语查询。


GET /_search
{
  "query": {
  	"regexp":{
  		"name.first": "s.*y"
  	}
  }
}

注意: ==的匹配会非常慢,你需要使用一个长的前缀, 通常类似.?+通配符查询的正则检索性能会非常低==。

3.6.2.9.模糊检索(fuzzy query)

fuzzy query 是基于Levenshtein Edit Distance(莱温斯坦编辑距离)基础上,对索引文档进行模糊搜索。 当用户输入有错误时,使用这个功能能在一定程度上召回一些和输入相近的文档。形式化地说,两个单词的莱文斯坦距离是一个单词变成另一个单词要求的最少单个字符编辑数量(如:删除、插入和替换)。莱文斯坦距离也被称做编辑距离,尽管它只是编辑距离的一种,与成对字符串比对紧密相关。

==例如,“kitten"和”sitting"的编辑距离是3,因为按照如下需3个字符编辑从源字符串到目标字符串且没有比这种方式更少的编辑方式==

1.kitten->sitten(用's'取代‘k')

2.sitten->sittin(用’i'取代’e')

3.sittin->sitting(在末尾插入’g')

参数名含义
fuzziness定义最大的编辑距离,默认为AUTO,即按照es的默认配置。fuzziness可选的值为0,1,2,也就是说编辑距离最大只能设置为2;在AUTO模式下,es将根据输入查询的term的长度决定编辑距离大小。用户也可以自定义term长度边界的最大和最小值,AUTO:[low],[high],如果没有定义的话,默认值为3和6,即等价于 AUTO:3,6,即按照以下方案:0-2:必须精确匹配;3-5:编辑距离为1;>5:编辑距离为2
prefix_length输入查询term的长度:定义最初始不会被“模糊”的term的数量。这是基于用户的输入一般不会在最开始犯错误的设定的基础上设置的参数。这个参数的设定将减少去召回限定编辑距离的的term时,检索的term的数量。默认参数为0.
max_expansions0-2:必须精确匹配定义fuzzy query会扩展的最大term的数量。默认为50.
transpositions3-5:编辑距离为1定义在计算编辑聚利时,是否允许term的交换(例如ab->ba),实际上,如果设置为true的话,计算的就是Damerau,F,J distance。默认参数为false。

注意:如果prefix_length设为0并且max_expansions设置为很大的一个数,这个查询的计算量将会是非常大。很有可能导致索引里的每个term都被检查一遍。

GET /_search
{
    "query": {
        "fuzzy" : {
            "user" : {
                "title": "ki",
                "boost": 1.0,
                "fuzziness": 2,
                "prefix_length": 0,
                "max_expansions": 100
            }
        }
    }
}

img

至于FST是什么,具体可以参考:lucene字典实现原理

如果想进一步深入了解如何根据编辑距离进行召回,可以参考:Levenshtein Automata

{
    "_index": "bitao_fuzzy_test",
    "_type": "doc",
    "_id": "2",
    "_score": 1,
    "_source": {
        "id": 2,
        "title": "组合沙发",
        "title_pinyin": "zu he sha fa",
        "title_pinyin_continuous": "zuheshafa"
    }
},
{
    "_index": "bitao_fuzzy_test",
    "_type": "doc",
    "_id": "4",
    "_score": 1,
    "_source": {
        "id": 4,
        "title": "卧室电视柜",
        "title_pinyin": "wo shi dian shi gui",
        "title_pinyin_continuous": "woshidianshigui"
    }
},
{
    "_index": "bitao_fuzzy_test",
    "_type": "doc",
    "_id": "5",
    "_score": 1,
    "_source": {
        "id": 5,
        "title": "酒柜",
        "title_pinyin": "jiu gui",
        "title_pinyin_continuous": "jiugui"
    }
},
{
    "_index": "bitao_fuzzy_test",
    "_type": "doc",
    "_id": "6",
    "_score": 1,
    "_source": {
        "id": 6,
        "title": "橱柜",
        "title_pinyin": "chu gui",
        "title_pinyin_continuous": "chugui"
    }
},
{
    "_index": "bitao_fuzzy_test",
    "_type": "doc",
    "_id": "1",
    "_score": 1,
    "_source": {
        "id": 1,
        "title": "沙发组合",
        "title_pinyin": "sha fa zu he",
        "title_pinyin_continuous": "shfazuhe"
    }
},
{
    "_index": "bitao_fuzzy_test",
    "_type": "doc",
    "_id": "3",
    "_score": 1,
    "_source": {
        "id": 3,
        "title": "电视柜",
        "title_pinyin": "dian shi gui",
        "title_pinyin_continuous": "dianshigui"
    }
}

每个文档都将经过ik_max_word的中文分词器,经过分词后,构建的词典含有以下词:
"token": "卧室",
"token": "电视机",
"token": "电视",
"token": "机柜",
"token": "组合",
"token": "沙发",
"token": "酒柜",
"token": "橱柜",
"token": "电视柜",
"token": "电视",
"token": "柜",

这时我们进行如下的模糊查询:
{
 "profile":"true",
  "query": {
    "multi_match": {
      "fields":  [ "title" ],
      "query":     "卧室电视机柜",
      "fuzziness": "1"
    }
  }
}

这时将得到以下的召回
{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 3,
    "successful": 3,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 4,
    "max_score": 3.1329184,
    "hits": [
      {
        "_index": "bitao_suggester_test",
        "_type": "doc",
        "_id": "4",
        "_score": 3.1329184,
        "_source": {
          "id": 4,
          "title": "卧室电视柜",
          "title_pinyin": "wo shi dian shi gui",
          "title_pinyin_continuous": "woshidianshigui"
        }
      },
      {
        "_index": "bitao_suggester_test",
        "_type": "doc",
        "_id": "3",
        "_score": 1.708598,
        "_source": {
          "id": 3,
          "title": "电视柜",
          "title_pinyin": "dian shi gui",
          "title_pinyin_continuous": "dianshigui"
        }
      },
      {
        "_index": "bitao_suggester_test",
        "_type": "doc",
        "_id": "5",
        "_score": 0.75678295,
        "_source": {
          "id": 5,
          "title": "酒柜",
          "title_pinyin": "jiugui"
        }
      },
      {
        "_index": "bitao_suggester_test",
        "_type": "doc",
        "_id": "6",
        "_score": 0.75678295,
        "_source": {
          "id": 6,
          "title": "橱柜",
          "title_pinyin": "chugui"
        }
      }
    ]
  }
}

为什么会召回这么多文档,按照编辑距离的定义,只有"卧室电视柜"与原query :"卧室电视机柜"编辑距离为1才对。

而我们的词典包含 "卧室","电视机","电视","机柜","组合","沙发", "酒柜", "橱柜","电视柜","电视", "柜"

img

实际上,当对“卧室电视机柜” 进行fuzzy query时,es首先对其进行分词,然后针对每个词,进行编辑距离为一的词典词召回。

其中分出来的词“卧室” 与词典中的 “卧室”的Levenshtein Distance都为0,所以都召回;

其中分出来的词“电视机” 与词典中的 “电视”的Levenshtein Distance为1,所以 “电视”被召回,与“电视柜”编辑距离为1,所以“电视柜”也被召回;

其中分出来的词“电视” 与词典中的 “电视”的Levenshtein Distance为0,所以 “电视”被召回,与“电视柜”编辑距离为1,所以“电视柜”也被召回;

其中分出来的词“机柜” 与词典中的 “柜”、“酒柜”、“橱柜”的Levenshtein Distance都为1,所以都被召回了;

3.6.2.10.Ids检索(ids query)

返回指定id的全部信息。

GET /my_index/_search
{
  "query": {
  	"ids" : {
  		"values" : ["2", "4", "100"]
  	}
  }
}

3.6.3.全文检索

高级全文查询通常用于在全文本字段(如电子邮件正文)上运行全文查询。他们了解如何对被查询的字段进行分析,并在执行前将每个字段的分析器(或search_analyzer)应用于查询字符串。

会对输入做分词,但是需要结果中也包含所有的分词,而且顺序要求一样。以"hello world"为例,要求结果中必须包含hello和world,而且还要求他们是连着的,顺序也是固定的,hello that word不满足,world hello也不满足条件。

3.6.3.1.匹配检索(match query)

匹配查询接受文本/数字/日期类型,分析它们,并构造查询。

1)匹配查询的类型为boolean。 这意味着分析所提供的文本,并且分析过程从提供的文本构造一个布尔查询,
可以将运算符标志设置为或以控制布尔子句(默认为或);

2)文本分析取决于mapping中设定的analyzer(中文分词,我们默认选择ik分词器);

3) fuzziness——模糊性允许基于被查询的字段的类型进行模糊匹配;

4)”operator”: “and”——匹配与操作(默认或操作);

5) “minimum_should_match”: “75%”——这让我们可以指定必须匹配的词项数用来表示一个文档是否相关。

GET /_search
{
    "query": {
        "match" : {
            "message" : {
                "query" : "this is a test",
                "operator" : "and"
            }
        }
    }
}

3.6.3.2.匹配解析检索 match_phrase

match_phrase查询分析文本,并从分析文本中创建短语查询。

match_phrase查询分析文本并根据分析的文本创建一个短语查询。match_phrase 会将检索关键词分词。match_phrase的分词结果必须在被检索字段的分词中都包含,而且顺序必须相同,而且默认必须都是连续的。

会对输入做分词,但是需要结果中也包含所有的分词,而且顺序要求一样。以"hello world"为例,要求结果中必须包含hello和world,而且还要求他们是连着的,顺序也是固定的,hello that word不满足,world hello也不满足条件。

举例如下:对于 quick fox 的短语搜索可能不会匹配到任何文档,因为没有文档包含的 quick 词之后紧跟着 fox 。

GET /my_index/my_type/_search
{
  "query": {
  	"match_phrase": {
  		"title": "quick brown fox",
        "slop":1
  	}
  }
}

3.6.3.3.匹配解析前缀检索(match_phrase_prefix)

与match_phrase查询类似,但是会对最后一个Token在倒排序索引列表中进行通配符搜索。Token的模糊匹配数控制:**max_expansions 默认值为50。**我们使用

用户已经渐渐习惯在输完查询内容之前,就能为他们展现搜索结果,这就是所谓的 即时搜索(instant search) 或 输入即搜索(search-as-you-type) 。

不仅用户能在更短的时间内得到搜索结果,我们也能引导用户搜索索引中真实存在的结果。

例如,如果用户输入 johnnie walker bl ,我们希望在它们完成输入搜索条件前就能得到:
Johnnie Walker Black Label 和 Johnnie Walker Blue Label 。

match_phrase_prefix与match_phrase相同,除了它允许文本中最后一个术语的前缀匹配。
举例:

GET / _search
{
    “query”:{
        “match_phrase_prefix”:{
            “message”:“quick brown f”
        }
    }
}


案例分析

# 导入测试数据
POST _bulk
{ "index" : { "_index" : "tehero_index", "_type" : "_doc", "_id" : "1" } }
{ "id" : 1,"content":"关注我,系统学编程" }
{ "index" : { "_index" : "tehero_index", "_type" : "_doc", "_id" : "2" } }
{ "id" : 2,"content":"系统学编程,关注我" }
{ "index" : { "_index" : "tehero_index", "_type" : "_doc", "_id" : "3" } }
{ "id" : 3,"content":"系统编程,关注我" }
{ "index" : { "_index" : "tehero_index", "_type" : "_doc", "_id" : "4" } }
{ "id" : 4,"content":"关注我,间隔系统学编程" }

img

1.match query 对应到mysql
GET /tehero_index/_doc/_search
{
  "query":{
    "match":{
      "content.ik_smart_analyzer":"系统编程"
    }
  }
}

DSL执行步骤分析:
1)检索词“系统编程”被ik_smart分词器分词为两个Token【系统】【编程】;
2)将这两个Token在【倒排索引】中,针对Token字段进行检索,等价于sql:【where Token = 系统 or Token = 编程】;
3)对照图【数据的倒排序索引】,可见,该DSL能检索到所有文档,文档3的评分最高(因为它包含两个Token),其他3个文档评分相同。

2.match_phrase query
GET /tehero_index/_doc/_search
{
    "query": {
        "match_phrase": {
            "content.ik_smart_analyzer": {
            	"query": "关注我,系统学"
            }
        }
    }
}

# 结果:只有文档1
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.7370664,
    "hits": [
      {
        "_index": "tehero_index",
        "_type": "_doc",
        "_id": "1",
        "_score": 0.7370664,
        "_source": {
          "id": 1,
          "content": "关注我,系统学编程"
        }
      }
    ]
  }
}

分析:上面的例子使用的分词器是ik_smart,所以检索词“关注我,系统学”会被分词为3个Token【关注、我、系统学】;而文档1、文档2和文档4 的content被分词后都包含这3个关键词,但是只有文档1的Token的顺序和检索词一致,且连续。所以使用 match_phrase 查询只能查询到文档1(ps:文档2 Token顺序不一致;文档4 Token不连续;文档3 Token没有完全包含)。使用 match查询可以查询到所有文档,是因为所有文档都有【关注、我】这两个Token。

3.match_phrase 核心参数:slop 参数-Token之间的位置距离容差值
# 将上面的 match_phrase 查询新增一个 slop参数
GET /tehero_index/_doc/_search
{
    "query": {
        "match_phrase": {
            "content.ik_smart_analyzer": {
            	"query": "关注我,系统学",
            	"slop":1
            }
        }
    }
}
# 结果:文档1和文档4都被检索出来



4.match_phrase_prefix query
content.ik_smart_analyzer 这个字段中的【系统学】(文档1、2、4 包含)和【系统】(文档3包含)这两个Token来讲解match_phraseprefix 的用法:(因为使用的是ik_smart分词器,所以【系统学】就只能被分词为一个Token)

# 1、先使用match_phrase查询,没有结果
GET tehero_index/_doc/_search
{
  "query": {
    "match_phrase": {
      "content.ik_smart_analyzer": {
        "query": "系"
      }
    }
  }
}

# 2、使用match_phrase_prefix查询, "max_expansions": 1,得到文档3
GET tehero_index/_doc/_search
{
  "query": {
    "match_phrase_prefix": {
      "content.ik_smart_analyzer": {
        "query": "系",
        "max_expansions": 1
      }
    }
  }
}

# 3、使用match_phrase_prefix查询, "max_expansions": 2,得到所有文档
GET tehero_index/_doc/_search
{
  "query": {
    "match_phrase_prefix": {
      "content.ik_smart_analyzer": {
        "query": "系",
        "max_expansions": 2
      }
    }
  }
}

结果分析:【语句1】查不到结果,是因为根据ik_smart分词器生成的倒排序索引中,所有文档中都不包含Token【系】;【语句2】查询到文档3,是因为文档3包含Token【系统】,同时 "max_expansions": 1,所以检索关键词【系】+ 1个通配符匹配,就可以匹配到一个Token【系统】;【语句3】查询到所有文档,是因为"max_expansions": 2,所以检索关键词【系】+ 2个通配符匹配,就可以匹配到两个Token【系统、系统学】,所以就可以查询到所有。回忆下,之前所讲的es倒排序索引原理:先分词创建倒排序索引,再检索倒排序索引得到文档,就很好理解了。

注意:"max_expansions"的值最小为1,**哪怕你设置为0,依然会 + 1个通配符匹配;所以,**尽量不要用该语句,因为,最后一个Token始终要去扫描大量的索引性能可能会很差。

3.6.3.4.多字段匹配检索( multi_match query)

multi_match 查询为能在多个字段上反复执行相同查询提供了一种便捷方式。

默认情况下,查询的类型是 best_fields, 这表示它会为每个字段生成一个 match 查询。

举例1:”fields”: “*_title”
——任何与模糊模式正则匹配的字段都会被包括在搜索条件中, 例如可以左侧的方式同时匹配 book_title 、 chapter_title和 section_title (书名、章名、节名)这三个字段。

举例2: “fields”: [ “*_title”, “chapter_title^2” ]

——可以使用 字符语法为单个字段提升权重,在字段名称的末尾添加 boost , 其中 boost 是一个浮点数。
举例3:”fields”: [ “first_name”, “last_name” ], “operator”: “and”

——两个字段必须都包含。

GET /_search
{
  "query": {
  	"multi_match" : {
  		"query": "this is a test",
  		"fields": [ "subject", "message" ]
  	}
  }
}

3.6.3.5.字符串检索(query_string)

一个使用查询解析器解析其内容的查询。

query_string查询提供了以简明的简写语法执行多匹配查询 multi_match queries ,布尔查询 bool queries ,提升得分 boosting ,模糊匹配 fuzzy matching ,通配符 wildcards ,正则表达式 regexp 和范围查询 range queries 的方式。
支持参数达10几种。

参考:

https://www.elastic.co/guide/en/elasticsearch/reference/6.8/query-dsl-query-string-query.html


GET /_search
{
  "query": {
  	"query_string" : {
  		"default_field" : "content",
  		"query" : "this AND that OR thus"
  	}
  }
}

GET /_search
{
    "query": {
        "query_string": {
            "query": "(content:this OR name:this) AND (content:that OR name:that)"
        }
    }
}

GET /_search
{
    "query": {
        "query_string" : {
            "fields" : ["content", "name.*^5"],
            "query" : "this AND that OR thus"
        }
    }
}

3.6.3.6.简化字符串检索(simple_query_string)

一个使用SimpleQueryParser解析其上下文的查询。 与常规query_string查询不同,==simple_query_string查询永远不会抛出异常,并丢弃查询的无效部分==。

GET /_search
{
    "query": {
        "simple_query_string" : {
            "fields" : ["content"],
            "query" : "foo bar -baz"
        }
    }
}

支持的操作如下: 
1)+表示AND操作 
2)| 表示OR操作 
3)- 否定操作 
4)*在术语结束时表示前缀查询 
5)(和)表示优先
6) "表示短语

GET /_search
{
  "query": {
    "simple_query_string" : {
        "query": "\"fried eggs\" +(eggplant | potato) -frittata",
        "fields": ["title^5", "body"],
        "default_operator": "and"
    }
  }
}

3.6.3.7.function_score 简介

在使用ES进行全文搜索时,搜索结果默认会以文档的相关度进行排序,而这个 "文档的相关度",是可以透过 function_score 自己定义的

  • function_score是专门用于处理文档_score的DSL,它允许爲每个主查询query匹配的文档应用加强函数, 以达到改变原始查询评分 score的目的
  • function_score会在主查询query结束后对每一个匹配的文档进行一系列的重打分操作,能够对多个字段一起进行综合评估,且能够使用 filter 将结果划分爲多个子集 (每个特性一个filter),并爲每个子集使用不同的加强函数

function_score 提供了几种加强_score计算的函数

  • weight : 设置一个简单而不被规范化的权重提升值

    • weight加强函数和 boost参数类似,可以用于任何查询,不过有一点差别是weight不会被Lucene nomalize成难以理解的浮点数,而是直接被应用 (boost会被nomalize)
    • 例如当 weight 爲 2 时,最终结果爲new_score = old_score * 2
  • field_value_factor: 将某个字段的值乘上old_score

4.实战案例分析(商品搜索功能设计与实现)

4.0.分词器原理

4.0.1.Analysis与Analyzer分词器

  • Analysis - 文本分析是把全文本转换一系列单词 (term/token)的过程,也叫分词
  • Analysis 是 通过 Analyzer来实现的
    • 可使用 Elasticsearch 内置的分析器/或者按需定制化分析器
  • 除了在数据写入时转换词条,匹配 Query 语句时候也需要用相同的分析器对查询语句进行分析

4.0.2.Analyzer的组成

  • 分词器是专门处理分词的组件,Analyzer 由三部分组成,分别入下:
    • Character Filters :针对原始文本处理,例如去除html
    • Tokenizer:按照原则切分为单词
    • Token Filter:将切分的单词进行加工,将单词从大写转换为小写,删除 stopwords,增加同义词

img

  • Elasticsearch内置的分词器
    • Standard Analyzer:默认分词器,按词切分,小写处理
    • Simple Analyzer:按照非字母切分(符号被过滤),小写处理
    • Stop Analyzer:小写处理,停用词过滤(the, a, is)
    • Whitespace Analyzer:按照空格切分,不转小写
    • Keyword Analyzer:不分词,直接将输入当做输出
    • Patter Analyzer:正则表达式,默认 \W+ (非字符分割)
    • Language:提供共了30多种常见语言的分词器
    • Customer Analyzer:自定义分词器
"index" : {
    "analysis" : {
        "analyzer" : {
            "telephone_analyzer" : {
                "tokenizer" : "telephone_tokenizer"
            }
        },
        "tokenizer" : {
            "telephone_tokenizer" : {
                "token_chars" : [
                    "letter",
                    "digit"
                ],
                "min_gram" : "2",
                "type" : "edge_ngram",
                "max_gram" : "18"
            }
        }
    }
}

4.1.中文分词器

使用IKAnalyzer

  • 使用默认分词器,可以发现默认分词器只是将中文逐词分隔,并不符合我们的需求;
GET /pms/_analyze
{
  "text": "小米手机性价比很高",
  "tokenizer": "standard"
}

img

  • 使用中文分词器以后,可以将中文文本按语境进行分隔,可以满足我们的需求。
GET /pms/_analyze
{
  "text": "小米手机性价比很高",
  "tokenizer": "ik_max_word"
}

img

  • 使用Query DSL调用Elasticsearch的Restful API实现;
POST /pms/product/_search
{
  "from": 0,
  "size": 2,
  "query": {
    "multi_match": {
      "query": "小米",
      "fields": [
        "name",
        "subTitle",
        "keywords"
      ]
    }
  }
}

img

4.2.综合商品搜索

首先来说下我们的需求,按输入的关键字搜索商品名称、副标题和关键词,可以按品牌和分类进行筛选,可以有5种排序方式,默认按相关度进行排序,看下接口文档有助于理解;

img

  • 这里我们有一点特殊的需求,比如商品名称匹配关键字的的商品我们认为与搜索条件更匹配,其次是副标题和关键字,这时就需要用到function_score查询了;
  • 在Elasticsearch中搜索到文档的相关性由_score字段来表示的,文档的_score字段值越高,表示与搜索条件越匹配,而function_score查询可以通过设置权重来影响_score字段值,使用它我们就可以实现上面的需求了;
  • 使用Query DSL调用Elasticsearch的Restful API实现,可以发现商品名称权重设置为了10,商品副标题权重设置为了5,商品关键字设置为了2;
POST /pms/product/_search
{
  "query": {
    "function_score": {
      "query": {
        "bool": {
          "must": [
            {
              "match_all": {}
            }
          ],
          "filter": {
            "bool": {
              "must": [
                {
                  "term": {
                    "brandId": 6
                  }
                },
                {
                  "term": {
                    "productCategoryId": 19
                  }
                }
              ]
            }
          }
        }
      },
      "functions": [
        {
          "filter": {
            "match": {
              "name": "小米"
            }
          },
          "weight": 10
        },
        {
          "filter": {
            "match": {
              "subTitle": "小米"
            }
          },
          "weight": 5
        },
        {
          "filter": {
            "match": {
              "keywords": "小米"
            }
          },
          "weight": 2
        }
      ],
      "score_mode": "sum",
      "min_score": 2
    }
  },
  "sort": [
    {
      "_score": {
        "order": "desc"
      }
    }
  ]
}

img

4.3.相关商品推荐

首先来说下我们的需求,可以根据指定商品的ID来查找相关商品,看下接口文档有助于理解;

img

  • 这里我们的实现原理是这样的:首先根据ID获取指定商品信息,然后以指定商品的名称、品牌和分类来搜索商品,并且要过滤掉当前商品,调整搜索条件中的权重以获取最好的匹配度;
  • 使用Query DSL调用Elasticsearch的Restful API实现;
POST /pms/product/_search
{
  "query": {
    "function_score": {
      "query": {
        "bool": {
          "must": [
            {
              "match_all": {}
            }
          ],
          "filter": {
            "bool": {
              "must_not": {
                "term": {
                  "id": 28
                }
              }
            }
          }
        }
      },
      "functions": [
        {
          "filter": {
            "match": {
              "name": "红米5A"
            }
          },
          "weight": 8
        },
        {
          "filter": {
            "match": {
              "subTitle": "红米5A"
            }
          },
          "weight": 2
        },
        {
          "filter": {
            "match": {
              "keywords": "红米5A"
            }
          },
          "weight": 2
        },
        {
          "filter": {
            "term": {
              "brandId": 6
            }
          },
          "weight": 5
        },
        {
          "filter": {
            "term": {
              "productCategoryId": 19
            }
          },
          "weight": 3
        }
      ],
      "score_mode": "sum",
      "min_score": 2
    }
  }
}

img

4.4.聚合搜索商品相关信息

首先来说下我们的需求,可以根据搜索关键字获取到与关键字匹配商品相关的分类、品牌以及属性,下面这张图有助于理解;

img

这里我们可以使用Elasticsearch的聚合来实现,搜索出相关商品,聚合出商品的品牌、商品的分类以及商品的属性,只要出现次数最多的前十个即可;

POST /pms/product/_search
{
  "query": {
    "multi_match": {
      "query": "小米",
      "fields": [
        "name",
        "subTitle",
        "keywords"
      ]
    }
  },
  "size": 0,
  "aggs": {
    "brandNames": {
      "terms": {
        "field": "brandName",
        "size": 10
      }
    },
    "productCategoryNames": {
      "terms": {
        "field": "productCategoryName",
        "size": 10
      }
    },
    "allAttrValues": {
      "nested": {
        "path": "attrValueList"
      },
      "aggs": {
        "productAttrs": {
          "filter": {
            "term": {
              "attrValueList.type": 1
            }
          },
          "aggs": {
            "attrIds": {
              "terms": {
                "field": "attrValueList.productAttributeId",
                "size": 10
              },
              "aggs": {
                "attrValues": {
                  "terms": {
                    "field": "attrValueList.value",
                    "size": 10
                  }
                },
                "attrNames": {
                  "terms": {
                    "field": "attrValueList.name",
                    "size": 10
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

比如我们搜索小米这个关键字的时候,聚合出了下面的分类和品牌信息;

img

聚合出了屏幕尺寸5.05.8的筛选属性信息;

img

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://alone95.cn/archives/elasticsearch初步