Elasticsearch 入门教程 – 多字段的最佳字段搜索及调优(dis_max/tie_breaker)

假设我们有一个让用户搜索博客文章的网站,就像这两份文档一样:

PUT /my_index/my_type/1
{
    "title": "Quick brown rabbits",
    "body":  "Brown rabbits are commonly seen."
}
PUT /my_index/my_type/2
{
    "title": "Keeping pets healthy",
    "body":  "My quick brown fox eats rabbits on a regular basis."
}

搜索title或content中包含Brown fox的帖子

GET /my_index/my_type/_search
{
  
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

Java代码:

  QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("title", "Brown fox"))
        .should(QueryBuilders.matchQuery("body", "Brown fox"));

然后我们发现文档1的分值更高:

 image.png

要理解原因,想想bool查询是如何计算得到其分值的:

1. 运行should子句中的两个查询

2. 相加查询返回的分值

3. 将相加得到的分值乘以匹配的查询子句的数量

4. 除以总的查询子句的数量

     文档1在两个字段中都包含了brown,因此两个match查询都匹配成功并拥有了一个分值。

     文档2在body字段中包含了brown以及fox,但是在title字段中没有出现任何搜索的单词。因此对body字段查询得到的高分加上对title字段查询得到的零分,然后在乘以匹配的查询子句数量1,最后除以总的查询子句数量2,导致整体分值比文档1的低。

在这个例子中,title和body字段是互相竞争的。我们想要找到一个最佳匹配(Best-matching)的字段。

计算每个document的relevance score:每个query的分数,乘以matched query数量,除以总query数量


算一下doc1的分数

  { "match": { "title": "Brown fox" }},针对doc1,是有一个分数的

  { "match": { " body":  "Brown fox" }},针对doc1,也是有一个分数的

   所以是两个分数加起来,比如说,1.1 + 1.2 = 2.3

   matched query数量 = 2

  总query数量 = 2

   总分数:2.3 * 2 / 2 = 2.3

算一下doc2的分数

  { "match": { "title": " Brown fox " }},针对doc2,是没有分数的

   { "match": { " body":  " Brown fox " }},针对doc2,是有一个分数的

   所以说,只有一个query是有分数的,比如2.3

   matched query数量 = 1

   总query数量 = 2

   2.3 * 1 / 2 = 1.15

 所以:doc5的分数 = 1.15 < doc4的分数 = 2.3

  

     如果我们不是合并来自每个字段的分值,而是使用最佳匹配字段的分值作为整个查询的整体分值呢?这就会让包含有我们寻找的两个单词的字段有更高的权重,而不是在不同的字段中重复出现的相同单词。

dis_max查询

     best fields策略,就是说,搜索到的结果,应该是某一个field中匹配到了尽可能多的关键词,被排在前面;而不是尽可能多的field匹配到了少数的关键词,排在了前面

简单的说: dis_max语法,直接取多个query中,分数最高的那一个query的分数即可

    { "match": { "title": " Brown fox " }},针对doc1,是有一个分数的,1.1

     { "match": { " body":  " Brown fox " }},针对doc1,也是有一个分数的,1.2 

取最大分数,1.2

    { "match": { "title": " Brown fox " }},针对doc2,是没有分数的

    { "match": { " body":  " Brown fox " }},针对doc2,是有一个分数的,2.3

取最大分数,2.3

   然后doc1的分数 = 1.2 < doc2的分数 = 2.3,所以doc2就可以排在更前面的地方,符合我们的需要

GET /my_index/my_type/_search
{
  
   "query": {
     "dis_max": {
       "queries": [
           { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
         ]
     }
   }
}

Java 代码:

SearchRequestBuilder srq =  client.prepareSearch("my_index").setTypes("my_type");  
 srq.setSearchType(SearchType.DFS_QUERY_AND_FETCH);  
        
 srq.setQuery(QueryBuilders.disMaxQuery().add(QueryBuilders.matchQuery("title", "Brown fox"))
                .add(QueryBuilders.matchQuery("body", "Brown fox")));

   image.png    

最佳字段查询的调优-tie_breaker

    如果用户搜索的是"quick pets",那么会发生什么呢?两份文档都包含了单词quick,但是只有文档2包含了单词pets。两份文档都没能在一个字段中同时包含搜索的两个单词。

GET /my_index/my_type/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ]
        }
    }
}

image.png

可以发现,两份文档的分值是一模一样的。

    我们期望的是同时匹配了title字段和body字段的文档能够拥有更高的排名,但是结果并非如此。需要记住:dis_max查询只是简单的使用最佳匹配查询子句得到的_score。

或者在查询中会有这种情况

    (1)某个帖子,doc1,title中包含java,content不包含java beginner任何一个关键词

    (2)某个帖子,doc2,content中包含beginner,title中不包含任何一个关键词

    (3)某个帖子,doc3,title中包含java,content中包含beginner

    (4)最终搜索,可能出来的结果是,doc1和doc2排在doc3的前面,而不是我们期望的doc3排在最前面

因为:dis_max只取某一个query最大的分数,完全不考虑其他query的分数

tie_breaker

   tie_breaker参数的意义,在于说,将其他query的分数,乘以tie_breaker,然后综合与最高分数的那个query的分数,综合在一起进行计算

   除了取最高分以外,还会考虑其他的query的分数  tie_breaker的值,在0~1之间,是个小数,就ok

GET /my_index/my_type/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ],
            "tie_breaker": 0.7
        }
        
    }
}

Java代码:

srq.setQuery(QueryBuilders.disMaxQuery().add(QueryBuilders.matchQuery("title", "Quick pets"))
                .add(QueryBuilders.matchQuery("body", "Quick pets")).tieBreaker(0.7f));

现在文档2的分值比文档1稍高一些。

   tie_breaker参数会让dis_max查询的行为更像是dis_max和bool的一种折中。它会通过下面的方式改变分值计算过程:

    1. 取得最佳匹配查询子句的_score。

   2. 将其它每个匹配的子句的分值乘以tie_breaker。

   3. 将以上得到的分值进行累加并规范化。

通过tie_breaker参数,所有匹配的子句都会起作用,只不过最佳匹配子句的作用更大。

    tie_breaker的取值范围是0到1之间的浮点数,取0时即为仅使用最佳匹配子句(译注:和不使用tie_breaker参数的dis_max查询效果相同),取1则会将所有匹配的子句一视同仁。它的确切值需要根据你的数据和查询进行调整,但是一个合理的值会靠近0,(比如,0.1 -0.4),来确保不会压倒dis_max查询具有的最佳匹配性质。

实战基于multi_match语法实现dis_max+tie_breaker:

GET /forum/article/_search
{
  "query": {
    "multi_match": {
        "query":                "java solution",
        "type":                 "best_fields", 
        "fields":               [ "title^2", "content" ],
        "tie_breaker":          0.3,
        "minimum_should_match": "50%" 
    }
  } 
}

GET /forum/article/_search
{
  "query": {
    "dis_max": {
      "queries":  [
        {
          "match": {
            "title": {
              "query": "java beginner",
              "minimum_should_match": "50%",
	      "boost": 2
            }
          }
        },
        {
          "match": {
            "body": {
              "query": "java beginner",
              "minimum_should_match": "30%"
            }
          }
        }
      ],
      "tie_breaker": 0.3
    }
  } 
}

 minimum_should_match,主要是用来干嘛的?

  去长尾,long tail

  长尾,比如你搜索5个关键词,但是很多结果是只匹配1个关键词的,其实跟你想要的结果相差甚远,这些结果就是长尾

  minimum_should_match,控制搜索结果的精准度,只有匹配一定数量的关键词的数据,才能返回

发表评论