4-《Elastic Stack应用宝典》第一章~第八章 ES

第一章 初识Elastic Stack

待补充》。。。。

Elasticsearch

用于数据存储和数据检索,天然支持数据分片和复制,可以轻松实现扩容,在很多情况下也被当做NoSQL数据库独立使用

1.端口

9300是集群内节点通信接口

9200是ES开放的REST接口

http://localhost:9200 

2.查看健康状态

http://localhost:9200/_cat/health 

green: 集群所有数据都处于正常状态

yellow: 集群所有数据都可以访问,但一些数据的副本还没有分配

red: 集群部署数据不可访问

http://localhost:9200/_cat/health?v 返回更详细信息

3.查看所有索引

http://localhost:9200/_cat/indices

http://localhost:9200/_cat/health?v 返回更详细信息

创建索引

PUT test

查看索引

GET test

GET _cat/indices

GET /stdin/_search   # stdin是索引名字,_search查看索引内的文档(后面可更新这块的说明)

添加文档

PUT /test/_doc/1

{

  "msg": "Hello World"  

}

会对文档字段的内容进行分析处理,然后创建倒排索引以提升检索速度。

查看文档

GET /test/_doc/1

Logstash

用于数据清洗、数据传输,可以适配多种输入和输出数据源,同时还提供丰富的过滤器插件

数据传输管道,可以从一个或多个数据源提取数据,并发送到一个或多个目标数据源。

基于插件开发,包括输入、过滤器、输出三大插件:

输入插件指定数据来源

过滤器插件对数据做过滤清洗

输出插件指定数据将被传输到哪里

config目录下的logstash-sample.conf提供了配置插件的参考。

启动:

logstash -e "input { stdin {} } output { stdout {} }"

默认情况,stdout输出插件的编解码器为rubydebug,可以改成plain或者line,如下

logstash -e "input { stdin {} } output { stdout {codec => plain} }"

 

Kibana

用于数据可视化和数据分析,可以看做是ElasticSearch的界面

访问地址:http://localhost:5601

Kibana7之前导航栏默认展开,7.0.0之后导航栏默认收起,可单机导航栏最下方的箭头展开。

侧边栏功能如下

0.Recently viewed


 

1.数据发现/文档发现(Discover)

2.数据可视化(Visualize)

3.仪表盘(Dashboard)

4.画布(Canvas)

6.地图(Maps)

7.基础设施(Infrastructure)

8.Logs

9.APM

10.Uptime

11.SIEM

12.Dev Tools

13.Management

 

Beats

Filebeats是Beats组件中的一种,是一种安装于宿主机的轻量级组件,核心作用是将宿主机上指定文件的内容提取出来并发送到指定的目的地。

Beats规范了数据采集的方式和流程,极大的扩宽了Elastic Stack的应用范围,Beats组件目前包括:Filebeat、Packetbeat、Metricbeat、Heartbeat、Auditbeat、Journalbeat、Winlogbeat、Functionbeat八种类型,他们基于同一的框架开发,在使用上有很多相似之处。

配置filebeat时(应该也有一个样例配置文件),主要指明两个参数

1.从哪里提取文件数据

2.提取出来的数据发送到哪里

第二章 Elasticsearch原理与实现

Elasticsearch是一种高度可伸缩的全文检索和分析引擎,这体现了Elasticsearch具有强大的文档检索和分析能力,另外也包含强大的数据存储能力。同时因为具有创建数据分片Shard和数据副本Replica的能力,所以可以满足大数据量下的高可用性和高性能要求,因此也被归类为一种基于文档的NoSQL数据库,类似于MongoDB。

2.1 全文检索与倒排索引

Elasticsearch中的索引是倒排索引(Inverted Index),是一种专门应用于全文检索的索引类型,这个与传统关系型数据库中的索引不同,但是为了便于理解,可以与关系型数据库做如下类比

  • 索引(Index)相当于库
  • 映射类型(Mapping Type)相当于表
  • 文档(Document)相当于行
  • 字段(Field)相当于列

2.1.1全文检索

从数据检索角度来看,数据大体上可以分为两种类型:

  • 一种是结构化数据,如传统关系型数据库的表结构
  • 一种是非结构化数据,如文本数据:文字、网页、邮件,非文本数据:图片、视频等,其中HTML网页具有一定格式的文档也称为半结构化数据

在Elastic官方文献中称全文本数据为全文数据,称全文数据汇总的一条数据为文档(Document),而称存储全文数据的数据库称为全文数据库

简单来说,全文检索是指在全文数据中检索单个文档或文档集合的搜索技术,而Elasticsearch从这个意义上来说也可以理解为是一个全文数据库。

2.1.2倒排索引

关系型数据库的索引

提升数据查询速度的常用方法就是给字段添加索引,有了索引的字段会根据字段值排序并创建类似排序二叉树的数据结构(如B树),这样就可以利用二分查找等算法提升查询速度,但由于添加索引后需要对字段排序,所以增加和删除数据时速度会变慢,并且还需要额外的空间存储索引,这是典型的利用空间换去时间的策略。

全文检索的倒排索引

1.倒排索引先将文档中包含的关键字全部提取出来,然后再将关键字和文档的对应关系保存起来,最后再对关键字本身做索引排序。用户在检索某一个关键词时,可以先对关键词的索引进行查找,再通过关键字与文档的对应关系找到所在的问题,类似于查字典。

字典的拼音表和部首表就是关键字索引,而拼音表和部首表中的内容就是关键字和文档的对应关系。有了倒排索引,用户检索就可以在倒排索引中快速定位到包含关键字的文档。倒排索引与关系型数据库索引类似,会根据关键字做排序。

2.关系型数据库索引一般是针对主键创建,然后索引指向数据内容;而倒排索引则正好相反,它是针对文档内容创建索引,然后索引指向主键(文档一、文档二),这就是这种索引被称为倒排索引的原因

在全文数据库中,文档在插入时还不是结构化的,需要应用程序根据规则自动提取关键字,并形成关键字与文档之间的结构化对应关系。由于文档在创建时需要提取关键字并创建索引,所以向全文数据库添加文档比关系型数据库要慢一些。

这些预先提取出来的关键字,在Elasticsearch中称为词项(Term,即关键词),词项提取在Elasticsearch中称为文档分析(Analysis),是整个全文检索较为核心的过程。

2.1.3Elasticsearch索引

在ES中,添加或者更新文档时最重要的动作是将它们编入倒排索引,未被编入倒排索引的文档将不能被检索,也就是说,ES中所有数据的检索都必须要通过倒排索引来检索,离开了倒排索引文档就相当于不存在。

在es中存储文档最好预先创建索引,尽管这不是必须的,但是预先创建索引可以指明文档存储时怎么分词,如何创建索引等重要配置信息,这些对于提升检索速度是有益的。

因为文档存储前的分析和索引过程比较耗资源,所以为了提升性能,文档在添加到Elasticsearch时并不是会立即被编入索引。默认情况下ES会每隔1s统一处理一次新加入的文档,可以通过index.refresh_interval参数设置,所以ES是准实时(Near Realtime,NRT)的,当然如果的确需要立即检索到,ES也提供强制刷新到索引的方式,包括使用_refresh接口和在操作文档时使用refresh参数,但这会对性能造成一定的影响。未被编入索引的文档会临时保存到缓冲区中,indeices.memory.index_buffer_size参数最小为48M且无上限。

2.1.4Elasticsearch映射

2.1.3总结:索引是存储文档的容器,文档在存储前会做文档分析(Analysis)并编入倒排索引。而文档从全文数据到索引的转变由映射(Mapping)定义,即映射介于文档与索引之间,所以一般是在创建索引时指定文档与索引的映射关系

ES中的文档使用JSON格式,JSON有一些格式规范要求,比如属性名称、数据类型等。所以严格来说,ES中存储的文档是一种半结构化数据,可以预先定义好属性和数据类型。文档中的JSON属性即为文档字段(Field)。

既然ES支持全文检索,为什么还要预先定义文档字段和数据类型呢?

- 全文数据在存储前需要做分析并提取词项,但在文档中并不是所有数据都需要这样做,比如文档创件时间、文章标题、作者等,本来就是结构化数据,没必要再做分析

- 一些结构化数据再检索时需要做精确匹配,如果做了文档分析并提取词项后,反而做不了精确匹配了

- 预先定义好文档字段可以增加数据检索的维度,提升检索质量

- 预先定义好数据类型可以优化存储结构,比如数值类型就没必要保存成字符串

综上,如果清楚知道文档存在的一些结构化特征,预先定义好它们对存储和检索都有好处,最后,在ES中存储文档也不是一定要先定义文档字段,ES也支持动态映射文档字段。

1. 映射类型(Mapping Type)是定义文档与索引映射关系的一种方式。

2. 在ES6之前,一个索引中可以定义多个映射类型,6.0版本以后映射类型的概念还将继续,但在映射中只能有一个映射类型,不允许定义多个映射类型。

3. 在7.0版本,映射类型的概念将被彻底删除,原因:映射只是逻辑上的隔离容器,在物理上并没有起到隔离文档的作用,且多个映射类型中的同名字段底层共享相同的Lucene字段,在某些情况下可能会有影响。

4. 7.0以后的版本不需要再添加映射类型,ES会为索引创建唯一的一种映射类型_doc。ES在高版本开始弱化映射类型这一概念,【未来定义不同映射类型就是创建不同的索引!!!】

创建索引的REST请求体中的mappings参数就是文档到索引的映射关系(映射类型Mapping Type)

ES 6.0

ES 7.0

补充:通过logstash的grok过滤器(或dissect过滤器、json过滤器等)从全文数据提取出结构化数据,并通过logstash的elasticsearch输出插件和json编解码器插件将logstash的事件编码为JSON格式的数据,然后输入到ES

1. logstash过滤器可以对原始数据做添加、删除、修改等基本的转换操作,也可以【对原始文本数据做结构化处理】,还可以对数据做一致性校验、清楚错误数据等,这相当于数据处理与分析领域中的数据清洗,是数据处理和分析中极为重要的一环。

2. logstash过滤器通用参数:add_field可以在过滤器执行成功后向事件中添加属性,remove_field会在过滤器执行成功后删除属性

2.2 字段

文档字段(Field)可以理解为文档的一个结构化特征。由于它在实现上是JSON的属性,所以有些文献中也将文档字段称为文档属性。

文档的具体内容都以字段为单位保存,所以字段决定了文档将以什么样的方式存储和索引。

索引文档和存储文档是两个不同的概念

  • 索引文档是将文档编入倒排索引
  • 存储文档则是将文档在物理上保存起来

索引文档是将文档(的字段)编入倒排索引,比如:text类型字段--->词项分析(Term Analysis)--->编入索引。

2.2.1字段索引

由于文档中的数据都是分散在各个字段中,所以索引文档肯定是针对文档字段进行的,一份文档一般会有多个字段,所以倒排索引一般是【多个相互关联的】倒排索引。前面所说的索引文档应该是以【文档字段为单位】对文档做索引,而并非以整个文档内容做索引

可以把索引文档和索引文档字段当做统一概念。

默认情况下,文档的所有字段都会创建倒排索引,可以通过字段的index参数设置,默认为true即字段会被编入索引。在编入索引时,一般不会将字段值整体编入。对于text类型的字段,会被解析为词项后再以词项为单位编入倒排索引。编入索引的信息包括文档ID、词项在字段中出现的频率(词频Term Frequency)、词项在字段中出现的次序(词序)、词项在字段中的起止偏移量(词项偏移量)等信息。默认情况下,除text类型的字段会保存文档ID、词频、词序以外,其余类型字段均值保存文档ID,可以通过在映射字段时通过index_option参数设置。

由上面看出,尽管默认情况下所有字段都会被索引,但是这些字段的原始值是不会被编入索引的,这意味着用户可以通过某一字段的词项检索到文档,但不能直接取到这个字段的原始值,因为字段的索引最多只包含文档ID、词频、词序、词项偏移量这4个信息。

_analyze接口查看文本分析结果

GET _analyze
{
  "analyzer": "standard",
  "text": "Elasticsearch is a search engine"
}

analyzer参数指明了使用的分析器为standard

text指明了需要做分析的文本数据

2.2.2字段存储

_source字段用于存储整个文档的原始值,特性:默认情况下不会被索引,但是每个查询默认都会带着_source字段返回

doc_values文档值存储的信息与_source基本相同,但他的存储结构是面向列的,类似于传统数据库中的表。

区别:

_source适用于text类型字段,doc_values适用于非text类型字段。

_source字段将源文档揉在一起保存,文档值则将他们按字段分别保存在不同的列中。

_source保存的是索引文档时以JSON形式传递过来的最原始信息,文档值则是经过一定分析处理的数据。

所以文档值相当于把文档中的结构化数据以结构化的方式存储起来,而对于非结构化的文本数据则不能使用文档值机制。所以再默认情况下非text类型的字段都支持文档值机制,并且都是开启的,对于text类型的字段,因为本身不是结构化数据,所以不支持文档值机制。

为啥提供文档值机制呢?

因为_source保存的是索引文档时以JSON形式传递过来的最原始信息,查看直观,但如果需要使用文档中的某一字段值做进一步运算就比较麻烦,例如在检索出文档后根据某一字段值进行排序或取字段的极值或平均值,针对这种情况ES提供了文档值机制。对于text类型的字段,ES提供了一种称为fielddata的机制处理相似的问题,和文档值效果类似,但是实现则完全不同:文档值的数据结构是保存在硬盘中,fielddata则是在内存中构件数据结构,所以使用fielddata可能导致JVM内存溢出,fielddata默认是关闭的。

在字段映射时还可以通过store参数将字段修改为true,以使索引单独保存这个字段。通常情况下,如果问到本身十分庞大,而一些单独字段又会经常单独使用,那么这样的字段就可以设置为单独存储,例如书名和书的内容。且可以通过stored_fields单独检索这些字段,参考4.3.2节。

补充:

1.动态映射就是预先不定义索引映射关系,而在添加文档时动态确定字段名称和类型

2.如果确定不需要使用_source字段保存源文档,可以在创建索引时通过映射类型参数_source将其关闭(不推荐),如下

PUT /users
{
  "mappings": {
    "_source": {
     "enabled": false
    }
  } }

3.关闭文档值机制示例

PUT users
{
  "mappings": {
    "properties": {
      "name": {"type":"text"},
      "age": "{"type":"integer","doc_value":false}",
      "address": {"type":"text","filedata":true}
    }
  } }

2.3 字段数据类型

ES支持的数据类型包括

核心数据类型:字符串、数值、日期、布尔、二进制、范围等

衍生数据类型:数值、对象

特殊数据类型:嵌套、关联、地里信息等

2.3.1核心类型

1.字符串类型

字符串类型包括text和keyword两种类型,两者的区别在于

  • text类型在存储前会做词项分析,而keyword类型则不会
  • text类型可以通过analyzer参数设置该字段的分析器,而keyword类型字段则没有这个参数
  • 由于词项分析,text类型字段在编入索引后可以通过词项做检索,但不能通过整体值做检索;而keyword类型则相反,只能通过字段整体值做检索,而不能用词项做检索

综上所述,所以text类型字段一般存储全文数据,如日志信息、文章正文、邮件内容等,而keyword类型则用于存储结构化的文本数据,如邮编、地址、电话等

2.数值类型

有省略部分,待补充》。。。。

特殊的一个类型是scaled_float,虽然是浮点数据类型,但在存储上却使用long类型来标识,基本思想是通过换算系数将浮点数放大为整型再保存。这样不会损失精度还能提升运算效率,应用场景如货币金额。

PUT my_index
{
  "mappings":{
    "properties":{
      "price":{
        "type":"scaled_float",
        "scaling_factor":100
      }
    }
  } }

2.3.3多数据类型

场景:文章标题在多数情况下是通过文章标题中的词项检索,但在标题比较短并且知道整个标题内容时,也有可能使用整个标题做检索。

如果将标题的字段类型设置为text,则标题在编入索引时会被提取词项而不能使用整个标题做检索,而如果设置keyword则不能使用词项做检索。针对字符串类型text和keyword,ES专门提供了一个拥有配置字段多数据类型的参数fields,它能让一个字段同时具备两种数据类型的特征,如下

title字段类型被设置为text,同时通过fields参数又为该字段添加了两个字段,其中一个子字段名称为raw,被设置为keyword类型,另一个length这块先忽略,使用fields设置的子字段,在添加文档时不需要单独设置值,它们与title共享相同的字段值,只是会以不同方式处理字段值。

所以如果需要根据词项做检索时应使用title字段,而如果需要使用整个值做检索或是在排序和聚集时则可以使用title.raw字段。

在默认情况下,如果没有明确定义字符串类型时,添加到索引中的字符串都会以上面的形式设置为多类型。 

2.4 分片与复制

面对海量全文数据,ES要解决存储问题,但要解决的不仅是文档能够存的下的问题,还要保证数据不会因为节点故障而丢失,同时还要保证文档的检索速度尽可能不受文档数量增加的影响。

2.4.1 分片与集群

分片

解决大数据存储的通用方案为分片(Shard),核心思想为将数据分解为大小合适的片段,然后再存储到集群的不同节点上。这样理论上数据存储的容量就没有上限了,因为在数据量增加时,只需要向集群中添加新的节点就可以增加整体容量了。

数据分片带来的收益不仅体现在数据存储上,对于数据处理来说也可以大幅提升性能和吞吐量,因为在现有硬件条件下,硬盘读写速度和CPU处理性能不在一个数量级上,所以硬盘往往是数据处理最大的瓶颈,所以即使多CPU或者多线程并发处理数据,但只要处理的数据在同一硬盘上,数据处理的速度也不会得到明显提升。在使用数据分片技术后,数据存储和读写被分散到不同机器的硬盘,这会显著提升数据处理速度。大数据处理开源框架Hadoop的HDFS和MapReduce正是基于这种思想,实现了短时间处理大量数据的功能。

ES同样支持分片,所以存储文档的容量理论上没有上限,也正是从这个意义上,它才被很多文献归类为一种数据库,一种基于文档的NoSQL数据库。

集群

分片的基础是创建集群,ES中创建集群,只要在集群中的节点网络互通,且具有相同的集群名即可。

集群名在config/elasticsearch.yml中的cluster.name中指定,默认为elasticsearch。所以在局域网中直接启动ES实例就可以创建一个集群。

查看节点情况

GET _nodes

分片一般会均匀地分散到集群的不同节点上,这就将存储和检索负载分散到了集群的不同节点上,索引分片数据通过number_of_shards参数设定,如下设置分片数量为10

索引定义好分片数量后,当有新节点加入集群,ES会将分片均匀地散列到新的节点,例如索引分片数量为2,当集群中只有一个节点A时,这些分片全部位于节点A,当有B加入到集群时,ES会动态的将一个分片复制(迁移)到集群的节点B上,这也意味着如果索引数量为1,那么这个索引未来将无法扩容 

2.4.2 路由

分片解决的是海量存储的问题,但引入的新问题是如何确定文档存储在哪个分片,在ES中确定文档存储在哪一个分片中的机制被称为路由(Routing),计算文档路由的运算公式为

 

shard_num为分片序号

hash为散列函数

_routing为路由参数,默认为文档ID,即元字段_id,可以在添加或者检索时通过routing参数修改,如下图

num_primary_shards为一个索引的主分片数量(不包含副本分片数量)

通过routing参数自定义路由规则

为提升检索效率,ES在检索文档时,并不会将所有分片整合到一起做检索,而是先根据路由规则路由到具体分片上,然后在分片上根据检索条件查找文档。 所以要确认文档添加和和文档检索是的路由规则相同,否则会导致检索失败。例如:添加文档时使用了自定义路由规则,而在检索文档时忘记使用自定义路由规则,为避免这种情况可以在创建索引时将路由参数设置为强制要求,如下图,设置后则对文档的CRUD都必须制定routing参数,否则在执行请求的时候将报错。

 从负载均衡的角度看,routing参数的值越分散,文档分散的越均匀,所以选择合适的routing对文档添加和文档检索都会有显著的性能提升。但往往选择的路由参数routing看似分散,但却会路由到相同的分片,为解决这个问题,ES又引入一个分区参数routing_partition_size,运算公式变为

 

此时,分区编号同时由路由参数routing和索引_id字段共同决定,加大了分区均衡的可能性

routing_partition_size参数需大于1且小于主分片数量,默认值为1表示不会使用公式(2-2),当使用公式(2-2)时,需要将路由参routing参数设置为强制,同时也不能再使用join类型字段构建父子关系。

2.4.3 容量规划

如果在运行时索引分片数量发生了变化,为了保证文档存储和检索都能路由到正确的分片,已经存储到分片中的文档就必须按照公式做分片的重新路由,这个过程在ES中叫做重新索引(Reindex),当分片中存储大量文档时,这个过程非常消耗资源。

为避免重新索引导致的性能开销,ES做了一个严格限制,即索引分片数量一旦在创建索引时确定后就不能再修改。

索引容量由分片数量和节点存储容量决定。节点存储容量决定了分片容量的上限,而索引总容量则是单个分片容量 * 分片数量,最好的办法是将分片平均到不同的节点上,但如果节点存储容量大于单分片容量上限时,也可以考虑在一个节点上存储多个分片

从性能角度考虑,分片太大会降低检索速度,所以单个分片的容量也不能过大,需要根据用户对检索性能的要求估算单个分片的容量上限。

综上,在创建索引时有必要对索引容量预先做好规划,如果用户在容量规划时低估了文档容量,那么索引将无法通过扩容来支持更多的文档。

索引容量规划主要是根据一些已知条件规划分片数量,这些已知条件主要包括:文档存储整体容量和检索性能要求两个方面。

通过检索性能要求可以估算出每个分片的最大容量,在使用整体容量除以分片大小就可以预估出分片数量。整体容量如果无法估算(如总日志量),可以选取一个固定的时间(一天、一月)段就创建一个新的索引出来,因为固定时间段内的文档数量还可以估算。可以通过_rollover接口,以滚动别名的方式实现(参考3.3.1节,待补充细节)。

事实上,无论容量规划多么科学,依然无法完全避免文档实际存储容量需求超出索引容量的情况,这种情况下唯一可行的办法就是重建索引,再将原索引中的文档存储到新索引。ES针对这种情况提供了三个接口,即_split接口、_shrink接口和_reindex接口,这三个接口都没有修改原索引容量的能力,而是通过创建新索引的方法间接改变索引容量,但它们的性能比手工创建索引和复制文档还是要好一些(参考3.3节)。

2.4.4 副本

分片解决文档存储容量的问题同时也提升了文档处理性能和吞吐量,但不能解决容灾容错等高可用性问题,为了解决这个问题ES在存储上有引入了一个称为副本(Replica)的技术。

副本是主分片的复制品,能够在主分片故障时迅速恢复数据,所以主分片和副本分片永远不会在同一节点上,因为这样对于数据恢复没有任何意义。

默认情况,ES为每个索引都设置了一个副本分片,即集群中至少应该有两个节点,如果集群只有一个节点,则副本分片永远不会被创建,ES的健康状态就为黄色,索引副本分片数量设置如下

 

如上test索引共有30个分片(10个主分片 + 2*10个副本分片)

查看机器中的分片情况

GET _cat/shards

除了可以使用number_of_replicas设置固定的副本分片以外,还可以分解节点数量使用auto_expend_replicas参数设置动态扩展副本分片。"1-10"代表副本分片数量是1到10个,"0-all"中all代表素有节点数量。

与主分片不同,副本分片数量可以在索引创建之后,随时动态更改(参考3.1.2节)。

2.5 客户端API概览

ES提供了丰富的客户端API,支持主流语言如Java、Python等,但ES最基本的访问方式还是通过REST接口,以HTTP的形式操作文档数据,另外ES还内置了一种称为Painless的脚本语言,可以在API中使用

2.5.1 REST接口

REST可以理解为一种架构风格,它的核心思想是一切皆为资源,而应用程序则是通过更改资源的状态实现具体功能。

在ES中,资源主要是索引、映射类型、文档,在URI是有次序的,如"/users/_doc/1"中,users是索引名,_doc是类型名,1是文档ID

ES的内置资源要么以下划线"_"开头,要么以"."开头,一般都是一些具有特殊用户的资源,ES提供的所有接口也都是已下划线"_"开头,如_search、_cat、_cluster、_template、_mapping、_aliases、_rollover、_settings等。

 

PUT和POST的区别

PUT新增资源是直接通过URI标明资源是哪一个所以对同一个URI多次PUT请求,第一次是新增操作,之后所有PUT请都是更新操作,所以一般说PUT请求是幂等的,即多次请求结果是一样的。

POST请求的URI一般是要操作资源的父资源,多次POST请求会一直新增资源。

在ES中可以同时使用POST和PUT更新文档,但是通过映射类型添加文档则只能使用POST请求。

 

支持多索引

REST接口的URI一般都对应一个资源,但ES中多个索引可以使用逗号分隔出现在URI中,也可以使用星号匹配任意字符,还可以在URI中使用_all代表所有索引,如下

说明:并不是所有接口都支持多索引,在不支持多索引的接口使用上述格式将会报错

 

通用参数

在接口请求

添加"?pretty=true",可以让返回的JSON结构更易读

添加"?format=yaml",可以让返回结果以YAML格式展示

添加"?human=true",可以让返回阶段字段以更易于人类理解的方式展示

添加"?flat_settings",可以是返回的JSON对象被平铺展示,以使结果更加紧凑,flat_settings参数默认值是false,一般在使用程序读取结果时可将其设置为true。

filter_path,可以过滤返回结构中的字段,如filter_path="hits.hits._source"

2.5.2 Painless脚本

脚本执行接口

Painless为每段脚本的执行定义了一个上下文,而这个上下文中则保存了脚本需要的大部分环境数据。示例省略

第三章 Elasticsearch索引与文档

索引和文档是REST接口操作的最基本资源。

索引一般以索引名称出现在请求操作的资源路径上,文档则是以文档ID为标识出现在资源路径上。

在ES7废除映射类型的背景下,_doc可以理解为接口名称,类似_alias、_settings、_update等,并不是一种实际的资源,或者可以当做是虚拟资源。

3.1 索引别名与配置

在ES中使用四种HTTP方法请求索引

PUT:创建索引

GET:查看索引

DELETE:删除索引

HEAD:索引存在性检验,索引存在则返回200,否则返回404

如下图所示:

实际使用中一般不会只使用PUT请求创建索引而不做任何配置,因为在向一个索引添加文档时,如果索引不存在,ES会使用索引的默认参数自动创建索引,所以完全可以等到添加文档时由ES自动创建。

任何一个索引都包括别名、映射、配置三个参数,在创建索引时使用aliases、mappings、settings设置。mappings在第二章已介绍,本节主要介绍aliases和settings。

索引名命名规范:

3.1.1 索引别名

索引别名可以类比为传统关系型数据库中的视图,ES在处理别名时会自动将别名转换为对应的索引名称。所以别名和索引完全相同就没有存在的意义。因此索引别名一般都会与过滤条件相关联,过滤条件可以使用第5章介绍的文档查询语句比如term、match查询。

示例:创建students索引,并根据性别添加男生和女生的别名:

 

如上,students索引关联了girls和boys两个别名,在别名的定义中,filter用于定义过滤器,routing用于定义路由规则,它们将在使用别名检索时自动应用。在定义过滤条件时通常要指定路由规则,这样会将同一别名的文档路由到相同的分片上,有效减少使用别名检索时的分片操作。这也要求存储文档时必须要通过别名,否则在使用别名检索时有可能会漏掉合法文档,例如使用students添加文档而使用girls、boys检索文档,就有可能会出现检索不到文档的情况。

别名可以与一个或多个索引管理,如在2.4.3讲解索引容量规则时提到,如果文档整体容量不可估算且每天都在增加,可以按时间段每隔一段时间创建一个索引,为了不在检索的时候指定多个索引名称,可以使用别名与这些索引相关联,这样在检索文档时就可以针对它们共同的别名做检索,避免不停更换索引名带来的麻烦。

索引别名不仅可以在创建时指定,也可以在索引创建后动态添加或者删除

1._alias和_aliases接口

在索引创建后,_alias和_aliases接口都可用于添加或删除别名,_alias针对某一具体的索引,aliases可以对多个索引做批量操作。

_alias示例:为students索引添加别名grade1

请求中使用_alias表明这是一个对别名资源的操作,students为索引名,grade1为别名。 索引名可以使用_all代表所有索引,也可以使用log_*匹配多个索引。

使用PUT或POST请求上述资源是添加别名,别名的过滤条件、路由等配置信息在请求体中以filter、routing参数定义

使用DELETE方法删除别名

使用GET方法请求/students/_alias查看students索引的所有别名

使用GET方法请求 _cat/aliases,以纯文本形式返回所有别名

_aliases示例:为students索引添加别名grade1 (同上)

_aliases接口请求的资源中也可以添加索引名称,如/students/_aliases,表示只针对students索引做操作。

_aliases接口的请求体结束数组类型的actions参数,可以指定的行为包括add(添加别名)、remove(删除别名)、remove_index(删除索引)等 

上例只给出了单个索引对应单个别名的情况,实际上可以通过indices和aliases指定多个索引和多个别名,所以_aliases可以针对多个索引、多个别名,实现添加、删除等多种操作。

2._rollover接口

_rollover接口根据一系列条件将别名指向一个新的索引,这些条件包括存续时间、文档数量、存储容量等。与日志文件使用的文件滚动类似,_rollover通过不断将别名指向新的索引,以保证索引容量不会过大。这其实是2.4.3节介绍容量规划时提到的无限容量存储的一种解决方案。

这种别名滚动不会自动完成,需要主动调用_rollover接口。

别名滚动的条件可以通过conditions参数设置,包括max_age、max_docs、max_size等参数。

示例3-5:创建一个索引logs-1并配置别名logs,然后调用logs别名的_rollover接口设置别名滚动条件

如上logs别名指向logs-1索引,最大存活周期为14天,最大文档数量为10000条,最大存储容量为4GB

因为logs-1索引刚创建,3个条件都不满足,所以请求_rollover接口返回如下

三个条件匹配结果都为false,所以不会触发索引滚动,可将max_age设置为1s,通过GET _cat/indices就会发现有新索引logs-000002产生,此时logs-1的别名被清空,logs-000002的别名中添加logs。

新索引的命名规则是在原索引名称数字的基础加1,并将数值长度补0凑足6位,所以使用_rollover接口时,要求索引名称必须以数字结尾,数字与前缀用"-"连接,即满足正则"^.*\\d+$"。

如果索引名称没有遵循这样的规则,则需要在调用_rollover接口时指定新索引名称,如下

由于_rollover接口再滚动更新索引时,会将别名与原索引取消关联,为保证原文档可检索,可以通过别名is_write_index参数保留索引与别名的关系。当使用is_write_index参数设置了哪一个索引为写索引时,_rollover接口滚动别名指向索引时不会取消别名与原索引之间的关系,而会将原索引的is_write_index参数设置为false,并将新索引的is_write_index参数设置为true,如下

再执行示例3-5,会发现logs-1的is_write_index被设置为false,新索引logs-000002的is_write_index被设置为true,且两者的别名列表都包含logs,业务可以继续通过logs别名对原索引进行查询 

补充:Elasticsearch:Index 生命周期管理入门

3.1.2 索引配置

索引的settings参数用于添加索引配置,索引的所有配置项都以index开头,在设置这些配置项时也可以通过JSON对象的形式辨析,如下两种方式都是正确的

索引配置包括静态配置和动态配置,静态配置只能在索引创建或关闭时设置,动态配置没有这个限制。

1. 索引的关闭和打开

索引关闭后除维护自身元数据信息外,基本上不会占用集群资源同时也不能被用户读写。关闭后可以再次打开,所以通过关闭索引可以实现索引存档的目的。

关闭和打开所以的REST接口如下

test为任意所以名称,索引名称可以使用_all或* 表示所有索引。

如果配置文件中设置了action.destructive_requires_name为true,则这里就不能写_all或者*。

如果配置文件中配置了cluster.indices.close.enable为false,则索引将不能被关闭。

2. 索引静态配置

索引静态配置主要有索引分片、压缩编码、路由等相关参数,他们只能在创建索引时设置,一旦索引创建完成就不能再修改静态配置,对于某些静态配置项可以先将索引关闭再更新。

静态配置参数表如下

number_of_shards和routing_partition_size是2.4.2节介绍分片路由运算时使用的主分片数量和分区参数。 

3. 索引动态配置

ES为查询和修改索引配置提供了_settings接口

GET请求_settings接口返回索引配置信息

PUT请求_settings接口可以修改配置

调用该接口时可以在路径中添加一个或多个索引名称,这时将只针对某一或多个索引,否则请求将针对所有索引

示例:查询和修改配置项

添加"?flat_settings",可以是返回的JSON对象被平铺展示,以使结果更加紧凑,flat_settings参数默认值是false,一般在使用程序读取结果时可将其设置为true。

修改索引配置项只能针对动态配置项,配置项见下表

其中number_of_replicas和auto_expand_replicas用于设置副本分片数量,在2.4.4节有介绍。

search.idle.after和refresh_interval会影响索引刷新频率在2.1.3有介绍,但这两个参数并不针对索引而是针对索引中具体的某一个分片。如果一个索引分片没有接收到数据查询请求,它会等search.idle.after设置的时间后才有可能去刷新索引,当然如果这段时间有新的数据检索请求,索引就会与其他索引一起做刷新。search.idle.after是在ES 7 之后才加入的新参数,目的是为了提升索引刷新的性能。由于这种处理机制只有在refresh_interval没有明确设置时才会起作用,所以如果不想使用这种机制,可以明确将refresh_interval设置为1s。其他参数多与具体的查询语言相关,讲解到查询语言时再介绍。

3.2 动态映射与索引模板

索引并非一定要预先创建,也可以在不创建索引的情况下直接向索引中添加文档。

ES的动态映射机制会根据文档内容,并依据索引模板自动创建一个与文档相匹配的索引。

如果不希望自动创建,可以在elasticsearch.yml配置文件中将action.auto_create_index设置为false来禁止。此参数还接收"+shop_*,-user_*"的形式,其中 + 代表允许自动创建索引,- 代表不允许,即允许以"shop_"开头的索引自动创建,不允许"user_"开头的索引自动创建,此外_cluster接口支持动态修改这个配置,如下

动态映射依赖一些预定义的动态字段类型映射规则,同时还可以使用动态字段模板自定义字段类型映射。如果希望在创建索引时自动应用某些配置信息,可以使用索引模板定义创建索引时默认添加的别名( aliases)、配置(settings)、映射关系(mappings)

3.2.1 动态字段

在索引创建后,ES依然支持向索引中新增字段,这种动态增加字段的特性可以通过dynamic参数来修改。

dynamic参数可以设置在映射类型上,或是对象类型的字段上,可选值为true、false、strict,默认值是true。当dynamic设置为false是,新添加字段将被忽略;而设置为strict时则会抛出异常。

示例:将动态添加字段功能关闭,所有未在properties中定义的新字段都将被忽略

当dynamic设置为false时,在添加新文档时出现的新字段依然会被保存到文档中,只是这个字段的定义不会被添加到索引映射的字段定义中。 

当dynamic设置为true时,新添加字段才会按一定的数据类型映射规则,将它们添加到索引映射的定义中。

下表列出了从JSON类型到ES字段类型的对应关系(字段类型映射)

如果字符串中含有日期或者数值,它们可以被解析为date或numeric类型。

ES默认对还有日期的字符串会自动解析为date类型,可以通过将映射类型的date_detection参数设置为false关闭解析。日期字符串在解析时使用的格式可以通过dynamic_date_formats设置,默认值为["stric_date_optional_time","yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z"] 

含有数值的字符串默认不会被解析为数值类型,但可通过numeric_detection参数开启数值解析。

3.2.2 动态模板

动态模板(Dynamic Template)用于自定义动态添加字段时的映射规则,可通过索引映射类型的dynamic_templates参数设置,该参数接收一组命名的动态模板,每一个模板由匹配条件和映射规则组成。

匹配条件定义了新字段是否可以使用当前模板,可根据新字段的数据类型、名称和路径来定义条件;而映射规则则由参数mapping定义,它需要给出新字段要使用哪些参数,可使用type定义新字段数据类型,也可以使用2.2.3节表2-2给出的适用参数设置新字段其他特性。

匹配规则可以使用关键字match_mapping_type匹配新字段数据类型,可以用于将一种默认类型转换为其他类型或者设置其他特性。

示例:JSON整型默认会被映射为long(参考上字段类型映射表格),可以将整型映射为integer,如下

上述示例中,不仅将整型设置为integer类型,还将文档值机制关闭,即doc_values设置为false。除了匹配新字段的数据类型,还可以使用match、match_pattern和unmatch匹配新字段名称。其中match和unmatch可以使用 * 做名称匹配,而match_pattern支持正则表达式。匹配新字段路径可以使用path_match和path_unmatch,路径与名称的区别是其中包含 . ,在mapping参数中,还可以使用{name}和{dynamic_type}代表新字段名称和类型。

示例:keep_original动态模板将所有以 origin_ 开头的字段的数据类型设置为原始类型

如果一个字段同时满足两个动态模板,最终会应用在动态模板中先定义的规则。例如上述示例中,如果新添加字段名为 original_age 且类型为long,则同时满足keep_original和to_integer两个模板,但它最终会应用第一个模板keep_original,因为这个模板是先定义的。

3.2.3 索引模板

索引模板(Index Template)与动态模板不是一个概念

动态模板定义了索引创建后新添加字段的映射规则

索引模板是在创建索引时默认为索引添加的别名(aliases)、配置(settings)和映射信息(mappings)。

 

索引模板包含该模适用索引的模式或规则,以及索引创建时默认包含的别名、配置、和映射关系等,它们分别通过index_patterns、aliases、settings和mappings四个参数设置,可以通过_template接口创建

示例:创建名为user_tpl的索引模板

上述示例中,适用PUT方法,_template为索引模板接口的关键字,user_tpl为模板名称。index_pattern定义了模板适用于名称以user或employee开头的索引,如果index_patterns在定义是出现规则重叠,索引创建时就可能与多个模板匹配。比如定义一个匹配模式为 * 的模板all_tpl,那么当创建user索引时,all_tpl和user_tpl都满足匹配模式。ES在处理这种冲突时,会将所有模板合并应用到索引上,这会导致后应用的模板规则覆盖先应用的模板规则。所以,索引模板提供了一个参数order用于指定合并应用的次序,如果不指定则order值为0

aliases定义了默认将创建索引的别名,示例中的{index}_by_gender别名,其中{index}是一个占位符,索引创建时将去索引实际名称替换。

settings定义了默认配置。

mappings定义了默认字段信息。

定义上述模板后,通过 PUT users请求创建索引,通过GET users查看索引信息会发现模板中预定义的别名、字段都出现在了索引的设置中。

注意:模板仅在索引创建时起作用,更改模板不会对已经创建的索引起作用。 

_template接口也可通过GET、DELETE和HEAD请求,分别用于查看、删除和存在性校验。如果想要查看所有的模板可以使用CAT接口,具体如下

索引模板在Logstash和Beats组件想ES传输数据时非常有用,它们基本上都会根据自身对索引的要求创建索引模板。 

3.2.4 _mapping接口

索引的映射关系可以在索引创建后通过_mapping接口查看或修改。访问该接口的方法包括:GET、POST、PUT。

GET可以查看索引的映射类型及字段,请求资源的路径基本格式为"/<索引>/_mapping/<映射类型>/field/<字段>",其中_mapping为接口关键字必须使用,其余为可选项。

如果没有指定索引名称而直接调用GET _mapping,将返回所有索引的映射关系

如果指定了索引名称则查看该索引的映射关系,指定了映射类型或字段则查看它们的映射关系。

示例:第一个查看students索引的映射关系,第二个查看其中gender的属性

POST和PUT方法可以向索引更新或添加新字段,或修改已有字段的某些配置

示例:想students添加age属性

如上请求中,索引可以指定一个或多个,对已存在的映射关系只能添加字段或多类型,但字段一旦创建就不能删除,如果一定要删除某一字段,唯一的办法是创建不包含该字段的新索引,然后再将原索引中的数据导入到新索引中做reindex。除了不能删除字段外,字段的多数参数也不能修改,唯一可修改的参数是ingore_above,修改方式与上述示例类似,将需要修改的参数放在字段配置中即可。 

由此可见,在设计索引时要做好规划,在初始时加入必要字段,为了隔离索引变化对用户的影响,可以只提供索引别名给用户访问,当索引发生变化时,只要把别名分配到新索引就可以了

3.3 容量控制与缓存机制

索引容量由分片数量和分片容量决定。

分片数量以通过_split接口扩容,也可以通过_shrink接口缩容。这种扩容、缩容的方式是将原索引扩容、缩容到新索引上,并不是在原索引上做扩展;而且使用 _split接口扩容时,分片扩容依然存在上限,如果增加分片的数量超过了容量上限(number_ of_routing_shards设置的值),就只能通过 _reindex接口对索引做重新索引。

除了这三个可以直接或间接控制索引容量的接口以外,Elasticsearch为了提升性能还默认开启了缓存机制。

3.3.1 _split接口

_split接口可以在新索引中将每个主分片分裂为两个或更多分片,所以使用_split扩容时分片总量都是成倍加而不能逐个增加。使用_split接口分裂分片虽然会创建新的索引,但新索引中的数据只是通过文件系统硬连接到新索引中,并不存在数据复制过程。且扩容的分片又是在本地分裂,所以不存在不同节点间网络传输数据的开销,所以_split扩容效率相对其他方案来说还是比较高的

_split 接口做动态扩容需要预先设置索引的number_of_routing_shards参数,Elasticsearch向分片散列文档采用一致性哈希算法,这个参数实际上设置了索引分片散列空间。所以分裂后分片数量必须是number_ of_routing_shards 的因数,同时是number_of_shards 的倍数。如设置number_of_routing_shards为12, number_of_shards为2, 则分片再分裂存在 2->4->12、2->6->12 和2->12 三种可能的扩容路径。分裂后分片数量可通过 _split 接口的 index.number_of_shards 参数设置,数量必须满足前述整数倍的要求。且这个参数的"index."前缀是不能省略的,因为这是在 _split 接口中而不是在创建索引接口中。

创建一个索引 employee,主分片数量number_of_shards 设置为 2, 散列空间number_of_routing_shards 设置为 12。将索引的blocks.wite 参数设置为true(索引设置为只读),这是因为使用 _split 接口要求索引必须为只读。并调用 _split 接口将 employee 索引的分片分裂到新索引splited_employee 中,index.number_of_shards 参数设置为 4, 即分裂为 4 个分片。示例如下

执行如上后,执行GET _cat/shards可看到employee索引共4个分片(2个主分片和2个副本分片),splited_employee有8个分片(4个主分片和4个副本分片)

创建新索引时会同时将原索引的配置一同设置到新索引中,所以index.block.write也会被复制,但在分裂分片的同时支持通过aliases和settings设置新索引的别名和配置,所以可以在分裂分片的同时将index.block.write覆盖为false,并添加别名stu,另外可以使用copy_setting=false(在版本8中可能被废弃,会有警告)

分裂分片后原索引不会自动删除,新旧索引都可以查到相同的数据,是否删除应根据业务需要具体判断。

3.3.2 _shrink接口

与_split相反,但应用时的规则基本上是一致的。如 _shrink 接口在缩減索引分片数量时也要求原始分片数量必须是缩减后分片数量的整数倍。例如原始分片数量为 12, 则可以按 12->6->3 的路径缩,也可以按 12->4->2->1 的路径缩減。在调用 _shrink 接口前要满足两个条件:

第一:要求索引在缩容期间必须只读;

第二:要求索引所有分片(包括副本分片)都要复制一份存储在同一节点,并且要求健康状态为 green,这可以通过 routing.allocation.require.name 指定节点名称实现。

查看节点名称,可调用"GET _nodes"接口查看集群所有节点。

与 _split 接口类似,索引缩减后的具体分片数量通过 _shrink 接口的 index.number_of_shards 设置。它的值必须与原始分片数量保持整数比例关系,如果不设置该参数将直接缩减为 1 个分片。

示例:缩减后索引分片数量为 2, 同时还清除了两项配置

同理,使用_shrink接口缩容会创建新的索引shrinked_employee,新旧索引都可以查询到相同的文档数据。

3.3.3 _reindex接口

尽管_split和_shrink可以对索引分片数量进行扩容和缩容,但在分片数量上有倍数要求,且分布总量受散列空间(number_of_routing_shards)的限制。

ES提供了_reindex接口支持从一个索引重新索引到另外一个索引,在性能开销上比_split和_shrink都大,所以尽量避免使用。

_reindex需要两个参数source(源索引)和dest(新索引),如下

重新索引过程不会复制原索引的配置信息,如果事先没有指定新索引的配置将使用默认配置创建索引和映射。另外使用_reindex接口必须将索引的_source字段开启。

实际中,_reindex不应该用于扩容和缩容,而主要应该用于索引数据的合并,所以还有一些参数处理在合并过程中出现的问题,如下

source参数指定了源索引,还添加了term查询过滤文档,dest参数则使用op_type参数设置了合并时只添加不存在的文档。 

3.3.4 缓存机制

为了提升数据检索时的性能,ES为索引提供了三种缓存。

第一种:节点查询缓存(Node Query Cache),负责存储节点査询结果。节点查询缓存是节点级别的,一个节点只有一个缓存,同节点上的分片共享同一缓存。默认情况下,节点查询缓存是开启的,可通过索引 index.queries.cache.enabled 参数关闭。节点查询缓存默认使用节点内存的 10%作为缓存容量上限,可通过 indices.queries.cache_size 更改,这个参数是节点的配置而非索引配置。

第二种:分片请求缓存(Shard Request Cache),负责存储分片接收到的查询结果。分片请求缓存不会缓存查询结果的 his 字段(即具体的文档内容),一般只缓存聚集查询的相关结果。默认情况下,分片请求缓存也是开启的,通过索引 index.requests. cache.enable 参数关闭。另一种关闭该缓存的办法,是在调用 _search 接口时添加 request_cache=false 参数。分片请求缓存使用的键是作为查询条件 JSON 字符串,所以如果查询条件 JSON 串完全相同,文档的查询几乎可以达到实时。但由于 JSON 属性之间并没有次序要求,这意味着即使 JSON 描述的是同一个对象,只要它们属性的次序不同就不能在缓存中命中数据。这一点在使用时需要格外注意。

第三种:text类型字段在开启fielddata机制后使用的缓存,它会将text类型字段提取的所有词项全部加载到内存中,以提高使用该字段做排序和聚集运算的效率。由于fielddata是text类型对文档值机制的代替,所以这种缓存天然就是开启的且不能关闭。但可通过indices.fielddata.cache.size设置这个缓存的容量,默认情况下该缓存没有容量上限。 

缓存的引入使得文档检索性能得到了提升,但缓存般会带来两个主要问题:

一是如何保证缓存数据与实际数据的一致

二是当缓存容量超出时如何清理存

数据一致性问题,ES是通过让缓存与索引刷新频率保持一致实现的。索引是准实时的,即默认情况下索引会以每秒一次的频率将文档编入索引,Elasticsearch会在索引更新的同时让缓存也失效,这就保证了索引数据与缓存数据的一致性。

缓存数据容量问题则是通过LRU的方式,将最近最少使用的缓存条目清除。同时,Elasticsearch还提供了一个_cache接口用于主动清理存。之所以要提供这个接口,是因为Elasticsearch为索引提供了一个主动刷新的接口_refresh,所以最好在主动刷新索引后再主动清理缓。

1. _refresh 接口(刷新索引)

_refresh 接口用于主动刷新一个或多个索引,将已经添加的文档编入索引以使它们在检索时可见。

示例

除使用_refresh接口主动刷新索引,也可以在操作文档时通过refresh参数刷新索引,参考3.4

1. _cache 接口(清理缓存)

_cache用于主动清理缓存,调用时需要在_cache后附件clear关键字,_cache可以清理所有缓存,也可以清理某一索引甚至某一字段的缓存,还可以只清理某一种类型的缓存

示例

query、request、fielddata分别对应不同的缓存类型,而fields则用于清理某个字段的缓存 

3.3.5 查看运行状态

ES提供了一组用于查看索引及分片运行情况的接口,包括_stat、_shard_stores和_segments等,这些接口一般在性能分析时使用。

1. _stats接口

用于查看索引上不同操作的统计数据,可以直接请求也可以与索引名称一起使用。_stats接口返回的数据非常多,如果只对其中某一组数据感兴趣,可以在_stats接口附加统计名称。如下

在_stats接口中可以使用的统计名称及含义如下表

2. _shard_stores和_segments接口

_shard_stores用于查询索引分片存储情况

_segments用于查看底层Lucene分段情况

这两个接口都只能通过GET请求,同时都可以针对一个或者多个索引,如下

3.4 操作文档

索引是ES中REST接口访问的最基本资源,其次是文档,上面介绍的接口都是基于索引,本节介绍文档相关接口。

尽管映射类型在ES7中已被废止,但在操作文档时,仍需指明映射类型且只能为_doc,可以将_doc看成类似_alias、_settings、_mapping一样的接口名称。

_id是文档的唯一标识,所以访问文档时需要在路径中标识其_id值,ES支持GET、HEAD、DELETE、PUT、POST请求_id标识的文档,分别对于应查看、存在性校验、删除、更新、添加文档。如下

POST\PUT在操作文档时,作用都是创建或更新文档

POST在REST中是非幂等操作,更多的是对映射类型访问以添加文档,所以POST请求可以不带_id,这样会自动生成文档ID并添加到索引中

如上这些操作都可使用refresh参数,将在操作文档的同时刷新索引。refresh可选值有3个,true、false、wait_for,其中wait_for的含义是在刷新索引未完成时不返回操作文档的响应。 

3.4.1 索引文档

索引文档是Elasticsearch官方的叫法,这里的索引为动词,意思是将文档分析处理后编入索引以使文档可检索,可以理解为添加文档或更新文档。

索引文档可通过PUT或POST请求实现,在索引文档时可以无差别使用。由于_id是元字段,所以不能再请求体中以<字段名>:<字段值>的形式设置。如果指定了_id,那么PUT和POST都相当于更新文档,文档的_id不变,但是文档版本字段_version会增加。如果路径没有指明_id值,ES会自动生成文档_id值,这相当于对映射类型操作,这时候只能使用POST方法请求。

另外使用PUT请求更新文档时,不能只更新某一字段,要更新就必须更新整个文档,如果更新时只发送了要变更的字段及其值,那么在更新后的文档将只包含这一个字段。其他字段会被删除。ES的_update端口支持更新文档单个字段

文档版本实际上提供了一种控制并发更新的锁机制,即常说的乐观锁。如果要使用这种锁机制,需要在请求中添加version参数,如 PUT test/_doc/1?version=1,只在文档版本为1时才会更新成功,否则将报版本冲突。

默认情况下文档版本号使用内部文档版本机制实现,版本号从1开始,并在更新的时候加1,除内部版本机制外,可以通过version_type使用外部版本机制,外部版本号也通过version传入,只有当version值大于或等于当前文档版本值,version值才会被读取出来,并设置为文档当前的版本号。

version_type可选值:

internal: 内部版本机制,类似于自增

external或external_gt: 外部版本机制,并在version参数大于当前版本号时有效

external_gt: 外部版本机制,并在version参数大于等于当前版本号时有效

如果不想让PUT请求在文档存在时更新文档,可以通过设置操作类型来禁止更新,这样文档存在时也会报版本冲突异常

从ES实现机制上,旧版本文档只是会标记为删除而不会立即做物理上的删除,被标记为已删除的文档用户就不能再访问了。这些被标记为删除的旧版本文档将在后台统一删除,这种机制主要是为了提升性能。 

3.4.2 获取文档

ES支持以GET、HEAD通过_id获取单个文档,也支持_mget根据多个_id获取多个文档,前者_id值将出现在请求的路径中,后者会将_id值通过请求体参数传递给_mget。

1.获取单个文档

根据_id,HEAD方法用于存在性校验,200标识存在,否则为不存在。GET方法返回文档基本信息

示例:GET test/_doc/1,返回如下

其中_index、_type、_id和_version 是文档的元字段,分别表示文档的索引、映射类型、ID和版本,found代表ID标识的文档是否存在,_source是原始存入索引的文档。如果没有找到ID对应的文档,found为false,_source也不会出现在结果中。

如果不想看到元字段而只对源文档感兴趣,可以在路径添加_source参数或_source路径,这时将只返回源文档,如下使用_source过滤结果

方式1是在版本7中才引入的,是在映射类型被废除的背景下用于取代方式2,所以以后应该尽可能使用方式1

除_source参数外,还有_source_include和_source_exclude参数可以精确指定要包含和排除的字段,也可参数中使用* 通配符。这三个参数还可与前面提到的 _source 路径放在一起使用,例如第5种请求方式就会只返回源文档中的gender字段。

强调:根据文档ID查看文档时接口满足实时性要求。如果文档已更新但未编入索引,该接口在执行查询前会先刷新索引。如果不希望这种实时性刷新,可通过参数realtime设置为false,从而禁止实时刷新。

2. 获取多个文档

路径中包含的_id值,不支持设置多个_id或使用*通配符。根据一组_id值查看多个文档,可使用_mget接口来实现。

_mget接口根据索引名称和文档_id获取多个文档,可使用GET或POST方法请求该接口。在请求地址中可以指定一个或多个索引,也可以不包含索引。

示例:_mget请求地址中不包含索引,而是在请求体中通过docs参数指明索引和_id

如果在请求地址中指明了索引,则在请求体的docs参数中就可以不用再指定索引。但如果在docs指定了_index参数,并与路径中的索引不一致,在检索时将覆盖路径中的索引。

同理,_mget可使用_source参数指定获取哪些字段,但_source不是在请求路径中,而是在docs参数中与_index、_id等参数一起使用。除此之外还有stored_fields、_routing等参数可使用,分别用于查询存储字段和指定路由规则。

以上,无论是获取单个文档还是多个文档都必须先要知道文档的 _id,这在实际应用中并不是常见的文档检索方法。有关文档检索的更多内容,请参考第4、5章。

3.4.3 删除文档

Elasticsearch提供了两种删除文档的接口:

一种是根据文档 _id从索引中将文档删除,使用DELETE方法发送请求。

另一种是根据查询条件找到满足条件的文档并删除,使用POST或GET方法发送请求

示例

如前所述,所有编入索引的文档都有版本信息。删除文档时,也可以像更新文档一样指定版本号,以确保要删除的文档在删除时没有更改

不仅如此,删除操作也会导致文档版本号增加。已删除文档的版本号在删除后短时间內仍然可用于控制并发,可用时长由索引的配置项index.gc_delete索引设置,默认值为60s。

3.4.4 更新文档

使用PUT方法可以更新文档,但它不能只更新文档中的某一字段值,且必须要知道文档的_id值。如果想只更新某个字段,或是根据非_id字段的条件更新文档,使用PUT就无法实现。

为解决这两个问题,Elasticsearch提供了_update接口和_update_ by_query这两个接口。

1. _update接口

主要用于解决更新文档单个字段的问题,可以使用doc参数指明要更新哪些字段。

示例:如果_id为1的文档包含gender字段,则gender字段将被更新为M;如果文档中不存在gender字段,则将会在文档中添加gender字段并更新值为M

_update接口在文档不存在时提示错误,如希望在文档不存在时创建文档,则可以在请求中添加upsert参数或doc_as_upsert参数,例如

upsert参数定义了创建新文档使用的文档内容,而doc_as_upsert参数的含义是直接使用doc参数中的内容作为创建文档时使用的文档内容。

_updatet除可以使用doc参数指定要更新的字段以外,还可使用script参数设置更新脚本。

script参数包含三个子参数,即source、lang和params。

source: 设置实际用于更新文档的脚本片段

lang: 指定了要使用的脚本语言

params: 定义了一个Map,包含在脚本片段中所需要的参数。

脚本中,一般使用Painless脚本语言,在2.5.2节有介绍。Painless在不同上下文中应用,会有一些不同的上下文变量可以使用。在_update接口中可以使用的变量如下表

利用ctx['_op']和 ctx['_source']可实现对源文档的更新。

示例:将users索引中_id为1的文档中age字段加

如上请求中,ctx.source即ctx['_source']的另一种访问形式。由于ctx['_source']的类型是Map,所以可以使用ctx.source.<字段名>访问或修改源文档字段,如果要访问的字段并不存在,则会将这个字段添加到文档中

如果要删除字段,可调用Map的remove方法

示例:将文档_id为3的gender字段删除

使用ctx['_op']变量可以设置更新时的行为,可选值包括index、delete和none。

示例:当_id为1的文档的age字段大于24时删除文档,否则不做任何操作

当执行的操作为none时,在返回结果中会包含 noop;而执行删除时,在返回结果中将包含delete

与_update类似,如果请求文档不存在Elasticsearch会返回文档不存在的错误提示。如果希望文档不存在时自动将文档插入,可以将scripted_upsert参数设置为true,或者使用upsert参数加入要插入的文档内容。

2. _update_by_query接口

前述更新文档都是通过_id找到文档,然后对文档进行更新。但很多场景下,用户并不知道文档_id,这时就要用到根据查询条件进行更新的 _update_by_query接口。该接口使用POST方法请求,并通过quey参数接收查询条件。

示例:只更新包含有age字段的文档,然后将它们的age字段加1

query字段中使用的是Elasticsearcht中专门使用的查询语言称为 DSL,有关这种查询语言的详细介绍请参见本书第 4 章。script使用了与_update类似的脚本,上下文变量除了ctx['_now']不可用以外,其余均可使用。

3.4.5 批量操作

如果需要批量地对Elasticsearch中的文档进行操作,可以使用_buk接口执行以提升效率和性能。

_buk接口接收一组请求体,请求体一般每两个一组,对应一种对文档的操作

第一个请求体代表操作文档的类型,包括index、create、delete、update等。

第二个请求体代表操作文档所需要的参数

但对于某些不需要参数的文档操作来说,则可能只有一个请求体。

其中index和create都代表创建文档,区别在于当要创建的文档存在时,create会失败,而index则可以变为更新;delete代表删除;update代表更新。

3.5 本章小结

本章介绍了Elasticsearch中使用REST请求可以操作的两种资源:索引和文档。

对于索引,主要介绍了索引的三个特征:别名、配置和映射。这三个特征在创建索引时可以定义,在索引创建之后也可以通过相应的接口更改。同时,索引还可以使用动态字段、动态模板和索引模板等机制,在添加文档时自动完成索引和字段的创建和更新。还简单地讨论了有索引相关的性能问题,包括索引容量的扩大和缩小,以及索引三种存机制等。

对于文档,本章则主要讨论了对文档的CRUD,这些主要是通过文档ID实现的。尤其是对文档的查询,单纯通过文档ID做查询是满足不了实际需求的。从下一章开始,将正式进入 Elasticsearche 的最强项一一文档检索。

第四章 Elasticsearch分析与检索

虽然通过_id可以获取文档,但_id一般都是无意义的值,实际应用中更多是使用文档其他有意义的字段做检索。

ES提供了专门用于检索的_search接口,可以根据指定的查询条件检索文档。聚集查询也是基于这个接口,只是参数及格式不同而已。

ES可用于文档检索的接口除了_search以外,还包括_count、_msearch、_scripts等,还有一组辅助文档检索的接口,可以查看检索的执行情况,为性能调优提供依据,包括:_validate、_explain、_field_caps、_search_shards等。

4.1 _search接口

_search接口可使用GET或POST方法请求,在请求路径中可指定一个或多个索引,还可以使用或者星号*匹配所有索引。如果不指定索引名称,实际上也是匹配所有索引。

Elasticsearch为这个接口定义了一种查询语言 DSL (Domain Specific  Language)。DSL是一套基于 JSON 的询语言,这种只在某一领域使用的语言通常称为领域特定语言,简写为DSL(第5章专门介绍)。

_search接口有两种请求方式,一种是基于URI的请求方式,一种是基于请求体的请求方式。无论是哪种,它们执行的语法根基都是DSL,只是在使用形式上不同而已。

4.1.1 基于URI

这种请求方式比较简单,DSL查询条件以请求参数q传递给接口。使用_search接口最简形式就是不挂任何参数直接调用,也可在路径中添加索引名称,也可不添加。示例如下

如上请求中,参数q定义的内容叫查询字符串(Query String),它的含义是检索message字段值中包含chrome或firefox的文档。

查询字符串不仅可以在基于URI的检索中使用,也可在基于请求体的检索中使用,是DSL定义的一种检索方法。

査询字符串属于全文检索,这意味着查询字符串在检索前会被分析器解析为一系列词项和运算符。如上示例中,chrome firefox会被解析为chrome和firefox两个词项,然后再与message字段的词项索引做匹配。只要message字段中包含chrome或firefox,这个文档就满足查询条件。

1. 查询字符串

査询字符串的基本格式为"<字段名>:<查询值>",其中字段名可以指定,也可以不指定。如果没有指定字段名,要匹配的字段由 lindex.query.default_field参数设置,其默认值为*.*,即在所有字段中查询。还可以使用参数df(Default Field)指定要查询的字段名,它与参数q一样是可以用在URI中的参数。如果指定了字段名,査询将在指定字段中匹配词项。除了直接指定字段名以外,还可使用通配符等形式匹配字段,例如

如上,査询字符串"geo.\*: CN US"将在geo的子字段中匹配CN或US。第二个查询字符串中的_exists_不是一个具体的字段名,而是代表所有非空的title字段。

对于查询字符串中的查询值,会在检索前通过分析器拆分为词项,在检索时只要字段中包含任意一个词项就视为满足条件。在实现上,这其实是使用了DSL语言中定义的match查询。如果使用双引号括起来,_search接口将使用DSL的 match_phrase做短语匹配,就类似于用整个短语做检索,而不是使用单个词项做检索。

查询值中除了包含词项本身以外,还可以包含操作符 OR 和 AND,注意它们必须大写否则将被识别为词项。例如,"(tom smith) AND jhon"代表的含义是同时包含 tom、jhon 或 smih、jhon 的字段。

除了可以包含词项、操作符以外,查询字符串的查询值中还可以包含通配符、正则表达式等。如下表给出了一些可能的用法

2.请求参数

基于URI调用_search接口时可以使用的参数,除了前述的q和df以外还有很多。如_source参数可以用来设置在返回结果中是否包含_source字段,还可使用 _source_include或_source_exclude包含或排除源文档的字段。

这样的参数还有很多,它们大多数与基于请求体的参数具有相同的名称和含义。不仅如此,部分参数对于其他接口也可使用,下表将这些参数总结出来供参考,表4-2

4.1.2 基于请求体

基本请求体的接口调用,可以在请求体中传递DSL检索条件。尽管可以GET或POST请求_search接口,但由于一些客户端不支持使用GET方法发送请求体,所以最好使用POST基于请求体去请求_search接口。使用请求体检索时,DSL检索条件通过请求体的query 参数设置。例如检索目的地为中国的航班

如上DSL基于词项(Term)查询,检索条件是Destcountry为CN。

DSL中最简单的查询关键字是 match_all 和 match_none,它们分别代表匹配所有和都不匹配。例如

除了这两种查询以外,DSL还定义了多种多样的查询语法(第 5 章做全面介绍)。在请求体中可以使用的参数除了query以外还有很多,它们很多与表 42 中的URI参数名称和含义都是相同。

4.2 分页与排序

在查询大量数据时需做分页,一方面便于用户浏览,更重要的是防止一次加载数据过大而导致内存溢出。_search提供了一组参数用于检索结果分页,但它们有各自不同的应用场景,需区别对待。

4.2.1 from/size参数

from代表检索文档的起始位置,默认为0

size代表每次检索文档的总量,默认为10

from和size既可以在URI参数中使用,也可以在请求体中使用,如下示例: 从第100条开始取20条文档

from与size的和不能超过index.max_result_window这个索引配置项设置的值。默认这个配置项的值为1000, 所以如果要查询1000条以后的文档,就必须要增加这个配置值。

例如,要检索第1000条开始的200条数据,则参数的值必须要大于10200, 否则将会抛出类似“Result window is too large”的异常。因此,Elasticsearch在使用from和size处理分页时会将所有数据全部取出来,再截取用户指定范围的数据返回。所以在查询非常靠后的数据时,即使使用from和size的分页机制依然有内存溢出的可能,而index.max_result_window设置的10000条则是对Elasticsearch的一种保护机制。

Elasticsearch这么设计的原因:首先,在互联网时代的数据检索应该通过相似度算法,提高检索结果与用户期望的附和度,而不应该让用户在检索结果中自己挑选满意的数据。以互联网搜索为例,用户在浏览搜索结果时很少会看到第3页以后的内容。假如用户在翻到第100条数据时还没有找到需要的结果,那么他对这个搜索引擎一定会非常失望。其次,如果真的需要遍历所有数据,不能单纯使用from和size,应该结合scroll接口使用。

4.2.2 scroll参数

scroll既是_search接口的参数也是接口,它提供了一种类似数据库游标的文档遍历机制,一般用于非实时性的海量文档处理需求。

例如,将一个索引中的文档导入到另一个索引中,或者将索引中的文档导入到 MYSQL中。使用scroll机制有两个步骤:

第一步是创建游标

第二步则是对游标遍历

这两个步骤基于_search 接口执行,例如

scroll参数只能在URI中使用,而不能出现在请求体中。它定义了检索生成的游标需要保留多长时间,比如2m代表2分钟,1h代表1小时。scroll保留时长不是处理完所有数据所需要的时长,而是处理单次遍历所需要的时间。从性能角度来看,保留时间越短,空间利用率就越高,所以应该根据单次处理能力设置这个值。size参数可以放在请求体中,也可以挂在地址后面,代表了每次遍历时返回的文档数量。size只能在初始查询时指定在遍历时不能更改,请求体中还可以包含其他_search接口的合法参数。在添加了scroll参数后,返回的结果中将包含一个名为_scroll_id的字段,它唯一地代表了一个scroll查询的结果。接下来,根据这个_scroll_id就可以对结果进行遍历了。例如

在遍历游标时,不需要指明索引或映射类型,反复调用_search/scroll接口就可实现对结果的遍历了。请求体中的scroll参数相当于延长了游标的存活时长,而scroll_id则是在初始查询时返回的 _scroll_id值。在遍历过程中将根据初始查询时设置的size值返回相应数量的文档,但在遍历过程中不能重新修改size值。每次调用scroll都会自动向后遍历,直到所有文档全部遍历结束。在遍历过程中,每次返回的结果中还是会包含_scroll_id字段,通常来说它的值会保持不变。

scroll在超时后将自动删除,但Elasticsearch也为用户提供了主动除scroll的接口。可以通过请求体发送要删除的游标,例如

对于海量文档的遍历,Elasticsearch还支持对scroll再做片段分割,每一个分割后的片段又可以被独立使用。例如 

max定义了分割片段的总量为 2, 而id则定义了当前请求返回哪一个片段。上面的请求将会把游标分为两个片段,当前请求返回第一个片段。id值从0开始,它的值应该小于max。在返回的结果中,同样也会包含_scroll_id 字段。每一个游标片段都是独立的,可以使用多线程并发处理。从物理角度来看,Elasticsearch会让游标片段分配到不同的索引分片上以提升遍历速度。所以游标片段数量不应该大于索引分片数量,否则游标分段的性能将受到影响。因此,游标片段数量也有上限为1024, 由  index.max_slices_per_scroll参数设置。

4.2.3 search_after参数

前面介绍了两种分页机制,一种是使用from/size,一种是使用scroll,这两种机制都会将数据整体加载进来,不同的是from/size机制下每一次请求都会加载,而scroll则只在初始时加载。scroll比较适合对同一结果集做多次迭代,但在数据量大时依然对性能有影响。

为此,ES提供了另外一种机制search after,它使用search_after参数定义检索应该在文档某些字段的值之后查询其他文档,所以需要预先以这些字段排序。例如示例4-10

在上面的请求中,kiana_ sample_ data_flights将按Destcountry和Flightnum字段排序,但只返回Destcountry为AE并且Flightnum为AR9OTDM之后的 10 条文档(size默认为 10)。这种机制本质上是通过匹配字段,动态决定第一条文档是哪一个,在这种情况下 from必须设置为0或-1。不仅如此,参与排序的字段值需要保证惟一。虽然这种唯一性保证并非必须,如果不唯一则在查询时将导致歧义,有可能返回不正确的结果。

注意:这种机制在匹配字段时并非使用精确匹配,而是只要部分满足即可。在上面的例子中,如果含有一个Flightnum为AR9OTDMXXX,也是满足匹配条件的。但由于AR9OTDMXXXE会排在AR9OTDM之后,所以还是不会出现问题。排序后的检索结果中,都会在最后附带一个排序字段的值,如上示例检索结果最后会包含如下内容

这个内容正好与当前检索结果中的Destcountry和Flightnum字段值相同,可以为下一次search after使用。讲到这里就涉及到检索的另外一个重要内容-排序 

4.2.4 sort参数

排序是文档检索中另一个重要的话题。如按商品售价销量排序。例如在示例4-10中,search after机制已经使用到了排序。Elasticsearch的排序可以依照文档一个或多个字段排序,包括两个虚拟字段 _score 和 _doc。

按_score排序就是按文档相似度得分排序

按_doc排序则是按索引次序排序。例如

示例中给出了几种排序方法,将会依次按 Avgticketprice、Flightdelaymin字段升序排列,再按 Distancekilometers、_score字段降序排列。排序执行的顺序与它们在sort数组中的次序一致。

与SQL语言类似,asc代表升序,desc代表降序。默认情况下,除_score按降序排列,其余字段都按升序排列。

Elasticsearch支持使用数组类型或多值类型字段做排序,但需要定义如何使用数组中的数据。包括 min、max、avg、sum、median等几种情况,分别代表取最小值、最大值、平均值、总和或中值参与排序,可通过参数mode来定义。如下示例,将按products. base_price字段的最大值做降序排列

默认情况下,查询结果会按_score字段降序排列, _score字段是文档与査询条件的相似度得分。即越是靠前的结果与查询条件的相似度越高,这与人们的使用习惯相符。相似度问题在全文检索中是一个非常重要的话题,将在本书第 6 章中专门讨论。

由于排序算法需要知道所有参与排序的值才能做运算,所以参与排序字段在文档中的值都需要加载到内存中来。这一方面对节点分配的内存提出了更高要求,另一方面也要求参与排序的字段必须支持文档值(Doc Value)或fielddata机制。这是因为倒排索引保存的是词项到文档的对应关系,适用于通过词项检索文档。但在排序时需要的是通过文档找到字段值参与排序,所以必须保证能够通过文档找到字段值。在默认情下,文档值机制对于非text类型的字段都是开启的,而text类型则只能通过开启fieldata机制才可能支持排序。这两种机制在第2章2.22节有过介绍。

4.3 字段投影

投影(Proiection)的概念源于关系型数据库,是指从一个关系中选取若干个属性形成一个新的关系。简单来说,就是在查询表时不将所有字段返回,而只返回其中的部分字段。Elasticsearch并没有直接引入投影的概念,但支持类似投影的操作。这主要体现在对查询结果的_source字段和 fields 字段的定制上。

4.3.1 _source参数

在2.2.2节中曾介绍,Elasticsearch查询结果中会包含_source元字段,这个字段存储了文档的最原始数据。_search接口提供了_source参数,以定制源文档中哪些字段出现中_source中。这个参数可以在URI中使用,也可以在请求体中使用。

示例:_source将只包含 Destcountry 字段的值

如果需要返回多个字段,可以使用数组设置_source,并且可以使用通配符星号“*”。如下面两个请求,都将返回Origincountry和Destcountry

当然,使用星号匹配的范围更大一些,如果索引中包含其他以Country结尾的字段,也将出现在返回结果中。类似地,_source也可设置为false,这将禁止在返回结果中包含_source源文档内容,而只包含元字段。还可以在_source字段中添加includes和excludes字段,以明确包含和排除字段。如下示例中,将所有包含lon、lat经纬度信息的字段包含进来,而排除了Destlocatione的子字段,所以在返回结果中应该只包含OriginLocation:

4.3.2 stored_fields参数

除使用_source字段过滤可以出现在源文档中的字段以外,还可以使用stored_fields字段指定哪些被存储的字段出现在结果中。当然这些字段的store属性要设置为true,否则即使在stored_fields中设置了它们也会被忽略。

在示例中,author字段的store参数为true而title设置为false,则在查询的结果中将忽略title:

在返回结果中会增加一个fields字段,其中包含了stored_fields中配置的字段值。此外,在使用 stored_fields之后,_source字段默认将不会出现在结果中,但可通过将 _source参数设置为true让它返回。字段的store参数在2.22节有介绍,当文档某字段单独使用的频率比较高而其他字段值占用空间又非常大时,就可以把这种常用的字段单独保存起来使用。 

4.3.3 docvalue_fields参数

docvalue_fields也是_search接口的参数,它用于将文档字段以文档值机制保存的值返回。

文档值机制是非text类型字段支持的一种在硬盘中保存字段原始值的机制,可通过字段的doc_value参数开启或关闭。在2.2.2节讲解。

上述示例,docvalue_fields 接收的对象有两个属性,field定义字段名称,而format则定义数值和日期的格式。示例中使用了日期格式 epoch_millis,所以返回结果将以毫秒数显示timestamp字段。format可以使用use_field_mapping关键字,它代表的含义是使用字段在索引映射中定义的格式。类似于 stored_fields,  docvalue_fields查询的返回结果中也会增加一个fields字段,包含了在 docvalue_fields中声明的字段及其文档值。与stored_fields不同的是,docvalue_fields的返回结果中默认会包含 _source字段。所以在示例中使用_source参数过滤了返回结果以保证_source 中也只包含timestamp字段。 

4.3.4 script_fields参数

script_fields同样是_search接口的参数,它可以通过脚本向检索结果中添加字段。与 stored_fields和 docvalue_fileds类似,通过脚本添加的字段也会出现在结果的fields字段中。默认情况下,使用了script_fields参数后,_source字段也不会出现在结果中,但可使用_source参数配置开启。如下示例中向返回结果添加了price_per_km字段,它通过Avgtlcketprice字段和Distancekilometers字段相除而得,反映了机票每公里的平均票价

Script fields 中默认使用的脚本也是 Painless,可以在这个上下文中使用的变量见表 4-3。

4.4 分析器和规整器

在Elasticsearch中,文档编入索引时会从全文数据中提取词项,这个过程被称为文档分析(Analysis)。文档分析不仅存在于文档索引时,也存在于文档检索时

分析器

文档分析会从查询条件的全文数据中提取词项,再根据这些词项检索文档。文档分析器(Analyzer)是Elasticsearch中用于文档分析的组件,通常由字符过滤器(Character Filter)、分词器(Tokenizer)和分词过滤器(Token Filter)三部分组成。它们就像是连接在一起的管道,共同完成对全文数据的词项提取工作。如下所示。

 

字符过滤器:读入最原始的全文数据,并对全文数据中的字符做预处理,比如从HTML文档中将类似<b>这样的标签删除。字符过滤器可以根据实际情况有零个、一个或多个。

分词器:接收由字符过滤器处理完的全文数据,然后将它们根据一定的规则拆分成词。英文分词规则比较简单,直接使用空格分隔单词即可;但中文分词规则复杂,需要根据词意做分词且需要字典支持。分词器是必不可少的,且只能有一个。在使用分析器时,可以根据文档内容更换分词器,如对于中文来说需要将分析器的分词器更换为中文分词器。

分词过滤器:接收由分词器提取出来的所有词项,对这些分词做规范化处理,比如将分词转换为小写、去除“的、地、得”等停止词(Stop Word)。分词过滤器与字符过滤器一样,可以根据需要添加和配置,可以没有也可以有多个。

规整器

除分析器外,Elasticsearch还提供一种称为规整器(Normalizer)的文档标准化工具。

规整器与分析器的最大区别在于规整器没有分词器,而只有字符过滤器和分词过滤器,所以它能保证分析后的结果只有一个分词。规整器只能应用于字段类型为keyword的字段,可通过字段的normalizer参数配置规整器。

规整器的作用就是对keyword字段做标准化处理,比如将字段值转为小写字母等。规整器与分析器共享相同的字符过滤器和词项过滤器,但规整器只能使用那些结果只有一个词项的过滤器

4.4.1 设置分析器

由于文档分析通过分析器完成词项提取,所以想要影响文档索引和检索时的词项提取,就要修改它们使用的分析器。对于所有text类型的字段,可以在创建索引时为它们指定分析器。

对于非text类型字段来说,由于本身不存在全文本数据词项提取的问题,所以也就没有设置分析器的问题。

如下示例中,title字段在文档编入索引时使用standard分析器,而通过title字段检索文档时则使用simple分析器

创建索引时如果没有指定分析器,Elasticsearch会查找名为default的分析器,如果没有则使用standard分析器。文档检索时使用的分析器有一点复杂,它依次从如下参数中查找文档分析器,如果都没有设置则使用standard分析器

1)检索请求的 analyzer参数

2) 索引映射字段的search_analyzer参数

3) 索引映射字段的analyzer参数

4) 索引配置中的default_search参数。

以上述示例中的articles索引为例,尽管设置的检索分析器为 simple,但如果在检索文档时使用analyzer参数设置为english分析器,最终使用的分析器依然是english分析器。如第4.1节所述,analyzer参数可以作为_search接口的URI参数也可以出现的请求体中,例如

4.4.2 _analyze接口

Elasticsearcht 提供了一个_analyze接口,可以用于看分析器处理结果,这在学习分析器时非常有帮助。由于_analyzef接口仅用于查看分析器功能,所以在发送请求时不需要指定索引,虽然在指定索时也不会报错。

如示例就是用standard分析器处理文本

_analyze接口可以使用 POST或GET发送请求。返回的结果中,将包含所有提取出来的词项以及它们的位置、偏移量等信息:

其中,token是提取出来的词项,start_offset与end_offset是词项在整个文本中的起始位置和终止位置(即偏移量), position是词项在所有词项中的次序,而type则是词项的类型(与词项本身和分析器相关)。

如果要查看分词中更详细的内容,可以在请求体中将explain参数设置为true。

实际上除了analyzer参数以外,还可以使用tokenizer、fiter、 char_filtere 的组合来测试分词器、分词过滤器、字符过滤器的功能。

4.4.3 _termvectors接口

_termvectors接口提供实时查看一段文本提取词项结果的功能,可以使用POST或GET方法请求该接口。

该接口查看词项结果时所使用的文本内容可以来自索引中某一文档的字段内容,也可以是在调用接口时动态指定一段文本。

所以该接口有两种请求方式,并且它们不能同时使用,只能任选其一。

第一种方式是在请求路径中指定文档_id,并在路径或请求体中通过设置fields参数指定要查看的字段名称。例如:

如上示例中,请求路径中的索引名称和文档ID(_termvectors后面的值)都必须要指定。文档ID由于太长做了一些节略。两个请求都是通过fields参数指定要查看词项的字段名称,可以指定一个也可以指定多个。

第二种方式要求路径中定不能包含文档id,而是在请求体中通过doc参数指定文本内容。例如

如上示例中,doc参数指定了两个字段message和agent,Elasticsearch会分别返回这两个字段词项的提取情况。

doc参数指定的字段必须是索引中已经存在的字段,该接口会忽略索引中不存在的字段。

以上两种方式都是实时做词项分析(不是直接去读取已经提取的词项,而是使用索引或字段设置的分析器实时提取词项)。

_termvectors接返回的结果中主要包含field_statistics和terms两个字段,它们分别给出提取词项的统计数据和详细信息,这些对于分析检索时出现的问题将会十分有用。

除此之外,Elasticsearch还提供了_mtermvectors接口,可以一次查询多个文档的词项信息。这些文档甚至可以分散在不同的索引和映射类型中,例如

如上示例中,_mtermvectors接口的是通过在请求体中添加docs数组,并设置数组每个元素的_index和_id参数指定。如果要查询的词项信息在同一个索引中,则可以在_mtermvectors前面添加索引名称,而无须在docs数组中再设置对象的_index参数。 

4.5 内置分析器和中文分析器

Elasticsearch内置很多分析器,这些分析器以名称标识,不需要做太多的配置就可以直接使用。比如前面提到的standard、simple、english等都是内置分析器,这些分析器可直接使用,或者通过配置生成自定义的分析器再使用。新分析器需要在创建索引时定义,例如

如上示例中,创建了一个索引analyzer_test,并定义了一个名为my_analyzer的分析器。该分析器基于standard分析器,但将词项最大长度设置为5, 并定义了组停止词。它们通过max_token_length和stopwords参数设置,有关它们的说明请参考下面standard分析器的介绍。完成了分析器的定义后,就可以在analyzer接口中使用了

由于定义了停止词,如上示例中分析的文档this和is这两个词会从最终结果中别除,而elasticsearch、logstash和kiana这三个词项,由于长度超过5将被拆分为多个词项。

在定制分析器使用的type参数指定了基于种分析器做定制,但并不是每一种分析器都可以定制。每一种可定制的分析器在定制时又有自己可用的参数,这些参数将在介绍具体分析器时讲解。 

4.5.1 standard分析器

standard分析器是默认分析器,它使用标准分词器(Standard Tokenizer, standard)提取词项。标准分词器提取词项的规则是根据Unicode文本分隔规范中定义的标准分隔符区分词项。比较常见的分隔符包括空格、换行、标点符号、数学运算符等,主要针对类似英文这种拼写类语言。standard分析器没有字符过滤器,但包含了三词项过滤器。它们分别是

标准词项过滤器(Standard Token Filter):只是占位,实际没做任何处理

小写字母过滤器(Low Case Token Filter):作用是将词项转换成小写字母

停止词过滤器(Stop Token Filter):将停止词删除,默认关闭。所以在默认情况下,standard分析器的实际分词效果是使用Unicode文本分隔规范提取词项并全部转为小写。

standard分析器是可配置分析器,可使用配置参数见下表

4.5.2 stop分析器

stop分析器使用小写字母分词器(Lowercase Tokenizer, ower case),分词规则是使用所有非字母分隔单词,并且会将提取出来的词项转换为小写。所以使用stop分析器提取出来的词项一定不会包含数字、空格、标点符号等特殊字符。

stop分析器没有字符过器但包含一个停止词过滤器。这个过滤器与standard中的停止词过滤器是相同的,只是它的默认值为_english_而不是_none_。停止词过滤器针对每一种语言内置了一组停止词,它们包含了每种语言中常见的停止词。

停止词除了可以使用上述内置停止词以外,还可以通过stopwords参数使用数组定义停止词全集,或者通过stopwords_path参数指定停止词文件路径。停止词文件是定义停止词全集的另一种方式,它的基本格式是每行定义一个停止词,所以可以按行将所有停止词声明在文件中。

所以stop分析器在使用上的实际效果就是以非字母分隔单词,并且在词项结果中去除所有英文停止词。stop分析器也是一种可配置的分析器,可使用配置参数见下表

4.5.3 pattern分析器

pattern分析器使用模式分词器(Pattern Tokenizer, pattern),该分词器使用Java正则表达式匹配文本以提取词项,默认使用的正则表达式为"W+",即以非字母非数字作为分隔符。pattern分析器没有字符过滤器,但包含两个过滤器:小写字母过滤器和停止词过滤器。这两个过滤器与standard分析器中的过滤器完全一样,所以pattern分析器认提取出来的词项也会被转换为小写并且不包含停止词

pattern分析器是可配置分析器,参数主要是设置正则表达式和停止词,见下表

4.5.4 custom分析器

custom分析器可以理解为一个虚拟的分析器,不能直接使用。

但custom分析器是一个可配置的分析器,一个专门用于自定义的分析器,在custom基础上配置出来的自定义分析器是可以使用的。前述几个可配置的分析器虽然也可配置,但不能在配置中替换字符过器、分词器和分词过滤器。而custom分析器的配置参数中包含 char_filter、tokenizer、filter三个参数,可以通过它们定义需要使用的字符过滤器、分词器和分词过滤器。由于分词器是分析器必要的组件,所以在配置custom分析器时tokenizer参数是必选项。custom分析器可用配置参数见下表

Elasticsearch内置提供了十多种分词器、几十种分词过滤器,但常用的几种在前述分析器介绍中已经见过。对于其他分词器和分词过器,因为它们并不常用,也限于篇幅,本节就不展开介绍了,可到 lasticsearch官方网站找到相关资料。

4.5.5 其他内置分析器

前述几个分析器都是可以配置的分析器,下面再来看几个不可配置的分析器 simple、whitespace和keyword。它们不仅不可配置,而且都没有字符过滤器和词项过滤器。

simple分析器与stop分析器一样使用小写字母分词器( Lowercase Tokenizer, lowercase),所以提取出来的分词与stop分析器相同。只是simple分析器没有过器,所以不能做停词处理。 whitespace分析器使用空格作为词项分隔符,提取出来的词项不做大小写转换,使用的分词器是空格分词器(Whitespace Tokenizer, whitespace)。keyword分析器是一个不做任何处理的分析器,它会将文档容整体作为一个词项返回,使用的分词器是关键字分词器(Keyword Tokenizer, keyword)。

除了以上分析器,Elasticsearch还提供了一组与语言相关的分析器,用于处理各种语言容的文档。这组分析器有30多种,支持世界上大多数语言。其中可以用来处理中文的分析器为cjk, cjk 是 China、 Japan和Korea三个单词的简写,代表以东亚国家为主的象形文字。但cjk在处理中文时并不好用,它会把每个汉字都提取为词项,所以意义不大。

4.5.6 中文分析器

中文分析器中比较有名就是IK,包括ik_smart、ik_max_word两种。这两者的区别在于它们提取词项的粒度上,前者提取粒度最粗,而后者则最细。比如“中文分析器”使用ik_smart只会提取出“中文”和“分析器”两个词项,而使用ik_max_word则会提取出“中文”“分析器”“分析”和“器”四个词项。也就是说,ik_max_word会从文本中穷尽所有可能的词语组合,所以它提取出来的词项会远多于ik_smart。

Elasticsearch默认并不支持IK,所以在使用IK前需要以插件的形式将它安装到Elasticsearch中。安装IK插件可以直接将插件解压缩到Elasticsearche的plugins目录中,也可以通过elasticsearch-plugin命令完成安装。先到IK在github上的地址,选择合适的版本下载安装包,IK版本必须要与Elasticsearch版本严格一致,否则在启动Elasticsearch时会报错。

如果采用直接解压缩的方式安装IK,需要在Elasticsearch的安装路径下找到plugins目录,并在这个目录创建一个新文件夹并命名为ik,然后将安装包直接解压缩到这个目录。IK插件安装的目录名称并非一定要叫ik,只要这个目录在plugins路径下即可。如果使用elasticsearch-plugin命令安装IK就更简单了,这个命令位于Elasticsearch安装目录下的bin目录中。直接进入命令行,键入如如下命

如上示例展示的是一条完整的命令,elasticsearch-plugin insall 后接的是IK下载地址,将路径中的7.0.0和文件名中的7.0.0替换成需要的版本号即可。键入回车后,elasticsearch-plugin会到指定的路径下载IK,并将它安装到plugins路径中的analysis-ik目录中。无论以哪种方式安装IK,安装结束后都需要重新启动Elasticsearch。接下来就可以体验一下 K 分析器了,如下所示

中文分析及IK分析器是一个很大的话题,本书限于篇幅不太可能面面俱到。有关IK分析器的更多介绍请参考官网。

4.6 其他检索接口

前面几个小节实际上都是围绕着_search接口,但实际上Elasticsearch还提供了许多与文档检索有关的接口。比如,如果想要查看索引中满足条件的文档数量可以使用_count接口,如果想要执行一组检索可以使用_msearch接口,而_scripts接口则提供了一种定义询模板的方法。

4.6.1 _count接口

Elasticsearch提供了看文档总数的_count接口,可通过GET或POST方法请求该接口。在请求该接口的路径上,可以添加索引、映射类型,以限定统计文档数量的范围。如下接口的请求都是正确的

如上所示,在请求_count接口时可以类似_search接口样通过URI参数,或通过请求体向接口传递DSL查询条件,_count接口会统计满足查询条件的文档数量。_count接口可使用的URI参数见下表, 查询字符串依然是通过参数设置,其余参数的含义与_search类似

_count接口使用请求体设置DSL询条件的参数也是query,并且所有DSL查询条件都可以在_count的请求体中使用。

4.6.2 _msearch接口

_mearch接口类似于_buk接口,可以在一次接口调用中执行多次查询,可以使用GET或POST方法请求。请求体每两行为一组视为一个查询,第一行为查询头包含index、search_type、preference和routing等基本信息,第二行为查询体包含具体要检索的内容如query、aggregations等。例如

如上示例中包含了3个査询,前两个查询的查询头都是空的,所以默认在请求路径中指定的kiana_sample_data_flights索引中查询;最后一个则在询头中指定了索引为kiana_sample_data_logs。 

4.6.3 _scripts接口

查询模板是一种基于Mustache语言的查询模板,通过它可以预先定义好查询结构和参数,然后在请求时指定参数执行查询。Mustache是一种基于“}”格式的模板语言,可到其官方网站査看相关知识。执行查询模板的接口为"_search/template",可以使用GET或POST方法请求。例如

如上示例中,source参数定义了查询模板为match查询,并使用{{field}}和 {{value}}定义了两个参数;而params参数则分别给出了这两个参数的值为Westcountry和CN,所以最终执行的查询是根据DestCountry字段与CN词项做匹配。

这样使用查询模没有太大意义。一般是将一些复杂的査询以查询模板的形式保存起来,然后在查询的时候指定参数值以执行查询模板。例如可以将上面的查询保存起来

如上示例中,_scripts是接口关键字,用于保存或看脚本片段,first_template则是查询模板的名称。

在请求体中,使用script参数分别定义了查询模板的语言、源代码等内容。创建成功后,以GET方法再请求这个接口就可以查看到新创建的查询模板。新创建的查询模板可以通过查询模板接调用执行,如下所示 

如果不想再使用查询模板,可以通过DELETE将其删除,请求的资源路径与POST和GET请求时一样。

需要注意的是,查询模板是全局的,不能在某个索引下创建查询模板

除了使用_search/template以外,与此相关的还有一个多查询模 板, 即_msearch/template。该接口请求体的格式与_msearch接口相同,也是以两行为一组,只是在请求体中放置的是模板和参数。

4.6.4 辅助接口

除了上述可执行文档检索的接口以外,Elasticsearch还提供了组用于查看、计或分析检索执行情况的接口。这包括_validate、_explain、_field_caps、_search_shards等,它们对于分析检索效率和性能调优有一定帮助。

1._validate 接口

用于在不执行查询的情下,评估一个查询是否合法可执行,这通常用于验证执行开销比较高的查询。可通过GET或POST请求,请求路径中必须要包含_validate/query,也可以在路径中添加索引名称以限定查询执行的范围。类似_search和_count接口,_validate接口也可以通过URI和请求体两种方式接收DSL查询条件。所以如下示例都是正确的

_validate接口执行后,会在返回结果中包含一个valid字段,true代表查询合法可执行而false则相反。validate接口URI参数见下表

2. _explain接口

explain接口用于给单个文档的查询相似度评分做解释,所以在使用_explain接口时必须要指定索引和文档_id,并且必须要通过URI参数q或请求体query参数将DSL查询条件传递给接口。在_explain接口返回的结果中,会包含matched和explanation字段,matched 字段代表DSL查询条件是否匹配当前文档,而explanation字段中会包含相似度评分及评分计算依据。例如

_explain接口的返回结果中包含了相关度评分计算依据,有关相关度评分计算公式及其相关的一些知识将在本书第 7 章中介绍。

_explain接口可以用URl参数见下表

3. _field_caps接口

用于查看某一字段支持的功能,主要包括字段是否可检索以及是否可聚集等。需要查看的字段可以通过URI参数fields设置,虽然可以使用GET或POST方法请求,但fields参数不能在请求体中设置。在请求地址中,还可以添加索引名称以限定查询范围。如下请求都是正确的

在返回结果中主要包含searchable和aggregatable两个布尔类型的字段,代表该字段是否支持检索和聚集。除此之外还可能会返回个数组类型的字段,其中包括indices代表字段所属索引名称、 non_searchable_indices代表字段不可检索的索引、non_aggregatable_indices代表字段不可聚集的索引。

4. _search_shards接口

返回查询基于哪些节点、索引和分片执行这些信息有助于分析查询时出现的各种问题。

_search_shards可以通过POST或GET方法请求,请求路径中可以指定索引以限定范围。与前面的几个接口不同,这个接口不能设置查询条件,但可以通过routing定义路由规则。

4.7 本章小结

本章主要介绍Elasticsearch中与文档检索相关的一些接口,在这些接口中最为核心的接口是 _search接口。_search接口是执行DSL和聚集查询的最直接接口,也是Elasticsearch全文检索能力的重要体现。 _search接口有基于URI和基于请求体的两种调用方式,它们的区别只是在传递查询参数时是通过不同的方式。所以两种调用方式支持基本相似的请求参数,这些参数可以实现检索时的分页、投影等功能。除了_search 接口,在第4.6节将其他一组与检索相关的接口也做了简要介绍。

本章另一个非常重要的内容是介绍了分析器,这是 Elasticsearch实现全文检索非常核心的技术环节,所以也是学习检索重要的基础知识

在接下来的两章中,本书将开始介绍在_search接口中可以执行的两种主要询方式,DSL和聚集查询。 

第五章 叶子查询与模糊查询

Elasticsearch检索接口_search可通过URI参数q或请求体参数query接收DSL描述的查询条件,其中参数q接收DSL中定义的查询字符串,而quey参数则可以接收所有DSL询条件。

DSL可以分为叶子查询(Leaf Query Clauses)和组合查询(Compound Query Clauses)两种类型。

叶子査询:在指定的字段中匹配查询条件,例如检索名称为tom的文档、年龄在 10~20 岁之间的文档等等。叶子查询大致上可分为基于词项的查询和基于全文的查询两大类,除了multi_match和query_string以外,它们大部分都只能针对一个字段设置查询条件。

组合查询:可以包含一个或多个子查询,这些查询以不同的逻辑运算并组装在一起共同执行检索。

由于DSL内容非常多,同时又涉及模糊查询、相关性计算等全文检索专业问题。本章将只介绍叶子查询和模糊查询等相关问题;而组合查询与相关性计算等问题将在下一章介绍。

此外,叶子查询中有两个最简单的查询match_all和match_none,它们代表的查询条件是匹配所有文档和一个文档都不匹配。它们没有参数且使用简单,在4.1.2节中有过介绍,所以本章也不会再单独介绍它们。

5.1 基于词项的查询

基于词项的查询属于叶子查询,这种査询一般只能针对一个字段设置条件。基于词项的查询会精确匹配査询条件,不会对查询条件做分词、规范化等预处理。但对于keyword类型字段,如果这字段通过normalizer参数定义了规整器,词项查询会将查询条件做标准化处理。有关标准化参考4.4节。

词项查询匹配字段索引中包含的词项值,由于text类型字段会做分词处理,所以不能直接匹配字段的全部内容。比如在text类型字段值为"tom smith",编入索引词项是tom和smith而没有"tom smith",所以使用"tom smith"做词项查询,将无法检索到这个字段。不仅如此,由于分析器在提取分词后还会通过分词过滤器对分词做处理,所以分词一般都会做一些规范化处理。以默认standard分析器为例,它包含一个lowercase分词过器,会将所有分词转换为小写字母。所以如果一个text字段的值为Tom Smith, 编入索引的词项将是tom和smith,使用Tom或Smith就无法检索到文档。

当然这里还有另外一个因素,那就是基于词项的查询不会对查询条件做分析和规范化处理。

因此,基于词项的查询一般不对text类型字段做检索,而用于类似数值、日期、枚举类型等结构化数据的精确匹配。

基于词项的查询有多种类型,每一种类型都有一个关键字。在下面各小节的中会直接使用它们的关键字指代这种查询类型。

5.1.1 term、terms和term_set

term、terms和terms_set都是对单个字段做词项值的精确匹配,区别在于term查询只能匹配一个词项,terms可以从一组词项中做匹配,而terms_set则可以匹配数组类型的字段。

1. term查询

term对字段做单词项的精确匹配,而不能对字段做多词项的匹配。如果以SQL语句来类比,term查询相当于SQL语句where条件中的等于号。所以在使用term查询时需要指定的就是字段与期望值的对应关系,例如

如上两个请求都正确,第二个请求中将message字段的值放在了value参数中。这种方式一般是在需要设置boost参数时使用,boost参数用于提升检索结果的相关性评分,请参考6.1.5节

2. terms查询

terms査询类似于SQL中的in操作符,可以在一组指定的词项范围内匹配字段值,只要字段满足这些词项中的一个就认为满足查询条件。

terms查询中由于要设置多个词项,所以字段期望值使用数组来设置

Elasticsearch在terms查询中还支持跨索引查询,这类似于关系型数据库中的一对多或多对多关系。比如,用户与文章之间就是一对多关系,可以在用户索引中存储文章编号的数组以建立这种对应关系,而将文章的实际内容保存在文章索引中(当然也可以在文章中保存用户ID)。如果想将ID为1的用户发表的所有文章都找出来,在文章索引中查询时,如下

如上terms要匹配的字段是_id,但匹配值则来自于另一索引。这里用到了index、id和path三个参数,它们分别代表要引用的索引、文档ID和字段路径。在上面的例子中,先会到users索引中查找id为1的文档,然后取出articles字段的值与articles索引里的_id做对比,这样就将用户1的所有文章都取出来了。

3. terms_set查询

terms_set查询与terms查询类似,不同的是被匹配的字段类型是数组。terms_set查询接收以数组类型表示的多个词项,被匹配字段只要包含期望词项中的几个即可。具体数量有两种方式设置,

一种方式是通过minimum_should_match_field参数指定文档中的一个字段,这个字段必须为数值类型并保存了期望匹配词项的个数;

另一种则是通过minimum_should_match_script参数以Painless脚本动态计算。例如

在脚本中可以使用params.num_terms上下文量获取terms参数中设置的期望匹配词项的实际总数量,Painless脚本在这里可使用的上下文参数见下表

5.1.2 range与exists

range用于匹配一个字段是否在指定范围内,一般应用于具有数值、日期等结构化数据类型的字段。例如

如上查询会返回所有延误时间在100-200min之间的航班。可以用在范围查询中的参数包括

  • gte:大于等于
  • gt:大于
  • lte:小于等于
  • lt:小于
  • boost:相关性评分

除以上参数,日期类型的字段还可使用format参数指定日期格式,使用time_zone参数指定时区;而范围类型的字段可通过relation参数指定字段与查询条件之间的关系,可选值为WITHIN、CONTAINS和INTERSECTS。范围类型在2.3.1节有介绍,包括integer_range、float_range、long_range、double_range等几种。

exists用于检索指定字段值不为空的文档,所以exists查询需要通过field字段设置需要检查非空的字段名称。同样的,field参数只能设置一个字段,不支持对多个字段的非空检验。例如

exists查询在验证非空时需要明确什么样的值是空,什么的值不是空。默认情况下,字段空值与Java语言的空值相同都是null。空值字段不会被索引,因此也不可检索。 

5.1.3 使用模式匹配

备注:

参考:https://www.elastic.co/guide/cn/elasticsearch/guide/current/_wildcard_and_regexp_queries.html
prefix
 、 wildcard 和 regexp 查询是基于词操作的,如果用它们来查询 analyzed 字段,它们会检查字段里面的每个词,而不是将字段作为整体来处理。

比方说包含 “Quick brown fox” (快速的棕色狐狸)的 title 字段会生成词: quick 、 brown 和 fox 。

会匹配以下这个查询:

{ "regexp": { "title": "br.*" }}

但是不会匹配以下两个查询:

{ "regexp": { "title": "Qu.*" }} 
{ "regexp": { "title": "quick br*" }}
- 在索引里的词是 quick 而不是 Quick 。
quick 和 brown 在词表中是分开的。

前述几种询类型都是对词项做精确匹配,但Elasticsearch也支持使用通配符、正则表达式等方式对词项做模糊匹配。这些查询类型包括prefix、wildcard、regex和fuzzy四种。

1. prefix查询

prefix可以用于检索字段值中包含指定前缀的文档,这对于只记得词项前缀时做文档检索比较有帮助。例如想要检索包含Mozilla的文档,但只记得前缀为Mo就可以使用prefix查询

上述示例中,尽管单词前缀是Mo,但由于分析器在分词后会将词项做规范化处理,所以查询条件中只能使用mo

2. wildcard查询

wildcard查询允许在字段查询条件中使用通配符“*”和“?”,其中“*”代表0个或多个字符,而“?”则代表单个字符。例如可使用 f*f?x 匹配firefox,它将检索所有以f开头并以f?x结尾的词项,其中问号代表任意字符

由于wildcard查询需要与多个词项做匹配,查询速度会比直接使用完整的词项要慢一些。所以在使用wildcard查询时,尽量不要让通配符出现在查询条件的第一位,因为这需要查询与所有词项做匹配。例如在上述示例中使用的f*f?x只需要与f开头的词项做匹配,而如果使用*iref?x则需要与所有词项做匹配。

3. regexp查询

regexp查询允许在查询条件中使用正则表达式与字段词项做匹配,正则表达式的语法与Lucene使用的正则表达式一致。例如同样是匹配firefox,上述示例中使用的的f*f?x 可以用正则表达式写为f.*f.x:

由于regexp询使用的是Lucence正则表达式,所以Java正则表达式中预定义的字符类型如\w、\d并不支持。 

5.1.4 type与ids

除了使用词项对业务字段做匹配以外,还可以根据索引的元字段做匹配,这包括type查询和ids查询,它们分别可以根据映射类型和文档_id字段做检索。

1. type查询

type据映射类型做查询,将所有属于指定映射类型的文档都查询出来。换句话说,就是根据文档的_type字段做匹配。如下请求,将会返回所有映射类型为_doc的文档

在请求路径中也可添加索引名称,这将把查询范围限定在指定的索引中。由于在Elasticsearch版本6以后索引只能定义一个映射类型,所以这种查询已没有太多意义。

2. ids查询

ids查询允许根据一组ID值査询多个文档,需要注意的ids查询所查询的元字段是_uid而不是_id。由于_uid字段由映射类型和文档ID共同决定,而在Elasticsearch版本6中已经将多映射类型废止,_uid是为了保证版本间兼容オ被保留下来。所以在版本6以后_uid的值与_id字段完全相同,因此可以认为ids查询的就是_id字段。如下请求就是将kibana_sample_data_logs索引中_id值为xxxxxxxxxxxxxxxx的文档检索出来:

在请求路径中也可以不指定索引名称,这样将会询所有索引。由于_id仅在索引内惟一,所以在这种情况下有可能通过一个ID检索到多个文档。 

5.1.5 停止词与common查询

有些词项在所有文档中出现的频率都很高,比如英文中的"the"、"of"、"to"等,中文中的"的"、"得"、"虽然"等。它们出现的范围和频率虽然很高,但是往往与文档要表达的核心意思关联并不大。这类词项在检索时就像是噪音一样,只有将它们剔除才可能得到更接近用户期望的结果。

1. 停止词

在处理这类问题时,最显而易见的办法就是在文档编入索引时将它们剔除,这类出现在文档中但并不会编入索引中的词项就是停止词(Stopword)。停止词一般是在定制分析器时预先定义好,文档在编入索引时分析器就会将这些停止词从文档中别除。例如:

但是以停止词的方式去除无意义词项在某些场景下会导致问题,比如在"你知道虽然这个词是什么含义吗?"这句话中,"虽然"是一般意义上的停止词,但在整个句子中却居于重要地位。如果将它从上述句子中去除,就失去了整句话的核心意义。类似的情况在英文中也不少,比如在莎士比亚经典台词"To be or not to be"中,所有的单词都是一般意义上的停止词,但去除了它们中任何一个整句子都会失去意义。所以针对这种情况,Elasticsearch提供了另外一种解决方案,这就是common查询。

2. common查询

common查询将词项分为重要词项和非重要词项两大类

重要词项:是那些出现频率相对较低的词项,所以也称为低频词项

非重要词项:是那些出现频率相对较高的词项,所以也称为高频词项。

这里的出现频率不是指词项在单个文档某字段中的出现次数,而是指在某字段中出现了该词项的文档数量

这种重要与非重要划分的标准是基于逆向文档频率(Inrert Document Frequency,可参考6.1.2节)思想,即如果词项在大多数文档中都出现了,那么它与结果的相关性就低,反之相关性就高。

在common查询中使用cutoff_frequency设置词顼频率,可以设置为一个绝对数量,代表出现了词项的文档个数;也可以设置为百分比,代表出现了词项的文档数量占总文档数量的百分比。cutoff_frequency区分绝对数量和百分比是看设置的值是否小于 1, 小于1时为百分比否则为绝对数量。需要特别注意的是,词项频率是分片运算的,在文档数据比较少的情况下,有可能出现各分片严重不平衡的现象。为了体验common查询,可以按如下示例创建只有一个分片的索引articles,并添加4个content字段分别为"this is elasticsearch"、"this is logstash"、"this is kiana"和"logstash kibana"的文档

按照重要词项和非重要词项的思想,“this”和“is”出现在3/4的文档中,应该归类为非重要词项,而"elasticsearch"、"logstash"和"kibana"则只出现在1~2份文档中应为重要词项。所以将cutoff_frequency设置为2次(小于3) 就可以按重要性将它们区分开来,而将cutoff_frequency设置为3或更大值时,所有查询词项都会被归类为非重要词项,如下所示

如上查询中,査询条件"this is elasticsearch, logstash"会被拆分为"this"、"is"、"elasticsearch"和"logstash"四个词项。按cutoff_frequency设置的标准,前两者会被识别为非重要词项,而后两者则会识别为重要词项。接下来common查询会执行两次检索,第一次检索是根据重要词项"elasticsearch"和"logstash"匹配文档,而第二次检索则是在第一次检索的基础上再次使用非重要词项"this"和"is"匹配文档,重要词项对相关性数值_score的影响大于非重要词项。读者可自行将cutoff_frequency设置为3, 看一看返回的文档及其_score分值有什么不同。

当查询条件中存在多个词项时,无论是重要词项还是非重要词项,它们与字段匹配结果之间的关系都是或者的关系。也就是说,只要满足任意一个词项匹配条件即可以被筛选出来,匹配多个只会使相关性评分升高。这种默认的关系可以通过三个参数修改,它们分别是 minimum_should_match、low_freq_operator和high_freq_ operator。后两者比较直观,分别是设置低频词项和高频词项的操作符。minimum_should_match略有些复杂,直接设置值时设置的是低频词项需要匹配的数量。如果要设置高频词项则需要使用low_freq和high_frec区分,例如

commont查询通过词项频率做重要性区别,将那些与结果相关性不大的词项筛选出来,使它们仅对结果相关性产生一定影响,同时还可以降低检索运算和相关性运算的复杂度。common查询是对停止词的一种替代方案,但比停止词又灵活了很多。它使用词项频率作为控制值,将词项分为高频词和低频词,而高频词就相当于停止词。这使得在一些专业性文章中,某些特定的词语能够自动成为停止词。比如在elastic官网中,包含elasticsearch、logstash等词项的网页肯定非常多,这些词项出现的频率就会自然升高。如果使用cutoff_frequency设置了合适的百分比,它们也就自然而然地成为高频词项,从而产生了类似停止词的效果。 

5.2 基于全文的查询

基于全文的查询与基于词项的查询最显著的区别是前者会对查询条件做分析,使用的分析器可以在索引创建时通过analyzer参数或search_analyzer参数设置,也可以在检索时通过_search接口的analyzer参数动态修改。尽管基于全文的查询也是叶子查询,但其中的 multi_match和query_string查询可以针对多个字段做查询。

5.2.1 词项匹配

match查询和multi_match查询都是使用查询条件中提取出来的词项与字段做匹配,不同的是前者只对一个字段做匹配,而后者则可以同时对多个字段做匹配。

1. match查询

match查询接收文本、数值和日期类型值,在检索时将查询条件做分词处理再以提取出来的词项与字段做匹配。如果提取出来的词项为多个,词项与词项之间的匹配结果按布尔运算,也就是说只要有匹配成功即认为是满足查询条件。例如在kiana_sample_data_logs中检索与Firefox和Chrome浏览器相关的文档

词项匹配的运算逻辑和匹配个数通过operator和minimum_should_match这两个参数来改变

operator参数的作用是定义分词匹配结果的逻辑组合关系,可选值为or或and(默认值为or)

minimum_should_match参数的作用则是定义分词匹配的最小数量。

如下查询为例,如果将operator设置为and则不会有任何文档返回:

类似如果将minimum_should_match参数为2, 即使设置operator为or也不会有文档返回,因为minimum_should_match参数要求提取出来的两个词项都要满足。minimum_should_match参数可选值有很多种,不仅可以使用正值,还可以使用负值。正值代表需要匹配的数量,负值代表不需要匹配的数量。但无论设置什么样的值,实际匹配的数量不能小于1, 也不能大于子句总数。具体见表 5-2。

2. multi_match查询

multi_match查询与match查询类似,但可以实现对多字段的同时匹配。例如

如上请求将同时检索Destcountry和Origincountry这两个字段,只要有一个字段包含AT词项即满足查询条件。当然两个字段如果都包含AT词项,它的_score分值会更高。

如果在查询条件中没有指定要匹配的字段,将由索引的配置参数index.query.default_field决定,默认为"*.*",即在索引定义的所有字段中检索。multi_match查询的相关性评分涉及分值的组合,可参考6.2.6节。 

5.2.2 短语匹配

短语查询在检索时匹配的不是单个词项,而是由多词项组成的短语。但不要被短语匹配的表面所迷惑,短语匹配并不是用整个査询条件与字段做匹配。如果是这样Elasticsearch就必须要给所有的短语做索引,这个数量将十分惊人;而如果不对所有短语做索引,那么文档就不能够通过短语检索到。所以短语匹配跟普通的match查询并没有本质区别,它在执行检索前也会分析并提取查询条件中的词项。只是在检索过程中,match查询只要包含一个或多个词即视为满足条件,而短语匹配不仅要求全部词项都要包含,还要保证它们在原始文档中出现的先后次序与查询条件中的次序一致。 Elasticsearch提供了两种基于全文的短语查询,它们是match_phrase查询和match_phrase_prefix查询。

1. match_phrase查询

match_phrase查询是基于全文的查询,所以会将查询条件按顺序分词,然后再查看它们在字段中的位置之差,只有差值都为1才满足查询条件。换句话说,这些词项要在字段中依次出现,并且是紧挨着的。例如:

如上示例使用firefox和6.0a1两个词项匹配message字段,并且它们在所有词项中的位置之差应该为1。match_phrase查询提供了一个用于控制词项之间位置差的参数slop,默认情况下它的值是1。可以通过加大slop的值,扩大短语检索的匹配范围。通过词项位置来匹配短语的方式大大降低了索引数量,也增强了短语匹配的灵活性。但这要求在文档编入索引时,将词项在字段中的位置也编入索引。默认情况下,只有text类型的字段才会自动将词项位置编入索引,其他类型字段只存储文档ID,可参考2.2.1节。

2. match_phrase_prefix查询

除了match_phrase查询以外,Elasticsearch还提供了基于前缀的短语匹配。在这种匹配中,最后一个词项可以设置为前缀。如上请求使用前缀短语匹配可以写成

如上査询中,最后一个词项只给出了前缀6.0, 这样会把Firefox版本6.0的文档检索出来。显然,前缀短语匹配可以用于智能提示。即在用户输入6.0时,将所有以6.0开头的短语全部找出来。为了控制提示数量,Elasticsearch还提供了一个参数max_expansions。用于控制匹配结果的上限,默认情况下这个参数的值是50。

5.2.3 查询字符串

在4.1节介绍基于URI的_search接口时讲解过查询字符串,这里的查询字符串与第4.1节介绍的是同一概念。查询字符串是具有一定逻辑含义的字符串,因此它不会直接使用分析器做分词提取,而是先通过某种类型的解析器解析为逻辑操作符和更小的字符串。例如,查询字符串"(firefox6.0a1) OR (chrome 110.696.50)"中,OR为逻辑操作符,它会先被解析为"firefox 6.0a1"和"chrome11.0.696.50"两部分,然后这两部分再使用字段的分析器提取词项。逻辑操作符除了OR以外还有AND,可以使用field_name: query_term的形式指定匹配的字段,所以查询字符串可以支持多字段检索。需要特别注意的是,操作符OR和AND必须大写,如果使用小写将会被解析为词项参与到查询条件中。

查询字符串还可以使用双引号做短语查询,例如“\"firefox6.0 a1" OR "chrome11.0.696.50\"”将被解析为两个短语查询"firefox6.0a1"和"chrome10.696.50"。

1. query_string查询

query_string查询是基于请求体执行査询字符串的形式,它与基于URI时使用的请求参数q没有本质区别。例如可以使用如下形式执行查询字符串

如上示例query_string查询使用quey参数接收查询字符串,而使用default_field参数指定匹配查询字符串的默认字段名称。default_field参数并不是必须要指定的,默认字段名称也是由index.query.default_field指定。所以如果没有指定default_field,也没有在查询字符串使用"field_name: query_term"的形式指定字段,检索将在所有字段上进行。

query_string查询可用参数还有很多,比如可以使用fields参数指定多字段匹配,使用default_operator指定词项之间的逻辑运算关系等。如下列出了quer_string支持的参数

由于query_string查询也支持多字段,所以它的相关性评分与multi_match一样比较复杂,可参见6.2.6节。

2. simple_query_string查询

simple_query_string查询是query_string查询的简化,它的简化体现在它解析查询字符串时会忽略异常,并且引入了一些更为便捷的简化操作符。在使用上simple_query_string没有default_field参数,而是使用fields参数指定检索的字段名称。如上示例中的请求使用simple_query_string查询时可以写成:

如上示例中,査询字符串中的"|"就是 simple_query_string引入的简化操作符,代表逻辑或操作即OR操作符。simple_query_string查询支持的简化操作符见下表

在默认情况下,simple_query_string 查询支持表中所有简化操作符,但可以通过flags参数来开启或关闭这些简化操作符。在flags可接收的参数中

ALL代表开启所有简化操作符,是默认值

NONE代表关闭所有简化操作符

WHITESPASE代表使用空格分词

其余flags可接收值在表中已经给出,可以使用"|"组合多个选项。

5.2.4 间隔查询

间隔查询(Intervals)是在Elasticsearchl版本7中オ引入的一种查询方法,这种方法与短语查询类似,但比短语查询更为强大。它可以定义一组词项或短语组成的匹配规则,然后按顺序在文本中检查这些规则。这种查询之所以被称为间隔查询,就是因为规则与规则之间可以通过max_gaps参数定义间隔。间隔査询的关键字为intervals,主要包括all_of、any_of和match三个参数,这三个参数又有各自的子参数。先来看一个示例

如上示例中,intervals查询首先定义了要匹配的字段为message字段,然后使用all_of指明所有规则都需要满足。在示例中,all_of参数通过intervals子参数定义了一组匹配词项或短语的规则,而ordered参数设置为true则表明这些规则需要按顺序匹配。 all_of可用子参数见表

在示例中,intervals参数中定义了两个匹配规则,而且它们之间必须按照定义的顺序匹配。但由于没有定义max_gaps,所以它们之间的间隔并没有限制。在第一个匹配规则中,使用match.query定义了一段文本,由于同时还使用match.max_gaps定义了间隔为0, 所以文本中的词项就必须是紧挨着的短语。match可用子参数见表。

示例中定义的第二个匹配规则使用了any_of,它的含义是只要匹配interval中的任意一个规则即可。所以示例整个查询条件的含义就是找到包含"get beats metricbeat"短语,并且在其后有404或503的文档。所以这相当于把请求"GET/ beats/metricbeat"并且返回响应状态码为404或503的请求日志全部检索出来了。

5.3 模糊查询与纠错提示

在Elasticsearch基于全文的查询中,除了与短语相关的査询以外,其余查询都包含有一个名为fuzziness的参数用于支持模糊查询。 Elasticsearch支持的模糊查询与SQL语言中模糊询还不一样,SQL的模糊査询使用"%keyword%"的形式,效果是查询字段值中包含  keyword的记录。Elaticsearch支持的模糊询比这个要强大得多,它可以根据一个拼写错误的词项匹配正确的结果,例如根据firefix匹配firefox。在自然语言处理领域,两个词项之间的差异通常称为距离或编辑距离,距离的大小用于说明两个词项之间差异的大小。计算词项编辑距离的算法有多种,在Elasticsearch中主要使用Levenshtein和Ngram两种。其他与此相关的算法也都是在这两种算法基础上进行的改造,基本思想都是一致的。所以理解这两个算法的核心思想是学习这一部分内容的关键。

5.3.1 Levenshtein与NGram

Levenshtein

这个算法可以对两个字符串的差异程度做量化。量化结果是一个正整数,反映的是一个字符串变成另一个字符串最少需要多少次的处理。由于Levenshtein算法是最为普遍接受的编辑距离算法,所以在很多文献中如果没有特殊说明编辑距离算法就是指Levenshtein算法。

在Levenshtein算法中定义了三种字符操作,即替换、插入和删除,后来又补充了一个换位操作。在转换过程中,每执行一次操作编辑距离就加1,编辑距离越大越能说明两个字符串之间的差距大。比如从firefix到firefox需要将"i"替换成"o",所以编辑距离为1而从fax到fair则需要将"z"替为"i"并在结尾处插入"r",所以编辑距离为2。显然在编辑距离相同的情况下,单词越长错误与正确就越接近。比如编辑距离同样为2的情况下,从fax到fair与从elastcsearxh到elasticsearch,后者elastcsearxh是由拼写错误引起的可能性就更大些。所以编辑距离这种量化标准一般还需要与单词长度结合起来考虑,在一些极端情况下編辑距离还应该设置为0,比如像at、on这类长度只有2的短单词。

NGram

Ngram一般是指N个连续的字符,具体的字符个数被定义为Ngram的size。size为1的Ngram称为Unigram, size为2时称为Bigram,而size为3时则称为Trigram。如果Ngram处理的单元不是字符而是单词,一般称之为Shingle。

使用Ngram计算编辑距离的基本思路是让字符串分解为Ngram,然后比较分解后共有Ngram的数量。假设有a、b两个字符串,则Ngram距离的具体运算公式为

式中,ngram (a)和ngram (b)代表a、b两个字符串Ngram的数量;ngram (a) U(这个符号打不出来) ngram (b)则是两者共有Ngram的数量。

例如按Bigram处理firefox和firefox两个单词,分别为"f, ir, re, ef, f, ⅸ"和"f, ir, re, ef, fo, ox"。那么两个字符串的Bigram个数都为6, 而共有Bigram为4, 则最终Ngram距离为6+6-2x4=4。

在应用上,Levenshtein算法更多地应用于对单个词项的模糊查询上,而Ngram则应用于多词项匹配中。Elasticsearch同时应用了两种算法,用户可以应用这些特征开发出更为便利的接口,比如模糊查询、纠错与提示等

5.3.2 模糊查询

DSL中基于词项的查询有一个专门用于模糊查询的类型,这就是fuzzy查询。fuzzy查询根Levenshtein算法,在文档字段中匹配不超过编辑距离的词项。例如

kiana_sample_data_logs的message字段中包含有词项firefox,而在示例的查询条件中给出的词项则为firefix。由于firefix到firefox的编辑距离为1, 所以仍然能够将包含有firefox词项的文档检索出来。但是如果使用firefityr作为查询条件,由于编辑距离为2则返回结果中将不包含任何文档。

fuzzy查询中的funzziness参数用于设置编辑距离长度,可以设置为0、1、2三个值中的任意设置大于2的编辑距离将被略而直接使用2。除了设置具体的编辑距离,还可以使用 AUTO: [Iow], hight的形式,它会根据词项的长度将编辑距离分为0、1、2三组。举例来说,AUTO:3,6 将会把单词长度分为三组[0,2]、[3. 5] 以及[6, +],而这三组长度范围允许的编辑距离分别为 0 1、2。这种编辑距离与单词长度挂勾的作法与前面讨论的思想一致,也是fuzzy查询使用的默认值。

显然模糊查询比精确匹配在计算开销上要高得多,可以通过另外两个参数prefix_length和max_expansions来少开销。

prefix_length设置了不做模糊处理的前缀长度,默认值为0, 加大这个参数的值会显著降低计算量

max_expansions则定义了模糊匹配结果的最大数量,默认值为50。

另外还有个transpositions参数,用于设置在Levenshtein算法中是否允许换位操作,默认为true。

由于基于词项的査询主要是做词项的精确匹配,所以在基于词项的查询中只有fuzzy查询支持以编辑距离为基础的模糊查询。

基于全文的査询与基于词项的查询在用途上正好相反,只有支持模糊查询才能体现其全文检索的特征。所以,除了match_ phrase、match_phrase_prefix以及intervals等与短语查询相关的DSL不支持模糊询,其余几种查询全都支持模糊査询。示例列举了match 、multi_match及query_string查询中使用模糊查询的方法

在示例中,每种査询可使用的模糊查询参数都已经列出了,这些参数与fuzzy查询中的参数含义相同,只是有些参数为了防止歧义添加了fuzzy_前缀。在这些模糊査询中,以查询字符串形式定义的模糊査询与其他几种模糊査询有些不同(即query_string查询和URI参数)。查询字符串中使用模糊査询时,必须在词项后面添加“~”符号,在“~”后面还可以再附加数字,代表编辑长度。例如在如上示例的最后一个请求中,"message:firefit~2"代表的含义就是使用firefith词且编辑长度为2匹配message字段。

5.3.3 纠错与提示

纠错是在用户提交了错误的词项时给出正确词项的提示,而提示则是在用户输入关键字时给出智能提示,甚至可以将用户未输入完的内容自动补全。大多数互联网搜索引擎都同时支持纠错和提示的功能,比如在用户提交了错误的搜索关键字时会提示:“你是不是想查找...”,而在用户输入搜索关键字时还能自动弹出提示框将用户可能要输入的容全都列出来供用户选择。

Elasticsearch也同时支持纠错与提示功能,由于这两个功能从实现的角度来说并没有本质区别,所以它们都由一种被称为提示器或建议器(Suggester)的特殊检索实现。由于输入提示需要在用户输入的同时给出提示词,所以这种功能要求速度必须快,否则就失去了提示的意义。在实现上,输入提示是由单独的提示器完成。而在使用上提示器则是通过检索接口_search的一个参数设置,例如:

如上示例中,_search接口的suggest参数中定义了一个提示msg-suggest,并通过text参数给出需要提示的内容。另一个参数term实际上是一种提示器的名称,它会分析text参数中的字符串并提取词项,再根据Levenshtein算法到满足编辑距离的提示词项。所以在返回结果中会包含一个suggest字段,其中列举了依照term提示器找到的提示词项

 

  

Elasticsearchー共提供了三种提示器,它们在本质上都是基于编辑距离算法。下面就来看看这些提示器如何使用。

1. term提示器

在上述示例中使用的提示器就是term提示器,这种提示器默认使用的算法是称为internal的编辑距离算法。internal算法本质上就是Levenshtein算法,但根据Elasticsearch索引特征做了一些优化而效率更高,可以通过string_distance参数更改算法。

term提示器使用的编辑距离可通过max_edits参数设置,默认值为2。提示词的数量由size参数控制,默认会在索引的每个分片上获取相同数量的提示词,然后再将它们整合起来返回给用户。这类似于terms聚集从分片获取词项,所以也不能保证提示词的完全精确,可通过shard_size参数加大从分片获取提示词的数量以提高精度。有关terms聚集的内容,参考7.3.1节。

2. phrase提示器

terms会将需要提示的文本拆分成词项,然后对每一个词项做单独的提示,而phrase提示器则会使用整个文本内容做提示。所以在phrase提示器的返回结果中,不会看到类似上述示例中一个词项一个词项的提示,而是针对整个短语的提示。但从使用的角度来看它们几乎是一样的,例如

但不要被phrase提示器返回结果骗,这个提示器在执行时也会对需要提示的文本内容做词项分析,然后再通过NGram算法计算整个短语的编辑距离。所以本质上来说,phrase提示是基于term提示器的提示器,同时使用了Levenshtein和NGram算法。在phrase提示器中设计了个direct_generator参数,这个参数用于指定单个词提示词应该如何生成。事实上direct_generator是一种候选生成器 (Candidate Generator)的名称,只是目前phrase提示器只支持direct_generator一种候选生成器。在候选生成器中可用的参数与term提示器基本都是一样的,它定义了phrase提示器在NGram算法中使用的单个提示词如何生成。

上述示例中还使用highlight参数定义了高亮,所示提示词在返回结果中都会使用em标签标识为高亮。

3. completion提示器

completion提示器一般应用于输入提示和自动补全,也就是在用户输入的同时给出提示或补全未输入內容。这就要求completion提示器必须在用户输入结束前快速地给出提示,所以这个提示器在性能上做了优化以达到快速检索的目的

首先要求提示词产生的字段为completion类型,这是一种专门为completion提示器而设计的字段类型,它会在内存中创建特殊的数据结构以满足快速生成提示词的要求。例如在示例中创建了articles索引并向其中添加了一份文档:

在向completion类型的字段添加内容时可以使用两个参数,input参数设置字段实际保存的提示词;而weight参数则设置了这些提示词的权重,权重越高它在返回的提示词中越靠前。在上述示例中给出了两种设置提示词权重的方式,第一种是将一组提示词的权重设置为统一值,另一种则是分开设置它们的权重值。需要注意的是,completion类型字段保存的提示词是不会分析词项的,上述示例中的"elastic stack"并不会拆分成两个提示词,而是以整体出现在提示词列表中。

completion提示器专门用于输入提示或补全,它根据用户已经输入的内容提示完整词项,所以在completion提示器中没有text参数而是使用prefix参数。例如:

总结一下,term和phrase提示器主要用于纠错,term提示器用于对单个词项的纠错而phrase提示器则主要针对短语做纠错。completion提示器是专门用于输入提示和自动补全的提示器,在使用上依赖前缀产生提示并且速度更快。

5.4 本章小结

本章介绍了Elasticsearch查询语言DSL中的叶子查询,叶子查询大体上可以分为基于词项的査询和基于全文的查询,两者的主要区就在于是否会对查询条件中的文本做分析。基于词项的查询仅在keyword类型字段定义了规整器的情况下,才会对查询条件做规范化处理,而对于其他任何类型的字段都不会做处理。所以,基于词项的查询适合对存储了结构化数据的字段做检索,而基于词项的查询则适合对text类型的字段做检索。

本章还介绍了在全文检索中非常重要的编辑距离算法,这类算法在elasticsearch中主要有Levenshtein和NGran两种实现算法。当然它们还衍生出其他一些算法,但核心思想都是基于这两种算法。编辑距离在应用上主要体现在模糊查询和纠错提示等提升用户检索便利性上,基于词项的查询主要通过fuzzy查询支持模糊査询,而基于全文的查询天然地就支持模糊查询。

本章没有介绍全文检索的相关性问题,这将在下一章与组合查询起介绍。

第六章 相关性评分与组合查询

在全文检索中,检索结果与査询条件的相关性是一个极为重要的问题,优秀的全文检索引擎应该将那些与询条件相关性高的文档在最前面。想象一下,如果满足询条件的文档成千上万,让用户在这些文档中再找出自己最满意的那一条,这无异于再做一次人工检索。用户一般很少会有耐心在检索结果中翻到第 3 页,所以处理好检索结果的相关性对于一个检索引擎来说至关重要。Google 公司就是因为发明了 Page Rank 算法,巧妙地解决了网页检索结果的相关性问题,才在众多搜索公司中迅速崛起。

相关性问题有两方面问题要解决:

一是如何评价单个查询条件的相关性

二是如何将多个查询条件的相关性组合起来

而相关性组问题主要出现在组合询中,所以本章在介绍相关性评分的同时也会介绍组合查询。

6.1 相关性评分

全文检索与数据库査询的一个显著区别,就是它并不一定会根据查询条件做完全精确的匹配。除了上一章介绍的模糊查询以外,全文检索还会根据查询条件给文档的相关性打分并排序,将那些与查询条件相关性高的文档排在最前面。相关性(Relevance)或相似性(Similarity)是指两个事物间相互关联的程度,在检索领域特指检索请求与检索结果之间的相关程度。在Elasticsearch 返回的每一条结果中都会包含一个_score字段,这个字段的值就是当前文档匹配检索请求的相关性评分。本书称_score字段记录的相关性分值为相关度,即相关性的程度。

解决相关性问题的核心是计算相关度的算法和模型,相关度算法和模型是全文检索引擎最重要的技术之。相关度算法和相关度模型并非完全相同的概念,相关度模型可以认为是具有相同理论基础的算法集合。所以在实际应用时都是指定到具体的相关度算法,而相关度模型则是从理论层面对相关度算法的归类

6.1.1 相关度模型

6.1.2 TF/IDF

TF/IDF实际上两个影响相关度的因素,即TF和IDF。其中,TF是 Term Frequency的缩写,即词项频率或简称词,指一个词项在当前文档中出现的次数;而IDF则是Invert Document Frequency缩写,即逆向文档频率,指词项在所有文档中出现的次数。Elasticsearch 提供的几种算法中都或多或少有 TF/IDFE的思想。

TF/ADF算法的核心思想是TF越高则相关度越高,而DF越高相关度越低。TF对相关度的影响比较容易理解,但IDF为什么会在词项出现次数多的时候反而相关度低呢?举例来说,如果使用"elasticsearch 全文检索"两个词项做检索,文档中"elasticsearch"出现次数高的文档比"全文检索"出现次数高的文档相关度要高。这是因为"elasticsearch"是专业性比较强的词汇,它在其他文档中出现的次数会比较少,也就是IDF低;而"全文检索"虽然也是专业性词汇,但它覆盖的面要比"elasticsearch"更广泛,所以它在其他文档中出现的次数会比较高,也就是 IDF 高。换句话说,介绍Elasticsearch的文章大概率会提到全文检索,但介绍全文检索的文章则不一定会提到Elasticsearch。比如一篇介绍MOongoDB的文章大概率会提到全文检索,但显然这样的文章与"elasticsearch 全文检索"的

相关度很低。

6.1.3 BM25

6.1.4 相关度解释

相关度算法可通过 text 或 keyword 类型字段的 Similarity 参数修改,也就是说相关度算法不针对整个文档而是针对单个字段,它的默认值是 BM25。

6.1.5 相关度权重

在一些情况下需要将某些字段的相关度权重提升,以增加这些字段对检索结果相关性评分的影响。比如,同时使用对文章标题title字段和文章内容content字段做检索,title字段在相关性评分中的权重应该比content字段高一些,这时就可以将title字段的相关度评分权重提高。所以相关度权重提升一般都是在多个查询条件时设置,这种类型的查询在下一节中将会有详细介绍。提升相关度权重有多种办法,下面分别来看一下。

1. boost参数

boost参数可以在创建索引时直接设置给字段,也可以在执行检索时动态更改。如果不做更改,boost参数的默认值为1。但并非所有类型的字段都可以设置boost,能够设置boost参数的字段类型在第2章中有过介绍,具体请参考表 2-2。在创建索引时设置boost参数并不是一个好的方法,因为这个参数在索引创建以后就不能再更改而降低了灵活性,所以在 Elasticsearchl 版本 5 中就已经被废止

所以更好的方式是检索时提升查询条件的相关度权重,几乎前面介绍的所有DSL查询都支持通过boost参数设置查询条件的相关度权重。在第 5.1 节示例 5-1 中就在term查询中设置了boost参数,在其他查询中也是类似,例如

 

在查询字符串中也可以设置 boost,使用的操作符为"^"。这是个二元操作符,第一个操作数为询条件,第二个操作数为boost数值。例如: 

"Origincountry:CN^2 OR US"使用"^"操作符将第一个查询条件的权重提升到 2。此外在请求体中还通过boost参数,将整个查询条件的权重又提升到了4但如果使用基于URI的请求方式,就不能再设置boost参数了。

2. indices boost参

除了在多查询条件时可以通过boost参数调整每个查询条件在相关度计算中的权重,还可以使用indices_boost参数调整多索引查询条件时每个索引的权重。_search接口可以针对多个索引做文档检索,但在有些情况下需要调整某一索引在查询结果中的相关度。比如,如果在商品和订单两个索引中检索某一商品的价格,但如果更关心商品的实际销售价格就可以将订单索引的相关度权重提升。在使用上,indices_boost参数可以使用对象和数组两种格式,但对象格式在版本 5.2.0 中已经被废止

正如本章开头讲的那样,相关性问题不仅要解决单个查询条件的相关度计算,还要考虑如何将多个查询条件产生的相关度组合起来。而相关度组合问题主要出现在组合查询中,接下来的一节中就将介绍组合查询及相关度组合问题。 

6.2 组合查询与相关度组合

组合查询是DSL中与叶子查询相对应的另一种查询类型,组合查询可以将通过某种逻辑将叶子查询组合起来,实现对多个字段与多个查询条件的任意组合。组合询组合的子查询不仅可以是基于词项或基于全文的叶子查询,也可以是另一个组合查询。

单纯从组合查询的使用上来看,组合查询并不复杂,复杂的是组合多个子查询相关度的逻辑,这也是它们的核心区别之一。

除了组合查询存在相关度组合问题以外,叶子查询中的query_string和multi_match查询由于在执行多字段检索时会转换为组合查询,所以也存在相关度组合问题,本小节也会一并介绍。

6.2.1 bool组合查询

bool组合查询将一组布尔类型子句组合起来,形成大的布尔条件。通过SQL语言查询数据时,如果条数据不满足where子句的查询条件,这条记录将不会作为结果返回。但Elasticsearch的bool组合查询则不同,在它的子句中,一些子句的确会决定文档是否会作为结果返回,而另一些子句则不决定文档是否可以作为结果,但会影响到结果的相关度。

bool组合查询可用的布尔类型子句包括must、filter、should和must_not四种,它们接收参数值的类型为数组,而数组中的元素即是以JSON对象表示的叶子査询。这4种子句的具体含义见表。

可见,filter和must_not只用于过滤文档,而它们对文档相关度没有任何影响。换句话说,这两种子句对查询结果的排序没有作用。在这四种子句中,should子句的情况有些复杂。首先它的执行结果影响相关度,但在过滤结果上则取决于上下文。当should子句与 must子句或filter子句同时出现在子句中时,should子句将不会过滤结果。也就是说,在这种情况下,即使should子句不满足,结果也会返回。例如 

只有message字段包含firefox词项的日志文档会被返回,而geo的src字段和dest字段是否为CN只影响相关度。但是如果在查询条件中将must子句删除,那么should子句就至少要满足有条。should子句需要满足的个数由query的minimum_should_match参数决定,默认情况下它的值为1。这个参数在第5.2.1节中有过详细介绍。

布尔查询在计算相关性得分时,采取了匹配越多分值越高的略。由于filter和must_not不参与分值运算,所以它会将must和should子句的相关性分值相加后返回给用户。

6.2.2 dis_max组合查询

dis_max 查询(Disjunction Max Query)也是一种组合查询,只是它在计算相关性度时与bool查询不同。dis_max查询在计算相关性分值时,会在子查询中取最大相关性分值为最终相关性分值结果,而忽略其他子查询的相关性得分。dis_max 查询通过queries参数接收对象的数组,数组元素可以是前面讲解的叶子查询。例如

在多数情况下,完全不考虑其他字段的相关度可能并不合适,所以可以使用tie_breaker参数设置其他字段参与相关度运算的系数。这个系数会在运算最终相关度时乘以其他字段的相关度,再加上最大得分就得到最终的相关度了。所以一般来说,tie_breaker应该小于 1, 默认值为0。例如在示例 6-6 的返回结果中,即使文档message和geo字段都满足查询条件它也不一定会排在最前面。可按示例添加tie_breaker参数并设置为0.7

在添加了tie_breaker参数后,相关度非最高值字段在参与最终相关度结果时的权重就降低为0.7。但它们对结果排序会产生影响,完全满足条件的文档将排在结果最前面。

6.2.3 constant_score查询

constant_score 查询返回结果中文档的相关度为固定值,这个固定值由boost参数设置,默认值为1.0。constant_score 查询只有两个参数filter和boost,前者与bool 组合查询中的fiter完全相同,仅用于过结果而不影响分值。

由于示例 6-8 中通过boost参数设置了相关度,所以满足查询条件文档的_score值将都是1.3。match_all 查询也可以当成是一种特殊类型的constant_score查询,它会返回索引中所有文档,而每个文档的相关度都是1.0。

6.2.4 boosting查询

boosting查询通过positive子句设置满足条件的文档,这类似于bool查询中的must子句,只有满足positive条件的文档才会被返回。 boosting查询通过negative子句设置需要排除文档的条件,这类似于bool查询中的must_not子句。但与bool查询不同的是,boosting查询不会将满足negative条件的文档从返回结果中排除,而只是会拉低它们的相关性分值。

