023.基于IT论坛案例学习Elasticsearch(二):Query高级知识(一)

时间:2022-07-23
本文章向大家介绍023.基于IT论坛案例学习Elasticsearch(二):Query高级知识(一),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1. 准备测试数据

PUT article
{
  "mappings": {
    "_doc": {
      "properties": {
        "articleId": {
          "type": "keyword"
        }
      }
    }
  }
}

POST /article/_doc/_bulk
{"index": {"_id": 1}}
{"articleId": "XHDK-A-1293-#fJ3", "userId": 1, "hidden": false, "postDate": "2017-01-01", "tag": ["java", "elasticsearch"], "tag_cnt": 2, "view_cnt": 30, "title" : "this is java and elasticsearch blog"}
{"index": {"_id": 2}}
{"articleId": "KDKE-B-9947-#kL5", "userId": 1, "hidden": false, "postDate": "2017-01-02", "tag": ["java"], "tag_cnt": 1, "view_cnt": 50, "title" : "this is java blog"}
{"index": {"_id": 3}}
{"articleId": "JODL-X-1937-#pV7", "userId": 2, "hidden": false, "postDate": "2017-01-01", "tag": ["elasticsearch"], "tag_cnt": 1, "view_cnt": 100, "title": "this is elasticsearch blog"}
{"index": {"_id": 4}}
{"articleId": "QQPX-R-3956-#aD8", "userId": 2, "hidden": true, "postDate": "2017-01-02", "tag": ["java", "elasticsearch", "hadoop"], "tag_cnt": 3, "view_cnt": 80, "title": "this is java, elasticsearch, hadoop blog"}
{"index": {"_id": 5}}
{"articleID": "DHJK-B-1395-#Ky5", "userID": 3, "hidden": false, "postDate": "2017-03-01", "tag": ["spark"], "tag_cnt": 1, "view_cnt": 10, "title": "this is spark blog"}

2. 手动控制搜索精确度

# 搜索标题中包含java或elasticsearch的blog
# 4条结果
GET /article/_doc/_search
{
  "query": {
    "match": {
      "title": "java elasticsearch"
    }
  }
}

# 搜索标题中包含java和elasticsearch的blog
# 2条结果
# 如果希望所有的搜索关键字都要匹配,那么就用and
GET /article/_doc/_search
{
  "query": {
    "match": {
      "title": {
        "query": "java elasticsearch",
        "operator": "and"
      }
    }
  }
}

# 搜索包含标题中java,elasticsearch,spark,hadoop,4个关键字中,至少3个的blog
# 1条结果
GET /article/_doc/_search
{
  "query": {
    "match": {
      "title": {
        "query": "java elasticsearch spark hadoop",
        "minimum_should_match": "75%"
      }
    }
  }
}

# 用bool组合多个搜索条件
# 查询标题中必须包含"java",必须不包含"spark","hadoop"和"elasticsearch"包含不包含都可以的帖子
GET /article/_doc/_search
{
  "query": {
    "bool": {
      "must": {
        "match": {"title": "java"}
      },
      "must_not": {
        "match": {"title": "spark"}
      },
      "should": [
        {"match": {"title": "hadoop"}},
        {"match": {"title": "elasticsearch"}}
      ]
    }
  }
}

# 结果
{
  "took" : 10,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 1.449981,
    "hits" : [
      {
        "_index" : "article",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.449981,
        "_source" : {
          "articleId" : "QQPX-R-3956-#aD8",
          "userId" : 2,
          "hidden" : true,
          "postDate" : "2017-01-02",
          "tag" : [
            "java",
            "elasticsearch",
            "hadoop"
          ],
          "tag_cnt" : 3,
          "view_cnt" : 80,
          "title" : "this is java, elasticsearch, hadoop blog"
        }
      },
      {
        "_index" : "article",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.5753642,
        "_source" : {
          "articleId" : "XHDK-A-1293-#fJ3",
          "userId" : 1,
          "hidden" : false,
          "postDate" : "2017-01-01",
          "tag" : [
            "java",
            "elasticsearch"
          ],
          "tag_cnt" : 2,
          "view_cnt" : 30,
          "title" : "this is java and elasticsearch blog"
        }
      },
      {
        "_index" : "article",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.19856805,
        "_source" : {
          "articleId" : "KDKE-B-9947-#kL5",
          "userId" : 1,
          "hidden" : false,
          "postDate" : "2017-01-02",
          "tag" : [
            "java"
          ],
          "tag_cnt" : 1,
          "view_cnt" : 50,
          "title" : "this is java blog"
        }
      }
    ]
  }
}

bool组合多个搜索条件,如何计算relevance score(相关度分数)?

relevance score = must和should搜索对应的分数加起来 / must和should的总数

排名第一:标题包含"java",同时包含should中所有的关键字即"hadoop"和"elasticsearch" 排名第二:标题包含"java",同时包含should中的任何一个关键字 排名第三:标题包含"java",不包含should中的任何关键字

should是可以影响相关度分数的,根据must的条件去计算出document对这个搜索条件的relevance score,在满足must的基础之上,should中的条件,不匹配也可以,但是如果匹配的更多,那么document的relevance score就会更高

# 使用bool组合多个搜索条件,控制全文检索的精确度
# 搜索标题中至少包含"java"、"hadoop"、"spark"、"elasticsearch"其中3个关键字的帖子
GET /article/_doc/_search
{
  "query": {
    "bool": {
      "should": [
        {"match": {"title": "java"}},
        {"match": {"title": "elasticsearch"}},
        {"match": {"title": "hadoop"}},
        {"match": {"title": "spark"}}
      ],
      "minimum_should_match": 3
    }
  }
}

默认情况下,在有must的情况下,should可以不匹配任何条件,在没有must的情况下,多个should的条件必须满足其中一个,可以使用"minimum_should_match"参数来精确控制should的行为。

minimum_should_match:

  • 正数,例如3,那么should的多个条件中必须满足3个条件
  • 负数,例如-2,代表可以有2个条件不满足,其他都应该满足
  • 百分比正数:代表should条件总数的百分比个条件应该满足,例如总共10个条件,百分比为30%,那么至少3个条件应该满足,需满足条件的个数向下取整
  • 百分比负数:代表占此比例的条件可以不满足,其余的均需要满足,计算结果向下取整
  • 百分比和数字组合:3<90%,如果条件个数<=3,那么必须全部满足,否则,满足90%(向下取整)即可
  • 多个组合(空格隔开):2<-25% 9<-3,如果条件个数<=2,则必须都满足,如果条件个数为[3,9],则需要25%的条件满足,否则,只能有3个条件不满足,其余都需要满足

3.match query的自动转化

# 在使用match query进行多值搜索的时候,es会在底层自动将match query转换为bool的语法
{
  "match": {"title": "java elasticsearch"}
}
# 转化为
{
  "bool": {
    "should": [
      {"term": {"title": "java"}},
      {"term": {"title": "elasticsearch"}},
    ]
  }
}

{
  "match": {
    "title": {
      "query": "java elasticsearch",
      "operator": "and"
    }
  }
}
# 转换为
{
  "bool": {
    "must": [
      { "term": { "title": "java" }},
      { "term": { "title": "elasticsearch"}}
    ]
  }
}

{
  "match": {
    "title": {
      "query":  "java elasticsearch hadoop spark",
      "minimum_should_match": "75%"
    }
  }
}
# 转换为
{
  "bool": {
    "should": [
      { "term": { "title": "java" }},
      { "term": { "title": "elasticsearch"}},
      { "term": { "title": "hadoop" }},
      { "term": { "title": "spark" }}
    ],
    "minimum_should_match": 3 
  }
}

4. boost:搜索条件权重控制

需求:搜索标题中包含"blog"的帖子,同时如果标题中包含"java"、hadoop"、"elasticsearch"或者"spark"也可以,但包含"spark"的帖子要求它被优先搜索出来

知识点,搜索条件的权重,boost,可以将某个搜索条件的权重加大,此时当匹配这个搜索条件和匹配另一个搜索条件的document,计算relevance score时,匹配权重更大的搜索条件的document,relevance score会更高,也就会优先被返回

默认情况下,搜索条件的权重都是一样的,都是1

GET /article/_doc/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "blog"}}
      ],
      "should": [
        {"match": {"title": "java"}},
        {"match": {"title": "hadoop"}},
        {"match": {"title": "elasticsearch"}},
        {
          "match": {
            "title": {
              "query": "spark",
              "boost": 5.0
            } 
          }
        }
      ]
    }
  }
}

