zl程序教程

您现在的位置是:首页 >  .Net

当前栏目

ElasticSearch使用经验总结

2023-02-18 15:49:48 时间

ElasticSearch总结

1.ElasticSearch的查询原理

Elasticsearch底层使用的Lucene的倒排索引技术来实现比关系型数据库更快的过滤的。所以要想了解Es的擦查询原理,需要先学习一下Lucene

1.1 Lucene的数据模型

Lucene中包含四种基本数据类型,分别是:

  • Index: 索引,由很多Document组成。相当与MySQL中数据库的概念。
  • Document:由很多Field组成。相当与MySQL中Row的概念。也是查询结果的最小单位。
  • Field: 由很多Term组成,包括Field Name和Field Value.相当于MySQL中的Column概念
  • Term: 由很多字节组成。常译为单词,一般将Text类型中的Field Value分词之后的最小单元叫做Term,如果是数值类型或者布尔型这种不可分的类型,那Field Value直接就是Term。

1.2 分词

倒排索引的核心就是分词,把text格式的字段按照分词器进行分词并编排索引。

分词器 作用特点
Standard es默认分词器,按单词分类并进行小写处理
Simple 按照非字母切分,然后去除非字母并进行小写处理
Stop 按照停用词过滤并进行小写处理,停用词包括the、a、is
Whitespace 按照空格切分
Language 据说提供了30多种常见语言的分词器
Patter 按照正则表达式进行分词,默认是\W+ ,代表非字母
Keyword 不进行分词,作为一个整体输出

常用的中文分词:

IK分词

1.3 倒排索引

索引一般就类似于字典中的目录,我们可以根据索引,快速精确的找到字典中某个字所在的页数,在编程中,我们也可以通过索引,快速找到数据所在的地址;像关系型数据库等都是使用传统的索引技术,即对关键字段进行索引,这样可以在使用索引字段进行查询的时候,提高查询速度;

es中采用的是倒排索引,又称反向索引,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引。Es底层依赖Lucene,其实说白了就是将一个个的Term(Field的key和value)作为索引,去组成一个大的集合Dictionary(保证集合内不重复),这个集合的里面放着每个Term所对应的地址,当我们去进行查询的时候,只需要去优化Dictionary的查询速度即可。

在Lucene中倒排索引寻址的流程如下:

graph LR; A[Term <br/> Index]-->B[Term <br/> Dictionary]; B-->C[Term <br/> Posting List];

1.4 Es的执行过程

graph TD; A((客户端))--> shard2 subgraph es执行线程; shard1 shard2 shard3 end; shard2-->dataX subgraph FileSystemCache data1 dataX dataY end dataX-->data* subgraph file datam data* datan datas end data*-->dataX dataX-->shard2 shard2-->A

1.5 Es的配置文件

2.ElasticSearch的缓存

2.1 缓存分类

Es的常用的缓存主要为 Shard RequestCache、Node Query Cache、Fielddata Cache、Linux OS page cache(文件系统缓存)

graph LR; A[Es Cache]-->B[Shard RequestCache]; A-->C[Node Query Cache]; A-->D[Fielddata Cache]; A-->E[Linux OS page cache]
  • Shard request cache,简称RequestCache,shard级别的缓存。 主要用于缓存size=0的聚合信息
  • Node Query Cache ,Node级别,被所有shard共享。 主要用于缓存在filter上下文里执行的Query,比如Range Query。 缓存的是压缩过的bitset,对应满足Query条件的docID列表。 这个缓存有一些智能的地方: 比如不是所有的Query都会被缓存,而是记录最近256个Query,只有重用过的,才会被挑选出来放到缓存; 对于小的segment会跳过不缓存; 5.1 以后对于term query完全不缓存; 新写入的文档会增量加入到bitset,而无需反复重建整个bitset。
  • Fielddata Cache,不是用于缓存Aggs,而是缓存所谓的field data。对于 text 分词类型进行聚合等获取字段值的行为时才会用到 fielddata,他会占用大量内存。fielddata默认关闭,因为在 text 类型上执行聚合一般是没有意义的
  • Linux OS page cache,也就是文件系统缓存,ES对于索引的访问是通过memory mapped file来访问的,经常访问的segment,只要没有合并,再次访问时可以直接从page cache里读取。 所以索引里被经常访问的热数据片段,等同于内存读取。

2.2 关于Es的内存性能

​ 因为es依赖于Lucene,所以在实际的生产环境中,关于内存资源的使用,这两者之间是竞争关系;内存对于 Elasticsearch 来说绝对是重要的,它可以被许多内存数据结构使用来提供更快的操作,但是并不是es的内存越大越好;因为lucene也决定着es的查询性能。

​ Lucene 被设计为可以利用操作系统底层机制来缓存内存数据结构。 Lucene 的段是分别存储到单个文件中的。因为段是不可变的,这些文件也都不会变化,这是对缓存友好的,同时操作系统也会把这些段文件缓存起来,以便更快的访问。Lucene 的性能取决于和操作系统的相互作用。如果你把所有的内存都分配给 Elasticsearch 的堆内存,那将不会有剩余的内存交给 Lucene。 这将严重地影响全文检索的性能。

标准的建议是把 50% 的可用内存作为 Elasticsearch 的堆内存,保留剩下的 50%。当然它也不会被浪费,Lucene 会很乐意利用起余下的内存。

如果你不需要对分词字符串做聚合计算(例如,不需要 fielddata )可以考虑降低堆内存。堆内存越小,Elasticsearch(更快的 GC)和 Lucene(更多的内存用于缓存)的性能越好。

Es内存配置尽量小于32G

这里有另外一个原因不分配大内存给 Elasticsearch。事实上, JVM 在内存小于 32 GB 的时候会采用一个内存对象指针压缩技术。

在 Java 中,所有的对象都分配在堆上,并通过一个指针进行引用。 普通对象指针(OOP)指向这些对象,通常为 CPU 字长 的大小:32 位或 64 位,取决于你的处理器。指针引用的就是这个 OOP 值的字节位置。

对于 32 位的系统,意味着堆内存大小最大为 4 GB。对于 64 位的系统, 可以使用更大的内存,但是 64 位的指针意味着更大的浪费,因为你的指针本身大了。更糟糕的是, 更大的指针在主内存和各级缓存(例如 LLC,L1 等)之间移动数据的时候,会占用更多的带宽。

Java 使用一个叫作 内存指针压缩(compressed oops)的技术来解决这个问题。 它的指针不再表示对象在内存中的精确位置,而是表示 偏移量 。这意味着 32 位的指针可以引用 40 亿个 对象 , 而不是 40 亿个字节。最终, 也就是说堆内存增长到 32 GB 的物理内存,也可以用 32 位的指针表示。

一旦你越过那个神奇的 ~32 GB 的边界,指针就会切回普通对象的指针。 每个对象的指针都变长了,就会使用更多的 CPU 内存带宽,也就是说你实际上失去了更多的内存。事实上,当内存到达 40–50 GB 的时候,有效内存才相当于使用内存对象指针压缩技术时候的 32 GB 内存。

这段描述的意思就是说:即便你有足够的内存,也尽量不要 超过 32 GB。因为它浪费了内存,降低了 CPU 的性能,还要让 GC 应对大内存。

2.3 缓存配置项

这里有两种方式修改 Elasticsearch 的堆内存。最简单的一个方法就是指定 ES_HEAP_SIZE 环境变量。服务进程在启动时候会读取这个变量,并相应的设置堆的大小。 比如,你可以用下面的命令设置它:

export ES_HEAP_SIZE=10g

此外,你也可以通过命令行参数的形式,在程序启动的时候把内存大小传递给它,如果你觉得这样更简单的话:

./bin/elasticsearch -Xmx10g -Xms10g 

确保堆内存最小值( Xms )与最大值( Xmx )的大小是相同的,防止程序在运行时改变堆内存大小, 这是一个很耗系统资源的过程。

通常来说,设置 ES_HEAP_SIZE 环境变量,比直接写 -Xmx -Xms 更好一点。

