(二)ElasticSearch 检索技术—— DSL

前言

  类似于 MySQL 的查询(过滤,分页,排序),Elasticsearch 提供了基于 JSON 的完整的 DSL(领域特定语言)来定义查询。

   DSL 查询可以被看作是由以下两种子句组成的 AST(抽象语法树)查询:

  • Leaf query clauses:叶子查询用于在特定字段中查找特定值,常见查询包括match, termrange
  • Compound query clauses:复合查询用于包装其他页,其也用于以逻辑方式组合多个查询(如bool 查询 或dis_max 查询),或改变其行为(如constant_score query查询)

  由于 DSL 的查询类别繁多,具体可以见 ES 7.6—— DSL,此处仅介绍常用的几种。

  DSL 查询以query为开始,根据具体场景选择相应查询进行组合。

前置概念

相关性评分

  默认情况下,Elasticsearch 对匹配的搜索结果,会根据相关性评分的高低来进行排序
  换而言之,相关性评分是用于衡量每个文档与查询的匹配程度

  相关性得分是一个正浮点数,返回在搜索 API 的得分源字段中。 分数越高,则文档越相关。
  需注意的一点是:虽然每种查询类型可以以不同的方式计算相关性分数,但分数计算还取决于查询子句是使用了查询关键字还是过滤关键字

查询 VS 过滤

查询(Query)

  在查询(搜索)中,查询子句主要回答以下问题:

  • 此文档与此查询子句的匹配程度如何?

  对于匹配程度,应该有一个相关得分,匹配程度高得分高,匹配程度低得分低。

过滤(Filter)

  在过滤中,过滤子句主要用于过滤出结构化数据,例如:

  • 年龄范围在 18 ~ 30 的人数
  • 性别为男性的人

  对于过滤而言,不应该也不需要相关得分。毕竟,过滤关注的不是文档匹配程度如何,而是如何对查询出的匹配结果进行处理。

  因此,在 Filter 元素下指定的查询对得分没有影响ーー scores 返回为 0.

场景适配

  那么,查询和过滤有什么区别呢?它们分别又适合在什么场景下使用呢?
  简单来讲,查询会计算相关得分,而过滤不会计算,因此若是电商类网站业务,可以使用查询去计算相关得分搜索相应数据,而对于不关心得分的报表类统计业务,则可以使用过滤去搜索相应数据。