5. 多shard场景下relevance score不准确问题

如果你的一个index有多个shard的话,可能搜索结果会不准确,原因如下:

当一个搜索请求被转发到一个shard后,会这样计算每条document的relevance score:

  • 计算关键词在这条docuemnt的指定field中出现的次数(TF)
  • 计算关键词在此shard的所有docuemnt的指定field中出现的次数(IDF)
  • 注意:某个shard中只是包含一个index的部分document,而在默认情况下,IDF就是在shard本地进行计算

relevance score与TF成正比,与IDF成反比,在不考虑其他因素的前提下(relevance score不仅与TF和IDF有关),我们可以认为score=TF/IDF

假设在A shard中,所有"title"中包含"java"关键词的doucment,在某一条document中,"java"在"title"字段中出现了10次,但是在A shard中,"java"在所有的document的"title"字段中出现了100次,那么在A shard中,score=10/100=0.1

假设在B shard中,所有"title"中包含"java"关键词的doucment,在某一条document中,"java"在"title"字段中出现了1次,但是在B shard中,"java"在所有的document的"title"字段中也出现了1次,那么在B shard中,score=1/1=1

这样就造成了结果的不准确,应该是A shard中的那条document的score比B shard中的docuemnt的score高,造成这种现象的原因就是IDF是在一个shard的本地计算的,如果是在所有的shard中计算就不会有这个问题

解决办法:

  • 生产环境下,数据量大,尽可能实现均匀分配 数据量很大的话,一般情况下,ES都是在多个shard中均匀路由数据的,路由的时候根据_id,进行负载均衡,比如说有10个document,其"title"都包含"java"关键词,一共有5个shard,如果负载均衡的话,每个shard都应该有2个document,这样计算出来的结果就没有问题了
  • 测试环境下,将索引的primary shard设置为1个,如果说只有一个shard,当然也就没有这个问题了
  • 测试环境下,搜索附带search_type=dfs_query_then_fetch参数,此参数的作用是计算IDF的时候,计算全局的IDF而非本地的IDF,这样可以解决这个问题,但是会带来性能问题,在生产环境不推荐使用

6. dis_max:实现搜索的best_fields策略

6.1 dis_max

# 为帖子增加"content"字段
POST /article/_doc/_bulk
{"update": {"_id": "1"}}
{"doc": {"content": "i like to write best elasticsearch article"}}
{"update": {"_id": "2"}}
{"doc": {"content": "i think java is the best programming language"}}
{"update": {"_id": "3"}}
{"doc": {"content": "i am only an elasticsearch beginner"}}
{"update": { "_id": "4"}}
{"doc": {"content": "elasticsearch and hadoop are all very good solution, i am a beginner"}}
{"update": { "_id": "5"}}
{"doc": {"content": "spark is best big data solution based on scala ,an programming language similar to java"}}

# 搜索title或content中包含java或solution的帖子
GET /article/_doc/_search
{
  "query": {
    "bool": {
      "should": [
        {"match": {"title": "java solution"}},
        {"match": {"content": "java solution"}}
      ]
    }
  },
  "_source": ["title", "content"]
}

# 结果
# 1. doc2(score=0.95): title匹配"java",content匹配"java"
# 2. doc4(score=0.81): title匹配"java",content匹配"solution"
# 3. doc5(score=0.58): title无匹配,content匹配"java"和"solution"
# 4. doc1(score=0.29): title匹配"java",content无匹配
# 我们期望的是doc5排名靠前,因为content同时匹配了"java"和"solution"
# 而实际却是doc2和doc4排名更靠前

# 分析一下doc4和doc5的socre
# score=每个query的分数 * 匹配到条件的个数 / 总条件个数
# 假设在每个查询条件中,匹配到一个单词得分1
# 那么doc4得分为(1+1)*2/2=2
# 同理doc5得分为(0+2)*1/2=1
# 于是doc4就排在了doc5之前
  • dis_max query:搜索到的结果,如果某一个field中匹配到了尽可能多的关键词,那么它应该评分更高,而不是尽可能多的field匹配到了少数的关键词就排在了前面
  • dis_max的原理是:多个query中,得分最高的query的分数,就是最终的分数