参数negative_boost设置了一个系数,当满足negative条件时相关度会乘以这个系数作为最终分值,所以这个值应该小于1而大于等于0。例如示例中的请求,如果geo.src为CN的文 档相关度为1.6, 那么geo.dest也是CN的文档相关度就需要再乘以0.2,所以最终相关度为0.32。

6.2.5 function_score查询

function_score查询提供了一组计算查询结果相关度的不同函数,通过为查询条件定义不同打分函数实现文档检索相关性的自定义打分机制。查询条件通过function_score的query参数设置,而使用的打分函数则使用functions参数设置。例如:

function_score查询在运算相关度时,首先会通过functions指定的打分函数算出每份文档的得分。如果指定了多个打分函数,它们打分的结果会根据score_mode参数定义的模式组合起来。以示例 6-10 为例,functions参数定义了两个打分函数random_score和 weight, random_scorel函数会在0-1之间产生一个随机数,而weight函数则会以指定的值为相关性分值。由于score_mode参数设置的值为max,即从所有评分函数运算结果中取最大值,而weight值为2, 它将永远大于random_score产生的值,所以评分函数最终给出的分值也将永远是2。score_mode 包括以下几个选项 multiply、sum、avg、frst、max、通过名称很容易判断它们的含义,分别是在所有评分函数的运算结果中取它们的乘积、和、平均值、首个值、最大值和最小值。

打分函数运算的相关性评分会与query参数中査询条件的相关度组合起来,组合的方式通过boost_mode参数指定,它的默认值与score_mode一样都是 multiply。boost_mode参数的可选值与score_mode也基本一致,但没有first而多了一个replace,代表使用评分函数计算结果代替查询分值。

可见function_score是一种在运算相关度上非常灵活的组合查询,这种灵活性主要体现在它提供了一组打分函数,以及组合这些打分函数的灵活方式。打分函数包括script_score、weight、random_score、field_value_factor 以及一组衰减函数,如果只需要个打分函数运算则可以直接使用打分函数名称做设置,而不用使用functions参数。在这些函数中,weight和random_score已经使用过,下面再来简单介绍一下其他打分函数。

1. script_score函数

script_score函数通过_script参数接收一段脚本运算相关度,脚本执行结果必须是非负的浮点数,返回负数会异常。例如:

上述示例中,由于只使用一个打分函数所以可以不使用functions参数,script_score函数通过script参数接收脚本。在示例的脚本中,_score代表查询自身按默认相关度算法计算出来的相关度,而doc[Avgticketprice]. Value 则代表取当前文档的 AvgTicketPrice-字段值。所以这段脚本实际上是将相关度按票价由低到高的次序做了权重的提升,票价越低最终的相关度越高。这相当是找出所有从中国到美国的航班,并按票价由低到高的次序排序。 script_scorel中使用的脚本认也是Painless,可使用的变量见下表

在这些变量中,params可通过params参数设置。以示例中的请求为例,如果将平均票价的基准系数1000设置为变量,可按如下方式设置

2. field_value_factor函数

field_value_factor函数在计算相关度时允许加入某一字段作为干因子,这类似于在上述示例中通过 AvgTicketPrice字段值提升或降低相关度的最终结果。只是通过field_value_factor函数时并不需要写脚本,而仅需要设置几个参数。例如下面示例中的需求按field_value_factorl函数来计算的话可以按如下方式请求:

field_value_factor打分函数通过field参数设置了干扰字段为AvgTicketPrice,而factor则是为干扰字段设置的调整因子它会与字段值相乘后再参与接下来的运算。modifier参数就有些复杂了,它代表了干扰字段与调整因子相乘的结果如何参与相关度运算在示例中给给出的是reciprocal,代表取倒数 1/x。所以如果使用Painless 脚本表式示例6-13的运算,则应该写成“1/(doc Avg Ticketprice] value*.001)”。这与示例6-11中的“score/ doc Avg Ticketprice] value/1000)”略有区别。所以两个示例运算出来的相关度并不相同,但排序不会有変化。读者可以将示例6-11中_score改成1, 则两者运算的相关性得分也会完全相同。

modifier可用运算方法除了reciproca以外还有很多,具体见下表

3 衰减函数

衰函数是一组通过递减方式计算相关度的函数,它们会从指定的原始点开始对相关度做衰减,离原始点距离越远相关度就越低。衰函数中的原始点是指某一字段的具体值,由于要计算其他文档与该字段值的距离,所以要求衰减函数原始点的字段类型必须是数值、日期或地理坐标中的一种。举例来说,如果在2019年3月25日前后系统运行出现异常,所以对这个日期前后的日志比较感兴趣,就可以按如下形式发送请求:

在示例中使用的衰减函数为高斯函数(gauss),定义原始点使用的字段为 timestamp,而具体的原始点则通过origin参数定义在了2019年3月25日。offset参数定义了在1天的范围内相关度不衰,也就是说2019年3月24~26日相关度不衰减。scale参数和decay参数

则共同决定了衰减的程度,前者定义了衰减的跨度范围,而后者则定义了衰减多少。以示例中的设置为例,代表的含义是7天后的文档相关度衰減至 0.3 倍。

衰减函数除了高斯函数gauss以外,还有线性函数linear和指数函数exp两种。它们在使用上与高斯函数完全相同,读者可以自行将示例中的gauss替换成lineare或exp,并看看它们在相关度运算结果上有什么不同。如果将这几种衰减函数以图形画出来就会发现,它们在衰减的平滑度上有着比较明显的区别,如图所示。

6.2.6 相关度组合

组合查询一般由多个查询条件组成,所以在计算相关度时都要考虑以何种方式组合相关度。而多数的叶子査询都只针对一个字段设置查询条件,所以只有相关度权重提升问题而没有相关度组合问题。但叶子查询中有两个特例,它们是query_string查询和multi_match查询。由于这两个查询都可以针对多个字段设置查询条件,所以它们在计算相关度时也需要考虑组合多个相关度的问题,并且它们在组合相关度时有着相似的逻辑。

query_string查询和multi_match查询都具有一个type参数,用于指定针对多字段检索时的执行逻辑及相关度组合方法。type参数有 5 个可选值,即best_fields、most_fields、cross_fields、phrase和phrase_prefix。例如:

1. best_fields、phrase与phrase_prefix类型

best_fields类型在执行时会将与字段匹配的文档都检索出来,但在计算相关度时会取得分最高的作为整个查询的相关度。例如在上述示例中,第一个查询通过"Origin Country^2"的形式将 OriginCountry字段的相关度权重提升到2, 所以这个字段相关度会高于 Destcountry字段。在best_fields类型下执行检索时,Destcountry字段对最终相关度就不会再有影响。通过查看返回结果也可以看到,Origincountry字段为CN的文档相关度都相同,即使Destcountry字段也是CN,文档的相关度也不会提升。

best_fields适用于用户希望匹配条件全部出现在一个字段中的情况,比如在文章标题和文章内容中同时检索elasticsearch和logstash时,如果在文章标题或是文章内容中同时出现了两个词项,该文章在相关度就会高于其他文章。

不知道读者是否还记得第 6.2.2 节介绍的dis_max查询,在相关度计算上dis_max查询是不是与best_fields类型很像?事实上,best_fields类型的查询在执行时会转化为dis_max查询,例如上述示例在执行时会转化为

dis_max有一个参数tie_breaker,可以设置非最高值相关度参与最终相关度运算的系数,在multi_match中使用best_fields类型时也可以使用这个参数。

phrase与phrase_prefix类型在执行逻辑上与best_fields完全相同,只是在转换为dis_max时 queries查询中的子查询会使用phrase或phrase_prefix而不是match

2. most_fields类型

most_fields类型在计算相关度时会将所有相关度累加起来,然后再除以相关度的个数以得到它们的平均值作为最终的相关度。还是以示例第一个检索为例,如果将type替换为most_fields,它会将 OriginCountry和Destcountry两个字段匹配CN时计算出的相关度累加,然后再用累加和除以2作为最终的相关度。所以只有当两个字段都匹配了 CN,最终的相关度才会更高。这在效果上相当于将出发地和目的地都是中国的文档排在了最前面,所以适用于希望检索出多个字段中同时都包含相同词项的检索。

在实现上,most_fields类型的查询会被转化为bool查询的should子句,示例6-15中的第一个检索在most_fields类型时会被转化为

3. cross_fields类型

如果询条件中设置了多个词项,best_fields类型和most_fields类型都支持通过operator参数设置词项之间的逻辑关系,即 and 和 or。但它们在设置operatore时是针对字段级别的而不是针对词项级别的来看一个例子

示例设置的查询条件为firefox和success两个词项,而匹配字段也是两个message和tags。当operator设置为and时,在best_fields类型下这意味着两个字段中需要至少有一个同时包含firefox和success两个词项,而这样的日志文档并不存在。而在cross_fields类型下则会将两个词项拆分出来,然后再一个字段分配一个词项。所以在效果上它并不要求字段同时包含两个词项,而要求词项分散在两个字段中。读者可以将示例中的best_fields替换为cross_fields来体验它们的别。

以上介绍的三大类型虽然都是以multi_match查询为例,但它们在使用query_string查询时也是有效的,本书在这里就不再展开举例

6.3 本章小结

本章核心内容虽然只有两节,但却介绍了全文检索中最为重要的相关性问题,而这个问题其实并不容易理解和掌握。

本章首先介绍了几种常见的相关度模型和相关度算法,其中的概率模型和BM25算法在Elasticsearch中较为常用,建议读者至少要理解其核心思想。而有关TF/DF的思想更是Elasticsearch中与相关性关系密切的核心概念,在许多文献中都会提及这方面的原理,所以建议读者要认真理解其精髓。

本章还介绍了组合查询,重点介绍了组合查询中的相关度组合问题。同时还介绍了query_string和multi_match在组合多个相关度时的算法。 

第七章 聚集查询

聚集查询(Aggregation)提供了针对多条文档的统计运算功能,它不是针对文档本身内容的检索,而是要将它们聚合到一起运算某些方面的特征值。

聚集查询与SQL语言中的聚集函数非常像,聚集函数在Elasticsearch中相当于是聚集查询的一种聚集类型。比如在SQL中的avg函数用于求字段平均值,而在Elasticsearch中要实现相同的功能可以使用avg聚集类型。

聚集查询也是通过_search接口执行,只是在执行聚集查询时使用的参数是aggregations或aggs。所以_search接口可以执行两种类型的查询

  • 通过query参数执行DSL
  • 通过aggregations执行聚集查询

两种查询方式可放在一起使用,执行逻辑是先通过DSL检索满足查询条件的文档,然后再使用聚集查询对DSL检索结果做聚集运算,这一规则适用于所有聚集查询。

聚集查询有着比较规整的请求结构,具体格式如下

aggregations和aggs都是_search的参数,其中aggs是aggregations的简写。

每一个聚集查询都需要定义一个聚集名称,并归属于一种聚集类型。聚集名称是用户自定义的,而聚集类型则是由Elasticsearch预先定义好。聚集名称会在返回结果中标识聚集结果,而聚集类型则决定了聚集将如何运算。比如前面提到的avg就是一种聚集类型。

特别强调:聚集中可以再包含子聚集,如上图中第7行。子聚集位于父聚集的名称中,与聚集类型同级,所以子聚集的运算都是在父聚集的环境中运算。Elasticsearch对子聚集的深度没有做限制,所以理论上说可以包含无限深度的子聚集。

聚集类型总体上被分为四种大类型

  • 指标聚集(MetricsAggregation):根据文档字段所包含的值运算某些统计特征值,如平均值、总和等,如前面提到的avg聚集就是指标聚集
  • 桶型聚集(Bucket Aggregation):根据一定的分组标准将文档归到不同的组中,这些分组在Elasticsearch中被称为桶(Bucket)。桶型聚集与SQL中group by类似,一般会与指标聚集嵌套使用
  • 管道聚集(Pipeline Aggregation):是聚集结果的聚集,它一般以另一个聚集结果作为输入,然后在此基础上再做聚集
  • 矩阵聚集(Matrix Aggregation):Elasticsearch中的新功能,由于是针对多字段做多种运算,所以形成的结果类似于矩阵,因而得名 

7.1 指标聚集

指标聚集是根据文档中某一字段做聚集运算,如计算所有产品销量总和、平均值等。

指标聚集的结果可能是单个值,这种称为单值指标聚集;也可能是多个值,称为多值指标聚集

7.1.1 平均值聚集

平均值聚集是根据文档中数值类型字段计算平均值的聚集查询,包括avg聚集和weighted_avg聚集两种类型。

  • avg聚集:直接取字段值后计算平均值
  • weighted_avg聚集:在计算平均值时添加不同的权重

1. avg聚集

avg聚集计算平均值有两种方式:

  • 直接使用字段值参与平均值运算
  • 使用脚本运算的结果参与平均值运算

如下示例中计算航班的平均延误时间:

如上示例使用了请求参数filter_path将返回结果的其他字段过滤掉了,否则在査询结果中将包含kiana_sample_data_flights索引中所有的文档。加了filter_path之后返回的结果为

在返回结果中,aggregations是关键字,代表这是聚集查询的结果。其中的delay_avg则是在聚集查询中定义的聚集名称,value是聚集运算的结果。在示例中运算航班延误时间时会将所有文档都包含进来做计算,如果只想其中一部分文档参与运算则可以使用query参数以DSL的形式定义査询条件。如下示例就是只计算了飞往中国的航班平均延误时间

在示例中请求_search接口,同时使用了query与aggs参数。在执行检索时会先通过query条件过滤文档,然后再在符合条件的文档中运算平均值聚集。

avg聚集也可以使用Painless脚本指定参与平均值运算的值,可使用script参数设置脚本。

如下示例中的请求是用延误时间除以飞行时间的值参与平均值运算,相当于平均每小时延误时间

 

示例中使用了missing参数,用于设置文档缺失值时的默认值。由于FlightTimeMin的值有些是0, 因此做了判断。

2. weighted_avg聚集

weighted_avg聚集在运算平均值时,会给参与平均值运算的值一个权重,权重越高对最终结果的影响越大,而权重越低影响越小。最终平均值遵照下面的公式计算:

avg聚集可以认为是权重值都为1的加权平均值运算,权重值可以从文档的某一字段中获取,也可以通过脚本运算。

例如根据航班飞行时调整延误时间的权重值,飞行时间越短权重越高

weighted_avg使用参数value设置参与平均值运算的值,而weight参数则用于设置这个值的权重。在示例中,参与平均值运算的是延误时间即FlightDelayMin字段,权重值则由飞行时间即FlightTimeMin字段决定,飞行时间大于2h的权重为1, 飞行时间小于2h的为2 

7.1.2 计数聚集与极值聚集

计数聚集用于统计字段值的数量,而极值聚集则是查找字段的极大值和极小值,它们都支持使用脚本。

1. 计数聚集

包括两种:

  • value_count聚集:统计从字段中取值的总数
  • cardinality聚集:统计不重复数值的总数

例如:

解释:

cardinality统计了Destcountry字段非重复值的数量,类似于SQL 中的distinct

value_count统计了Destcountry字段所有返回值的数量,类似于SQL中的count

说明:

cardinality聚集采用Hyperloglog算法实现,这个算法使用极小内存实现统计结果的基本准确。所以cardinality在数据量极大的情况下是不能保证完全准确的。

2. 极值聚集

极值聚集是在文档中提取某一字段最大值或最小值的聚集,包括max聚集和min聚集。例如:

示例中的max_price和min_price分别计算了机票价格的最大值和最小值。 

7.1.3 统计聚集

统计聚集是一个多值指标聚集,也就是返回结果中会包含多个值,都是一些与统计相关的数据。统计聚集包含

  • stats聚集:返回一些比较基本的统计数据值
  • extended_stats聚集:返回一些比较专业的统计数据值

1. stats聚集

stats聚集返回结果中包括字段的五项内容:

最小值(min)

最大值(max)

总和(sum)

数量(count)

平均值(avg)

例如对机票价格做统计

stats聚集使用field参数指定参与统计运算的字段为AvgTicketPrice,也可以通过script参数设置脚本计算参与统计的值

2. extended_stats聚集

extended_stats聚集增加了几项统计数据,包括平方和、方差、标准方差和标准方差偏移量。

从使用的角度来看,extended_stats聚集与stats聚集完全相同,只是聚集类型名称不同。

如果将上述示例聚集类型stats替换为extended_stats,则返回的结果为

新增的字段中:

sum_of_squares代表平方和

varlance代表方差

std_deviation代表标准方差

std_deviation_bounds.upper代表标准方差偏移量的上限

std_deviation_bounds.lower代表标准方差偏移量的下限。

7.1.4 百分位聚集

百分位聚集根据文档字段值,统计字段值按百分比的分布情况,包括

percentiles聚集:统计的是百分比与值的对应关系

percentile_ranks聚集:正好相反统计值与百分比的对应关系

百分位聚集可以针对字段,也可以使用脚本取值。

percentiles聚集通过percents参数设置一组百分比,然后按值由小到大的顺序划分不同区间,每个区间对应一个百分比。

percentile_ranks聚集则通过values参数设置一组值,然后根据这些值分别计算落在不同值区间的百分比。

以上示例返回的结果为

在percentiles返回结果price_percentile中,"25.0":410.0127977258341代表的含义是25%的机票价格都小于410.012797258341, 其他以此类推。

在percentile_ranks返回结果price_percentile_rank中,"600.0":45.39892372745635代表的含义是600.0以下的机票占总机票价格的百分比为45.39892372745635%。

7.2 使用范围分桶

桶型聚集与SQL语句中的group by相似。

桶型聚集(Bucket Aggregation)是Elasticsearch官方对这种聚集的叫法,它起的作用是根据条件对文档进行分组。可以将桶理解为分组的容器,每个桶都与一个分组标准相关联,满足这个分组标准的文档会落入桶中。在默认情况下,桶型聚集会根据分组标准返回所有分组,同时还会通过doc_count字段返回每一桶中的文档数量。

单纯使用桶型聚集只返回桶内文档数量,意义并不大,所以多数情下都是将桶型聚集指标聚集父子关系的形式组合在一起使用。桶型聚集作为父聚集起到分组的作用,而指标聚集则以子聚集的形式出现在桶型聚集中,起到分组统计的作用。比如将用户按性别分组,然后统计他们的平均年岭。

按返回桶的数量来看,桶型聚集可以分为单桶聚集和多桶聚集。

在多桶聚集中,有些桶的数量是固定的,而有些桶的数量则是在运算时动态决定。由于桶型聚集基本都是将所有桶一次返回,而如果在一个聚集中返回了过多的桶会影响性能,所以单个请求允许返回的最大桶数受search.max_bucket参数限制。这个参数在7.0之前的版本中默认值为-1, 代表没有上限。但在Elasticsearch版本7中,这个参数的默认值已经更改为10000。所以在做桶型聚集时要先做好数据验证,防止桶数量过多影响性能。

桶型聚集的种类非常多,为了便于记忆,将分按照它们的类型分几个小节讲解。本小节主要介绍使用范围分桶的聚集,包括根据数据定义范围根据间隔定范围两种,过滤器从某种意义上来说也是一种范围,所以也在本小节讲解。

7.2.1 数值范围

range、date_range与ip_range这三种类型的聚集都用于根据字段的值范围内对文档分桶,字段值在同一范围内的文档归入同一桶中。每个值范围都可通过from和to参数指定,范围包含from值但不包含to值,即[from, to)。在设置范围时,可以设置一个也可以设置多个,范围之间并非定要连续,可以有间隔也可以有重叠。

1. range聚集

range聚集使用ranges参数设置多个数值范围,使用field参数指定一个数值类型的字段。range聚集在执行时会将该字段在不同范围内的文档数量统计出来,并在返回结果的doc_count字段中展示出来。

例如统计航班不同范围内的票价数量:

返回结果中,每个范围都会包含一个key字段,代表了这个范围的标识,它的基本格式是"<from>-<to>"。如果觉得返回的这种key格式不容易理解,可以通过在range聚集的请求中添加keyed和key参数定制返回结果的key字段值。其中

keyed是ranges的参数,用于标识是否使用key标识范围,为布尔类型

key参数则是与from、to同级的参数,用于定义返回结果中的key字段值

2. date_range聚集

date_range聚集与range聚集类似,只是范围和字段的类型为日期而非数值。date_range聚集的范围指定也是通过ranges参数设置,具体的范围也是使用from和to两个子参数,并且可以使用keyed和key定义返回结果的标识。date_range聚集与range聚集不同的是多了个指定日期格式的参数format,可以用于指定from和to的日期格式。例如

3. ip_range聚集

ip_range聚集根据ip类型的字段统计落在指定IP范围的文档数量,使用的聚集类型名称为ip_range。

例如统计了两个IP地址范围的文档数量

7.2.2 间隔范围

histogram、date_histogram与auto_date_histogram这三种聚集与上节中使用数值定义范围的聚集很像,也是统计落在某一范围内的文档数量。但与数值范围聚集不同的是,这三类聚集统计范围由固定的间隔定义,也就是范围的结束值和起始值的差值是固定的

1. histogram聚集

histogram聚集以数值为间隔定义数值范围,字段值具有相同范围的文档将落入同一桶中。

例如以100为间隔做分桶,可以通过返回结果的doc_count字段获取票价在每个区间的文档数量

interval参数用于指定数值间隔必须为正值,而offset参数则代表起始数值的偏移量,必须位于[0, interval)范围内。order参数用于指定排序字段和顺序,可选字段为_key和_count。当keyed参数设置为true时,返回结果中每个桶会有一个标识,标识的计算公式 bucket_key=Math.floor ((value-offset)/inerval)*interval+offset

2. date_histogram聚集

date_histogram聚集以时间为间隔定义日期范围,字段值具有相同日期范围的文档将落入同一桶中。同样,返回结果中也会包含每间隔范围內的文档数量doc_count。

例如统计每月航班数量:

参数interval指定时间间隔为month,即按月划分范围。时间间隔可以是下表中的9种形式之一

 

3. auto_date_histogram聚集

前述两种聚集都是指定间隔的具体值是多少,然后再根据间隔值逐一返回每一桶满足条件的文档数。最终会有多少桶取决于两个条件,即间隔值和字段值在所有文档中的实际跨度。反过来,如果预先指定需要返回多少个桶,那么间隔值也可以通过桶的数量以及字段值跨度共同确定。auto_date_histogram聚集就是这样一种聚集,它不是指定时间间隔值,而是指定需要返回桶的数量。

例如定义需要返回10个时间段的桶

参数field设置通过哪一个字段做时间分隔,而参数buckets则指明了需要返回多少个桶。默认情况下,buckets的数量为10。需要注意的是,buckets只是设置了期望返回桶的数量,但实际返回桶的数量可能等于也可能小于buckets 设置的值。例如示例的请求中期望返回 10 个桶,但实际可能只返回 6 个桶。

auto_date_histogram聚集没有精确匹配buckets数量的目的是提升结果的可读性,因为精确匹配buckets数量必然导致隔时间粒度更精细,这样返回结果中每一桶的范围可能都会具体到每一分钟甚至每一秒。但如果只是大致满足buckets数量,则会使得间隔时间粒度更大,从而更利于结果的阅读和处理。

auto_date_histogram聚集在返回结果中还提供了一个interval字段,用于说明实际采用的间隔时间,例如示例请求结果实际采用的间隔时间可能是7天。

从实现的角度来说,不精确匹配buckets数量也有利于提升检索的性能。Elasticsearch内置了一组时间间隔,以供匹配buckets数量时选择,见下表。

此外,通过buckets参数设置桶数量不要大于10000, 即  search.max_buckets参数的默认值。

7.2.3 聚集嵌套

前面两个小节介绍的桶型聚集,它们的结果都只是返回满足聚集条件的文档数量。在实际应用中,桶型聚集与SQL中的group by具有相同的意义,用于将文档分桶后计算各桶特定指标值。比如根据用户性别分组,然后分别求他们的平均年龄。

Elasticsearch这样的功能通过嵌套聚集来实现,这也是聚集査询真正强大的地方。

例如,先按月对从中国起飞的航班做了分桶,然后又通过聚集嵌套计算每月平均延误时间:

_search接口共使用了两个参数:

query参数以term查询条件将所有OriginCountry字段是CN的文档筛选出来参与聚集运算。

aggs参数则定义了一个名称为date_price_histogram的桶型聚集,这个聚集内部又嵌套了一个名称为avg_price的聚集。由于avg_price这个聚集位于date_pice_histogram中,所以它会使用这个聚集的分桶结果做运算而不会针对所有文档。最终的效果就是将按月计算从中国出发航班的平均延误时间。

使用嵌套聚集时要注意,嵌套聚集应该位于父聚集名称下而与聚集类型同级,并且需要通过aggs参数再次声明。如果与父聚集一样位于aggs参数下,那么这两个聚集就是平级而非嵌套聚集(如7.3.1中的例子就不是嵌套聚集)。

嵌套聚集在实际应用中是常态,需要认真掌握。

7.3 使用词项分桶

使用字段值范围分桶主要针对结构化数据,比如年龄、IP地址等。但对于字符串类型的字段来说,使用值范围来分桶显然是不合适的。由于字符串类型字段在编入索引时会通过分析器生成词项,所以字符串类型字段的分桶一般通过词项实现。本小节就来介绍这些使用词项实现分桶的聚集,包括terms、significant_terms和significant_text聚集。由于使用词项分桶需要加载所有词项数据,所以它们在执行速度上都会比较慢。为了提升性能,Elasticsearch提供了sampler和diversified_samplers聚集,可通过缩小样本数量减少运算量。

7.3.1 terms聚集

terms聚集根据文档字段中的词项做分桶,所有包含同一词项的文档将被归入同一桶中。聚集结果中包含字段中的词项及其词频,在默认情况下还会根据词频排序,所以terms聚集也可以应用于热词展示。由于terms聚集在统计词项的词频数据时需要访问字段的全部词项数据,所以对于text类型的字段来说在使用terms聚集时需要打开它的fielddata机制。fieldata机制对内存消耗较大且有导致内存溢出的可能,所以terms聚集一般针对keyword类型而非text类型。

备注:默认情况下,如果没有明确定义字符串类型时,添加到索引中的字符串都会以上面的形式设置为多类型(即既支持text也支持keyword)。类似下面这样:

# 根据词项做检索,使用title

# 在排序或聚集的场景下,需要使用整个值做检索,则使用title.raw,如果没有这种显示定义raw,则可以直接使用title.keyword 

PUT articles
{
    "mappings": {
        "properties": {
            "title": {
                "type": "text",   
                "fields": {
                    "raw": {      
                        "type": "keyword"  
                    }
                }
            }
        }
    }
}

在7.1 节中介绍的cardinality聚集可以统计字段中不重复词项的数量,而terms聚集则可以将这些词项全部展示出来。与cardinality聚集一样,terms聚集统计出来的词频也不能保证完全精确。例如:

定义了两个聚集,由于它们都是定义在aggs下,所以不是7.2.3节介绍的嵌套聚集。terms聚集的field参数定义了提取词项的字段为Destcountry,它的词项在返回结果中会按词频由高到低依次展示,词频会在返回结果的doc_count字段。另一个参数size则指定了只返回10个词项,这相当把Destcountry字段中词频前10名检索出来。terms聚集返回结果如下所示,限于篇幅省略了部分词项:

在2.2.1节中曾经介绍过,text类型字段会在编入索引时保存词项的词频,所以统计词项的总词频只需要把这些词项加起来就行了。但由于文档在Elasticsearch中的存储是分片的,所以还需要将每一个分片中的词频都累积起来。由于在每个分片中词项都按词频由高到低排序,所以如果要统计词频前10名的词项,只要将每个分片上前10名的词频分别加到一起就可以了。这在大多数情况下是正确的,而且Elasticsearch也是这么做的。但在一些特殊情况下,这可能是不正确的。例如在下面的例子,假定有四个词项 CN、US、IT 和CA,它们在两个分片中词频排名分别如下所示:

分片1:

CN 11

CA 10

IT 9

US 1

 

分片2:

US 18

CN 15

IT14

CA 1

如果在词项聚集时只取前2名做累加,这样会得到3个词项的累加结果,即CN出现26次、US出现18次而CA为10次。而第三位的IT累计词频为23次,比US还要高,但由于它在两个分片中都排在第三位,所以没有机会出现在返回的结果中。可见,terms聚集并不能保证定是严格按词频排序。但如果terms聚集在计算词频时取到前3名,那么词频累加的结果就跟实际一致了。所以,terms聚集从分片上提取词项的范围越大,返回的结果越接近正确次序,但显然运算量也跟着增加了。在terms聚集中可以使用shard_size参数来控制单个分片上运算的范围,它与size参数值存在着一些关联,比如在设置shard_size时不应该比size参数小,否则将被忽略并取size值为shard_size值。

如果有多个分片则shard_size的默认值为(size*1.5 + 10),而如果只有一个分片则shard_size值与size值相同。这种默认值策略已经在很大程度上避免了结果的不准确,但依然存在着偏差的可能。所以,在返回结果中doc_count_error_upper_bound字段描述了在最坏情下错误的上限,而sum_other_doc_count则描述了未出现在统计结果中的词项还有多少个。最坏情况下错误上限的计算也很简单,就是每个分片上提取的最后一位做累加。例如在上面的例子中,每个分片提取了前两名,则最坏情況下的上限是不可能超过第 2名的累加,也就是25。

在上面的例子中,US出现的频率返回值为18, 但实际上为19。这是因为在第一个分片中US并没有进入前两名,所以丢失了它在第一个分片中统计数量。terms聚集提供了show_term_doc_count_error参数,如果将它设置为true时则每个词项的词频统计错误上限就会显示出来。词项的词频统计错误上限计算也很简单,如果词项在所有分片中的数据都已经统计过了,那么错误上限为0,而如果有一些分片中没有统计上,那么错误上限就是这些分片最后一位词项的词频累加。

terms聚集还提供了_count、_key、_term三个虚拟字段用于排序,其中_count是按照词频排序,而_key和_term 都是按照词项字母排序,_key是在7.0.0之后版本中用于取代_term的。

7.3.2 significant_terms聚集

terms聚集统计在字段中的词项及其词频,聚集结果会按各词项总的词频排序,并将岀现次数最多的词项排在最前面,这非常适合做推荐及热词类的应用。但按词频总数排序并不总是正确的选择,在些检索条件已知的情况下,一些词频总数比较低的词项反而有可能是更合适的推荐热词。举例来说,假设在10000篇技术类文章的内容中提到Elasticsearch的只有200篇,占比为2%;但在文章标题含有NoSQL的1000篇文章中,文章内容提到Elasticsearch的为180篇,占比为18%。这种占比显著的提升,说明在文章标题含有NoSQL的条件下,Elasticsearch变得更为重要。换句话说,如果一个词项在某个文档子集中与在文档全集中相比发生了非常显著的变化,就说明这个词项在这个文档子集中是更为重要的词项。

significant_terms聚集就是针对上述情况的一种聚集查询,它将文档和词项分为前景集(Foreground Set)和背景集(Background Set)。前景集对应一个文档子集,而背景集则对应文档全集。 significant_terms聚集根据query指定前景集,运算field参数指定字段中的词项在前景集和背景集中的词频总数,并在结果的doc_count和bg_count中保存它们。例如:

query参数使用DSL指定了前景集为出发国家为IE(即爱尔兰)的航班,而聚集査询中则使用significant_terms统计到达国家的前景集词频和背景集词频。来看一下返回结果:

在返回结果中,前景集文档数量为119, 背景集文档数量为13059。在buckets返回的所有词项中,国家编码为GB 的航班排在第一位。它在前景集中的词频为12, 占比约为10%(12/19);而在背景集中的词频为449,占比约为3.4%(445/13059)。词项GB在前景集中的占比是背景集中的3倍左右,发生了显著变化,所以在这个前景集中GB可以被视为热词而在第一位。GB 代表的国家是英国,从爱尔兰出发去英国的航班比较多想来也是合情合理的。

除了使用 query参数指定前景集以外,还可以将terms聚集与significant_terms聚集结合起来使用,这样可以一次性列出一个字段的所有前景集的热词。例如:

示例中使用terms聚集将OriginCountry字段的词项全部查询出来做前景集,然后再与significant_terms聚集一起査询它们的热词。 

7.3.3 significant_text聚集

如果参与significant_terms聚集的字段为text类型,那么需要将字段的fielddata机制开启,否则在执行时会返回异常信息。significant_text聚集与significant_terms聚集的作用类似,但不需要开启字段的fieldata机制,所以可以把它当成是一种专门为text类型字段设计的significant_terms聚集。例如在kiana_sample_data_logs中,message字段即为text类型,如果想在这个字段上做词项分析就需要使用significant_text聚集

在示例中,前景集为响应状态码response为200的日志,significant_texts聚集则查看在这个前景集下message字段中出现异常热度的词项。返回结果片段如示例 7-26 所示

通过返回结果可以看出,排在第一位的词项200在前景集和背景集中的数量是一样的,这说明message中整地记录了200状态码;而排在第二位的词项beats前景集和背景集分别为3462和3732, 这说明请求“/ beats”地址的成功率要远高于其他地址;最后filebeat 排在metricbeat之前, 说明请求“/ beats/filebeat”的成功率 高于“/ beats/metricbeat”

significant_text聚集之所以不需要开启fieldata机制是因为它会在检索时对text字段重新做分析,所以significant_text聚集在执行时速度其他聚集要慢很多。如果希望提升执行效率,则可以使用sampler聚集通过减少前景集的样本数量降低运算量。

7.3.4 样本

samper聚集的作用是限定其部嵌套聚集在运算时采用的样本数量,样本数量是在每个分片上的数量而不是整体数量。sampler提取样本时会按文档检索的相似度排序,按相似度分值由高到低的顺序提取。所以从整体效果来看,它就是在每个分片上提取相似度最高组样本参与其套聚集的运算。例如:

示例 中共定义了sample_data和dest_country两个聚集,其中dest_country是sample_data聚集的子聚集或嵌套聚集,因此dest_country在运算时就只从分片上取一部分样本做运算。samplers聚集的shard_size就是定义了每个分片上提取样本的数量,这些样本会根据DSL查询结果的相似度得分由高到低的顺序提取。执行示例的请求会发现,这次目的地最热的目的地国家由GB变成了KR,这就是样本范围缩小导致的数据失真。为了降低样本减少对结果准确性的影响,需要将一些重复的数据从样本中剔除。换句话说就是样本更加分散,加大样本数据的多样性。Elasticsearch提供的diversified_samplers聚集提供了样本多样性的能力,它提供了field或script两个参数用于去除样本中可能重复的数据。由于相同航班的票价可能是相同的,所以可以将票价相同的航班从样本中剔除以加大样本的多样性,例如:

示例中diversified_sampler通过field参数设置了AvgTicketPrice字段,这样在返回结果中GB就又重新回到了第一位。  

7.4 单桶聚集与聚集组合

前面三节介绍的桶型聚集都是多桶型聚集,本节主要介绍单桶聚集。除此之外还会介绍两种比较特殊的多桶型聚集,它们是composites聚集和adjacency_matrix聚集。这两种聚集都是以组合不同条件的形式形成新桶,只是在组合的方法和组件的条件上存在着明显差异

7.4.1 单桶聚集

单桶聚集在返回结果中只会形成一个桶,它们都有比较特定的应用场景。在Elasticsearch中,单桶聚集主要包括 filter、global、missing等几种类型。另外还有一种filters聚集,它虽然属于多桶聚集,但与filter聚集很接近。

1. 过滤器聚集

过滤器聚集通过定义一个或多个过滤器来区分桶,满足过器条件的文档将落入这个过滤器形成的桶中。

过滤器聚集分为单桶型过器聚集和多桶型过滤器聚集两种,对应的聚集类型名称为filter和filters。

先来看filter桶型聚集,它属于单桶型聚集。一般会同时嵌套一个指标聚集,用于在过滤后的文档范围内计算指标,例如:

在示例中一共定义了 3 个聚集,最外层是两个聚集,最后个聚集为嵌套聚集。origin_cn聚集为单过器的桶型聚集,它将所有OriginCountry为CN的文档归入一桶。origin_cn桶型聚集嵌套了cn_ticket_price指标聚集,它的作用是计算当前桶内文档 AvgTicketPrice字段的平均值。另一个外层聚集avg_price虽然也是计算AvgTicketPrice字段的平均值,但它计算的是所有文档的平均值实际上,使用query与aggs结合起来也能实现类型的功能,区别在于过器不会做相似度计算,所以效率更高一些也更灵活一些。

多过滤器与单过器的作用类似,只是包含有多个过滤器,所以会形成多个桶。多过滤器桶型聚集使用filters参数接收过滤条件的数组,一般也是与指标聚集一同使用。

例如使用两个过滤器计算从中国、美国出发的航班平均机票价格

2. global聚集

global桶型聚集也是一种单桶型聚集,它的作用是把索引中所有文档归入个桶中。这种桶型聚集看似没有什么价值,但当global桶型聚集与query结合起来使用时,它不会受query定义的查询条件影响,最终形成的桶中仍然包含所有文档。global聚集在使用上非常简单,没有任何参数,例如:

在示例中query使用term查询将航空公司为 "Kibana Airline"的文档都检索出来,而 kiana_avg_delay定义的平均值聚集会将它们延误时间的平均值计算出来。但另一个all_flights聚集由于使用了global聚集所以在嵌套的all_avg_delay 聚集中计算出来的是所有航班延误时间的平均值。

3. missing聚集

missing聚集同样也是一种单桶型聚集,它的作用是将某一字段缺失的文档归入一桶。missing聚集使用fields参数定要检缺失的字段名称,例如:

示例将 kiana_sample_data_flights中缺失 AvgTicketPrice字段的文档归入一桶,可以通过返回结果的doc_count査询数量也可以与指标聚集做嵌套,计算这些文档的某一指标值 

7.4.2 聚集组合

composite聚集可以将不同类型的聚集组合到一起,它会从不同的聚集中提取数据并以笛卡尔乘积的形式组合它们,而每一个组合就会形成一个新桶。

例如想看平均票价与出发机场天气的对应关系:

composite聚集中通过sources参数定义了两个需要组合的子聚集。第一个聚集avg_price是一个针对AvgTicketPrice以500为间隔的histogram聚集,第二个则聚集weather则一个针对Originweather的terms聚集。sources参数中还可以定义更多的聚集它们会以笛卡儿乘积的形式组合起来。

在返回结果中除了由各聚集组合形成的桶以外,还有一个after_key字段,它包含了当前聚集结果中最后一个结果的key。所以请求下一页聚集结果就可以通过after和size参数指定,例如

7.4.3 邻接矩阵

7.5 管道聚集 

管道聚集不是直接从索引中读取文档,而是在其他聚集的基础上再进行聚集运算。所以管道聚集可以理解为是在聚集结果上再次做聚集运算,比如求聚集结果中多个桶中某一指标的平均值、最大值等。要实现这样的目的,管道聚集都会包含一个名为buckets path 的参数,用于指定访其他桶中指标的路径。buckets_path参数的值由三部分组成,即聚集名称、指标名称和分隔符。聚集名称与聚集名称之间的分隔符是">",而聚集名称与指标名称之间的分隔符使用".",在后面讲解具体的管道聚集时会有比较详细的例子。

按管道聚集运算来源分类,管道聚集可以分为

基于父聚集:使用父聚集的结果并将运算结果添加到父聚集结果中

基于兄弟聚集:使用兄弟聚集的结果并且结果会展示在自己的聚集结果中。

7.5.1 基于兄弟聚集

基于兄弟聚集的管道聚集包括 avg_bucket、max_bucket min_bucket、sum_bucket、stats_bucket、extended_stats_bucket、percentile_bucket 七种。如果将它们名称中的bucket去除,它们就与7.1 节介绍的部分指标聚集同名了。事实上,它们不仅在名称上接近,而且在功能上也类似,只是聚集运算的范围由整个文档变成了另一个聚集结果。以avg_bucket为例,它的作用是计算兄弟聚集结果中某一指标的平均值:

示例中,最外层包含有两个名称分别为carriers和all_stat的聚集,这两个聚集就是兄弟关系。carries聚集是一个针对Carries字段的terms聚集,Carries字段保存的是航班承运航空公司,所以这个聚集的作用是按航空公司将航班分桶。在这个聚集中嵌套了一个名为 carrier_state的聚集,它是一个针对 AvgTicketPrice字段的stats聚集,会按桶计算票价的最大值、最小值、平均值等统计数据。all_stata聚集是一个avg_bucket管道聚集,在它的buckets_path参数中指定了运算平均值的路径"carriers> carrier_stat.avg",即从兄弟聚集carrers中查找carrier_stat指标聚集,然后再用其中的avg字段参与平均值计算。所以all_stata最终计算出来的是四个航空公司平均票价的平均值实际上就是所有航班的平均票价。

尽管示例是针对avg_bucket管道聚集的检索,但使用其余六种基于兄弟的管道聚集类型的关键字直接替换avg_bucket,它们就变成了另一种合法的管道聚集请求并且可以正确执行。可以自行尝试,这里就不再赘述。

示例

POST /wblive_nginx-2021.04.27/_search?filter_path=aggregations
{
    "query": {
        "term": {"url.keyword": "/2/wblive/head/get_top_head_list.json"} 
    },
    "aggs": {
        "url_qps": {
            # "date_histogram": {"field": "@timestamp", "interval": "second"}
       "date_histogram": {"field": "create_time", "interval": "second"} # 这个时间的字段要选择对啊!!!! },
"max_qps": { "max_bucket": { "buckets_path": "url_qps>_count" # 这块是_count } } } }

如备注:使用_count,field是@timestamp的时候转换成时间的时候注意标准时区和东八区,或者可以选择其他字段,或者可以在date_histogram下添加"time_zone": "Asia/Shanghai" 参数

参考:

https://blog.csdn.net/qq_32165041/article/details/84132957  note:buckets_path用于指示桶最大值聚合获取per_state聚合中terms聚合中的doc_count的最大值。

https://stackoverflow.com/questions/34445376/use-doc-count-as-cumulative-count

返回结果

{
  "aggregations" : {
    "url_qps" : {
      "buckets" : [
        {
          "key_as_string" : "2021-04-27T00:00:01.000Z",
          "key" : 1619481601000,
          "doc_count" : 4408
        },
        {
          "key_as_string" : "2021-04-27T00:00:02.000Z",
          "key" : 1619481602000,
          "doc_count" : 0
        },
        ......
        {
          "key_as_string" : "2021-04-27T23:59:59.000Z",
          "key" : 1619567999000,
          "doc_count" : 11618
        }
      ]
    },
    "max_qps" : {
      "value" : 24995.0,
      "keys" : [
        "2021-04-27T03:42:21.000Z"
      ]
    }
  }
}

 

 

7.5.2 基于父聚集

基于父聚集的管道聚集包括 moving_avg、moving_fn、 bucket_script, bucket_selector、 bucket_sort、 derivative、cumulative_sum、serial_diff八种。

1. 滑动窗口

moving_avg和 moving_fn这两种管道聚集的运算机制相同,都是基于滑动窗口(Sliding Window)算法对父聚集的结果做新的聚集运算。滑动窗口算法使用一个具有固定宽度的窗口滑过一组数据,在滑动的过程中对落在窗口内的数据做运算。moving_avg管道聚集是对落在窗口内的父聚集结果做平均值运算,而moving_fn管道聚集则可以通过脚本对落在窗口内的父聚集结果做各种自定义的运算。由于moving_avg管道聚集完全可以使用moving_fn管道聚集实现,所以moving_avg在Elasticsearchk版本6.4.0中已经被废止。

由于使用滑动窗口运算时每次移动一个位置,这就要求moving_avg和moving_fn所在父聚集桶与桶间隔必须固定,所以这两种管道聚集只能在histogram和date_histogram聚集中使用。例如

示例中,最外层的父聚集day_price是一个date_histogram桶型聚集,它根据文档的timestamp字段按天将文档分桶。day_price聚集包含avg_price和smooth_pricel两个子聚集,其中avg_price聚集是个求AvgTicketPrice字段在一个桶内平均值的avg聚集,而 smooth_avg则是一个使用滑动窗口做平均值平滑的管道聚集,窗口宽度由参数Window设置为10, 默认值为5。

通过返回结果比较avg_price与smooth_price就会发现,后者由于经过了滑动窗口运算,数据变化要平滑得多。返回结果中还会包含废止警告,提示应该使用moving_fn代替这个聚集。如果使用moving_fn则smooth_price可以改为

moving_fn聚集包含一个用于指定运算脚本的script参数,在脚本中可以通过values访问buckets_path参数指定的指标值。moving_fn还内置了一个MovingFunctions类,包括多个运算函数见下表。

2. 单桶运算

上ー小节介绍的两种管道聚集会对父聚集结果中落在窗口内的多桶做聚集运算,而bucket_script、bucket_selector、bucket_sort这三个管道聚集则会针对父聚集结果中的每一个桶做单独的运算。其中bucket_script会对每个桶执行一段脚本,运算结果会添加到父聚集的结果中;bucket_selector同样也是执行一段脚本,但它执行的结果 定是布尔类型,并且决定当前桶是否出现在父聚集的结果中; bucket_sort则根据每一桶中的具体指标值决定桶的次序。下面通过示例来说明这三种管道聚集的具体用法

在示例中同时应用这三种管道聚集,它们的聚集名称分别为 diff、gt990 和 sort_by。最外层的date_price_diff 聚集是一个以天为固定间隔的date_histogram聚集,其中又嵌套了包括上述三个聚集在内的子聚集。其中stat_price_day是一个根据AvgTicketPrice字段生成统计数据的stats聚集。

diff是一个bucket_script管道聚集,它的作用是向最终聚集结果中添加代表最大值与最小值之差的diff字段。它通过buckets_path定义了两个参数max_price和min_price,并在script参数中通过脚本计算了这两个值的差作为最终结果,而这个结果将出现在整个聚集结果中。

gt990是一个bucket_selector管道聚集,它的作用是筛选哪些桶可以出现在最终的聚集结果中。它也在buckets_path中定义了相同的参数,不同的是它的script参数运算的不是差值,而是差值是否大于 990, 即"params.max_price - params.min_price > 990"。如果差值大于990即运算结果为true,那么当前桶将被选取到结果中,否则当前桶将不能在结果中出现。

sort_by是一个bucket_sort管道聚集,它的作用是给最终的聚集结果排序。它通过sort参数接收一组排序对象,在示例中是使用聚集的结果按倒序排序。

所以示例整体的运算效果就是将那些票价最大值与最小值大于990的桶选取出来,并在桶中添加diff字段保存最大值与最小值的差值,并按diff字段值降序排列。

3. 特定数学运算

其余几种基于父聚集的管道聚集都是用于特定的数学运算,包括derivative、cumulative_sum、serial_diff等。它们有一个共同特点,那就是它们都只能应用于histogram或date_histogram父聚集中。

示例

将三种管道聚集都应用上了,其中derivative和cumulative_sum分别用于求导数和累积和;而serial_diff则用于计算所谓的时序差分,这里所说的时序差分是指某一指标值的当前值与指定时间段之前值的差值。例如在示例中,serial_diff通过lag参数指定了间隔为7, 则serial_diff就会计算当前桶与之前的第7个桶之间的差值。由于示例中的父聚集是按天分桶,所以这相当于反映了机票平均价格的周环比价格变化。

7.6 矩阵聚集

7.7 本章小结

本章介绍了Elasticsearch中功能最为强大的聚集查询,包括指聚集、桶型聚集、管道聚集和矩阵聚集四种类型。其中,指标聚集用于计算多个文档中某一字段值的统计数据,而桶型聚集则用于根据定的分桶规则将文档分成不同的桶。指标聚集经常与桶型聚集一起使用形成嵌套聚集,可以计算不同桶中的指标值。管道聚集不以文档作为输入,而以其他聚集的结果作为输入,包括以父聚集为输入和以兄弟聚集为输入的两种类型。

聚集査询可以看成是与DSL样的查询语言,可以在_search接口中通过aggs或aggregations参数使用,这类似于使用query参数执行DSL一样。聚集查询有着固定的格式,可以嵌套多层使用,是Elasticsearch数据统计和数据分析能力的重要体现。

在后续介绍 Kibana时,Kibana可视化对象中的许多图表都是基于聚集查询,没有这部分知识做基础就没办法理解Kiana可视化功能。本章并未介绍所有聚集查询类型。有一部分与特定的数据类型相关聚集查询在第8章中介绍,包括父子关系、地理信息等。

第八章 处理特殊数据类型

Elasticsearch索引字段中定义了一些特殊数据类型,用于反映某些特殊的数据关系或数据表示方法。由于这些数据类型都与一组DSL查询和聚集査询相关联,所以2.3节中并没有介绍它们,而是集中在本章统一介绍。这些特殊数据类型主要包括join类型、 nested 类型和地理坐标。

除了DSL和聚集查询以外,Elasticsearch在Basic授权中还提供了基于SQL语法的查询语言,这种査询语言可以以类似SQL语言的形式执行文档检索。由于这种SQL语言在Kibana画布功能中需要使用,所以本章会对它做简要介绍。

8.1 父子关系

Elasticsearch中的父子关系是单个索引内部文档与文档之间的一种关系,父文档与子文档同属一个索引并通过父文档_Id建立联系,类似于关系型数据库中单表内部行与行之间的自关联。

8.1.1 join类型

在Elasticsearch中并没有外键的概念,文档之间的父子关系通过给索引定义join类型字段实现。例如创建一个员工索引employees,定义个join类型的management字段用于确定员工之间的管理与被管理关系

示例中,management字段的数据类型被定义为join,同时在该字段的relations参数中定义父子关系为manager与member,其中manager为父而member为子,它们的名称可由用户自定义。

文档在父子关系中的地位,是在添加文档时通过join类型字段指定的。

还是以employees索引为例,在向employees索引中添加父文档时,应该将management字段设置为manager;而添加子文档时则应该设置为 member。具体如下

示例中,编号为1的文档其management字段通过name参数设置为manager,即在索引定义父子关系中处于父文档的地位;而编号为2和3的文档其management字段则通过name参数设置为member,并通过parent参数指定了它的父文档为编号1的文档。在使用父子关系时,要求父子文档必须要映射到同一分片中,所以在添加子文档时routing参数是必须要设置的。显然父子文档在同一分片可以提升在检索时的性能。

可在父子关系中使用的查询方法有 has_child、 has_parent和parent_id查询,还有parent和children两种聚集。 

8.1.2 has_child查询

has_child查询是根据子文档检索父文档的一种方法,它先根据查询条件将满足条件的子文档检索出来,在最终的结果中会返回具有这些子文档的父文档。

例如,如果想检索smith的经理是谁

has_child查询的type参数需要设置为父子关系中子文档的名称member,这样has_child查询父子关系时就限定在这种类型中检索;query参数则设置了查询子文档的条件,即名称为smith。最终结果会根据smith所在文档,通过member对应的父子关系检索它的父文档。 

8.1.3 has_parent查询

has_parent查询与has_child查询相反,是通过父文档检索子文档的一种方法。在执行流程上,has_parente查询先将满足询条件的父文档检索出来,但在最终返回的结果中展示的是具有这些父文档的子文档。

例如,如果想查看tom的所有下属

has_parent查询在结构上与has_childe查询基本相同,只是在指定父子关系时使用的参数是parent_type而不是type。 

8.1.4 parent_id查询

parent_id查询与has_parent查询的作用相似,都是根据父文档检索子文档。不同的是,has_parent可以通过query参数设置不同的查询条件;而parent_id则只能通过父文档_id做检索。

例如,查询_id为1的子文档:

以上三种查询都属于 DSL,基本逻辑都是通过子文档检索父文档,或是通过父文档检索子文档。接下来再来看看针对父子关系的聚集查询。 

8.1.5 children聚集

如果想通过父文档检索与其关联的所有子文档就可以使用children聚集。

以employess索引为例,如果想要查看tom的所有下属就可以按如下方式检索:

示例中,quey参数设置了父文档的查询条件,即名称字段name为tom的文档;而聚集查询members中则使用children聚集将它的子文档检索出来,同时还使用了一个嵌套聚集member_name将子文档name字段的词项全部展示出来了。 

8.1.6 parent聚集

parent聚集与children聚集正好相反,它是根据子文档查找父文档,parent聚集在Elasticsearch版本6.6以后支持。

例如通过name字段为smith的文档,查找该文档的父文档

8.2 嵌套类型

第2.3节介绍的对象类型虽然可按JSON对象格式保存结构化的对象数据,但由于Lucene并不支持对象类型,所以Elasticsearch在存储这种类型的字段时会将它们平铺为单个属性。例如:

示例中的colleges文档,address字段会被平铺为address.country和address.city两个字段存储。这种平铺存储的方案在存储单个对象时没有什么问题,但如果在存储数组时会丢失单个对象内部字段的匹配关系。例如: 

示例中的colleges文档在实际存储时,会被拆解为"address.country": ["CN", "US"]和"address.city": [" BJ", "NY"]两个数组字段。这样一来,单个对象内部country字段和city字段之间的匹配关系就丢失了。换句话说,使用CN与NY作为共同条件检索文档时,上述文档也会被检索出来,这在逻辑上就出现了错误

示例中使用了bool组合查询,要求country字段为CN而city字段为NY。这样的文档显然并不存在,但由于数组中的对象被平铺为两个独立的数组字段,文档1仍然会被检索出来。 

8.2.1 nested类型

为了解决对象类型在数组中丢失内部字段之间匹配关系的问题, Elasticsearch提供了一种特殊的对象类型nested。这种类型会为数组中的每一个对象创建一个单独的文档,以保存对象的字段信息并使它们可检索。由于这类文档并不直接可见,而是藏匿在父文档之中,所以本书后续章节将称这类文档为隐式文档或嵌入文档。

还是以colleges索引为例,将它的address字段设置为nested类型

当字段被设置为nested类型后,再使用上面示例的bool组合查询就不能检索出来了。这是因为对nested类型字段的检索实际上是对隐式文档的检索,在检索时必须要将检索路由到隐式文档上,所以必须使用专门的检索方法。也就是说,现在即使将上面示例中的查询条件设置为CN和BJ也不会检索出结果。nested类型字段可使用的检索方法包括DSL的nested查询,还有聚集查询中的nested和reverse_nested两种聚集。 

8.2.2 nested查询

nested查询只能针对nested类型字段,需要通过path参数指定nested类型字段的路径,而在query参数中则包含了针对隐式文档的具体询条件。例如:

示例中再次使用CN与NY共同作为查询条件,但由于使用nested类型后会将数组中的对象转换成隐式文档,所以在nested查询中将不会有文档返回了。读者可以自行将上面条件更换为CN和BJ看是否有文档返回。

除了path和quey两个参数以外,nested查询还包括score_mode和ignore_unmapped 两个参数。前者用于指定嵌入对象如何影响相关度,可选值包括 avg、max、min、sum和none,其中avg为默认值。 ignore_unmapped用于控制在path参数指向出错时的行为,默认情况下为false,即在出错时会抛出异常。

8.2.3 nested聚集 

nested聚集是一个单桶聚集,也是通过path参数指定nested字段的路径,包含在path指定路径中的隐式文档都将落入桶中。

所以nested字段保存数组的长度就是单个文档落入桶中的文档数量,而整文档落入桶中的数量就是所有文档nested字段数组长度的总和。

理解上面这段话,要先理解ES中nested类型会为数组中的每个对象创建一个单独的文档,以保存对象的字段信息并使它们可以检索,即neste类型的数组如果有多个元素,会被保存为多个单独的文档。

有了nested聚集,就可以针对nested数组中的对象做各种聚集运算,例如

 

示例中,nested_address是一个nested聚集的名称,它会将address字段的隐式文档归入一个桶中。而嵌套在nested_address聚集中的city_names聚集则会在这个桶中再做terms聚集运算,这样就将对象中city字段所有的词项枚举出来了。 

8.2.4 reverse_nested聚集 

reverse_nested聚集用于在隐式文档中对父文档做聚集,所以这种聚集必须作为nested聚集的嵌套聚集使用。例如

示例中,city_names聚集也是将隐式文档中city字段的词项全部聚集出来。不同的是在这个聚集中还嵌套了一个名为avg_age_in_city的聚集,这个聚集就是一个reverse_nested聚集。它会在隐式文档中将city字段具有相同词项的文档归入个桶中,而avg_age_in_city聚集嵌套的另外一个名为avg_age的聚集,它会把落入这个桶中文档的age字段的平均值计算出来。所以从总体上来看这个聚集的作用就是将在同一城市中大学的平均校龄计算出来。

8.3 处理地里信息

越来越多的互联网应用需要处理地理信息,Elasticsearch对地理信息数据也提供了比较好的支持。它提供了两种用于存储地理信息的数据类型,同时还提供了基于这两种数据类型的查询功能。本小节将通过一个存储了几所宾馆地理位置的索引hotels,详细讲解与地理相关的数据类型和查询方法。在介绍Elasticsearch这些特性之前,先来学习ー下有关地理信息编码的重要知识Geohash。

8.3.1 GeoHash

Geohash是一种地理坐标编码系统,可以将地理位置按一定规则转换为字符串,以方便对地理位置信息建立空间索引。首先必须要明确的是,Geohash代表不是一个点而是一个区域

此外,Geohash还有两个非常显著的特点:

一是Geohash编码的字符串越长表示的区域越精确,Elasticsearch支持Geohash字符串的最大长度是 12, 这个精度已经达到了厘米级别,可以满足大多数应用的要求;

另ー个更有价值的特点是如果不同位置的Geohash字符串前缀相同,那它们一定在同一区域中。

Geohash将地理坐标编码为字符串的方法与搜索算法中的二分查找有几分相似。它将经纬度的范围一分为二,分成左右两个区间;坐标落入左区间为0, 落入右区间为1。按此方法,分别对经度和纬度不停递归逼近实际坐标值,就会得到两组由0、1组成的数字串。然后按照偶数位放经度,奇数位放纬度的方法,将两组数字串组合起来。最后将组合形成的数字串转换成十进制数,并用BASE32对数字编码就得到Geohash的最终编码了。

根据经纬度的定义,经度整体范围为[-180,180],纬度整体范围为[-90,90]。所以第一次递归分割坐标后,经度的左右区间为[-180,0) 和[0,180],即西半球和东半球;而纬度的左右区间则为[-90,0) 和[0,90],即南半球和北半球。

以北京某地坐标(116.403874,39.915125) 为例,经度和纬度在第一次递归后都落入右区间,则它们第一位数字就都是1。由于落入了右区间,所以经纬度的第二次递归就是针对右区间分割:经度为[0,90) 和[90,180】,纬度为[0,45) 和[45,90]。这次经度落入了右区间而纬度则落入了左区间,所以它们的第二位数字分别是1和0。按此方法不停递归下去,就能无限地接近于实际坐标。

可以看到,Geohash实际上是将地球假设为一个平面,然后在这平面上划分网格的过程。在这个递归的过程中,每一次都是将整个区域一分为二,区域面积也就越分越小,而且在相同区域的位置它们前缀数字一定是相同的。下表列出了经度116.403874经过10次递归的运算过程及结果。

经过10次递归,经度116.403874得到的数字串为1101001011以同样的方式对纬度做Geohash运算,下表列出了纬度39.915152经过10次递归的运算过程及结果。

同样经过10次递归,纬度得到的数字串为1011100011下面按偶数位经度、奇数位纬度的方法合并两个数字串,注意位数是从0开始。或者也可以理解为先经度后纬度,各取一位交叉合并。总之最终的结果为

经度:1101001011;

纬度:1011100011;

合并:11100111010010001111

合并后的结果按每五位为一组,依次转换为十进制数28、29、4、15, 并使用下表对应的BASE32编码转换为字符串。

 

所以最终结果为wx4g, 可以到 http://GeoHash.org 网站上输入 39.915152,116.403874”验证结果为 Wx4g0f6dwgek。这个结果的长度达12位,说明做了30次递归。下表列出了Geohash字符串长度与实际位置误差的关系。

根据上表所示,当Geohash字符串长度达到 12 位时,位置精度误差可以控制在不到8cm2的范围内,这个面积还不如人类的手掌大。

8.3.2 地理类型字段

Elasticsearch提供了两种与地理相关的数据类型

geo_point:用于保存地理位置,即一个具体的坐标

geo_shape:用于保存地理形状,如矩形和多边形。

1. geo_points类型

众所周知,地理位置由经度和纬度共同定义,所以 geo_point定义地理位置坐标最基本的形式也是通过提供经度和纬度来实现的。例如定义索引hotels存储宾馆名称及其地理位置

在示例中,location的数据类型即为 geo_point,这种类型有四种数据表示方式,例如

 

需要注意的是字符串形式的经纬度格式为"lat, lon",而数组形式的经纬度格式则为[Ion, lat]。前三种方式比较容易理解,最后种形式使用一个长度为12的字符串,这个字符串就是前面介绍的Geohash编码。

2. geo_shape类型

geo_shape类型的字段用于存储地理形状,支持GeoJSON及WKT中描述的大多数地理形状。为了体验 geo_shape 类型,需要先给hotels添加geo_shape类型的area字段

在设置geo_shape类型字段的值时,可使用type参数指定形状类型,而使用coordinates参数指定地理坐标。geo_shape支持的形状类型包括 point、linestring、polygon、multipoint、multilinestring、 multipolygon、geometrycollection、envelope和circle九种,这些类型就是type参数的可选值。例如,给友谊宾馆添加polygon多边形

示例中定义的形状是一个由7个点组成的多边形,其中第1个点与第7个点必须相同,以闭合整个多边形,否则将会被识别为一条折线而报错。接下来再给其他几个饭店添加不同种类的形状

在这些形状中,envelop是两个坐标定义的矩形,point是由单个坐标定义的点,而inestring则是由多个坐标定义的折线。有了这些数据,下面来看一下如何通过地理信息实现数据检索。  

8.3.3 geo_shape查询

geo_shape询是根据geo_shape 型来过滤文档,将那些与指定形状相交的文档选取出来。所以在geo_shape查询中,必须要指定个地理形状以过滤文档。有两种方式指定地理形状,

一种是直接在查询中指定

一种则是引用在索引中预先定义好的地理形状

例如取北京市三环路左上角和右下角形成一个envelope形状,査询位于这个形状之内的宾馆:

在示例中,area是hotels索引中的字段名称,代表了geo_shape查询要检索的字段。shape参数定义了检索时使用的形状,relation参数则指定了文档与查询条件中指定形状的关系。 relation 参数有四个可选值 intersects、disjoint、within 和 contains,默认值为intersects。

  • intersects:会将所有与查询形状相交的文档都过出来
  • within :要求文档中的形状必须包含在查询条件指定的形状中
  • disjoint:与intersects正好相反,要求文档不能与查询条件中指定的形状有任何交集
  • contains: 会返回那些包含了查询条件中指定形状的文档

可自行将示例中的 relation 替换为其他类型,以体验它们的区别。

示例中的geo_shape查询是通过shape定义ー个envelope形状,但对于些比较常用的形状,比如一个市、区、镇的形状等,每次都这样定义就会非常麻烦。所以,Elasticsearch提供了另一种方式指定查询形状,这就是使用索引中预先定义好的形状参与查询,例如

在示例中,indexed_shape取代了shape,并通过index、id和path三个参数指定了形状存储的索引、ID 和路径。(补充:在上面已经为id2的定义了形状envelope)

8.3.4 geo_bounding_box查询

geo_bounding_box查询与geo_shape查询有些类似,也是在查询条件中指定形状。不同的是,geo bounding_box查询条件中指定的形状一定是矩形,并且检索时针对的字段类型是geo_point。所以正如它名称中展示的那样,它就是在查询条件中定义一个矩形的盒子,所有落在这个盒子中的点都满足查询条件

例如,同样是以北京市三环路左上角和右下角定义盒子,查询所有在三环路內的宾馆

示例中,location是hotels索引中的字段,类型必须是geo_point,参数top_lef和bottom_right则分别定义了盒子的左上角和右下角。top_left和 bottom_right可以使用geo_point类型值的四种形式指定,这相当于使用geo_shape的within关系査询定义了一个envelope形状,必须是完全落在定义的盒子中才会被返回。

8.3.5 geo_distance查询

geo_distance查询条件针对的字段是geo_point类型,它会将所有该字段存储点到指定点距离小于某一特定值的文档查询出来。所以geo_distance査询需要提供两个参数,一是定义一个基准点,另是指定一个距离。

例如以坐标[116.403872,39.915095] 为基准点,将所有距离小于5km的宾馆都查找出来

在示例中,geo_distance参数的distance参数用于设置距离,而location参数则用于设置基准点。显然,geo_distance对于某些App中的查找附近功能非常有用。 

8.3.6 geo_polygon查询

geo_polygon查询与geo_bounding_box查询类似,只是定义的查询条件为多边形,并不定要求是矩形。例如询指定多边形内的宾馆

在示例中,location为hotels存储了geo_point类型的字段名称,而其中的points参数则定义了多边形的点。可见,geo_polygon查询也是针对geo_point类型字段。 

8.3.7 geohash_grid与geo_distance

Elasticsearch提供了两种与地理信息相关的聚集,它们是

geohash_grid聚集:据区域分组

geo_distance聚集:根据距离分组

1. geohash_grids聚集

geohash_gid聚集用于根据Geohash运算区域,并根据文档geo_point类型的字段将文档归入不同区域形成的桶中。在kibana_sample_data_flights索引中,包含有两个坐标类型的字段Originlocation和Destlocation。下面对Originlocation字段做一个geohash_grid聚集,如示例所示:

在示例中,field参数指定了在哪个字段上做聚集,而precision则指定了Geohash的精度,也就是返回Geohash字符串的长度。geohash_grid聚集会根据Originlocation字段的Geohash结果,将它们归入不同的Geohash字符串中。

在Elasticsearchk版本7中新增加了一种gentile_grid聚集,它要实现的功能与geohash_grid聚集相同。它也是根据geo_points类型字段的值将文档归入不同地理区域,只是它划分地理区域的方法不是Geohash而是Tile。尽管划分区城的方法不同,但它与geohash_grid

的使用方法完全相同,直接将示例中的 geohash_grid替换为geotile_grid即可,读者可自行尝试。

2. geo_distance聚集

geo_distance聚集根据文档中某个坐标字段到指定地理位置的距离分组,所以要通过origin参数指定一个坐标位置,并通过ranges参数给出分桶的距离范围。例如示例中根据Originlocation字段的坐标到"wx4g"标识位置的距离分桶

默认情况下距离的单位为米,同时也可以使用参数unit修改,可选单位包括 mi(英里)、in(英寸)、yd(英码)、km(千米)、 cm(厘米)、mm(毫米) 

8.4 使用SQL语言

Elasticsearch在Basic授权中支持以SQL语句的形式检索文档,SQL语句在执行时会被翻译为DSL执行。从语法的角度来看, Elasticsearch中的SQL语句与RDBMS中的SQL语句基本一致,所以对于有数据库编程基础的人来说大大降低了使用Elasticsearch的学习成本。除此之外,由于在Kibana新提供的画布功能中不支持使用Elasticsearch聚集查询功能,所以如果需要使用聚集查询时就必须要使用Elasticsearch SQL定义。

Elasticsearch提供了多种执行SQL语句的方法,可使用类似_search一样的REST接口执行也可以通过命令行执行。它甚至还提供了JDBC和ODBC驱动来执行SQL语句,但JDBC和ODBC属于Platinum(白金版)授权需要付费,所以本小节将只介绍_sql接口。

8.4.1 _sql接口

在早期版本中,Elasticsearch执行SQL的REST接口为_xpack/sql,但在版本7以后这个接口已经被废止而推荐使用_sql接口。例如

在示例中,SQL接口通过query参数接收SQL语句,而SQL语句也包含有 select、fom、where、order by 等子句。_sql接口的URL请求参数format定义了返回结果格式,也可以通过在调用_sql接口时设置Accept请求报头设置返回结果格式。比如在上面示例中定义了返回结果格式为txt, 而将该请求报头Accept设置为text/plaint也可以实现相似的效果。除了text以外,_sql接口还支持 csv、json、tsv、txt、yaml、cbor、smile格式。其中,cbor和smilez 是两种二进制格式,适用于通过程序解析的应用场景。有关这些格式的说明,请参考下表

示例中的请求会将所有档检索出来,并以文本表格的形式返回,这大概有1000多条。对于总量比较大的SQL查询,SQL接口还支持以游标的形式实现分页。当_sql接口的请求参数中添加了fetch_size参数,_sql接口在返回结果时就会根据fetch_size参数设置的大小返回相应的条数,并在返回结果中添加游标标识。具体来说,当请求_sql接口时设置的format为json时,返回结果中会包含cursor属性;而其他情况下则会在响应中添加 Cursor报头。例如还是执行上述示例中的 SQL,但这次加入分页的支持

在示例的第一个请求中,为了能够在返回结果中直接看到cursor值,我们将format设置为json;而在第二个请求中,参数cursor就是第一个请求返回结果中的cursor值,由于cursor值非常长,我们在示例中使用点将中间的内容省略掉了。反复执行第二个请求,Elasticsearch 就会将第一次请求的全部内容以每次10个的数量全部迭代出来。在请求完所有数据后, 应该使用_sql/close接将游标关以释放资源。除了fetch_size以外还有一些可以在_sql接口请求体中使用的参数,见下表

在这些参数中,filter可以使用DSL对文档做过滤,支持DSL中介绍的所有查询条件。query中的SQL语句在翻译为DSL后,会与filter中的DSL查询语句共同组合到bool查询中。其中,SQL语句生成的DSL将出现在must子句,而filter中的DSL则出现在filter子句中。如果想要查看SQL语句翻译后的DSL,可以使用_sql/translatei执行相同的请求在返回结果中就可以看到翻译后的DSL了。  

8.4.2 SQL语法

Elasticsearch支持传统关系型数据库SQL语句中的查询语句,但并不支持DML、DCL语句。句话说,它只支持SELECT语句,不支持INSERT、UPDATE、DELETE语句。除了SELECT语句以外, Elasticsearch还支持DESCRIBE和SHOM语句。

1. SELECT语句

SELECTT语句用于查询文档,基本语法格式如示例 8-29 所示

通过示例可以看出,Elasticsearch的SELECT语句跟普通SQL几乎没有什么区别,支持SELECT、FROM、WHERE、GROUP BY、HAVING、ORDER BY及LIMIT子句。SELECT子句中可以使用星号或文档字段名称列表,FROM子句则指定要检索的索引名称,而WHERE子句则设定了检索的条件。一般的SQL查询使用这三个子句就足够了,而GROUP BY和HAVING子句则用于分组,ORDER BY子句用于排序,而LIMT一般则可以用于分页。由于与传统SQL语句非常接近,这里就不再对它们的详细使用方法做更进一步介绍了。

2. DESCRIBE语句

DESCRIBE语句用于查看一个索引的基础信息,在返回结果中一般会包含 column、type、mapping三个列,分别对应文档的字段名称、传统数据库类型及文档字段中的类型。

例如要查看索引的基本信息,可以按示例发送DESCRIBE语句

3. SHOW语句

SHOW语句包括三种形式,即 SHOW COLUMNS、SHOW FUNCTIONS和SHOW TABLES。

SHOW COLUMNS用于查看一个索引中的字段情况,它的作用与DESCRIBE语句完全一样,甚至连返回结果都是一样的。

SHOW FUNCTIONS用于返回在Elasticsearch SQL中支持的所有函数,返回结果中包括 MIN、MAX、COUNT 等常用的聚集函数。

SHOW TABLES用于查看Elasticsearch中所有的索引。

示例展示在_sql接口中如何使用这三种形式

这三种形式都支持使用LIKE子句过滤返回结果,LIKE子句在用法上与SQL语句中的LIKE类似。例如,"show functions like'a%"将只返回以a开头的函数。

8.4.3 操作符与函数

Elasticsearch SQL中支持的操作符与函数有100多种,限于篇幅这里不太可能将这些全部介绍一遍。好在这些操作符大多与普通SQL语言一致,所以这里只介绍一些与普通SQL语句不一样的地方。

先来看一下比较操作符。一般等于比较在SQL中使用等号“=”,这在Elasticsearch SQL中也成立。但是Elasticsearch SQL还引入了另一个等号比较“<=>”,这种等号可以在左值为null时不出现异常。例如

再来看一下LIKE操作符。在LIKE子句中可以使用%代表任意多字符,而使用代表单个字符。Elasticsearch SQL不仅支持 LIKE 子句,还支持通过RLIKE子句以正则表达式的形式做匹配,这大大扩展了SQL语句模糊匹配的能力。

尽管使用LIKE 和RLIKE可以实现模糊匹配,但它离全文检索还差得很远。SQL语句的WHERE子句一般都是使用字段整体值做比较,而没有使用词项做匹配的能力。为此 Elasticsearch SQL提供了MATCH和QUERY两个函数,以实现在SQL做全文检索。前者最终会翻译为 DSL中的match或multi_match查询,而后者则为query_string

例如在示例中的两个请求分别使用match和query函数,它们的作用都是检索Destcountry字段为CN的文档

在示例中的两个请求的select子句中都使用了SCORE 函数它的作用是获取检索的相关度评分值。

Elasticsearch SQL支持传SQL中的聚集函数,这包括 MAX MIN、AVG、COUNT、SUM等。同时,它还支持一些 Elasticsearch特有的聚集函数,这些聚集函数与Elasticsearche聚集查询相对应。这包 FIRST/FRST_VALUE和LAST/LAST_VALUE,可用于查看某个字段首个和最后一个非空值;PERCENTILE和PERCENTILE_RANK 用于百分位聚集,KURTOSIS、SKEWNESS、STDDEV_POP、SUM_OF_SQUARES和VAR_POP可用于运算其他统计聚集。

除了以上这些函数和操作符,Elasticsearch SQL还定义了一组用于日期、数值以及字符串运算的函数。由于它们数量众多但使用起来并不复杂,本小节在这里就不再介绍了。

8.5 本章小结

本章介绍了父子关系、嵌套关系和地理坐标三类特殊数据类型,同时还将这些字段的DSL和聚集查询一并做了介绍。下面将这三类特殊类型及其检索方法做一下总结。

父子关系定义的是在一个索引内部文档与文档之间的从属关系。父子关系对应的数据类型为join,可用DSL包括has_child、has_parent和parent_id查询,可用聚集查询为children和parent聚集。

嵌套关系定义的是一种不会丢失对象属性匹配信息的对象数组。嵌套关系对应的数据类型为nested,可用DSL为nested查询,可用聚集查询为nested和reverse_nested聚集。

地理坐标有两种数据类型geo_point和geo_shape,可用DSL包括geo_shape、geo_bounding_box、geo_distance和geo_polygoni四种,可用聚集为 geohash_grid与geo_distance聚集两种。

 

第九章 Elasticsearch查询遇到的问题

1.查询ES显示hits.total.value最大值10000的解决方法

查询时加上"track_total_hits": true即可,就能正确的显示总记录数了

GET /jm/_search
{
  "track_total_hits": true, 
  "query":{
    "match_all": {}
  }
}

2.在aggs或者query中,可以指定size的个数,默认为10,即返回10条聚合查询结果。

POST /wblive_nginx-2021.04.28/_search
{
  "track_total_hits": true, 
  "size": 12, 
  "query": {
    "range": {
      "status": {
        "gte": 400,
        "lt": 500
      }
    }
  }
}

3.枚举status字段有哪些指

POST /wblive_nginx-2021.04.28/_search?filter_path=aggregations
{
  "aggs": {
    "status_kind": {
      "terms": {
        "field": "status",
        "size": 20
      }
    }
  }
}

4.查看指定url status=200的条数

POST /wblive_nginx-2021.04.28/_search?filter_path=hits.total
{
  "track_total_hits": true,
  "query": {
    "bool": {
      "filter": [
        {"term": {"status": 200}},
        {"term": {"url.keyword": "/2/wblive/head/get_top_head_list.json"}}
      ]
    }
  }
}

结果

{
  "hits" : {
    "total" : {
      "value" : 504675888,
      "relation" : "eq"
    }
  }
}

5.查看指定url 4xx的条数(5xx同理)

POST /wblive_nginx-2021.04.28/_search
{
  "track_total_hits": true,
  "query": {
    "bool": {
      "filter": [
        {"range": {"status": {"gte": 400, "lt": 500}}},
        {"term": {"url.keyword": "/2/wblive/head/get_top_head_list.json"}}
      ]
    }
  }
}

6.查看4xx的所有URL,通过size参数指定显示12条

POST /wblive_nginx-2021.04.28/_search
{
  "track_total_hits": true, 
  "size": 12, 
  "query": {
    "range": {
      "status": {
        "gte": 400,
        "lt": 500
      }
    }
  }
}

7.统计每个url的qps和max_qps,这块比较特殊的地方在于buckets_path的变量值中使用_count,参考https://www.cnblogs.com/TianFang/p/12495234.html

POST /wblive_nginx-2021.04.29/_search?filter_path=aggregations
{
    "query": {
        "term": {"url.keyword": "/2/wblive/head/get_top_head_list.json"} 
    },
    "aggs": {
        "url_qps": {
            "date_histogram": {"field": "@timestamp", "interval": "second"}
        },
        "max_qps": {
          "max_bucket": {
            "buckets_path": "url_qps>_count"
          }
        }
    }
}

如果只想获得max_qps可以加过滤条件,POST /wblive_nginx-2021.04.29/_search?filter_path=aggregations.max_qps

8.1 根据upstream_response_time计算所有请求的tp95、tp98、tp99

POST /wblive_nginx-2021.04.27/_search?filter_path=aggregations
{
    "aggs": {
        "upstream_response_time_percentile": {
            "percentiles": {"field": "upstream_response_time", "percents": [95,98,99]}
        }
    }
}

获取指定url的TP99(保留三位小数),第一次比较慢,后面有缓存了,就会快一些

POST /wblive_nginx-2021.04.28/_search?filter_path=aggregations
{
"query": { "term": {"url.keyword": "/2/wblive/head/get_top_head_list.json"} }, "aggs": { "upstream_response_time_percentile": { "percentiles": {"field": "upstream_response_time", "percents": [95,98,99]} } } }

8.2 根据upstream_response_time计算指定请求的tp95、tp98、tp99

POST /wblive_nginx-2021.05.06/_search?filter_path=aggregations
{
    "query": {
        "term": {"url.keyword": "/2/wblive/pc/room/show_topic.json"} 
    },
    "aggs": {
        "upstream_response_time_percentile": {
            "percentiles": {"field": "upstream_response_time", "percents": [95,98,99]}
        }
    }
}

9. 先获取所有200的数量,在计算各个URL的200的数量,等价于,先过来URL,再过滤200

POST /wblive_nginx-2021.04.29/_search?filter_path=aggregations
{
  "query": {
    "term": {"status": 200}
  }, 
  "aggs": {
    "url_kind": {
      "terms": {
        "field": "url.keyword",
        "size": 10
      }
    }
  }
}

10. 获取每个URL请求的总数量,并分组统计每个URL下各个状态码的数量,只是展示数字,没有计算各状态码数量占总请求量的占比

POST /wblive_nginx-2021.04.29/_search?filter_path=aggregations
{
  "aggs": {
    "url_kind": {
      "terms": {
        "field": "url.keyword",
        "size": 10
      },
      "aggs": {
        "status_count": {
          "terms": {
            "field": "status",
            "size": 10
          }
        }
      }
    }
  }
}

结果

{
  "aggregations" : {
    "url_kind" : {
      "doc_count_error_upper_bound" : 85993,
      "sum_other_doc_count" : 16649144,
      "buckets" : [
        {
          "key" : "/2/wblive/head/get_top_head_list.json",
          "doc_count" : 509013620,
          "status_count" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : 200,
                "doc_count" : 508657570
              },
              {
                "key" : 499,
                "doc_count" : 306431
              },
              {
                "key" : 403,
                "doc_count" : 40161
              },
              {
                "key" : 503,
                "doc_count" : 9458
              }
            ]
          }
        },
.......

11.获取每个URL请求的总数量,过滤status=200的请求,并统计数量,只是展示数字,没有计算各状态码数量占总请求量的占比

POST /wblive_nginx-2021.04.28/_search?filter_path=aggregations
{
  "aggs": {
    "url_kind": {
      "terms": {
        "field": "url.keyword",   # 根据需要判定,是否需要.keyword
        "size": 10
      },
      "aggs": {
        "status_200": {
          "filter": {
            "term": {"status": 200}
          }
        }
      }
    }
  }
}

结果

{
  "aggregations" : {
    "url_kind" : {
      "doc_count_error_upper_bound" : 76697,
      "sum_other_doc_count" : 13344475,
      "buckets" : [
        {
          "key" : "/2/wblive/head/get_top_head_list.json",
          "doc_count" : 505019171,
          "status_200" : {
            "doc_count" : 504675888
          }
        },
...省略

12. 获取每个URL请求的总数量,过滤status=200的请求,并计算200状态码数量占总请求量的占比,这块比较特殊的地方在于buckets_path的变量值中使用_count,参考https://www.cnblogs.com/TianFang/p/12495234.html

POST /wblive_nginx-2021.04.28/_search?filter_path=aggregations
{
  "aggs": {
    "url_kind": {
      "terms": {
        "field": "url.keyword",
        "size": 1000
      },
      "aggs": {
        "status_200": {
          "filter": {
            "term": {"status": 200}
          }
        },
        "available": {
          "bucket_script": {
            "buckets_path": {
              "url_total_count": "_count",
              "status_200_count": "status_200._count"
            },
            "script": "params.status_200_count / params.url_total_count"
          }
        }
      }
    }
  }
}

结果

{
  "aggregations" : {
    "url_kind" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "/2/wblive/head/get_top_head_list.json",
          "doc_count" : 505019171,
          "status_200" : {
            "doc_count" : 504675888
          },
          "available" : {
            "value" : 0.9993202574878093
          }
        },
        ......省略

 

报错及解决方法 

1.too_many_buckets_exception

curl -H 'Content-Type: application/json' http://10.92.133.105:9200/_cluster/settings -d '{"transient": {"search.max_buckets":86400}}'

如果有验证,需要加用户名密码,或者这kibana的开发工具中修改

posted @ 2020-10-20 10:31  番茄土豆西红柿  阅读(17)  评论(0)    收藏  举报
TOP