【中间件_ElasticSearch_02】ElasticSearch 常用的检索匹配

本文主要有以下内容:

  • Term 查询
  • 组合查询
  • 聚合查询

为了方便后续行文方便,新建一个高级索引并添加几条数据。

bash 复制代码
PUT people_info
{
    "mappings": {
        "properties": {
            "p_id": {
                "type": "keyword"
            },
            "p_name_en": {
                "type": "text"
            },
            "p_name_zh":{
              "type": "text"
            },
            "age":{
                "type": "long"
            },
            "birth":{
                "type": "date",
                "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
            },
            "address":{
              "type": "text"
            },
            "salary":{
              "type": "long"
            }
        }
    },
    "settings": {
        "number_of_shards": 2,
        "number_of_replicas": 1
    }
}
# 添加三条数据 杜撰的数据!
PUT people_info/_create/1
{
    "p_id": "2023082201",
    "p_name_en": "Su Shi",
    "p_name_zh": "苏轼",
    "age": 80,
    "birth": "2023-01-08",
    "address": "四川眉山",
    "salary": 8000
}
PUT people_info/_create/2
{
    "p_id": "2023082202",
    "p_name_en": "Zong yuan Liu",
    "p_name_zh": "柳宗元",
    "age": 54,
    "birth": "2023-11-28",
    "address": "京城长安",
    "salary": 10000
}
PUT people_info/_create/3
{
    "p_id": "2023082203",
    "p_name_en": "Zhou zhou hu",
    "p_name_zh": "胡诌诌",
    "age": 30,
    "birth": "2023-08-22",
    "address": "胡说八道",
    "salary": 12000
}

Term 查询

Term Level Query 会将输入的内容会作为一个整体来进行检索,不会对输入的内容进行分词!不会对输入的内容进行分词!不会对输入的内容进行分词!使用相关性算分公式对包含整个检索内容的文档进行相关性算分。Term 是文本(字段类型为text)经过分词处理后得出来的词项,是 ES 中表达语义的最小单位。常用的Term查询有6种!

Term Query

Term Query API 返回在指定字段中准确包含了检索内容的文档。

bash 复制代码
# 1.Term Query 查询苏轼
POST people_info/_search
{
  "query": {
    "term": {
      "p_name_en": {
        "value": "su"
      }
    }
  }
}

需要注意的是,要避免将 Term Query 用在 text 类型的字段上 ,因为文档在被存储时,会被分词器分词,大小写会统一,而term查询不会进行分词!这就是为什么value取值为su!在上面的查询中,如果value:"su shi"将查询不出来结果!因为su shi 匹配不到Term!

如果要对 text 类型的字段进行搜索,应该使用 match API而不是 Term Query API。

Terms Query

Terms Query 的功能跟 Term Query 类似,不过可以同时检索多个词项的功能。如查询2023082201、2023082202 的两个人

bash 复制代码
# 使用 Terms Query 进行查询
POST people_info/_search
{
  "query":{
    "terms": {
      "p_id": [
        "2023082201",
        "2023082202"
      ]
    }
  }
}

Range Query

Range Query API 可以查询字段值符合某个范围的文档数据,可以理解为SQL种的Between and 查询条件。只不过这里不是用大于小于符号!

  • gt:表示大于
  • gte: 表示大于或者等于
  • lt: 表示小于
  • lte: 表示小于或者等于
bash 复制代码
# 使用 Range Query 查询年龄大于等于53小于等于80的人
POST people_info/_search
{
  "query": {
    "range": {
      "age": {
        "gte": 53,
        "lte": 80
      }
    }
  }
}

Exist Query

使用 Exist Query API 可以查询那些在指定字段上有值的文档。即JSON对象是否"拥有"这个字段

一个字段的值为空可能是由于下面这个几种原因导致的:

  • 字段的JSON值为null或者 [],如果一个字段压根不存在于文档的 _source 里,也被认为是空的。
  • 一个字段在 Mapping 定义的时候设置了 "index" : false
  • 一个字段的值的长度超出了 Mapping 里这个字段设置的 ignore_above 时。【可暂时不管这个】
  • 当字段的值不合规,并且 Mapping 中这个字段设置了 ignore_malformed 时。
bash 复制代码
# 查询出所有存在 "age" 字段的文档
POST people_info/_search
{
  "query": {
    "exists": {
      "field": "age"
    }
  }
}

Prefix Query

Prefix Query 可以查询在指定字段中包含特定前缀的文档。

bash 复制代码
# 使用 Prefix Query 查询含有 "zo" 前缀的人:柳宗元
POST people_info/_search
{
  "query": {
    "prefix": {
      "p_name_en": {
        "value": "zo" 
      }
    }
  }
}
# 这里匹配的的zong,因为名字被分词器分词了的!

使用了 Prefix Query 查询含有 "linu" 前缀的文档,如果书本名字中含有 "linu" 开头的词语的文档将会被匹配上。需要注意的是,text 类型的字段会被分词,成为一个个的 term,所以这里的前缀匹配是匹配这些分词后term!

Wildcard Query

Wildcard Query 允许使用通配符表达式进行匹配。Wildcard Query 支持两个通配符:

  • ?,使用 ? 来匹配任意字符。
  • *,使用 * 来匹配 0 或多个字符。
bash 复制代码
# 使用 Wildcard Query 查询名称中含有z的人, 柳宗元和胡诌诌!
POST people_info/_search
{
  "query": {
    "wildcard": {
      "p_name_en": {
        "value": "z*"
      }
    }
  }
}

需要注意的是,Prefix Query 和 Wildcard Query 在进行查询的时候需要扫描倒排索引中的词项列表才能找到全部匹配的词项,然后再获取匹配词项对应的文档 ID。所以使用 Wildcard Query API 的时候需要注意性能问题,要尽量避免使用左通配匹配模式。

组合查询

组合查询中可以组合match API 和 Term 查询进行组合使用。分为以下几种:

Bool query

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

bool 查询采用匹配越多越好的方法,因此每个匹配的 must 或 should 子句的分数将被加在一起以提供每个文档的最终 _score。

bash 复制代码
# 查询80岁且生日为2023-01-08的人, 苏轼
POST people_info/_search
{
    "query": {
        "bool": {
            "must": [
                {
                    "term": {
                        "age": {
                            "value": 80
                        }
                    }
                },
                {
                    "term": {
                        "birth": {
                            "value": "2023-01-08"
                        }
                    }
                }
            ]
        }
    }
}
# 如果修改生日为2023-01-09将返回null

Boosting Query

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

bash 复制代码
# "max_score" : 0.9331132
POST people_info/_search 
{
  "query": {
    "match": {
      "p_name_en": "zong"
    }
  }
}
# "max_score" : 0.18662265,
POST people_info/_search
{
  "query": {
    "boosting": {
      "positive": {
        "match": {
          "p_name_en": "zong"
        }
      },
      "negative": {
        "match": {
          "p_name_en": "yuan"
        }
      },
      "negative_boost": 0.2
    }
  }
}

观察上面得分可知 :当negative满足匹配时,得分=negative_boost * positive的query分数,如果不匹配则不会影响 !如把yuan改为su则得分不会变还是0.9331132

constant_score Query

常量算分:包装了一个过滤器查询,不进行算分。使用 Constant Score 可以将 query 转化为 filter,可以忽略相关性算分的环节,并且 filter 可以有效利用缓存,从而提高查询的性能。其相关性得分等于 boost 参数值。

bash 复制代码
# 使用 Range 查询,并且不进行相关性算分
# "max_score" : 1.0,
GET people_info/_search
{
  "query": {
    "range": {
      "age": {
        "gte": 50,
        "lte": 80
      }
    }
  }
}
# "max_score" : 1.2,
GET people_info/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "age": {
            "gte": 50,
            "lte": 80
          }
        }
      },
      "boost": 1.2
    }
  }
}

dis_max

disjunction max query 简称 dis_max,最佳匹配查询

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

所以 disjunction max query 的效果是:将所有与任一查询匹配的文档作为结果返回,但是只将最佳匹配的得分作为查询的算分结果进行返回。即文档会匹配其各个子句查询,如果文档匹配多个子句,则 dis_max 查询计算文档的相关性分数如下:

总分 = 最佳匹配子句分数 + sum(其他子句)* tie_breaker

bash 复制代码
POST people_info/_search
{
  "query": {
    "dis_max": {
      "tie_breaker": 0.7,
      "boost": 1.2,
      "queries": [
           {
            "match": {
              "p_name_en": "z"
            }
          },
          {
            "match": {
              "address": "长"
            }
          }
        ]
    }
  }
}

function_score Query

函数算分,ES内置了如下几种函数:

  • script_score:允许您包装另一个查询并自定义它的评分,使用脚本表达式从文档中的其他数字字段值派生的计算(可选的)
  • weight:为每一个文档设置一个简单且不会被规范化的权重。
  • random_score :为每个用户提供一个不同的随机算分,对结果进行排序。如果您希望分数可重复,可以提供seedfield。然后,将根据该seed、所考虑文档的最小字段值和基于索引名称和shardid计算的salt来计算最终得分,以便具有相同值但存储在不同索引中的文档获得不同的得分。
  • field_value_factor:使用文档字段的值来影响算分,例如将好评数量这个字段作为考虑因数。
  • decay functions:衰减函数,以某个字段的值为标准,距离指定值越近,算分就越高。例如我想让书本价格越接近 10 元,算分越高排序越靠前。

field_value_factor

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

modifier: 具体取值含义

Modifier 含义
none 不要对字段值应用任何乘数
log 取字段值的常用对数
log1p 字段值加1,取常用对数
log2p 字段值加2,取常用对数
ln 取字段值的自然对数
ln1p 字段值加1取自然对数
ln2p 字段值加2取自然对数
square 将字段值平方(乘以它本身)
sqrt 取字段值的平方根
reciprocal 倒数字段值,类似于 1/x ,其中 x 是字段值
bash 复制代码
POST people_info/_search
{
  "query": {
    "function_score": {
      "query": {
        "term": {
          "p_name_en": {
            "value": "zong"
          }
        }
      },
      "field_value_factor": {
        "field": "age",
        "factor": 1.2,
        "modifier": "reciprocal",
        "missing": 1
      },
      "boost_mode": "multiply"
    }
  }
}

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

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

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

random_score

在使用 random_score 算分函数的时候,需要指定 seed 和 field,如果只指定 seed,需要在 _id 字段上加载 fielddata,这样将会消耗大量的内存

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

bash 复制代码
POST people_info/_search
{
  "query": {
    "function_score": {
      "random_score": {
        "seed": 0,
        "field": "_seq_no"
      }
    }
  }
}

可以改变seed的值观察文档顺序!

聚合查询

聚合就是按照某些条件从数据集合中统计一些信息 ,ES 的聚合可以进行多种组合来构建的统计查询

Metric Aggregation

矩阵聚合(Metric Aggregations) : 提供求 sum(求总和)、average(求平均数) 等数学运算,可以对字段进行统计分析。

  • 单值分析:只输出一个分析结果的聚合操作,例如 min、max、sum、avg、cardinality(类似于 SQL 中的 distinct count)等。
  • 多值分析:会输出多个分析结果的聚合操作,例如:stats、extended_stats、percentiles、percentile ranks、top hits 等。
bash 复制代码
# 查看年龄最大的人
POST people_info/_search
{
  "aggs": {
    "oldest_man": {
      "max": {
        "field": "age"
      }
    }
  }
  , 
  "size": 1
}
# 结果
{
  ......
   "aggregations" : {
    "oldest_man" : {
      "value" : 80.0
    }
  }
}
# 一个请求里同时最大年龄 最小年龄 平均年龄
POST people_info/_search
{
  "aggs": {
    "oldest_man": {
      "max": {
        "field": "age"
      }
    },
    "young_man":{
      "min": {
        "field": "age"
      }
    },
    "avg_age":{
      "avg": {
        "field": "age"
      }
    }
  }
  , 
  "size": 10
}
{
......
  "aggregations" : {
    "avg_age" : {
      "value" : 54.666666666666664
    },
    "young_man" : {
      "value" : 30.0
    },
    "oldest_man" : {
      "value" : 80.0
    }
  }
}

size: 控制的是搜索返回的文档数据条数!

Bucket Aggregation

Bucket 可以理解为一个桶,或者一个分组,当遍历文档库的时候会把符合条件的文档放到一个分组里面去,分组就相当于 SQL 中的 Group By

ES 提供的 Bucket Aggregations 中常用的有以下几个:

  • Terms:根据某个字段进行分组。
  • Range、Data Range:根据用户指定的范围参数作为分组的依据来进行聚合操作。
  • Histogram、Date Histogram:可以指定间隔区间来进行聚合操作。
bash 复制代码
# 根据年龄进行分组,相同年龄的一组
POST  people_info/_search
{
  "aggs": {
    "customer_bucket_search_name": {
      "terms": {
        "field": "age",
        "size": 10
      }
    }
  }
}
{
........
"aggregations" : {
    "customer_bucket_search_name" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : 30,
          "doc_count" : 1
        },
        {
          "key" : 54,
          "doc_count" : 1
        },
        {
          "key" : 80,
          "doc_count" : 1
        }
      ]
    }
  }
}

Pipeline Aggregations

Pipeline Aggregations 可以对其他聚合输出的结果进行再次聚合,类似Java中的stream流的操作,有许多不同类型的管道聚合,计算的信息各不相同。大概可以分为两类:

  • Parent:父级聚合的输出提供了一组管道聚合,它可以计算新的存储桶或新的聚合以添加到现有存储桶中
  • sibling:同级聚合的输出提供管道,并且能够计算与该同级聚合处于同一级别的新聚合。

管道聚合仅将计算结果添加到输出中,因此在链接管道聚合时,每个管道聚合的输出都将包含在最终输出

buckets_path语法:通过此规则可以建立各种聚合计算之间的关系,如某个聚合运算作为另一个聚合运算的输入

bash 复制代码
AGG_SEPARATOR       =  `>` ;
METRIC_SEPARATOR    =  `.` ;
AGG_NAME            =  <the name of the aggregation> ;
METRIC              =  <the name of the metric (in case of multi-value metrics aggregation)> ;
MULTIBUCKET_KEY     =  `[<KEY_NAME>]`
PATH                =  <AGG_NAME><MULTIBUCKET_KEY>? (<AGG_SEPARATOR>, <AGG_NAME> )* ( <METRIC_SEPARATOR>, <METRIC> ) 

如以下示例演示计算没个月的平均工资:

bash 复制代码
# 新建一个工资索引
PUT pipeline_salary_demo
{
    "mappings": {
        "properties": {
            "p_id": {
                "type": "keyword"
            },
            "p_salary":{
              "type": "long"
            },
            "p_date": {
               "type": "date",
                "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
            }
        }
    },
    "settings": {
        "number_of_shards": 2,
        "number_of_replicas": 1
    }
}
# 如下四条数据 每月工资 七月还发了5000块的奖金
PUT pipeline_salary_demo/_create/1
{
    "p_id":"LPL001",
    "p_salary":6000,
    "p_date":"2023-08-15"
}
PUT pipeline_salary_demo/_create/2
{
    "p_id":"LPL001",
    "p_salary":9000,
    "p_date":"2023-07-15"
}
PUT pipeline_salary_demo/_create/3
{
    "p_id":"LPL001",
    "p_salary":12000,
    "p_date":"2023-09-15"
}
PUT pipeline_salary_demo/_create/4
{
    "p_id":"LPL001",
    "p_salary":5000,
    "p_date":"2023-07-15"
}

分别执行如下查询比对返回结果:

bash 复制代码
# 1.按月分组
POST pipeline_salary_demo/_search
{
  "size": 0, 
  "aggs": {
    "salary_per_month": {
      "date_histogram": {
        "field": "p_date",
        "interval": "month"
      }
    }
  }
}
# 2.计算每月工资总和
POST pipeline_salary_demo/_search
{
  "size": 0, 
  "aggs": {
    "salary_per_month": {
      "date_histogram": {
        "field": "p_date",
        "interval": "month"
      },
      "aggs": {
        "salarys": {
          "sum": {
            "field": "p_salary"
          }
        }
      }
    }
  }
}
# 查看每月的平均工资
POST pipeline_salary_demo/_search
{
  "size": 0, 
  "aggs": {
    "salary_per_month": {
      "date_histogram": {
        "field": "p_date",
        "interval": "month"
      },
      "aggs": {
        "salarys": {
          "sum": {
            "field": "p_salary"
          }
        }
      }
    },
    "avg_salary":{
      "avg_bucket": {
        "buckets_path": "salary_per_month>salarys"
      }
    }
  }
}

这里只是简单的演示了一下,更多的聚合运算我也没有深入的学习,更多聚合相关的计算请参看这里

ps:官方文档虽然看着慢,但是收获很大~

参考资料:

相关推荐
白露与泡影6 分钟前
springboot + nacos + k8s 优雅停机
spring boot·后端·kubernetes
摇滚侠20 分钟前
elasticSearch是什么,如何使用,有什么用
大数据·elasticsearch·搜索引擎
菜鸟谢24 分钟前
windows xp 下载 sp0 sp1 sp2 sp3 sp4
后端
AirMan26 分钟前
你真的懂 MySQL 的一致性读和当前读的区别吗?
后端·面试
David爱编程32 分钟前
容器性能优化实战指南——防止“吃爆”服务器就靠这些招!
后端·docker·容器
Android洋芋35 分钟前
GitHub项目部署的终极指南:从零到一掌握Docker实战
后端
林太白1 小时前
Next.js超简洁完整篇
前端·后端·react.js
前端付豪1 小时前
汇丰登录风控体系拆解:一次 FaceID 被模拟攻击的调查纪实
前端·后端·架构
无名之逆1 小时前
大三自学笔记:探索Hyperlane框架的心路历程
java·开发语言·前端·spring boot·后端·rust·编程
yang_xiao_wu_1 小时前
springboot+mybatis面试题
spring boot·后端·mybatis