ElasticSearch7.x入门教程之全文搜索(八)

文章目录

  • 前言
  • [一、函数评分查询:function_score query](#一、函数评分查询:function_score query)
  • [二、boosting query](#二、boosting query)
  • [三、文档嵌套类似MySQL连表查询 query](#三、文档嵌套类似MySQL连表查询 query)
  • 总结

前言

es的搜索排序默认评分策略只是考虑相关性,如果我们不仅仅需要考虑相关性,还是根据特定的字段来排序,例如餐厅的评分等字段,这是就要用到一下搜索类型。


一、函数评分查询:function_score query

function_score 查询 是用来控制评分过程的终极武器,它允许为每个与主查询匹配的文档应用一个函数,以达到改变甚至完全替换原始查询评分 _score 的目的。

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

准备两条测试数据:

javascript 复制代码
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 关键字的文档:

javascript 复制代码
GET blog/_search
{
  "query": {
    "match": {
      "title": "java"
    }
  }
}

查询结果如下:

默认情况下,id 为 2 的记录得分较高,因为它的 title 中包含两个 java。

如果我们在查询中,希望能够充分考虑 votes 字段,将 votes 较高的文档优先展示,就可以通过 function_score 来实现。

具体的思路,就是在旧的得分基础上,根据 votes 的数值进行综合运算,重新得出一个新的评分。

具体有几种不同的计算方式:

weight、random_score、script_score、field_value_factor

官网说明:https://www.elastic.co/guide/cn/elasticsearch/guide/current/function-score-query.html

  • 1、weight
    weight 可以对评分设置权重,就是在旧的评分基础上乘以 weight,他其实无法解决我们上面所说的问题。具体用法如下:
javascript 复制代码
GET blog/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "title": "java"
        }
      },
      "functions": [
        {
          "weight": 10
        }
      ]
    }
  }
}

结果如下:

可以看到,此时的评分,在之前的评分基础上 * 10。

  • 2、random_score
    random_score 会根据uid字段进行 hash 运算,生成分数,使用 random_score 时可以配置一个种子,如果不配置,默认使用当前时间。
javascript 复制代码
GET blog/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "title": "java"
        }
      },
      "functions": [
        {
          "random_score": {}
        }
      ]
    }
  }
}
  • 3、script_score
    自定义评分脚本。假设每个文档的最终得分是旧的分数加上votes。查询方式如下:
javascript 复制代码
GET blog/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "title": "java"
        }
      },
      "functions": [
        {
          "script_score": {
            "script": {
              "lang": "painless",
              "source": "_score + doc['votes'].value"
            }
          }
        }
      ]
    }
  }
}

现在,最终得分是 (oldScore+votes) * oldScore。

如果不想乘以 oldScore,查询方式如下:

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

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

1、multiply:分数相乘

2、sum:分数相加

3、avg:求平均数

4、max:最大分

5、min:最小分

6、replace:不进行二次计算

  • 4、field_value_factor

这个的功能类似于 script_score,但是不用自己写脚本。

假设每个文档的最终得分是旧的分数乘以votes。查询方式如下:

javascript 复制代码
GET blog/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "title": "java"
        }
      },
      "functions": [
        {
          "field_value_factor": {
            "field": "votes"
          }
        }
      ]
    }
  }
}

默认的得分就是oldScore * votes。

还可以利用 es 内置的函数进行一些更复杂的运算:

javascript 复制代码
GET blog/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "title": "java"
        }
      },
      "functions": [
        {
          "field_value_factor": {
            "field": "votes",
            "modifier": "sqrt"
          }
        }
      ],
      "boost_mode": "replace"
    }
  }
}

此时,最终的得分是(sqrt(votes))。

modifier 中可以设置内置函数,其他的内置函数还有:

另外还有个参数 factor ,影响因子。字段值先乘以影响因子,然后再进行计算。以 sqrt 为例,计算方式为 sqrt(factor * votes)

javascript 复制代码
GET blog/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "title": "java"
        }
      },
      "functions": [
        {
          "field_value_factor": {
            "field": "votes",
            "modifier": "sqrt",
            "factor": 10
          }
        }
      ],
      "boost_mode": "replace"
    }
  }
}

还有一个参数 max_boost,控制计算结果的范围:

javascript 复制代码
GET blog/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "title": "java"
        }
      },
      "functions": [
        {
          "field_value_factor": {
            "field": "votes"
          }
        }
      ],
      "boost_mode": "sum",
      "max_boost": 100
    }
  }
}

max_boost 参数表示 functions 模块中,最终的计算结果上限。如果超过上限,就按照上线计算。

二、boosting query

官网地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html

boosting query 中包含三部分:

1、positive:得分不变

2、negative:降低得分

3、negative_boost:降低的权重

查询如下:

javascript 复制代码
GET books/_search
{
  "query": {
    "boosting": {
      "positive": {
        "match": {
          "name": "java"
        }
      },
      "negative": {
        "match": {
          "name": "2008"
        }
      },
      "negative_boost": 0.5
    }
  }
}

结果如下:

可以看到,id 为 86 的文档满足条件,因此它的最终得分在旧的分数上 * 0.5。

三、文档嵌套类似MySQL连表查询 query

关系型数据库中有表的关联关系,在 es 中,我们也有类似的需求,例如订单表和商品表,在 es 中,这样的一对多一般来说有两种方式:
嵌套文档(nested)、父子文档。

1、嵌套文档

假设:有一个电影文档,每个电影都有演员信息:

javascript 复制代码
PUT movies
{
  "mappings": {
    "properties": {
      "actors":{
        "type": "nested"
      }
    }
  }
}