GET /article/_doc/_search
{
  "query": {
    "dis_max": {
      "queries": [
        {"match": {"title": "java solution"}},
        {"match": {"content": "java solution"}}
      ]
    }
  },
  "_source": ["title", "content"]
}

# 结果
# 1. doc2(score=0.75): title匹配"java",content匹配"java"
# 2. doc4(score=0.64): title匹配"java",content匹配"solution"
# 3. doc5(score=0.58): title无匹配,content匹配"java"和"solution"
# 4. doc1(score=0.29): title匹配"java",content无匹配
# 说明
# 这里没有达到我的预期效果,预期应该是doc5的排名应该更靠前,这可能与影响评分的其他因素有关
# 但是doc2和doc4的得分都下降了,说明dis_max还是有有效果的

6.2 tie_breaker

# 换一个搜索条件 
GET /article/_doc/_search
{
  "query": {
    "dis_max": {
      "queries": [
        {"match": {"title": "java beginner"}},
        {"match": {"content": "java beginner"}}
      ]
    }
  },
  "_source": ["title", "content"]
}

# 结果
# 1. doc2(score=0.75): title匹配java,content匹配java
# 2. doc4(score=0.64): title匹配java,content匹配beginner
# 3. doc5(score=0.29): title不匹配,content匹配java
# 4. doc1(score=0.29): title匹配java,content不匹配
# 5. doc3(score=0.29): title不匹配,content匹配beginner

# tie_breaker
# 这里结果还是比较符合预期的,以下是一种不符合预期的结果:
# 1. docA: title匹配java,content不匹配
# 1. docB: title不匹配,content匹配beginner
# 2. docC: title匹配java,content匹配beginner
# 对于上述结果,我们期望的可能是docC排在其他两个doc之前,这就需要使用tie_breaker
# dis_max只取某一个query最大的分数,完全不考虑其他query的分数
# 使用tie_breaker可以将其他query的分数也考虑进去
# tie_breaker参数的作用:
# 将其他query的分数乘以tie_breaker的值
# 然后与最高的分数综合在一起进行计算
# 除了取最高分以外,还会考虑其他的query的分数
# tie_breaker的值,在0~1之间,是个小数

GET /article/_doc/_search
{
  "query": {
    "dis_max": {
      "queries": [
        {"match": {"title": "java beginner"}},
        {"match": {"content": "java beginner"}}
      ],
      "tie_breaker": 0.3
    }
  },
  "_source": ["title", "content"]
}

# 结果
# 1. doc2(score=0.81): title匹配java,content匹配java
# 2. doc4(score=0.69): title匹配java,content匹配beginner
# 3. doc5(score=0.29): title不匹配,content匹配java
# 4. doc1(score=0.29): title匹配java,content不匹配
# 5. doc3(score=0.29): title不匹配,content匹配beginner
# 可以看出对分数还是有影响的

6.3 multi_match、dis_max和tie_breaker的联合使用

GET /article/_doc/_search
{
  "query": {
    "multi_match": {
      "query": "java solution",
      "type": "best_fields", # best fields就是dis_max的策略,即认为一个字段匹配到尽可能多的关键词就评分更高
      "fields": ["title^2", "content"], # "title^2:将title字段的权重乘以2
      "tie_breaker": 0.3,
      "minimum_should_match": "50%"
    }
  },
  "_source": ["title", "content"]
}

# 上面的查询语法与下面的查询语法的结果是一样的
GET /article/_doc/_search
{
  "query": {
    "dis_max": {
      "queries": [
        {
          "match": {
            "title": {
              "query": "java solution",
              "minimum_should_match": "50%",
              "boost": 2
            }
          }
        },
        {
          "match": {
            "content": {
              "query": "java solution",
              "minimum_should_match": "50%"
            }
          }
        }
      ],
      "tie_breaker": 0.3
    }
  },
  "_source": ["title", "content"]
}

# minimum_should_match的作用:去长尾
# 比如搜索5个关键词,但是很多结果只匹配1个关键词的,这些结果与预期相差甚远,这些结果就是长尾
# minimum_should_match,控制搜索结果的精准度,只有匹配一定数量的关键词的数据,才能返回

7. most-fields策略

  • best-fields策略:某一个field匹配尽可能多的关键词的doc评分更高
  • most-fields策略:尽可能多的field可以匹配到关键词,那个这个doc的评分会更高
# 增加测试数据
POST /article/_mapping/_doc
{
  "properties": {
      "sub_title": { 
          "type": "text",
          "analyzer": "english",
          "fields": {
              "std": { 
                "type": "text",
                "analyzer": "standard"
              }
          }
      }
  }
}

POST /article/_doc/_bulk
{"update": {"_id": "1"}}
{"doc": {"sub_title": "learning more courses"}}
{"update": {"_id": "2"}}
{"doc": {"sub_title": "learned a lot of course"}}
{"update": {"_id": "3"}}
{"doc": {"sub_title" : "we have a lot of fun"}}
{"update": {"_id": "4"}}
{"doc": {"sub_title": "both of them are good"}}
{"update": {"_id": "5"}}
{"doc": {"sub_title": "haha, hello world"}}

GET /article/_doc/search
{
  "query": {
    "match": {
      "sub_title": "learning courses"
    }
  }
}
# 结果,两条:doc1和doc2
# 使用english分词器进行分词后,learning->learn,learned->learn,courses->course
# 所以两条都可以搜索到

# 只有doc1一条结果
# sub_title.std使用standard分词器,按照非字母和非数字字符进行分隔,单词转为小写,不会进行常规化处理
GET /article/_doc/_search
{
  "query": {
    "match": {
      "sub_title.std": "learning courses"
    }
  }
}

# most_fieds策略
GET /article/_doc/_search
{
  "query": {
    "multi_match": {
      "query": "learning courses",
      "type": "most_fields",
      "fields": ["sub_title", "sub_title.std"]
    }
  },
  "_source": ["sub_title"]
}}
# 1. doc2(score=1.39): sub_title匹配learn和course,sub_title.std无匹配
# 2. doc1(score=1.15): sub_title匹配learn和course,sub_title.std匹配learning和courses
# 这里结果也是很难理解的,因为我们预期doc1是比doc2优先返回的
# 评分计算时很复杂的, 不只是TF/IDF算法,不同的query,不同的语法,都有不同的计算score的细节,所以这里就不再深究了

best_fields与most_fields策略的区别:

  • best_fields,对多个field进行搜索,挑选某个field匹配度最高的那个分数,同时在多个query最高分相同的情况下,在一定程度上考虑其他query的分数,简单来说,对多个field进行搜索,某一个field尽可能包含更多关键字的doc评分更好 优点:通过best_fields策略,以及综合考虑其他field,还有minimum_should_match支持,可以尽可能精准地将匹配的结果推送到最前面 缺点:除了那些精准匹配的结果,其他差不多大的结果,排序结果不是太均匀,没有什么区分度了 例子:百度,最匹配的排到最前面
  • most_fields,综合多个field一起进行搜索,尽可能多地让所有field的query参与到总分数的计算中来,有越多的field可以匹配到关键词,这条doc的评分就更高 优点:将匹配到更多field的结果推送到最前面,整个排序结果是比较均匀的 缺点:可能那些精准匹配的结果,无法推送到最前面 例子:wiki,明显的most_fields策略,搜索结果比较均匀,但是的确要翻好几页才能找到最匹配的结果

8. cross-fields搜索

cross-fields搜索:

搜索的文本包含在多个field中,比如搜索"James Bob","James"在"first_name"字段中保存,"Bob"在"last_name"字段中保存,或者搜索一个地址文本,这些信息散落在"country","province","city"等字段中,这样的搜索就叫做cross-fields搜索。

使用most_fields策略进行cross-fields搜索是比较合适的,因为cross-fields本来就是需要在多个field中去搜索,而most_fields策略就是尽可能得去多个field中去匹配关键词

# 添加测试数据
POST /article/_doc/_bulk
{"update": {"_id": "1"}}
{"doc" : {"author_first_name": "Peter", "author_last_name": "Smith"}}
{"update": {"_id": "2"}}
{"doc" : {"author_first_name": "Smith", "author_last_name": "Williams"}}
{"update": {"_id": "3"}}
{"doc" : {"author_first_name": "Jack", "author_last_name": "Ma"}}
{"update": {"_id": "4"}}
{"doc" : {"author_first_name": "Robbin", "author_last_name": "Li"}}
{"update": {"_id": "5"}}
{"doc" : {"author_first_name": "Tonny", "author_last_name": "Peter Smith"}}

GET /article/_doc/_search
{
  "query": {
    "multi_match": {
      "query": "Peter Smith",
      "type": "most_fields",
      "fields": ["author_first_name", "author_last_name"]
    }
  },
  "_source": ["author_first_name", "author_last_name"]
}

# 结果
# 1. doc2(score=0.69): author_first_name匹配Smith,author_last_name无匹配
# 2. doc5(score=0.58): author_first_name不匹配,author_last_name匹配Peter和Smith
# 3. doc1(score=0.58): author_first_name匹配Peter,author_last_name匹配Smith
# 结果分析:为什么只匹配到"Smith"的doc反而排名最高
# score受TF/IDF的影响,IDF就是搜索词在所有文档中出现的次数,这个数字越高,TF/IDF越低
# 在所有文档的 "author_last_name"字段中,Smith出现2次,Peter出现1次
# 在所有文档的 "author_first_name"字段中,Smith出现1次,Peter出现1次
# 所有,在author_last_name中匹配到Smith的分数就不如在author_first_name中匹配到Smith的分数
# 当然影响分数的因素是很多的,这里是说一个普适的规律

使用most_fields进行cross-fields搜索的一些问题:

  • 问题1:越多的field的匹配到关键词其分数会高与少量field匹配到多个关键词的分数
  • 问题2:没办法用minimum_should_match去掉长尾数据,就是匹配的特别少的数据
  • 问题3:TF/IDF算法可能导致结果无法符合预期,比如上面例子中的情况

解决办法一:copy_to,将多个field组合成一个field,用了copy_to语法之后,就可以将多个字段的值拷贝到一个字段中,并建立倒排索引,但是在index中是查不到这个字段的,这是一个隐藏的字段

PUT my_index
{
  "mappings": {
    "_doc": {
      "properties": {
        "first_name": {
          "type": "text",
          "copy_to": "full_name" 
        },
        "last_name": {
          "type": "text",
          "copy_to": "full_name" 
        },
        "full_name": {
          "type": "text"
        }
      }
    }
  }
}

PUT my_index/_doc/1
{
  "first_name": "Peter",
  "last_name": "Smith"
}

PUT my_index/_doc/2
{
  "first_name": "Smith",
  "last_name": "Williams"
}

PUT my_index/_doc/3
{
  "first_name": "Jack",
  "last_name": "Ma"
}

PUT my_index/_doc/4
{
  "first_name": "Robbin",
  "last_name": "Li"
}

PUT my_index/_doc/5
{
  "first_name": "Tonny",
  "last_name": "Peter Smith"
}

GET my_index/_search
{
  "query": {
    "match": {
      "full_name": "Peter Smith"
    }
  }
}

# 结果
# 1. doc2(score=0.69): first_name匹配Smith,last_name无匹配
# 2. doc5(score=0.58): first_name不匹配,author_last_name匹配Peter和Smith
# 3. doc1(score=0.58): first_name匹配Peter,last_name匹配Smith
# 这个结果也是不符合预期的,这与ES计算分数的算法有关,影响分数的因素是很多的

GET my_index/_search
{
  "query": {
    "match": {
      "full_name": { 
        "query": "Peter Smith",
        "operator": "and"
      }
    }
  }
}

# 结果
# 1. doc5(score=0.58): first_name不匹配,author_last_name匹配Peter和Smith
# 2. doc1(score=0.58): first_name匹配Peter,last_name匹配Smith

解决办法二:使用cross-fields的原生语法

most-fields,只需要任意一个字段中出现一个关键词即可
GET my_index/_search
{
  "query": {
    "multi_match": {
      "query": "Peter Smith",
      "type": "cross_fields",
      "operator": "and",
      "fields": ["first_name", "last_name"]
    }
  }
}

# 结果
[
    {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 0.5753642,
        "_source" : {
            "first_name" : "Tonny",
            "last_name" : "Peter Smith"
        }
    },
    {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.5753642,
        "_source" : {
            "first_name" : "Peter",
            "last_name" : "Smith"
        }
    }
]

# type=cross_fields的原理
# 要求Peter必须在first_name或last_name中出现
# 要求Smith必须在first_name或last_name中出现
# doc2(first_name=Smith,last_name=Williams)就无法满足条件了
# 而原来的most-fields,只需要任意一个字段中出现一个关键词即可