1 QueryCache: ES查询的时候,使用filter查询会使用query cache, 如果业务场景中的过滤查询比较多,建议将querycache设置大一些,以提高查询速度。

indices.queries.cache.size: 10%(默认),可设置成百分比,也可设置成具体值,如256mb。

当然也可以禁用查询缓存(默认是开启), 通过index.queries.cache.enabled:false设置。

2 FieldDataCache: 在聚类或排序时,field data cache会使用频繁,因此,设置字段数据缓存的大小,在聚类或排序场景较多的情形下很有必要,可通过indices.fielddata.cache.size:30% 或具体值10GB来设置。但是如果场景或数据变更比较频繁,设置cache并不是好的做法,因为缓存加载的开销也是特别大的。

indices.fielddata.cache.size:30% 

3 ShardRequestCache: 查询请求发起后,每个分片会将结果返回给协调节点(Coordinating Node), 由协调节点将结果整合。

如果有需求,可以设置开启; 通过设置index.requests.cache.enable: true来开启。

index.requests.cache.enable: true

不过,shard request cache只缓存hits.total, aggregations, suggestions类型的数据,并不会缓存hits的内容。也可以通过设置indices.requests.cache.size: 1%(默认)来控制缓存空间大小。

2.4 清除缓存

使用POST接口

POST http:127.0.0.1:9500/{索引名}/_cache/clear

3.ElasticSearch的查询语句

Es作为一款检索引擎,查询是主要的功能,这里针对Es自身的查询机制及Dsl语法进行一些汇总

3.1 查询样例

Es单纯的Get查询:

通过Get请求,指定索引名及文档类型和id属性即可精确获取数据

GET 127.0.0.1:9500/{索引名称}/{文档类型}/{_id属性值}

Es的轻量搜索:

es的轻量级搜索可以通过get请求,携带参数进行指定查询

GET 127.0.0.1:9500/{索引名称}/_search?q=name:"张三"

Es的简单查询:

此部分开始,均为POST请求

{
    "query":{
        "match":{  //指定条件的查询
            "content":"this is a match query"
        }
    }
}

Es的多条件标准查询结构样例:

{
    "query":{
        "bool":{
            "must":[],
            "must_not":[],
            "should":[],
            "filter":[]
        }
    },
    "from":0,
    "size":10,
    "_source":{
        "inculdes":[],
        "exculdes";[]
    },
    "sort":[
        {
            "XXX":"DESC"
        }
    ]
}

聚合标准结构:

{
    "size":0,
    "aggs":{
        "自定义聚合名称":{
            "terms":{
                "field":"聚合字段",
                "size":10
            }
        }
    }
}

3.2 查询结果

对Es的查询返回结果进行统一解释

一个查询结果demo:

{
   "took": 45,
   "time_out" : false,
   "_shards":{
     "total":3,
     "successful":3,
     "skipped": 0, 
     "failed":0  
   },
   "hits": {
      "total":      2,
      "max_score":  0.16273327,
      "hits": [
         {
            ...
            "_score":         0.16273327, 
            "_source": {
               "first_name":  "John",
               "last_name":   "Smith",
               "age":         25,
               "about":       "I love to go rock climbing",
               "interests": [ "sports", "music" ]
            }
         },
         {
            ...
            "_score":         0.016878016, 
            "_source": {
               "first_name":  "Jane",
               "last_name":   "Smith",
               "age":         32,
               "about":       "I like to collect rock albums",
               "interests": [ "music" ]
            }
         }
      ]
   }
}

took : 该命令请求花费了多长时间,单位:毫秒。

timed_out : 搜索是否超时。

_shards :搜索分片信息。

total : 搜索分片总数。

successful : 搜索成功的分片数量。

skipped : 没有搜索的分片,跳过的分片。

failed : 搜索失败的分片数量。

hits : 搜索结果集。项目中,我们需要的一切数据都是从hits中获取。

total : 此次查询结果的总数

max_score : 返回结果中,最大的匹配度分值。