PUT movies/_doc/1
{
  "name":"霸王别姬",
  "actors":[
    {
      "name":"张国荣",
      "gender":"男"
    },
    {
      "name":"巩俐",
      "gender":"女"
    }
    ]
}

注意: actors 类型要是 nested。

缺点如下

查看文档数量:GET _cat/indices?v

查看结果如下:

说明:这是因为 nested 文档在 es 内部其实也是独立的 lucene 文档,只是在我们查询的时候,es 内部帮我们做了 join 处理,所以最终看起来就像一个独立文档一样。因此这种方案性能并不是特别好。

2、嵌套查询

这个用来查询嵌套文档:

javascript 复制代码
GET movies/_search
{
  "query": {
    "nested": {
      "path": "actors",
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "actors.name": "张国荣"
              }
            },
            {
              "match": {
                "actors.gender": "男"
              }
            }
          ]
        }
      }
    }
  }
}

3、父子文档

相比于嵌套文档,父子文档主要有如下优势:

1、更新父文档时,不会重新索引子文档

2、创建、修改或者删除子文档时,不会影响父文档或者其他的子文档。

3、子文档可以作为搜索结果独立返回。

例如学生和班级的关系:

javascript 复制代码
PUT stu_class
{
  "mappings": {
    "properties": {
      "name":{
        "type": "keyword"
      },
      "s_c":{
        "type": "join",
        "relations":{
          "class":"student"
        }
      }
    }
  }
}

s_c 表示父子文档关系的名字,可以自定义。join 表示这是一个父子文档。relations 里边,class 这个位置是 parent,student 这个位置是 child。

插入两个父文档:

javascript 复制代码
PUT stu_class/_doc/1
{
  "name":"一班",
  "s_c":{
    "name":"class"
  }
}
PUT stu_class/_doc/2
{
  "name":"二班",
  "s_c":{
    "name":"class"
  }
}

再来添加三个子文档:

javascript 复制代码
PUT stu_class/_doc/3?routing=1
{
  "name":"zhangsan",
  "s_c":{
    "name":"student",
    "parent":1
  }
}
PUT stu_class/_doc/4?routing=1
{
  "name":"lisi",
  "s_c":{
    "name":"student",
    "parent":1
  }
}
PUT stu_class/_doc/5?routing=2
{
  "name":"wangwu",
  "s_c":{
    "name":"student",
    "parent":2
  }
}

首先大家可以看到,子文档都是独立的文档。特别需要注意的地方是,子文档需要和父文档在同一个分片上,所以 routing 关键字的值为父文档的 id。另外,name 属性表明这是一个子文档。

父子文档需要注意的地方:

1、每个索引只能定义一个 join filed

2、父子文档需要在同一个分片上(查询,修改需要routing)

3、可以向一个已经存在的 join filed 上新增关系

4、has_child query

通过子文档查询父文档使用 has_child query。

javascript 复制代码
GET stu_class/_search
{
  "query": {
    "has_child": {
      "type": "student",
      "query": {
        "match": {
          "name": "wangwu"
        }
      }
    }
  }
}

查询 wangwu 所属的班级。

5、has_parent query

通过父文档查询子文档:

javascript 复制代码
GET stu_class/_search
{
  "query": {
    "has_parent": {
      "parent_type": "class",
      "query": {
        "match": {
          "name": "二班"
        }
      }
    }
  }
}

查询二班的学生。但是大家注意,这种查询没有评分。

可以使用 parent id 查询子文档:

javascript 复制代码
GET stu_class/_search
{
  "query": {
    "parent_id":{
      "type":"student",
      "id":1
    }
  }
}

通过 parent id 查询,默认情况下使用相关性计算分数。

6、总结

1、普通子对象实现一对多,会损失子文档的边界,子对象之间的属性关系丢失。

2、nested 可以解决第 1 点的问题,但是 nested 有两个缺点:更新主文档的时候要全部更新,不支持子文档属于多个主文档。

3、父子文档解决 1、2 点的问题,但是它主要适用于写多读少的场景。


总结

本文仅简单记录了工作当中很少用到的查询,毕竟大多数时候都是普通查询,更多信息可以参考官网。

相关推荐
wenwen201412072 小时前
linux上jdk1.8安装elasticsearch6.8.5踩坑总结
linux·运维·服务器·elasticsearch·jdk·jenkins
Elastic 中国社区官方博客2 小时前
使用数据层进行数据生命周期管理
大数据·数据库·elasticsearch·搜索引擎·全文检索·时序数据库
小黑屋说YYDS2 小时前
ElasticSearch7.x入门教程之全文搜索聚合分析(十)
elasticsearch
forestsea3 小时前
【Elasticsearch】实现分布式系统日志高效追踪
大数据·elasticsearch·搜索引擎·日志搜索
Elastic 中国社区官方博客4 小时前
使用历史索引监控 Elasticsearch 索引生命周期管理
大数据·数据库·elasticsearch·搜索引擎·全文检索·时序数据库
小黑屋说YYDS9 小时前
ElasticSearch7.x入门教程之全文搜索(九)
elasticsearch
动态一时爽,重构火葬场19 小时前
elasticsearch是如何进行搜索的?
大数据·elasticsearch·搜索引擎
P.H. Infinity19 小时前
【Elasticsearch】06-JavaRestClient查询
大数据·elasticsearch·搜索引擎
羽_羊1 天前
Elasticsearch scroll 之滚动查询
elasticsearch·scroll
URBBRGROUN4671 天前
Spring Data Elasticsearch
java·spring·elasticsearch