前言
类似于 MySQL 的查询(过滤,分页,排序),Elasticsearch 提供了基于 JSON 的完整的 DSL(领域特定语言)来定义查询。
DSL 查询可以被看作是由以下两种子句组成的 AST(抽象语法树)查询:
Leaf query clauses:叶子查询用于在特定字段中查找特定值,常见查询包括match,term或rangeCompound 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) 是指有关探询那些具有内在结构数据的过程。
比如日期、时间和数字都是结构化的,因为它们有精确的格式,我们可以对这些格式进行逻辑操作。
比较常见的操作包括比较数字或时间的范围,或判定两个值的大小。
当然,文本也可以是结构化的。比如:
- 一篇博客被标注了关键词
ElasticSearch和Lucene - 彩色笔可以有离散的颜色集合: 红(
red) 、 绿(green) 、 蓝(blue)
在结构化查询中,我们得到的结果非是即否:要么存于集合之中,要么存在集合之外。
结构化查询不关心文件的相关度或评分,仅仅是简单的对文档包括或排除处理。
这在逻辑上是能说通的,因为一个数字不能比其他数字更适合存于某个相同范围,其结果只能是:存于范围之中,抑或反之。
同样,对于结构化文本来说:一个值要么相等,要么不等,没有似乎这种概念。
精确匹配—— term
精确匹配的包括:
term:单值精确匹配terms:多值精确匹配
term这个词第一篇文章提到过,代表了倒排索引的索引表中的单词。
单值精确匹配
单值精确匹配指的是必须完全匹配倒排索引中的某个term,根据该term查询相应的结果文档。
比如,根据单值精确匹配查找标题包括Java的帖子:1
2
3
4
5
6
7
8
9
10GET /post/_search
{
"query": {
"term": {
"title": {
"value": "Java"
}
}
}
}
多值精确匹配
多值精确匹配指的是组合匹配倒排索引表中的多个term,根据组合term查询相应的结果文档。
比如,根据多值精确匹配查找标题包括Java与多态的帖子:1
2
3
4
5
6
7
8
9
10
11GET /post/_search
{
"query": {
"terms": {
"title": [
"Java",
"多态"
]
}
}
}
返回结果会将包含:
- 标题包含
Java的文章 - 标题包含
多态的文章 - 标题包含
Java和多态的文章
范围查询—— range
假如现在需要查询所有价格大于 20 且小于 40 元的产品。
在 SQL 中,范围查询可以表示为:
1 | SELECT * |
而在 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 | GET /product/_search |
如果想要范围无界(比方说 >20 ),只须省略其中一边的限制即可:
1 | "range" : { |
日期范围
range 查询同样可以应用在日期字段上:
1 | GET /product/_search |
当使用它处理日期字段时, range 查询支持对 日期计算(date math) 进行操作。
比方说,现在查找创建时间在过去一小时内的所有商品文档:
1 | GET /product/_search |
日期计算还可以被应用到某个具体的时间,并非只能是一个像 now 这样的占位符。只要在某个日期后加上一个双管符号 (||) 并紧跟一个日期数学表达式就能做到:
1 | "range" : { |
字符串范围
range 查询同样可以处理字符串字段,字符串范围可采用 字典顺序(lexicographically) 或字母顺序(alphabetically)。
例如,下面这些字符串是采用字典序(lexicographically)排序的:
- 5, 50, 6, B, C, a, ab, abb, abc, b
在倒排索引中的单词就是采取字典顺序(lexicographically)排列的,这也是字符串范围可以使用这个顺序来确定的原因。
如果我们想查找从 a 到 b (不包含)的字符串,同样可以使用 range 查询语法:
1 | "range" : { |
Null 值查询
若一个字段没有值,那么如何将它存入倒排索引中的呢?
这是个有欺骗性的问题,因为无法做到!所以答案是:什么都不存。
简单的说,一个倒排索引只是一个 term 列表和与之相关的文档信息,若字段不存在,那么它也不会持有任何 token,也就无法在倒排索引结构中表现。
最终,这也就意味着,null, [] (空数组)和 [null]它们都是等价的,都无法存于倒排索引中。
但是,世界并不简单,数据往往会有缺失字段或有显式的空值或空数组。
为了应对这些状况,Elasticsearch 提供了一些查询来处理空或缺失值。
存在查询——exist
exist 查询会返回那些在指定字段有任何值的文档。
存在查询类似于 SQL:1
2
3SELECT field
FROM t
WHERE field IS NOT NULL
缺失查询——missing
缺失查询正好与存在查询相反,只会查询哪些不存在的字段。
缺失查询类似于 SQL:1
2
3SELECT 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
8GET /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
12GET /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
14GET /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 | { |
数据结果处理
对于查询出来的数据,还可以进行如下处理:
- 分页
- 排序
- 高亮部分字段
分页
类似于 MySQL 中的 LIMIT,ES 通过使用from和size参数来达到分页结果。
size:显示应该返回的结果数量,默认为 10from:显示应该跳过的初始结果数量(偏移量),默认为 0
若每页展示 5 条结果,可以用下面方式请求得到 1 到 3 页的结果:1
2
3GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10
当然,from和size除了可以被设置在请求参数中,也可以设置在请求体内:1
2
3
4
5GET /_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
18GET /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
15GET /post/_search
{
"query": {
"term": {
"title": {
"value": "Java"
}
}
},
"highlight": {
"fields": {
"title": {}
}
}
}