经过前面两章,我们已经学习了部分与搜索相关的 API,但是到现在为止这些 API 都只能单独工作,今天我们来学习如何将这些 API 进行组合操作来解决更复杂的搜索需求。

带有组合功能的 API 有以下几个:

  • Bool Query,布尔查询,可以组合多个过滤语句来过滤文档。
  • Boosting Query,在 positive 块中指定匹配文档的语句,同时降低在 negative 块中也匹配的文档的得分,提供调整相关性算分的能力。
  • constant_score Query,包装了一个过滤器查询,不进行算分。
  • dis_max Query,返回匹配了一个或者多个查询语句的文档,但只将最佳匹配的评分作为相关性算分返回。
  • function_score Query,支持使用函数来修改查询返回的分数。

我们将继续沿用在线书店的模型与数据,那下面就正式开始今天的内容。

一、Bool Query

如其名,Bool Query 使用一个或者多个布尔查询子句进行构建,每个子句都有一个类型,这些类型如下:

  • must,查询的内容必须在匹配的文档中出现,并且会进行相关性算分。简单来说就是与 AND 等价。
  • filter,查询的内容必须在匹配的文档中出现,但不像 must,filter 的相关性算分是会被忽略的。因为其子句会在 filter context 中执行,所以其相关性算分会被忽略,并且子句将被考虑用于缓存。简单来说就是与 AND 等价。
  • should,查询的内容应该在匹配的文档中出现,可以指定最小匹配的数量。简单来说就是与 OR 等价。
  • must_not,查询的内容不能在匹配的文档中出现。与 filter 一样其相关性算分也会被忽略。简单来说就是与 NOT 等价。

那下面来看看 Bool Query 是如何使用的。现在有一个需求是,在线书店需要提供多条件查询的功能,可以按作者、出版日期进行过滤。其示例如下:

  1. POST books/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "must": [
  6. {
  7. "term": {
  8. "author": {
  9. "value": "Wolfgang Mauerer"
  10. }
  11. }
  12. },
  13. {
  14. "term": {
  15. "date": {
  16. "value": "2010-06-01"
  17. }
  18. }
  19. }
  20. ]
  21. }
  22. }
  23. }

如上示例,我们使用了 must 子句来实现需求。must 子句中包含了两个 term query,分别对作者和日期进行查询。

除了使用 must 子句外,还可以使用 filter 子句和 should 子句来做实现。使用 filter 子句来做实现其实很好理解,因为 filter 子句就是做过滤嘛,只不过 filter 子句会忽略相关性算分而已。但是 should 子句呢?其实 should 子句有一个 minimum_should_match 参数,可以指定最少匹配的查询数量或者百分比。其示例如下:

  1. POST books/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "should": [
  6. {
  7. "term": {
  8. "author": {
  9. "value": "Wolfgang Mauerer"
  10. }
  11. }
  12. },
  13. {
  14. "term": {
  15. "date": {
  16. "value": "2010-06-01"
  17. }
  18. }
  19. }
  20. ],
  21. "minimum_should_match": 2
  22. }
  23. }
  24. }

如上示例,我们把子句改为 should,并且指定了 minimum_should_match 为 2,使得 should 子句中的查询必须命中两个或以上,这个文档才会被匹配。需要注意的是,当 Bool Query 包含至少一个 should 查询并且没有 must 、filter 的情况下,其值默认为 1,否则默认为 0。minimum_should_match 还有更多的玩法,你可以参考官方文档

另外,我们可以组合多个子句来实现这个需求,例如我们使用 must 子句来查询作者名字,而使用 filter 子句来过滤出版日期,其示例如下:

  1. POST books/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "must": [
  6. {
  7. "term": {
  8. "author": {
  9. "value": "Wolfgang Mauerer"
  10. }
  11. }
  12. }
  13. ],
  14. "filter": [
  15. {
  16. "term": {
  17. "date": {
  18. "value": "2010-06-01"
  19. }
  20. }
  21. }
  22. ]
  23. }
  24. }
  25. }

二、Boosting Query

Boosting Query 可以指定两个块:positive 块和 negative 块。可以在 positive 块来指定匹配文档的语句,而在 negative 块中匹配的文档其相关性算分将会降低。相关性算分降低的程度将由 negative_boost 参数决定,其取值范围为:[0.0, 1.0]。

  1. POST books/_search
  2. {
  3. "query": {
  4. "boosting": {
  5. "positive": {
  6. "term": {
  7. "name": {
  8. "value": "linux"
  9. }
  10. }
  11. },
  12. "negative": {
  13. "term": {
  14. "name": {
  15. "value": "programming"
  16. }
  17. }
  18. },
  19. "negative_boost": 0.5
  20. }
  21. }
  22. }

如上示例,我们查询书名中含有 “linux” 的文档,并且想让含有 “programming” 字样的文档的相关性降低一半。在 negative 块中匹配的文档,其相关性算分为:在 positive 中匹配时的算分 * negative_boost

三、constant_score Query

constant_score Query 其实前面的内容已经有过介绍了,其包装 了一个过滤器查询,不进行算分。使用 Constant Score 可以将 query 转化为 filter,可以忽略相关性算分的环节,并且 filter 可以有效利用缓存,从而提高查询的性能。

constant_score Query 的用例其实前面的内容以及介绍过了,下面再贴一下其使用示例:

  1. # 使用 Range 查询,并且不进行相关性算分
  2. POST /books/_search
  3. {
  4. "query": {
  5. "constant_score": {
  6. "filter": {
  7. "range": {
  8. "price": {
  9. "gte": 10,
  10. "lte": 20
  11. }
  12. }
  13. }
  14. }
  15. }
  16. }

如上示例,我们过滤出了书本价格大于等于 10 并且小于 20 的文档。

四、dis_max Query

disjunction max query 简称 dis_max,是分离最大化查询的意思。注意这个名字中的两个重点:分离、最大化。

  • disjunction(分离)的含义是:表示把同一个文档中的每个字段上的查询都分开,分别进行算分操作。
  • max(最大化): 是将多个字段查询的得分的最大值作为最终评分返回。

所以 disjunction max query 的效果是:将所有与任一查询匹配的文档作为结果返回,但是只将最佳匹配的得分作为查询的算分结果进行返回。不过其他匹配的字段可以使用 “tie_breaker” 参数来进行“维权”。

dis_max Query 的使用示例如下:

  1. POST books/_search
  2. {
  3. "query": {
  4. "dis_max": {
  5. "queries": [
  6. {
  7. "term": {
  8. "name": {
  9. "value": "linux"
  10. }
  11. }
  12. },
  13. {
  14. "term": {
  15. "intro": {
  16. "value": "kernel"
  17. }
  18. }
  19. }
  20. ],
  21. "tie_breaker": 0.9
  22. }
  23. }
  24. }

如上示例,我们查询书名中出现 “linux” 或者书本简介中出现 “kernel” 的文档,而最终返回的相关性评分将以匹配 “linux” 或者匹配 “kernel” 中最大的那个评分为准。

在介绍 mutil-match 的时候也有一个 tie_breaker 参数,其作用跟本实例中的一样,今天我们来复习一下。 当指定 “tie_breaker” 的时候,算分结果将会由以下算法来决定:

  1. 令算分最高的字段的得分为 s1
  2. 令其他匹配的字段的算分 * tie_breaker 的和为 s2
  3. 最终算分为:s1 + s2

“tie_breaker” 的取值范围为:[0.0, 1.0]。当其为 0.0 的时候,按照上述公式来计算,表示使用最佳匹配字段的得分作为相关性算分。当其为 1.0 的时候,表示所有字段的得分同等重要。当其在 0.0 到 1.0 之间的时候,代表其他字段的得分也需要参与到总得分的计算当中去。通俗来说就是其他字段可以使用 “tie_breaker” 来进行“维权”

五、function_score Query

function_score Query 允许你在查询结束以后去修改每一个匹配文档的相关性算分,所以使用算分函数可以改变或者替换原来的相关性算分结果。

function_score Query 提供了以下几种算分函数:

  • script_score:利用自定义脚本完全控制算分逻辑。
  • weight:为每一个文档设置一个简单且不会被规范化的权重。
  • random_score:为每个用户提供一个不同的随机算分,对结果进行排序。
  • field_value_factor:使用文档字段的值来影响算分,例如将好评数量这个字段作为考虑因数。
  • decay functions:衰减函数,以某个字段的值为标准,距离指定值越近,算分就越高。例如我想让书本价格越接近 10 元,算分越高排序越靠前。

由于篇幅限制,下面将介绍 field_value_factor 和 random_score 这两个算分函数,其他的算分函数你可以参考官方文档

1. field_value_factor

field_value_factor 的作用是用文档某个字段的值来影响相关性算分,其可以解决这样的需求: 价格优惠的优先推荐、点赞数多的优先推荐、购买量多的优先推荐等。

field_value_factor 提供了以下几个参数选项:

  • field:文档的字段。
  • factor:指定文档的值将会乘以这个因子,默认为 1。
  • modifier:修改最终值的函数,其值可以为:none、log、log1p、log2p、ln、ln1p、ln2p、square、 sqrt、reciprocal,默认为 none。

下面来看看一个具体实例,假如我想让书本的价格影响相关性算分,随着价格的增加,相关性算分将相应地降低,要满足这个需求可以这样做:

  1. POST books/_search
  2. {
  3. "query": {
  4. "function_score": {
  5. "query": {
  6. "term": {
  7. "name": {
  8. "value": "linux"
  9. }
  10. }
  11. },
  12. "field_value_factor": {
  13. "field": "price",
  14. "factor": 1.2,
  15. "modifier": "reciprocal",
  16. "missing": 1
  17. },
  18. "boost_mode": "multiply"
  19. }
  20. }
  21. }

如上示例,我们使用书本的价格字段来影响相关性算分,其中 factor 为 1.2,将会乘以书本价格。而 modifier 使用的是 reciprocal,其作用类似于 1/x,这里 x 的值就是 price * factor 了。boost_mode 为 multiply,其作用是使得旧算分与 field_value_factor 产生的算分相乘。

所以最终得分的计算过程如下:新算分 = 匹配过程产生的旧算分 * reciprocal(1.2 * doc[‘price’].value)

对于 boost_mode 参数,它的值有以下几种:

  • multiply:算分与函数值的积,multiply 是默认值。
  • replace:使用函数值作为最终的算分结果。
  • sum:算分与函数值的和。
  • avg:算分与函数值的平均数。
  • min:在算分与函数值中取最小值。
  • max:在算分与函数值中去最大值。

2. random_score

随着运营团队的辛苦劳作,在线书店的用户量上来了,现在想给用户推荐一些书籍。我们当然可以上线一个推荐系统,但这个明显不是现阶段需要做的事情,我们需要尽量简单地完成这个需求。

为了满足给用户推荐书籍的需求,可以使用 random_score 算分函数来实现。需要为每一个用户推荐随机的数据,但是希望一段时间内同一个用户访问的时候,这部分内容的排序都是一样的。

random_score 算分函数的使用示例:

  1. POST books/_search
  2. {
  3. "query": {
  4. "function_score": {
  5. "random_score": {
  6. "seed": 81819,
  7. "field": "_seq_no"
  8. }
  9. }
  10. }
  11. }

如上示例,当 seed 的值不变的时候,随机内容的排序结果将不会变化。需要注意的是,在使用 random_score 算分函数的时候,需要指定 seed 和 field,如果只指定 seed,需要在 _id 字段上加载 fielddata,这样将会消耗大量的内存

一般来说,使用 “_seq_no” 作为 field 的值是比较推荐的,但是如果 seed 不变的情况下,文档被更新了,这个时候文档的 _seq_no 是会变化的,将会导致排序结果的变化。这里简单介绍一下 “_seq_no”,在同一个 Index 里,每次文档写入时 “_seq_no” 都会自增,所以 “_seq_no” 代表着当前写入的顺序。

六、总结

今天我们学习了部分 ES 提供的组合查询 API,通过这些 API 我们可以组合多个查询语句来满足复杂的查询需求。

其中 Bool Query 是日常使用中用的比较多的,Bool Query 提供了 must、filter、should、must_not 这几种类型来构建查询语句,其中 filter 与 must_not 的算分是会被忽略的

Boosting Query 可以将部分匹配文档的得分进行降低,只需要在 negative 块中指定如何匹配文档即可,并且使用 negative_boost 参数来决定减小得分程度,其取值范围为:[0.0, 1.0]。

constant_score Query 可以将 query 转化为 filter,可以忽略相关性算分的环节,并且 filter 可以有效利用缓存,从而提高查询的性能。非常适合一些不需要算分的查询,例如精确值的查询、枚举量的查询等。

dis_max Query 是分离最大化查询的意思。其将所有与任一查询匹配的文档作为结果返回,但是只将最佳匹配的得分作为查询的算分结果进行返回。为了避免“一家独大”的情况,其他匹配的字段可以使用 “tie_breaker” 参数来进行“维权”。

function_score 是修改相关性算分的终极武器,它允许用户在查询结束以后去修改每一个匹配文档的相关性算分。

好了今天的内容到此为止,最后为了更好的熟悉 ES 的 API,需要你结合前面两章内容中介绍的 API 来练习一下今天的新介绍的 API。更多关于组合查询 API 的用法,可以参考官方文档