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 点的问题,但是它主要适用于写多读少的场景。


总结

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

相关推荐
forestsea2 小时前
【Elasticsearch 】 聚合分析篇:聚合概述
大数据·elasticsearch·搜索引擎
里音日黑10 小时前
Elasticsearch 和arkime 安装
linux·运维·服务器·elasticsearch·arkime
m0_5127446419 小时前
重学SpringBoot3-整合 Elasticsearch 8.x (二)使用Repository
大数据·elasticsearch·jenkins
牛马程序员‍21 小时前
【云岚到家】-day04-数据同步方案es-Canal-MQ
大数据·elasticsearch·canal·mq
Elastic 中国社区官方博客1 天前
Observability:最大化可观察性 AI 助手体验的 5 大提示(prompts)
大数据·人工智能·elasticsearch·搜索引擎·信息可视化·prompt·全文检索
Elastic 中国社区官方博客2 天前
如何通过 Apache Airflow 将数据导入 Elasticsearch
大数据·数据库·elasticsearch·搜索引擎·全文检索·apache
小白的一叶扁舟2 天前
Elasticsearch(ES)与 MySQL 的对比分析及在 Spring Boot 中的使用
java·数据库·spring boot·后端·mysql·elasticsearch·中间件
Jet-W2 天前
elasticsearch线程池配置
大数据·运维·elasticsearch·jenkins
小豆豆儿2 天前
【PyCharm】连接 Git
git·elasticsearch·pycharm
Elastic 中国社区官方博客2 天前
Elasticsearch:Jira 连接器教程第二部分 - 6 个优化技巧
大数据·数据库·elasticsearch·搜索引擎·全文检索·kibana·jira