hits :结果集(数组),默认查询前十条数据,根据分值降序排序。

_index : 该数据所属的索引

_type : 类型名称

_id : 该条数据的id

_score:关键字与该条数据的匹配度分值。

_source:查询结果,包含指定字段

3.3 Es的普通查询

这里我单纯的根据自身使用的习惯对Es的一些查询进行分类,并不代表Es的官方定义

3.4 Es的查询条件

即各种条件算子,最基础的条件比对,例如: = ,>,<,like,in ……,配合Query关键字,组成简单的DSL,即满足Es的简单查询体

可供Es进行条件筛选的条件有:

关键字 含义
match 全文匹配查询,如果是全文字段(text),会将查询内容进行分词,即待查询内容中只要包含查询内容即匹配,如果是精准字段,则是精准查询
match_phrase 短语匹配,同样是对全文字段(text),进行短语的匹配
term 等价于sql中的 ”=“操作,即等于,精准匹配
fuzzy 模糊匹配,应对输入拼写错误的查询
prefix 前缀匹配,即内容的起始部分是否含有指定信息
range 等价于sql中的between,配合lt(lte)大于(大于等于),gt(gte)小于(小于等于)使用
wildcard 正则表达式匹配,可使用正则的语法作为匹配内容(慎用,性能差!)
terms 等价于SQL中的IN,表示包含,后面跟一个数组结构
exists 检测数据中是某字段是否为null
missing 与exists相反,指定查询某字段为null

https://elastic-search-in-action.medcl.com/3.site_search/3.3.search_box/fuzzy_query/

3.5 多条件叠加

当使用多条件叠加查询的时候,需要使用bool进行条件的组合,然后使用条件组进行套娃搭配;一个bool下可以有 must、must_not、should、filter四种条件组,每个条件组内又可以根据bool组进行套娃组合不同的条件

条件组:

关键字 含义 用法
must ”是“条件组,参数类型为[] 数组内可配多个条件单元,各条件单元之间为且的关系
must_not ”否“条件组,参数类型为[] 数组内可配多个条件单元,各条件单元内的关系为取反状态
should ”或“条件组 ,参数类型为[] 数组内可配多个条件单元,各条件单元内之间为或的关系
filter 筛选器,参数类型为[] 数组内可配多个条件单元,各条件单元内的关系为且关系

可以根据不同的条件组,组成更为复杂的查询条件:

{
    "bool": {
        "must":     { "match": { "tweet": "elasticsearch" }},
        "must_not": { "match": { "name":  "mary" }},
        "should":   { "match": { "tweet": "full text" }},
        "filter":   { "range": { "age" : { "gt" : 30 }} }
    }
}

一条复合语句可以合并 任何 其它查询语句,包括复合语句,了解这一点是很重要的。这就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。

{
    "bool": {
        "must": { "match":   { "email": "business opportunity" }},
        "should": [
            { "match":       { "starred": true }},
            { "bool": {
                "must":      { "term": { "folder": "inbox" }},
                "must_not":  { "match": { "spam": true }}
            }}
        ]
    }
}

套娃类似于以下逻辑:

查询名字为张三且年龄为12岁,或者部门不在人事部且年龄在10到20岁之间的人

一般我们翻译为sql是:

select * from user where (name = "张三" AND age = 12) OR (dept != "人事部" AND age >= 10 AND age<=20 )

而使用es的套娃dsl则是:

{
    "query":{
        "bool":{
            "should":[        //对标SQL 中 的 OR
                {
                    "bool":{
                        "flter":[    //对标SQL 中 OR左侧()中的AND
                            {
                                "term":{
                                    "name":"张三"
                                }
                            },
                            {
                                "term":{
                                    "age":12
                                }
                            }
                        ]
                    }
                },       // ↑ 为OR左侧的() 中的条件
                {
                   "bool":{
                       "must_not":[   // 对标dept != "人事部"
                         {
                             "term":{
                                 "dept":"人事部"
                             }
                         }  
                       ],
                       "filter":[
                           {
                               "range":{
                                   "age":{
                                       "gte":10,
                                       "lte":20
                                   }
                               }
                           }
                       ]
                   }   
                }   //// ↑ 为OR右侧的() 中的条件
            ]
        }
    },
    "from":0,
    "size":10
}

4.ElasticSearch的聚合语句

ElasticSearch的聚合功能可以同比为关系型数据库中的groupby的概念,即将相同的内容进行合并,并配合各种数据函数,进行数据分析。Es中使用aggs(aggregations)关键字进行聚合

这里es提到一个的概念:即一个满足条件的数据集合称为一个桶,当聚合开始被执行,每个文档里面的值通过计算来决定符合哪个桶的条件。如果匹配到,文档将放入相应的桶并接着进行聚合操作。桶与桶可以进行嵌套

另一个概念是指标 :桶能让我们划分文档到有意义的集合,但是最终我们需要的是对这些桶内的文档进行一些指标的计算。分桶是一种达到目的的手段:它提供了一种给文档分组的方法来让我们可以计算感兴趣的指标。大多数 指标 是简单的数学运算(例如最小值、平均值、最大值,还有汇总),这些是通过文档的值来计算。在实践中,指标能让你计算像平均薪资、最高出售价格、95%的查询延迟这样的数据。

4.1一个聚合样例的拆解

4.1.1 基础概念

demo:

以下的聚合代表,我想要看所有男生中各个年龄的人员数量

{
    "query":{
       "bool":{
         "filter":[
           {
             "term":{
               "sex":"男"
             }
           }
         ]
       }
    }
    "size":0,
    "aggs":{
        "age":{
            "terms":{
                "field":"age",
                "size":10
            }
        }
    }
}
  • query: 筛选条件,即选择出所有男生的数据

  • size : 这里的size是指条件的符合完整文档数据展现的数量,即满足query条间的数据,一般如果只关注聚合结果的话,这里可以设置成0,并且设置成0的话,聚合会被自动缓存,下次再进行相同的聚合时,便会快速响应

  • aggs:聚合关键字,与size、query等关键字平级,一个aggs的关键字出现即代表一个桶,aggs的结构较为固定

    aggs关键字:        			#聚合关键字aggregations,一般简写为aggs,效果相同
    	桶名称(自定义):			    #此桶的一个别名,对应聚合结果
       		指标关键字:                   #可以进行指标计算的关键字,一般为terms、avg、max、min等
       			field:                      #具体针对的字段,key为固定的field,值一般填写字段名 
       			size:                       #桶内聚合结果展示数量,不填写的话,默认只展示10条
                ……
    

常用指标关键字:

关键字 作用
terms 最基础的指标,我称之为累加,桶内满足同一条件下数据条数进行求和
max 最大值,求指定字段的桶中数据的最大值
min 最小值,求指定字段的桶中数据的最小值
avg 平均值,求指定字段的桶中的数据的均值
sum 对指定字段的值进行求和

全量指标关键字:

4.1.2 多桶嵌套

一个多桶聚合的例子:

​ 此例子用来求每个班级中,男女生的数量,按着聚合的逻辑,我们应该先对班级进行聚合,以班级为桶,将相同版本的人放在一个桶内,然后在这个桶内再以性别为桶进行划分

{
  "size":0,  
  "aggs":{
      "ClassName":{
          "terms":{
              "field":"classname",
              "size":100
          },
          "aggs":{
              "SexNum":{
                  "terms":{
                      "field":"sex",
                      "size":2
                  }
              }
          }
      }
  }
}

5.ElasticSearch的配置文件

es的配置文件主要包含两个,基础配置和Jvm配置

文件名 相关内容
elasticsearch.yml es的基础运行配置文件,大部分的配置在此
jvm.options es的GC及运行虚拟机相关配置

yml配置文件:官方

#ES集群名称,同一个集群内的所有节点集群名称必须保持一致
cluster.name: ES-Cluster

#ES集群内的节点名称,同一个集群内的节点名称要具备唯一性
node.name: ES-master-01

#允许节点是否可以成为一个master节点,ES是默认集群中的第一台机器成为master,如果这台机器停止就会重新选举
node.master: true

#允许该节点存储索引数据(默认开启)
node.data: true

#数据存储路径(path可以指定多个存储位置,分散存储,有助于性能提升)
#目录还要对elasticsearch的运行用户有写入权限
path.data: /usr/local/elasticsearch-cluster/elasticsearch-a/data
path.logs: /usr/local/elasticsearch-cluster/elasticsearch-a/logs

#在ES运行起来后锁定ES所能使用的堆内存大小,锁定内存大小一般为可用内存的一半左右;锁定内存后就不会使用交换分区
#如果不打开此项,当系统物理内存空间不足,ES将使用交换分区,ES如果使用交换分区,那么ES的性能将会变得很差
bootstrap.memory_lock: true

#es绑定地址,支持IPv4及IPv6,默认绑定127.0.0.1;es的HTTP端口和集群通信端口就会监听在此地址上
network.host: 192.168.3.21

#是否启用tcp无延迟,true为启用tcp不延迟,默认为false启用tcp延迟
network.tcp.no_delay: true
#是否启用TCP保持活动状态,默认为true
network.tcp.keep_alive: true
#是否应该重复使用地址。默认true,在Windows机器上默认为false
network.tcp.reuse_address: true
#tcp发送缓冲区大小,默认不设置
network.tcp.send_buffer_size: 128mb
#tcp接收缓冲区大小,默认不设置
network.tcp.receive_buffer_size: 128mb

#设置集群节点通信的TCP端口,默认是9300
transport.tcp.port: 9301
#设置是否压缩TCP传输时的数据,默认为false
transport.tcp.compress: true
#设置http请求内容的最大容量,默认是100mb
http.max_content_length: 200mb

#是否开启跨域访问
http.cors.enabled: true
#开启跨域访问后的地址限制,*表示无限制
http.cors.allow-origin: "*"
#定义ES对外调用的http端口,默认是9200
http.port: 9201

#Elasticsearch7.x新增参数,写入候选主节点的设备地址,来开启服务时就可以被选为主节点,由discovery.zen.ping.unicast.hosts:参数改变而来
discovery.seed_hosts: ["192.168.3.21:9301", "192.168.3.22:9301","192.168.3.23​:9301"]
#Elasticsearch7新增参数,写入候选主节点的设备地址,来开启服务时就可以被选为主节点
cluster.initial_master_nodes: ["192.168.3.21​:9301", "192.168.3.22:9301","192.168.3.23:9301"]

#Elasticsearch7新增参数,设置每个节点在选中的主节点的检查之间等待的时间。默认为1秒
cluster.fault_detection.leader_check.interval: 2s 
#Elasticsearch7新增参数,启动后30秒内,如果集群未形成,那么将会记录一条警告信息,警告信息未master not fount开始,默认为10秒
discovery.cluster_formation_warning_timeout: 30s 
#Elasticsearch7新增参数,节点发送请求加入集群后,在认为请求失败后,再次发送请求的等待时间,默认为60秒
cluster.join.timeout: 30s
#Elasticsearch7新增参数,设置主节点等待每个集群状态完全更新后发布到所有节点的时间,默认为30秒
cluster.publish.timeout: 90s 
#集群内同时启动的数据任务个数,默认是2个
cluster.routing.allocation.cluster_concurrent_rebalance: 32
#添加或删除节点及负载均衡时并发恢复的线程个数,默认4个
cluster.routing.allocation.node_concurrent_recoveries: 32
#初始化数据恢复时,并发恢复线程的个数,默认4个
cluster.routing.allocation.node_initial_primaries_recoveries: 32

# 开启xpack安全验证
xpack.security.enabled: true
xpack.license.self_generated.type: basic
xpack.security.transport.ssl.enabled: true
# 证书配置
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.keystore.path: certs/elastic-certificates.p12
xpack.security.transport.ssl.truststore.path: certs/elastic-certificates.p12

jvm配置:

#堆内存配置
#Xms表示ES堆内存初始大小
-Xms16g
#Xmx表示ES堆内存的最大可用空间
-Xmx16g                                      
#GC配置
#使用CMS内存收集
-XX:+UseConcMarkSweepGC
#使用CMS作为垃圾回收使用,75%后开始CMS收集
-XX:CMSInitiatingOccupancyFraction=75
#使用手动定义初始化开始CMS收集
-XX:+UseCMSInitiatingOccupancyOnly

6.ElasticSearch集群相关

7.ElasticSearch日志使用

8.关于模糊查询在Es中的体验

es中的模糊查询速度非常的快,快的根源在于es的分词与倒排索引,es在录入数据时,会对数据进行分词,分成最小的词项,再根据词项构建倒排索引,对内容进行模糊查询的时候,本质是对分完的词项查询其倒排索引;以此依据,分词器的不同,可能导致部分的模糊匹配是查询不到数据的。

8.1 keyword和text类型的区别

keyword类型的字段,不会进行分词,是以整体进行存入,本身自身的全部内容就是一个词;

text类型的字段,会在录入数据的时候,es就会进行分词,其内容可能是很多个词条;

8.2 match、match_phrase和term的区别

首先term关键字查询,不会进行分词,是精准匹配,对keyword类型的数据使用,需要精准查询;对text类型的数据使用,无法匹配,必须为text字段分词后中的某一个才行。如“我是猪”分词为["我","是","猪"],term必须为“我”或“是”或“猪”,才能查到,而“我是”、“是猪”不行。对text类型,可以先将其转换为keyword,再进行精准匹配;

match关键字查询,会进行分词,对于keyword类型的数据使用时,因为keyword类型的数据本身就是一个词项,所以必须全内容精准匹配;而对于text类型的,它是支持分词的,并且分词的词也可以无限分词下去,所以是可以进行模糊查询的;

match_phrase关键字查询,会进行分词,对于keyword类型的数据使用时,因为keyword类型的数据本身就是一个词项,所以必须全内容精准匹配;match_phrase 查询text字段,只需要match_phrase 分词结果中和text分词有匹配且查询语句必须包含在text分词结果中,同时顺序相同且连续,才可以查出。如“我真帅”分词为["我","真","帅",“真帅”],match_phrase 的查询语句“真帅”被分词为["真帅"],其中“真帅”能匹配上text字段的分词结果,连续且顺序相同,所以能查出。

9.关于Es的查询分页

9.1 size+from分页

同mysql一样,通过from和size进行分页,使用起来很简单

使用get请求

GET /{索引名}/_search
{
  "from": 0, 
  "size" : 10,
  "query": {
    "bool": {
      "must": [
        {"match": {
          "customer_first_name": "Diane"
        }}
      ],
      "filter": {
        "range": {
          "order_date": {
            "gte": "2020-01-03"
          }
        }
      }
    }
  }, 
  "sort": [
    {
      "order_date": {
        "order": "asc"
      }
    }
  ]
}

不过ES默认的分页深度是10000,也就是说from+size超过10000就会报错

{
  "error": {
    "root_cause": [
      {
        "type": "illegal_argument_exception",
        "reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10009]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
      }
    ],
    "type": "search_phase_execution_exception",
    "reason": "all shards failed",
    "phase": "query",
    "grouped": true,

如果我们的业务场景确实需要超过10000条记录的分页,可以通过index.max_result_window这个参数控制分页深度,我们可以针对特定的索引来修改这个值。

curl -XPUT IP:PORT/index_name/_settings -d '{ "index.max_result_window" :"100000"}'

分页运行原理

在分布式环境下深度分页的查询效率会非常低。比如我们现在查询第from=990,size=10这样的条件,这个在业务层就是查询第990页,每页展示10条数据。

但是在ES处理的时候,会分别从每个分片上拿到1000条数据,然后在coordinating的节点上根据查询条件聚合出1000条记录,最后返回其中的10条。所以分页越深,ES处理的开销就大,占用内存就越大。

9.2 search after分页

有时候我们会遇到一些业务场景,需要进行很深度的分页,但是可以不指定页数翻页,只要可以实时请求下一页就行。比如一些实时滚动的场景。

ES为这种场景提供了一种解决方案:search after。

search after利用实时有游标来帮我们解决实时滚动的问题,简单来说前一次查询的结果会返回一个唯一的字符串,下次查询带上这个字符串,进行下一页的查询。

GET /{索引名}/_search
{
  "size" : 2,
  "query": {
    "bool": {
      "must": [
        {"match": {
          "customer_first_name": "Diane"
        }}
      ],
      "filter": {
        "range": {
          "order_date": {
            "gte": "2020-01-03"
          }
        }
      }
    }
  }, 
  "sort": [
    {
      "order_date": "desc",
      "_id": "asc"

    }
  ]
}

首先查询第一页数据,我这里指定取回2条,条件跟上一节一样。唯一的区别在于sort部分需要追加排序字段,这个是为了在order_date字段一样的情况下告诉ES一个可选的排序方案。因为search after的游标是基于排序产生的。

在下一次的查询中要携带上次查询的一个游标search_after

{
  "size" : 2,
  "query": {
    "bool": {
      "must": [
        {"match": {
          "customer_first_name": "Diane"
        }}
      ],
      "filter": {
        "range": {
          "order_date": {
            "gte": "2020-01-03"
          }
        }
      }
    }
  }, 
  "search_after": 
      [
          1580597280000,
          "RZz1f28BdseAsPClqbyw"
        ],
  "sort": [
    {
      "order_date": "desc",
      "_id": "asc"

    }
  ]
}

比如通过一直下一页,翻到了990页,当继续下页时,因为有了排序的唯一标识,ES只需从每个分片上拿到满足条件的10条文档,然后基于这30条文档最终聚合成10条结果返回即可。

9.3 Scroll api分页

还有一种查询场景,我们需要一次性或者每次查询大量的文档,但是对实时性要求并不高。ES针对这种场景提供了scroll api的方案。这个方案牺牲了实时性,但是查询效率确实非常高。

使用POST接口

POST /{索引名称}/_search?scroll=1m
{
    "size": 10,
    "query": {
        "match_all" : {
        }
    }
}

首先我们第一次查询,会生成一个当前查询条件结果的快照,后面的每次滚屏(或者叫翻页)都是基于这个快照的结果,也就是即使有新的数据进来也不会别查询到。

上面这个查询结果会返回一个scroll_id,拷贝过来,组成下一条查询语句

{
    "scroll" : "1m",
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAA5AWNGpKbFNMZnVSc3loXzQwb0tJZHBtZw=="
}

以此类推,后面每次滚屏都把前一个的scroll_id复制过来。注意到,后续请求时没有了index信息,size信息等,这些都在初始请求中,只需要使用scroll_id和scroll两个参数即可。

很多人对scroll这个参数容易混淆,误认为是查询的限制时间。这个理解是错误的。这个时间其实指的是es把本次快照的结果缓存起来的有效时间。

scroll 参数相当于告诉了 ES我们的search context要保持多久,后面每个 scroll 请求都会设置一个新的过期时间,以确保我们可以一直进行下一页操作。

我们继续讨论一个问题,scroll这种方式为什么会比较高效?

ES的检索分为查询(query)和获取(fetch)两个阶段,query阶段比较高效,只是查询满足条件的文档id汇总起来。fetch阶段则基于每个分片的结果在coordinating节点上进行全局排序,然后最终计算出结果。

scroll查询的时候,在query阶段把符合条件的文档id保存在前面提到的search context里。后面每次scroll分批取回只是根据scroll_id定位到游标的位置,然后抓取size大小的结果集即可。

9.4 对比

  • from/size方案的优点是简单,缺点是在深度分页的场景下系统开销比较大,占用较多内存。
  • search after基于ES内部排序好的游标,可以实时高效的进行分页查询,但是它只能做下一页这样的查询场景,不能随机的指定页数查询。
  • scroll方案也很高效,但是它基于快照,不能用在实时性高的业务场景,建议用在类似报表导出,或者ES内部的reindex等场景。