结构化搜索

  结构化搜索(Structured search) 是指有关探询那些具有内在结构数据的过程。
  比如日期、时间和数字都是结构化的,因为它们有精确的格式,我们可以对这些格式进行逻辑操作。
  比较常见的操作包括比较数字或时间的范围,或判定两个值的大小。

  当然,文本也可以是结构化的。比如:

  • 一篇博客被标注了关键词ElasticSearchLucene
  • 彩色笔可以有离散的颜色集合: 红(red) 、 绿(green) 、 蓝(blue

  在结构化查询中,我们得到的结果非是即否:要么存于集合之中,要么存在集合之外。
  结构化查询不关心文件的相关度或评分,仅仅是简单的对文档包括或排除处理。
  这在逻辑上是能说通的,因为一个数字不能比其他数字更适合存于某个相同范围,其结果只能是:存于范围之中,抑或反之。
  同样,对于结构化文本来说:一个值要么相等,要么不等,没有似乎这种概念。

精确匹配—— term

  精确匹配的包括:

  • term:单值精确匹配
  • terms:多值精确匹配

  term这个词第一篇文章提到过,代表了倒排索引的索引表中的单词。

单值精确匹配

  单值精确匹配指的是必须完全匹配倒排索引中的某个term,根据该term查询相应的结果文档。
  比如,根据单值精确匹配查找标题包括Java的帖子:

1
2
3
4
5
6
7
8
9
10
GET /post/_search
{
"query": {
"term": {
"title": {
"value": "Java"
}
}
}
}

多值精确匹配

  多值精确匹配指的是组合匹配倒排索引表中的多个term,根据组合term查询相应的结果文档。
  比如,根据多值精确匹配查找标题包括Java多态的帖子:

1
2
3
4
5
6
7
8
9
10
11
GET /post/_search
{
"query": {
"terms": {
"title": [
"Java",
"多态"
]
}
}
}

  返回结果会将包含:

  • 标题包含Java的文章
  • 标题包含多态的文章
  • 标题包含Java多态的文章

范围查询—— range

  假如现在需要查询所有价格大于 20 且小于 40 元的产品。
  在 SQL 中,范围查询可以表示为:

1
2
3
SELECT *
FROM product
WHERE price BETWEEN 20 AND 40

  而在 Elasticsearch 中,通过range达到同样目的:

1
2
3
4
5
6
"range" : {
"price" : {
"gte" : 20,
"lte" : 40
}
}

  range 查询可同时提供包含和不包含这两种范围表达式,可供组合的选项如下:

  • gt: > 大于(即 greater than)
  • lt: < 小于(即 less than)
  • gte: >= 大于或等于(即 greater than or equal to)
  • lte: <= 小于或等于(即 less than or equal to)

  下面是一个范围查询的例子:.

1
2
3
4
5
6
7
8
9
10
11
GET /product/_search
{
"query": {
"range": {
"price": {
"gte": 20,
"lte": 40
}
}
}
}

  如果想要范围无界(比方说 >20 ),只须省略其中一边的限制即可:

1
2
3
4
5
"range" : {
"price" : {
"gt" : 20
}
}

日期范围

  range 查询同样可以应用在日期字段上:

1
2
3
4
5
6
7
8
9
10
11
GET /product/_search
{
"query": {
"range": {
"gmt_create": {
"gte": "2020-05-20 00:00:00",
"lte": "2020-05-21 00:00:00"
}
}
}
}

  当使用它处理日期字段时, range 查询支持对 日期计算(date math) 进行操作。
  比方说,现在查找创建时间在过去一小时内的所有商品文档:

1
2
3
4
5
6
7
8
9
10
GET /product/_search
{
"query": {
"range": {
"gmt_create": {
"gt": "now-1h"
}
}
}
}

  日期计算还可以被应用到某个具体的时间,并非只能是一个像 now 这样的占位符。只要在某个日期后加上一个双管符号 (||) 并紧跟一个日期数学表达式就能做到:

1
2
3
4
5
6
"range" : {
"timestamp" : {
"gt" : "2014-01-01 00:00:00",
"lt" : "2014-01-01 00:00:00||+1M"
}
}

字符串范围

  range 查询同样可以处理字符串字段,字符串范围可采用 字典顺序(lexicographically) 或字母顺序(alphabetically)。
  例如,下面这些字符串是采用字典序(lexicographically)排序的:

  • 5, 50, 6, B, C, a, ab, abb, abc, b

  在倒排索引中的单词就是采取字典顺序(lexicographically)排列的,这也是字符串范围可以使用这个顺序来确定的原因。

  如果我们想查找从 ab (不包含)的字符串,同样可以使用 range 查询语法:

1
2
3
4
5
6
"range" : {
"title" : {
"gte" : "a",
"lt" : "b"
}
}

Null 值查询

  若一个字段没有值,那么如何将它存入倒排索引中的呢?
  这是个有欺骗性的问题,因为无法做到!所以答案是:什么都不存。
  简单的说,一个倒排索引只是一个 term 列表和与之相关的文档信息,若字段不存在,那么它也不会持有任何 token,也就无法在倒排索引结构中表现。

  最终,这也就意味着,null, [] (空数组)和 [null]它们都是等价的,都无法存于倒排索引中。

  但是,世界并不简单,数据往往会有缺失字段或有显式的空值或空数组。
  为了应对这些状况,Elasticsearch 提供了一些查询来处理空或缺失值。

存在查询——exist

  exist 查询会返回那些在指定字段有任何值的文档。
  存在查询类似于 SQL:

1
2
3
SELECT field
FROM t
WHERE field IS NOT NULL

缺失查询——missing

  缺失查询正好与存在查询相反,只会查询哪些不存在的字段。
  缺失查询类似于 SQL:

1
2
3
SELECT field
FROM t
WHERE field IS NULL

小提示:缺失查询只能在 filter中使用,若想在查询中使用,需要结合must_not + exists使用。

全文本检索

  前文提到过,ElasticSearch 会对全文本进行分词并建立倒排索引。
  因此,与结构化搜索不同的是:结构化搜索会根据term去完全匹配倒排索引表中的term,而全文本检索将对给定的查询关键字进行分词,分词得到Term后再去倒排索引表中搜寻与这些Term匹配的文档!!!

match query 匹配查询

  match query 用于执行全文查询的标准查询,包括模糊匹配和短语或邻近查询。

1
2
3
4
5
6
7
8
GET /post/_search
{
"query": {
"match": {
"content": "Lucene is a Java full-text search engine"
}
}
}

核心参数

operator

  默认情况下,分词后的各Token间的关系为or,而operator参数可以控制Token之间的逻辑关系,比如将or修改为and
  举个例子:分析:检索词“系统学es”被分词为【系统学、es】两个Token,【语句1】的operator默认值为or,所以文档1和2可以被检索到;【语句2】的operator的值是and,也就是需要同时包含【系统学、es】这两个Token才行,所以没有结果。

match_phrase query 短语查询

  match_phrase query,即短语查询,短语查询会分析文本并根据分析的文本创建一个短语查询。
  短语查询将检索关键词分词后,对分词的结果,要求必须在被检索字段的分词中都包含,不仅要求顺序必须相同,默认还必须都是连续的。

match_phrase_prefix query

  与 match_phrase 查询类似,但是会对最后一个Token在倒排序索引列表中进行通配符搜索。
  Token 的模糊匹配数控制:max_expansions 默认值为50。

multi_match query 多字段版本

  举个例子:查询 product_name 和 product_desc 字段中,只要有 toothbrush 关键字的就查询出来。

1
2
3
4
5
6
7
8
9
10
11
12
GET /product_index/product/_search
{
"query": {
"multi_match": {
"query": "toothbrush",
"fields": [
"product_name",
"product_desc"
]
}
}
}

  与match query类型,multi_match 的operator参数默认为or,若将其修改为and,在跨多个 field 查询时,表示查询分词必须都出现在相同字段中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /product_index/product/_search
{
"query": {
"multi_match": {
"query": "PHILIPS toothbrush",
"type": "cross_fields",
"operator": "and",
"fields": [
"product_name",
"product_desc"
]
}
}
}

common terms query——对停顿词的检索优化(了解)

  

query_string query

  允许我们在单个查询字符串中指定AND | OR | NOT条件,同时也和 multi_match query 一样,支持多字段搜索。

注意点:1、中间的连接词【AND | OR | NOT】必须是全大写;2、各个检索词依然会被对应的分词器分词,单个检索词就相当于match query。

simple_query_string query

类似于query_string ,但是会忽略错误的语法,永远不会引发异常,并且会丢弃查询的无效部分。

组合查询

  bool query 默认是用于组合多个子查询子句的查询。
  bool query 使用一个或多个布尔子句构建,每个子句具有一个类型化的事件,类型如下:

  • must:等价于AND,子查询必须出现在匹配的文档中(有助于得分)
  • should:等价于OR,子查询应该出现在匹配的文档中(增加得分)
  • must_not:等价于NOT,子查询不能出现在匹配的文档,该查询常被被用于缓存
  • filter:等价于AND, 子查询必须出现在匹配的文档中,与must不同的是,查询的得分将被忽略。该查询常被被用于缓存。

  举个例子:

1
2
3
4
5
6
7
8
9
10
{
"bool": {
"must": { "match": { "title": "how to make millions" }},
"must_not": { "match": { "tag": "spam" }},
"should": [
{ "match": { "tag": "starred" }},
{ "range": { "date": { "gte": "2014-01-01" }}}
]
}
}

  上面的查询用于:

  • 查找 title 字段匹配 how to make millions
  • 并且不被标识为 spam 的文档
  • 那些被标识为 starred 或在2014年之后的文档,将比另外的文档拥有更高的排名。如果 两者 都满足,那么它排名将更高:

fileter 查询

  如果我们不想因为文档的时间而影响得分,可以用filter语句来重写前面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
{
"bool": {
"must": { "match": { "title": "how to make millions" }},
"must_not": { "match": { "tag": "spam" }},
"should": [
{ "match": { "tag": "starred" }}
],
"filter": {
"range": { "date": { "gte": "2014-01-01" }}
}
}
}

  通过将 range 查询移到 filter 语句中,我们将它转成不评分的查询,将不再影响文档的相关性排名。
  由于它现在是一个不评分的查询,可以使用各种对filter查询有效的优化手段来提升性能。

  所有查询都可以借鉴这种方式。将查询移到 bool 查询的 filter 语句中,这样它就自动的转成一个不评分的filter了。

  若需要通过多个不同的标准来过滤文档,bool 查询本身也可以被用做不评分的查询。简单地将它放置到 filter 语句中并在内部构建布尔逻辑即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"bool": {
"must": { "match": { "title": "how to make millions" }},
"must_not": { "match": { "tag": "spam" }},
"should": [
{ "match": { "tag": "starred" }}
],
"filter": {
"bool": {
"must": [
{ "range": { "date": { "gte": "2014-01-01" }}},
{ "range": { "price": { "lte": 29.99 }}}
],
"must_not": [
{ "term": { "category": "ebooks" }}
]
}
}
}
}

数据结果处理

 对于查询出来的数据,还可以进行如下处理:

  • 分页
  • 排序
  • 高亮部分字段

分页

  类似于 MySQL 中的 LIMIT,ES 通过使用fromsize参数来达到分页结果。

  • size:显示应该返回的结果数量,默认为 10
  • from:显示应该跳过的初始结果数量(偏移量),默认为 0

  若每页展示 5 条结果,可以用下面方式请求得到 1 到 3 页的结果:

1
2
3
GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10

  当然,fromsize除了可以被设置在请求参数中,也可以设置在请求体内:

1
2
3
4
5
GET /_search
{
"size": 20,
"from": 0
}

排序

  类似于 SQL 的排序,ES 中通过 sort 可以对查询的数据根据字段进行排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /user/_search
{
"query": {
"range": {
"age": {
"gte": 10,
"lte": 20
}
}
},
"sort": [
{
"age": {
"order": "desc"
}
}
]
}

高亮

  在 ES 中,highlight 属性可以对查询出的数据进行高亮显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /post/_search
{
"query": {
"term": {
"title": {
"value": "Java"
}
}
},
"highlight": {
"fields": {
"title": {}
}
}
}

参考

0%