一. Filter执行原理
filter底层原理涉及到两块内容: bitset机制和cache机制。
1.1 执行流程
假设现在有一批数据的倒排索引如下:
Word Docs
2016 1,4,5,6
2017 1,2,4,5,6
2018 1,2,3
2019 2,3,4,5,6
2020 5
搜索时,使用的filter条件如下:
GET /search
{
"query": {
"constant_score": {
"filter": {
"term": {
"field": 2016
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
1.1.1 构建bitset
首先,ES会从index的倒排索引中遍历Word,找到2016对应的符合搜索条件的Docs——“1,4,5,6”。
接着,ES会将"1,4,5,6"构造成一个bitset。bitset是一个二进制数组,数组的下标对应着document数据存储(插入)的顺序,数组的容量由document的数量来决定,在bitset中,0代表不符合搜索条件,1代表符合搜索条件。
比如本例中,ES将会构建一个容量为6的二进制数组,假设document插入的顺序为5,2,4,6,1,3,那么当前filter对应的bitset数组内的组成情况是: [1,0,1,1,1,0]
1.1.2 遍历bitset
一次搜索中,可能会出现多个filter,每一个filter都会对应一个bitset。ES会从最稀疏的bitset开始遍历,以达到最佳的执行效率。
bitset的稀疏性指的是包含符合过滤条件的document的数量,可以从bitset数组内1的数量体现出来。比如[1,0,1,1,1,0] 就比[1,1,1,1,1,1,]要更稀疏。
为什么从最稀疏的bitset开始过滤?因为过滤的目的是为了筛选出符合条件的数据,丢弃无用的数据。bitset越稀疏,代表筛选条件越严格,本次过滤的垃圾数据就越多。
ES5中,query内不能直接连接filter,query和filter之间需要通过bool来间隔。ES7中,and不能写在query中,所以坦白说,目前我没有发现一次搜索中出现多个filter的写法。况且完全可以在一个filter内通过bool条件达到写多个过滤条件的目前,因此没必要写多个filter
1.1.3 缓存bitset
ES会缓存bitset数据到内存中,如果后续的搜索语句中filter过滤条件在之前已经使用过,那么ES会从缓存中直接找到对应的bitset,并根据bitset数组的内容,直接通过_id搜索目标document并返回结果集。ES缓存的是bitset数组,而不是通过filter搜索出的结果。
不是所有的bitset都会被ES缓存到内存中,不进行缓存的bitset大致分为以下两种情况:
情况1: 整个index内存储document的数量不足1000条。
情况2: 索引分段后的数据不足索引中所有数据的3%。
正是因为bitset缓存,使得filter的执行效率比不使用filter时query要高。
注意: ES在缓存bitset的同时,会返回搜索结果。换句话说,"缓存"和"根据bitset的内容查询、返回结果集"这两步操作是并发执行的。
1.2 执行特性
1.2.1 query和filter的执行顺序
一般来说,ES会优先执行filter,,通过filter过滤掉一部分不符合搜索条件的数据,再执行query。况且query需要计算相关度分数来进行排序,因此执行效率低。如果先执行query,那么就会把那些不符合搜索条件的数据也进行了相关度分数计算,浪费性能。
1.2.2 bitset cache auto_update
ES缓存的bitset后,不会置之不理。如果有修改、新增、删除操作,导致原先fitler对应的bitset结构或内容发生变化时,ES会及时的更新内存中缓存的bitset,值得注意的是,这个更新操作由ES自动完成。
1.2.3 bitset cache应用的时机
只要ES中执行的query内包含filter,那么ES首先就会在缓存中搜索该filter条件是否曾经执行过,有没有对应的bitset缓存存在,若有,则使用缓存,若没有,则创建bitset,查询、返回数据,同时缓存bitset。
二. 倒排索引
2.1 倒排索引的结构:
(1)包含这个关键词的document list
(2)包含这个关键词的所有document的数量:IDF(inverse document frequency)
(3)这个关键词在每个document中出现的次数:TF(term frequency)
(4)这个关键词在这个document中的次序
(5)每个document的长度:length norm
(6)包含这个关键词的所有document的平均长度
2.2 倒排索引不可变的好处:
(1)不需要锁,提升并发能力,避免锁的问题
(2)数据不变,一直保存在os cache中,只要cache内存足够
(3)filter cache一直驻留在内存,因为数据不变,同一个过滤条件下得到的bitset一定不会变。
(4)可以压缩,节省cpu和io开销
三. 重建索引
没有哪一个架构师敢说自己设计的index完美无缺,经过一段时间的使用后,往往会根据实际的业务场景,对原有结构进行调整。但遗憾的是,ES不允许对已存在的字段进行修改和删除,比如不能修改字段的类型,字段的分词器,增加新的子字段等操作,原因是这样会倒排索引的结构发生变化。
重建索引的方法如下: 新建一个index,为其设置定制化的mapping,接着将旧数据从旧索引中迁移到新索引中。迁移数据的方法非常多,比如通过scroll滚动读取数据,接着通过bulk POST create操作,将数据批量新增至新索引。
但重建索引不可避免的带来一个问题: 需要重新创建一个新索引。如果程序中操作ES的代码在书写index时使用了硬编码,那么重建索引就会导致整个系统不可用,必须修改代码再重启系统才能恢复功能。为了避免重启系统,实现零停机,我们需要在程序中使用index的别名。
四. Document写入原理
ES为了实现进实时搜索,在写入Document时利用了Buffer(内存),OS Cache(系统缓存 集成于CPU),Disk(磁盘)三种存储方式,尽可能的提升搜索的能力。ES的底层lucene实现的,在 luncene中一个index会被分为若干个数据段segment,每一个segment都会存放index的部分document。从流程上讲,ES会先把一个index中的document分散存储在若干个shard(指的是主分片)上,在shard中,使用若干个segment来存储具体的数据。
ES写入数据的流程大致如下:
客户端发起请求(增、删、改)到ES中。
ES将本次请求要操作的document写入到buffer中。ES为了保证搜索的近实时(Near Real Time 简称 NRT),默认每秒刷新一次buffer,这个刷新时间间隔可以手动修改,也可以通过命令触发buffer的刷新。建议刷新时间间隔设置在1秒左右,好处使在服务器宕机后,只会丢失1秒左右的数据。
POST /index_name/_refresh
PUT index_name
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1,
"refresh_interval": "1s"
}
}
1
2
3
4
5
6
7
8
9
10
ES在将document写入到缓存的同时,也会将本次操作的具体内容写入到translog文件中,这份文件存在的意义在于即便ES宕机了,也能尽可能的减少丢失的数据(简单来说,就是把translog中的记录重新执行一遍),当然translog也不能保证数据绝对不丢失,其原因在第6点详细的讲出了。由于translog存储在磁盘Disk中,因此为了提高访问效率,ES与translog文件之间会建立并保持一个长连接(不然每次访问都要获取和释放文件流)。
步骤2中提到过ES每隔一段时间就会刷新buffer,这个刷新的动作会在内存中创建一个全新的index segment,并将buffer中的document数据全部到这个新的index segment中。值得注意的是,index segment同样是文件,只不过我们目前访问的是内存中的File(ES底层使用java开发,new File()后文件会被读取到内存当中)。
segment中存储的是buffer指定时间间隔内接收到的document写操作数据(因为Disk与内存有速度差,为了让数据持久化落盘的速度适应数据写入内存的速度,我们使用了buffer,index segment,比如数据写入了10秒,默认每秒刷新一次buffer,则产生10个index segment,而来得及写入Disk的index segment可能只有2个)。
在index segment被创建并写入了来自buffer的数据后,ES会立刻将index segment对应的File写入到系统缓存OS Cache中并打开,这样就可以立刻为客户端提供最新数据的请求服务,而不必等待index semeng写入到磁盘后,再打开index segment。毕竟IO操作是一个重量级的操作,非常费时,一定会影响ES的近实时搜索能力。
前面说了,translog中记录的是ES操作的过程,万一遇到系统宕机,在系统重启后,ES会重新读取磁盘Disk中保存的数据(一份份的 index segment文件)至系统缓存,接着读取translog中的操作日志,并逐条执行,以此来达到恢复数据的目的。ES默认每隔5秒执行一次translog文件的持久化。如果在持久化的过程中恰好ES在做document写操作,那么本次持久化操作将暂停,直到写操作彻底完成后,才继续执行。translog的持久化方式默认同步,如果修改成异步,那么在translog持久化的过程中新执行的写操作对应的日志就会丢失,如果恰好此时ES所在的服务器宕机,那么这段时间还没来得及持久化到Disk,仅位于内存中index segment或OS Cache缓存中的数据便无法恢复,永久丢失。
PUT index_name
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1,
"index.translog.durability" : "async",
"index.translog.sync_interval" : "5s"
}
}
1
2
3
4
5
6
7
8
9
随着时间的推移,translog文件会不断的增大,在内存中积压的数量众多的index segment file的文件流也在不断的增大,当translog文件大到一定程度或默认30分钟执行一次,ES会自动触发commit操作。commit操作的具体内容有:
将buffer中的数据刷新到一个新的index segment中;
将index segment写入到OS Cache并打开index segment为搜索提供服务;
执行一个commit point操作,将OS Cache中所有的index segment标识记录在这个commit point中,并持久化到系统磁盘Disk;
commit point操作会触发fsync操作(file sync),将内存中已经写入数据的index segment落盘到Disk,持久化成文件。
清空本次持久化的index segment对应在translog中的日志。
按照上述的流程来看,每1秒会生成一个index segment文件,每30分钟会将index segment文件流持久化到磁盘,照这样来看,磁盘中的index segment文件会非常多,从而需要处于开启状态的index segment也非常多,在执行搜索操作时,找到数据对应的index segment就会比较费时了。为了解决这个问题,ES会自动的执行segment merge操作,merge时,被标记为deleted状态的document会被物理删除。
merge的大致流程如下:
ES会选取一些大小相近的segment文件流,合并成一个大的segment文件流(注意: segment可能是尚未持久化到磁盘的segment file,也可能是已经持久化到磁盘的segment file)。
执行commit操作,在Disk中记录commit point,这个commit point不仅包含新增的segment,还包含合并后,需要被删除的segment源文件的标识。
commit操作结束后,ES会将merge后的segment文件重新打开,为搜索提供服务,而那些旧的需要被删除的segment文件则进行关闭并物理删除。
此外,ES在执行search搜索时,目标数据可能位于不同的index segment上,因此ES会扫描所有已经开打的index segment文件并找到目标数据。文件打开指的是将数据读到了OS Cache系统缓存,而不是内存中。
对index执行document的删除和更新操作都不会立刻生效,而是标记成deleted状态。当ES空闲(比如每隔30分钟执行一次commit)或Disk存储空间不足,ES就会物理删除这些待删除的数据。(猜测在内存不足时,也会执行删除操作,因为总不能让尚未落盘的index segment大量的占用内存吧)
在buffer数据写入到segment的同时,会生成一个.del文件专门记录哪一个index segment中哪一条document是deleted状态(在merge后,这个.del文件会被更新)。因此ES搜索时,如果在多个index segment中查到了不同版本(version)的相同id值的document时,会根据.del文件中的记录来继续过滤,保证搜索结果的唯一性和正确性(比如segment1中包含一条document version=1,对应新增状态;而segment2中包含相同id的document version=2,对应更新状态。由于后者的版本号更新,因此在.del中,version1被视作旧document,会被标记成deleted状态,从而在搜索时就会得到segment2中包含的version=2的数据了)。
五. 缓存原理
Elasticsearch的缓存主要分成三类: Node Query Cache, Shard Query Cache, Fielddata Cache
5.1 Node Query Cache
5.2 Shard Query Cahce
5.3 Fielddata Cache
六. 相关度评分算法
6.1 概念
relevance score算法用于计算document与搜索条件的相关匹配度。
ES使用了term frequency / inverse document frequency 算法,简称TF/IDF算法,它是ES相关度评分算法的一部分,也是 最重要的一部分。
TF 对搜索条件进行分词后,各词条在document中指定的field内出现的次数越多,则相关度越高。
eg: 搜索条件为hello es,document1对应 {“field_test”: “hello world, I am learning es”},document2对应{“field_test”: “hello Wuhan”}。
分析:由于搜索条件分词后,在document1中出现了2次,在document2中出现了1次,因此document1的相关度分数比document2高。
IDF 对搜索条件进行分词后,统计各词条在所有document中出现的次数,出现的次数越多,则该词条在后续用于评定相关度分数时,起到的作用越低。
eg: 搜索条件为hello es,document1对应 {“field_test”: “hello world, I am learning something new”},document2对应{“field_test”: “hello es”} 其中,hello在index中出现了1000次,es出现了100次。
分析:由于es比hello的相关度评定价值更高,因此document2比document1的相关度分数要高。
Field-length norm: 在匹配成功后,计算field字段的数据长度,长度越长,则相关度分数越低。Field-length-norm是TF的一部分。
eg: 搜索条件为hello es,document1对应 {“field_test”: “hello world, I am learning something new”}, document2对应{“field_test”: “heelo es”}
分析:document1和document2都包含了相同数量的搜索词条,但是前者除了目标词条外,还包含了许多无用的数据信息,因此document1的相关度分数比document2要低。
七. Doc values
ES存储document时,会根据数据对应的field类型建立对应的索引。通常来说只创建倒排索引,倒排索引是为了搜索而存在的,但如果对数据进行排序、聚合、过滤等操作时,再使用倒排索引就明显不适合了。这个时候就需要在ES中创建正排索引(doc values)。doc values保存在磁盘中,如果OS Cache系统缓存的空间足够大,ES会缓存doc values,因此性能还是很不错的。
问: 为什么说倒排索引不适合做聚合、排序等操作?
答: 比如有数据如下:
{
“name” : “张三”,
“remark” : “java开发工程师”,
"_id": 1
}
{
“name” : “李四”,
“remark” : “java架构工程师”,
"_id": 2
}
1
2
3
4
5
6
7
8
9
10
为remark字段创建倒排索引:
Term Doc
java 1,2
开发 1
架构 2
工程师 1,2
如果在倒排索引的基础上进行聚合,那么到底是根据java进行聚合呢、还是根据开发或者架构来聚合呢?不难发现,用哪一个都不合适。排序也是如此。倒排索引因分词得到了许多好处,但也因此留下了弊端。
所以,为了应对这种不需要分词的需求和场景,ES设计了正排索引Doc values。Doc values不会对字段作任何分词处理,皆保留原值。正排索引的大致结构如下:
Doc values terms
Doc1 java开发工程师
Doc2 java架构工程师
ES会根据document中每个字段是否分词,有选择性的实现字段对应的倒排索引或正排索引(Doc values)。
比如,如果字段类型为keyword,long,date,那么这些类型修饰的字段一定会有正排索引(Doc values)。
比如,如果字段类型为text,则只会有倒排索引,不会主动创建正排索引。
如果想在同一个字段中,既实现正排索引,又实现倒排索引,只需要使用fielddata即可,实现方式如下:
若遇到聚合、排序等需求时,ES使用test_field.keyword,若遇到搜索需求时,ES使用test_field。
{
"mappings": {
"type": {
"properties": {
"test_field": {
"type": "text",
"analyzer": "standard",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
八. 搜索技术深入
8.1 手工控制搜索结果的精准度
8.1.1 搜索包含查询条件分词后所有词条的数据
搜索包含first或content词条的document:
GET /test_sort/_search
{
"query": {
"content": {
"remark": "first content"
}
}
}
1
2
3
4
5
6
7
8
升级: 搜索包含first和content词条的document:
GET /test_sort/_search
{
"query": {
"match": {
"content": {
"query": "first content",
"operator": "and"
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
上述搜索方式中,如果把operator设置成or,则查询的接口与不使用operator没有区别。and表明查询的document中必须既包含first,又包含content。(注意: 并不强制要求first和content必须紧挨着出现)
8.1.2 搜索包含查询条件分词后一定比例数量的词条的数据
在搜索document时,希望目标数据包含一定比例的查询条件分词后的词条个数,可以使用minimum_should_match,填入百分比或固定数字来实现。百分比代表需要包含搜索条件中词条的个数的百分比,如果无法整除,则向下匹配。固定数字代表至少需要包含多少个词条。
GET /test_sort/_search
{
"query": {
"match": {
"content": {
"query": "first content",
"minimum_should_match": "50%"
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
minimum_should_match可以和bool should搭配使用,should本身代表着多个条件只需要满足一个即可,如果使用了minimum_should_match=2,则代表至少满足2个条件才能被视为目标数据。
GET /test_sort/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"content": "first"
}
},
{
"match": {
"content": "C++"
}
},
{
"match": {
"content": "second"
}
}
],
"minimum_should_match": 2
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
8.1.3 match的底层转换
官方建议搜索时,尽量使用转换后的形式,执行效率更高(不需要ES自行转换了)。 在条件较少时转换请求的数据结构不会带来太明显的性能提升,但如果条件非常多,那么节约的时间就很可观了。
转换前1:
GET /test_sort/_search
{
"query": {
"content": {
"content": "first content"
}
}
}
1
2
3
4
5
6
7
8
转换后1:
GET /test_sort/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"content": {
"value": "first"
}
}
},
{
"term": {
"content": {
"value": "content"
}
}
}
]
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
转换前2:
GET /test_sort/_search
{
"query": {
"match": {
"content": {
"query": "first content",
"operator": "and"
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
转换后2:
GET /test_sort/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"content": {
"value": "first"
}
}
},
{
"term": {
"content": {
"value": "content"
}
}
}
]
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
8.2 boost权重控制
人为的控制搜索条件在进行相关度分数计算时的权重大小。
比如搜索document中content字段内包含first的数据,如果content包含java或C++,则优先显示包含它们的数据,并且java的权重是C++的三倍。
boost权重控制一般用于搜索时搭配相关度排序使用。比如: 电商平台对商品进行综合排序。将一个商品的销量、广告费、评价值、库存、单价等信息进行比较并综合排序。排序时,库存的权重最低,广告费的权重最高。
GET /test_sort/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "first"
}
}
],
"should": [
{
"match": {
"content": {
"query": "java",
"boost": 3
}
}
},
{
"match": {
"content": {
"query": "C++",
"boost": 1
}
}
}
]
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
搜索结果为:
"hits" : [
{
"_index" : "test_sort",
"_type" : "sort_type",
"_id" : "4",
"_score" : 3.2636642,
"_source" : {
"content" : "first content java"
}
},
{
"_index" : "test_sort",
"_type" : "sort_type",
"_id" : "5",
"_score" : 1.8323578,
"_source" : {
"content" : "first content C++"
}
},
{
"_index" : "test_sort",
"_type" : "sort_type",
"_id" : "1",
"_score" : 0.48120394,
"_source" : {
"content" : "first content",
"order" : 1
}
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
8.2.1 多shard环境中相关度分数不准确问题
在 ES的搜索结果中,相关度分数不是一定准确的。在多个shard环境中,使用相同的搜索条件得到的相关度分数可能会有误差(如果index只有一个主分片,则不会出现误差)。只要数据量达到一定的程度,那么相关度分数的误差就会逐渐趋近于0。
请看以下场景:
现在有两个主分片: shard0和shard1,它们分别持有10000条document数据,其中,shard0含有100个包含java的document,shard1含有10个包含java的document。ES在shard本地计算相关度分数(不是把数据统一上报到coordinate协调节点后,才进行相关度分数的计算),当以java作为条件进行搜索时,虽然经过TF计算出的值相同,但使用IDF算法计算相关度分数时,shard0内的得分比shard1低。
如果数据量足够大,使得含有java的document数据均匀的分布在所有分片上时,那么无论在哪个shard上计算相关度分数,得到的结果就都是相同的了。
在开发测试阶段,我们可以通过在设置settings时,将number_of_shards的值设置为1来解决问题,也可以通过增加dfs_query_then_fetch请求参数来解决问题:
GET /test_sort/_search?search_type=dfs_query_then_fetch
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "first"
}
}
]
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
search_type=dfs_query_then_fetch不建议在生产环境中使用,因为它会使相关度分数的计算时机从shard本地搜索目标数据,挪到每个shard反馈到协调节点后,由协调节点统一做相关度分数的计算。由于所有的目标结果都会在协调节点汇总,因此协调节点的数据量非常大,而为了计算相关度分数又不得不把数据全部读取到内存中,因此这种方式不仅对内存的压力非常大,还会增加额外的IO开销。这就是ES官网明明知道多shard时相关度分数不准确的问题,却不得不在shard本地计算相关度分数的原因。
8.3 基于dis_max实现多字段搜索
8.3.1 dis_max的基本用法
概念
best fields策略: 搜索的document中的某一个field字段,尽可能多的匹配搜索搜索条件。
most fields策略: 尽可能多的让document中多个field参与到计算相关度分数中。显然,这种策略下搜索的精准度没有best field高。比如某个document中,虽然有一个字段包含了非常多的关键字(由搜索条件提供),但其它字段没有包含关键字。而其它document与之对应的字段中虽然没有包含这么多关键字,但胜在包含关键字的字段数目多,因此在搜索结果返回时,会被排列到前一个document之前。百度就搜索利用了most fields策略,因为往往匹配维度多的document比只匹配了寥寥几个维度(哪怕这些维度匹配的非常精准)的document,对用户更有用。
best field的实现手段: dis_max 对每个搜索条件进行评估,以单独作为条件进行搜索时,得到的最高相关度分数进行排序。
例子 单独使用条件1和条件2进行搜索时,各document计算出的相关度分数如下:
document1 document2
条件1 0.5 1.2
条件2 1.1 0.3
最大值 1.1 1.2
现在想把条件1和条件2结合在一起使用,共同进行搜索,但是希望document1得到的相关度分数为1.1,document2得到的相关度分数为1.2,各个条件互不影响,最终得到的排序结果为: document2 -> document1
做法是在搜索时使用dis_max。 可以注意到,使用dis_max时,不再配合使用should或must,而是使用queries。
GET test_dis_max/_search
{
"query": {
"dis_max": {
"queries": [
{
"条件1"
},
{
"条件2"
}
]
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
8.3.2 使用tie_breaker参数优化dis_max的搜索效果
dis_max是只取多个query条件中相关度分数最高的用于排序,忽略其它的query条件对应的分数。在某些情况下,我们还需将其它query条件对应的分数加入到最后相关度分数的计算上,这个时候就可以使用tie_breaker参数来优化dis_max的功能。郑州不孕不育医院:http://www.xasgnk.com/
tie_breaker参数的含义是:将其它所有搜索条件对应的相关度分数乘以一个比例,再参与到结果的排序中。默认参数值为0,因此不定义此参数,会导致其它搜索条件不参与到结果的排序中。
GET test_dis_max/_search
{
"query": {
"dis_max": {
"queries": [
{
"条件1"
},
{
"条件2"
}
],
"tie_breaker" : 3
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
此时,相关度分数的计算如下:
document1 document2
条件1 0.5 1.2
条件2 1.1 0.3
最大值 1.1 1.2
最终结果 1.1 + 0.5*3 = 2.6 1.2 + 0.3*3 = 2.1
最终得到的排序结果为: document1 -> document2
8.3.3 使用multi_match简化dis_max+tie_breaker
可以通过multi_match整合boost, query, minimum_should_match, tie_breaker以及fields。使用方式如下: