11、ES实战:复合查询

本学习笔记基于ElasticSearch 7.10版本,旧版本已经废弃的查询功能暂时不做笔记,以后有涉及到再做补充。
参考官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/compound-queries.html

1、constant_score

当我们使用全文查询或词项查询时,ElasticSearch 默认会根据词频(TF)对查询结果进行评分,再根据评分排序后返回查询结果。

如果我们不关心检索词频(TF)对搜索结果排序的影响时,可以使用 constant_score 将查询语句或者过滤语句包裹起来。

GET books/_search
{
   
     
  "query": {
   
     
    "term": {
   
     
      "name": "java"
    }
  }
}

GET books/_search
{
   
     
  "query": {
   
     
    "constant_score": {
   
     
      "filter": {
   
     
        "term": {
   
     
          "name": "java"
        }
      },
      "boost": 1.5
    }
  }
}

2、bool

bool 底层使用的是 Lucene 的 BooleanQuery ,可以将任意多个简单查询组装在一起,有四个关键字可供选择,四个关键字所描述的条件可以有一个或者多个。

  • must :文档必须匹配 must 选项下的查询条件。
  • should :文档可以匹配 should 下的查询条件,也可以不匹配。
  • must_not :文档必须不满足 must_not 选项下的查询条件。
  • filter :类似于 must,但是 filter 不评分,只是过滤数据。

例如查询 name 字段中必须包含 “计算”,同时书价不在 [0,30] 区间内,info 字段可以包含 “程序设计” 也可以不包含:

GET books/_search
{
   
     
  "query": {
   
     
    "bool": {
   
     
      "must": [
        {
   
     
          "term": {
   
     
            "name": "计算"
          }
        }
      ],
      "must_not": [
        {
   
     
          "range": {
   
     
            "price": {
   
     
              "gte": 1,
              "lte": 30
            }
          }
        }
      ],
      "should": [
        {
   
     
          "match": {
   
     
            "info": "程序设计"
          }
        }
      ]
    }
  },
  "sort": [
    {
   
     
      "price": {
   
     
        "order": "asc"
      }
    }
  ],
  "from": 0,
  "size": 40
}

minmum_should_match

bool 查询还涉及到一个关键字 minmum_should_match 参数,在 ElasticSearch 官网上称作 “最小匹配度” 。在之前学习的 match 或者这里的 should 查询中,都可以设置 minmum_should_match 参数。

假设我们查询 name 中包含 “程序设计” 关键字的文档:

GET books/_search
{
   
     
  "query": {
   
     
    "match": {
   
     
      "name": "程序设计"
    }
  }
}

首先对关键字进行分词,分词结果如下:
*
然后使用 should 将分词后的每一个词项查询 term 子句组合构成一个 bool 查询。换句话说,上面的查询和下面的查询等价:

GET books/_search
{
   
     
  "query": {
   
     
    "bool": {
   
     
      "should": [
        {
   
     
          "term": {
   
     
            "name": "程序设计"
          }
        },
        {
   
     
          "term": {
   
     
            "name": "程序"
          }
        },
        {
   
     
          "term": {
   
     
            "name": "设计"
          }
        }
      ]
    }
  }
}

在这两个查询语句中,都是文档只需要包含词项中的任意一项即可,文档就回被返回,在 match 查询中,可以通过 operator 参数设置文档必须匹配所有词项。

但如果只想匹配一部分词项,就要使用 minmum_should_match,指定至少匹配的词项,可以指定个数或者百分比,以下三个查询等价:

GET books/_search
{
   
     
  "query": {
   
     
    "match": {
   
     
      "name": {
   
     
        "query": "程序设计",
        "operator": "and"
      }
    }
  }
}
# 指定个数
GET books/_search
{
   
     
  "query": {
   
     
    "match": {
   
     
      "name": {
   
     
        "query": "程序设计",
        "minimum_should_match": 3
      }
    }
  }
}
# 指定百分比
GET books/_search
{
   
     
  "query": {
   
     
    "bool": {
   
     
      "should": [
        {
   
     
          "term": {
   
     
            "name": "程序设计"
          }
        },
        {
   
     
          "term": {
   
     
            "name": "程序"
          }
        },
        {
   
     
          "term": {
   
     
            "name": "设计"
          }
        }
      ],
      "minimum_should_match": "100%"
    }
  }
}

3、dis_max

之前的books 索引中的数据不好演示 dis_max 查询,我们使用一些新的数据:

PUT blog
{
   
     
  "mappings": {
   
     
    "properties": {
   
     
      "title":{
   
     
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "content":{
   
     
        "type": "text",
        "analyzer": "ik_max_word"
      }
    }
  }
}

POST blog/_doc
{
   
     
  "title":"如何通过Java代码调用ElasticSearch",
  "content":"大方哥力荐,这是一篇很好的解决方案"
}

POST blog/_doc
{
   
     
  "title":"初识 MongoDB",
  "content":"简单介绍一下 MongoDB,以及如何通过 Java 调用 MongoDB,MongoDB 是一个不错 NoSQL 解决方案"
}

现在假设搜索 “Java解决方案” 关键字,但是不确定关键字是在 title 还是在 content,所以两者都搜索:

GET blog/_search
{
   
     
  "query": {
   
     
    "bool": {
   
     
      "should": [
        {
   
     
          "match": {
   
     
            "title": "Java解决方案"
          }
        },
        {
   
     
          "match": {
   
     
            "content": "Java解决方案"
          }
        }
      ]
    }
  }
}

按照我们理解,第二篇文档中的content同时包含了 “Java” 和 “解决方案” ,应该评分更高。但是实际搜索并非这样,第一篇文档的评分反而比较高:
*
要理解为什么会这样,我们需要看一下 should 查询的评分策略:

1、 首先执行should中的所有查询;
2、 对所有查询结果的评分求和;
3、 对求和结果*有查询结果的子句数量;
4、 再将上一步结果/所有查询子句数量;

根据评分策略,再来看看具体的查询:

第一篇

1、 title中包含Java,假设评分是1.1;
2、 content中包含解决方案,假设评分是1.2;
3、 有查询结果的子句数量为2;
4、 所有查询子句数量也是2;
最终结果:(1.1 + 1.2)* 2 / 2 = 2.3

第一篇

1、 title中不包含查询关键字,没有得分;
2、 content中包含Java解决方案,假设评分时2;
3、 有查询结果的子句数量为1;
4、 所有查询子句数量也是2;
最终结果:2 * 1 / 2 = 1

should 查询中,title 和 content 相当于是相互竞争的关系。

为了解决这一问题,就需要用到 dis_max (disjunction max query,分离最大化查询):匹配的文档依然返回,但是只将最佳匹配的子句评分作为查询的评分。

GET blog/_search
{
   
     
  "query": {
   
     
    "dis_max": {
   
     
      "queries": [
        {
   
     
          "match": {
   
     
            "title": "Java解决方案"
          }
        },
        {
   
     
          "match": {
   
     
            "content": "Java解决方案"
          }
        }
      ]
    }
  }
}

dis_max 查询中,只考虑最佳匹配子句的评分,完全不考虑其他子句的评分。但是,有的时候,我们又不得不考虑一下其他 query 的分数,此时,可以通过 tie_breaker 参数来优化。

tie_breaker 参数取值范围:0 ~ 1,默认取值为 0,会将其他查询子句的评分,乘以 tie_breaker 参数,然后再和最佳匹配的子句进行综合计算。

GET blog/_search
{
   
     
  "query": {
   
     
    "dis_max": {
   
     
      "queries": [
        {
   
     
          "match": {
   
     
            "title": "Java解决方案"
          }
        },
        {
   
     
          "match": {
   
     
            "content": "Java解决方案"
          }
        }
      ],
      "tie_breaker": 0.5
    }
  }
}

4、function_score

场景:例如想要搜索附近的肯德基,搜索的关键字是肯德基,我希望能够将评分较高的肯德基餐厅优先展示出来。但是默认的评分策略是没有办法考虑到餐厅评分的,他只是考虑相关性,这个时候可以通过 function_score query 来实现。

准备测试数据:

PUT blog
{
   
     
  "mappings": {
   
     
    "properties": {
   
     
      "title":{
   
     
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "votes":{
   
     
        "type": "integer"
      }
    }
  }
}

PUT blog/_doc/1
{
   
     
  "title":"Java集合详解",
  "votes":100
}

PUT blog/_doc/2
{
   
     
  "title":"Java多线程详解,Java锁详解",
  "votes":10
}

查询标题中包含 java 关键字的文档:

GET blog/_search
{
   
     
  "query": {
   
     
    "match": {
   
     
      "title": "java"
    }
  }
}

查询结果如下:
*
因为第二篇文档的 title 中有两个 java,所有得分会比较高,如果希望能够考虑 votes 字段,将 votes 高的文档优先展示,就可以通过 function_score 来实现。

具体的思路,就是在旧的得分基础上,根据 votes 的数值进行综合运算,重新得出一个新的评分。具体的计算方式有以下几种:

  • weight
  • random_score
  • script_score
  • field_value_factor
  • decay functions (本文不做介绍)

4.1、weight

weight 可以对评分设置权重,在旧的评分基础上乘以 weight,他其实无法解决我们上面所说的问题。具体用法如下:

GET blog/_search
{
   
     
  "query": {
   
     
    "function_score": {
   
     
      "query": {
   
     
        "match": {
   
     
          "title": "java"
        }
      },
      "functions": [
        {
   
     
          "weight": 10
        }
      ]
    }
  }
}

4.2、random_score

random_score 会根据 uid 字段进行 hash 运算,生成随机分数,使用 random_score 时可以配置一个种子,如果不配置,默认使用当前时间。

GET blog/_search
{
   
     
  "query": {
   
     
    "function_score": {
   
     
      "query": {
   
     
        "match": {
   
     
          "title": "java"
        }
      },
      "functions": [
        {
   
     
          "random_score": {
   
     }
        }
      ]
    }
  }
}

random_score 也不能解决我们的问题。

4.3、script_score

script_score 是自定义评分脚本。

假设每个文档的最终得分是旧的分数加上 votes,查询方式如下:

GET blog/_search
{
   
     
  "query": {
   
     
    "function_score": {
   
     
      "query": {
   
     
        "match": {
   
     
          "title": "java"
        }
      },
      "functions": [
        {
   
     
          "script_score": {
   
     
            "script": {
   
     
              "lang": "painless",
              "source": "_score + doc['votes'].value"
            }
          }
        }
      ]
    }
  }
}

查询结果得分如下:
*
注意: ElasticSearch 并不是简单的将旧得分和 votes 相加,底层的计算公式是:(旧得分 + votes) * 旧得分。第一篇文档的旧得分为 0.21799318,代入计算 :(0.21799318 + 100) * 0.21799318 = 21.84684

如果不想乘以旧得分,查询方式如下:

GET blog/_search
{
   
     
  "query": {
   
     
    "function_score": {
   
     
      "query": {
   
     
        "match": {
   
     
          "title": "java"
        }
      },
      "functions": [
        {
   
     
          "script_score": {
   
     
            "script": "_score + doc['votes'].value"
          }
        }
      ],
      "boost_mode": "replace"
    }
  }
}

通过boost_mode 参数,可以设置最终的计算方式。该参数还有其他取值:

  • multiply:查询得分和 functions 得分相乘 (默认)
  • replace:只使用 functions 得分,忽略查询得分
  • sum:查询得分和 functions 得分相加
  • avg:查询得分和 functions 得分的平均值
  • max:查询得分和 functions 得分的最大值
  • min:查询得分和 functions 得分的最小值

4.4、field_value_factor

field_value_factor 类似于 script_score,但是不用自己写脚本,而是使用影响因子和内置函数进行计算。具体有以下三个参数:

1、 field:需要进行计算的文档字段;
2、 factor:计算时与字段值相乘的影响因子,默认为1;
3、 modifier:可选择ElasticSearch内置函数,默认不使用;

下面演示一个稍复杂查询,将 votes 字段值乘以影响因子 1.6,结果再取平方根,最后忽略 query 查询的分数 sqrt(1.2 * doc['votes'].value)

GET blog/_search
{
   
     
  "query": {
   
     
    "function_score": {
   
     
      "query": {
   
     
        "match": {
   
     
          "title": "java"
        }
      },
      "functions": [
        {
   
     
          "field_value_factor": {
   
     
            "field": "votes",
            "factor": 1.6, 
            "modifier": "sqrt"
          }
        }
      ],
      "boost_mode": "replace"
    }
  }
}

modifier 其他的内置函数还有:

  • none:不进行任何计算 (默认)
  • log:字段值取常用对数。如果字段值为0到1之间,将导致错误,可以考虑使用下面的 log1p 函数
  • log1p:字段值加1后,取常用对数
  • log2p:字段值加2后,取常用对数
  • ln:字段值取自然对数。如果字段值为0到1之间,将导致错误,可以考虑使用下面的 ln1p 函数
  • ln1p:字段值加1后,取自然对数
  • ln2p:字段值加2后,取自然对数
  • square:字段值的平方
  • sqrt:字段值求平方根
  • reciprocal:字段值取倒数

4.5、其他参数:

score_mode 参数表示 functions 模块中不同计算方法之间的得分计算方式,有以下几种:

  • multiply:得分相乘 (默认)
  • sum:得分相加
  • avg:得分取平均值
  • first:取第一个有效的方法得分
  • max:最大的得分
  • min:最小的得分
GET blog/_search
{
   
     
  "query": {
   
     
    "function_score": {
   
     
      "query": {
   
     
        "match": {
   
     
          "title": "java"
        }
      },
      "functions": [
        {
   
     
          "field_value_factor": {
   
     
            "field": "votes",
            "factor": 1.6, 
            "modifier": "sqrt"
          }
        },
        {
   
     
          "script_score": {
   
     
            "script": "_score + doc['votes'].value"
          }
        }
      ],
      "boost_mode": "replace"
    }
  }
}

max_boost 参数表示 functions 模块中得分上限:

GET blog/_search
{
   
     
  "query": {
   
     
    "function_score": {
   
     
      "query": {
   
     
        "match": {
   
     
          "title": "java"
        }
      },
      "functions": [
        {
   
     
          "field_value_factor": {
   
     
            "field": "votes",
            "factor": 1.6, 
            "modifier": "sqrt"
          }
        }
      ],
      "boost_mode": "replace",
      "max_boost": 10
    }
  }
}

5、boosting

boosting 中包含三部分:

  • positive:得分不变
  • nagetive:降低得分
  • nagetive_boost:降低得分的权重,取值范围 [0 ~1]
# 正常查询
GET books/_search
{
   
     
  "query": {
   
     
    "match": {
   
     
      "name": "java"
    }
  }
}
# 使用 nagetive 和 nagetive_boost 降低字段值中包含 “2008” 的文档得分
GET books/_search
{
   
     
  "query": {
   
     
    "boosting": {
   
     
      "positive": {
   
     
        "match": {
   
     
          "name": "java"
        }
      },
      "negative": {
   
     
        "match": {
   
     
          "name": "2008"
        }
      },
      "negative_boost": 0.5
    }
  }
}

版权声明:

本文仅记录ElasticSearch学习心得,如有侵权请联系删除。
更多内容请访问原创作者:江南一点雨
*

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: