0%

在未安装之前替换

  1. 替换 brew 默认源
1
export HOMEBREW_BREW_GIT_REMOTE=https://mirrors.ustc.edu.cn/brew.git
  1. 替换 homebrew/homebrew-core 默认源(中科大源):
1
export HOMEBREW_CORE_GIT_REMOTE=https://mirrors.ustc.edu.cn/homebrew-core.git

如果想下次启动的时候,还使用这两个源,需要做以下修改:

1
2
3
4
5
6
7
替换brew.git:
cd "$(brew --repo)"
git remote set-url origin https://mirrors.ustc.edu.cn/brew.git

替换homebrew-core.git:
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git

参考链接:

https://lug.ustc.edu.cn/wiki/mirrors/help/brew.git

本文用的 MySQL 是阿里云的 RDS,一主一从,主 1 核,从 2 核。配置很低,这不是本文讨论内容,不多说为什么。

背景

在很长一段时间的上线流程中,我们都要跑一个脚本(Laravel artisan 命令),用来对所有用户的权限做一些可能的更新操作。这个过程需要很长的时间,30w 左右的客户,19 个进程跑这些脚本短则 12 小时左右,多则一天以上。

影响

运行这个脚本的过程中,整个网站响应速度都慢了很多。查看服务器 CPU、内存 负载,没有什么压力,最后发现是 MySQL 从库 CPU 负载一直 100%(2 核的配置)。

分析

  1. 大家会说,很明显啊,升级一下 MySQL 从库,让它读得快一点就好了啊。我开始也是这么想的,从 2 核升到 8 核后,发现虽然快了,但是没有快 4 倍,后来去看主库发现,这个时候主库的 CPU 100% 了(原来平稳 30% 左右)。

  2. 现在发现了主库满载,我也想到了可以考虑升级一下主库,但是主库是包年包月的,一升就要按之前付费的时间来补足这些费用,但这些我决定不了。所以暂时放弃这个想法了。

本地测试

上一次上线的时候,也是发现了这个问题,上一次个人是想从代码层面找到优化的地方的(比如,会不会有嵌套的循环等,这个可以用 xhprof),后来把这个给忘了。

发现升级 MySQL 服务器的想法失败了,想起了自己之前写过的一个记录 SQL 语句的工具,便想着可以看看是不是有什么 SQL 执行太久了。

  1. 本地测试发现,一个脚本执行下来,有 200 个 SQL 语句,其中有很多是可以修改为批量操作的。因为 Laravel 模型的 createMany 是将关联数据一条一条插入的,所以将 createMany 修改为 insert 同时插入多条记录。

  2. 我们模型里面还用了 spatie/laravel-activitylog 这个包,这就导致,上面的批量语句有多少条,最终产生的 SQL 语句是这个数量的两倍。考虑到这个包记录的那些数据从来就没有用过,就把相关模型记录变更的操作禁用掉了

更新后的效果

把这两百个 SQL 缩减为 17 个之后,信心满满地将代码更新上去,失望地发现,感觉一点都没有快(Laravel horizon 面板 job 数量减少还是很缓慢)。然后接手这个功能的人告诉我,其实真实场景是写的情况是很少的,大部分是读语句。

本地测试的时候,打印的语句好像并没有非常慢的,但是有几个 80ms 左右的查询,个人觉得这是一个比较正常的。但事实证做优化的时候不能太感性,优化应该是有某些指标、方向指导的。

记录生成的 SQL 语句

既然去掉了 90% 的 SQL 语句都没有效,那就可能是一些语句在测试环境和生产的执行效率差别太大(生产数据多一些)。所以就写了下面的代码:

1
2
3
4
5
6
7
8
9
10
11
$cache = \Cache::get('permission');
if (!$cache) {
enable_mySQL_log();
enable_mongo_log();
\Cache::put('permission', 1, 120);
}
// ... 这里省略业务代码 ...
if (!$cache) {
disable_mySQL_log();
disable_mongo_log();
}

这里的 enable_mySQL_log 几个函数是自定义用来记录 SQL 语句的,使用 Cache 的目的是为了只记录一次(一次就够)。

从记录的语句中,发现了有两条语句都是用了 300ms 以上,而其他语句平均时间是 3ms。但这两个语句所在的表数据不是很多(20w左右),而且只是一个简单的 join 加一些条件筛选。

到这里原因其实已经很明显了,把这两条 SQL 语句拿去 explain 一下,答案没有让我失望,type: All

接下来的事情就很简单了,给涉及的那张表加了两个字段的独立索引,再去看 SQL 语句,300ms 降到了平均水平 3ms。

原因

MySQL join 表的时候,关联表的关联字段如果没有索引的话,会导致全表扫描。

回到测试环境加上索引后测试发现,原来 80ms 的语句,现在只需要 10ms 左右。

其他可以优化的地方

因为我们的脚本是扫描全表做处理的,所以可以考虑将里面的大部分查询转换为批量查询,查询到结果之后再进行业务逻辑处理。

总而言之,可以批量就不要分开操作。

反思总结

  1. 优化的时候如果找不到方向,不妨可以看看产生的 SQL 语句,可能只是某个表忘记加索引了。

  2. 关于 spatie/laravel-activitylog,虽然我们从来没有用过,但是它一直在记录,感觉如果从来不需要去看模型变更记录的话,还不如不要。

  3. 关于定位系统性能问题,上一次上线的时候,虽然想从代码着手,但是并没有发现有什么特别影响性能的地方。经过这次经历发现,发现系统有性能的时候,靠谱的做法是先定位到哪里产生了性能的问题。因为这个系统从开始到现在一直很多地方都有一些性能问题,另外一方面觉得那一部分业务负责,所以觉得慢是正常的。这些都是非常感性的想法,非常有害的,这会导致我们做不出正确的判断。

正确的做法是,从各方面去定位原因,比如应用服务器负载、数据库服务器负载,如果有某一个方面到达了瓶颈的话,马上升级并不是一个明智的做法,我们需要明确的知道是不是服务器不够用,还是代码写得有问题,如果最终确定的确是服务器支撑不住的话,再考虑升级。因为有些严重的性能问题,带来的性能下降是指数级的,单靠升级服务器要付出很大的成本。

如果我们遇到问题就凭感觉来断定的话,往往导致走很多弯路(走就算了,更坏的结果是到最后问题也还是没有解决)。

  1. 那些你觉得理所当然的未必就是理所当然的。这个问题从出现到解决经历的周期说实话非常长了,接手它的人和大家其实一致觉得,是因为里面业务逻辑复杂,可能需要的查询很多,处理很多东西。但事实证明,事情不像表明看到的那样。

nested 类型是一种特殊的对象 object 数据类型,允许对象数组彼此独立地进行索引和查询。

对象数组如何扁平化

内部对象 object 字段的数组不能像我们期望的那样工作。Lucene 没有内部对象的概念,所以 Elasticsearch 将对象层次结构扁平化为一个字段名称和值简单列表。例如,以下文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
curl -XPUT 'localhost:9200/my_index/my_type/1?pretty' -H 'Content-type: application/json' -d '
{
"group": "fans",
"user": [
{
"first": "John",
"last": "Smith"
},
{
"first": "Alice",
"last": "White"
}
]
}
'

说明

user 字段被动态的添加为 object 类型的字段。

在内部其转换成一个看起来像下面这样的文档:

1
2
3
4
5
{
"group": "fans",
"user.first": ["alice", "john"],
"user.last": ["smith", "white"]
}

user.firstuser.last 字段被扁平化为多值字段,并且 alice 和 white 之间的关联已经丢失。本文档将错误地匹配 user.first 为 alice 和 user.last 为 smith 的查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
curl -XGET 'localhost:9200/my_index/_search?pretty' -H 'Content-Type: application/json' -d '
{
"query": {
"bool": {
"must": [
{
"match": {
"user.first": "Alice"
}
},
{
"match": {
"user.last": "Smith"
}
}
]
}
}
}
'

对对象数组使用嵌套字段

如果需要索引对象数组并维护数组中每个对象的独立性,则应使用 nested 数据类型而不是 object 数据类型。在内部,嵌套对象将数组中的每个对象作为单独的隐藏文档进行索引,这意味着每个嵌套对象都可以使用嵌套查询 nested query 独立于其他对象进行查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
curl -XPUT 'localhost:9200/my_index?pretty' -H 'Content-Type: application/json' -d '
{
"mappings": {
"my_type": {
"properties": {
"user": {
"type": "nested"
}
}
}
}
}
'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
curl -XPUT 'localhost:9200/my_index/my_type/1?pretty' -H 'Content-Type: application/json' -d '
{
"group": "fans",
"user": [
{
"first": "John",
"last": "Smith"
},
{
"first": "Alice",
"last": "White"
}
]
}
'

说明:

user 字段映射为 nested 类型,而不是默认的 object 类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
curl -XGET 'localhost:9200/my_index/_search?pretty' -H 'Content-Type: application/json' -d '
{
"query": {
"nested": {
"path": "user",
"query": {
"bool": {
"must": [
{
"match": { "user.first": "Alice"}
},
{
"match": { "user.last": "Smith" }
}
]
}
}
}
}
}
'

说明:

此查询得不到匹配,是因为 Alice 和 Smith 不在同一个嵌套对象中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
curl -XGET 'localhost:9200/my_index/_search?pretty' -H 'Content-Type: application/json' -d '
{
"query": {
"nested": {
"path": "user",
"query": {
"bool": {
"must": [
{ "match": { "user.first": "Alice" } },
{ "match": { "user.last": "White" } }
]
}
},
"inner_hits": {
"highlight": {
"fields": {
"user.first": {}
}
}
}
}
}
}
'

说明:

此查询得到匹配,是因为 Alice 和 White 位于同一个嵌套对象中。

inner_hits 允许我们突出显示匹配的嵌套文档。

嵌套文档可以:

  • 使用 nested 查询进行查询
  • 使用 nested 和 reverse_nested 聚合进行分析
  • 使用 nested 排序进行排序
  • 使用 nested_inner_hits 进行检索与突出显示

嵌套字段参数

嵌套字段接受以下参数:

  • dynamic:是否将新属性动态添加到现有的嵌套对象。共有 true(默认),false 和 strict 三种参数。
  • include_in_all:(_all 字段已经废弃了)
  • properties:嵌套对象中的字段,可以是任何数据类型,包括嵌套。新的属性可能会添加到现有的嵌套对象。

备注:

类型映射(type mapping)、对象字段和嵌套字段包含的子字段,称之为属性 properties。这些属性可以为任意数据类型,包括 object 和 nested。属性可以通过以下方式加入:

  • 当在创建索引时显式定义它们
  • 当使用 PUT mapping API 添加或更新映射类型时显式地定义它们
  • 当索引包含新字段的文档时动态的加入

重要:

由于嵌套文档作为单独的文档进行索引,因此只能在 nested 查询,nested/reverse_nested 聚合或者 nested_inner_hits 的范围内进行访问。

限制嵌套字段的个数

索引一个拥有 100 个嵌套字段的文档,相当于索引了 101 个文档,因为每一个嵌套文档都被索引为一个独立的文档。为了防止不明确的映射,每个索引可以定义的嵌套字段的数量已被限制为 50 个。

多嵌套查询方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"query": {
"bool": {
"must": [
{
"nested": {
"path": [
"ajxx"
],
"query": {
"bool": {
"must": [
{
"match": {
"ajxx.ajzt": "破案"
}
},
{
"range": {
"ajxx.sasj": {
"gte": "2017-01-01 12:10:10",
"lte": "2017-01-02 12:10-40"
}
}
}
],
"should": [
{
"query_string": {
"query": "20170316盗窃案"
}
}
]
}
}
}
}
]
}
}
}

查询字段名称的模糊匹配编辑 字段名称可以用模糊匹配的方式给出:任何与模糊式正则匹配的字段都会被包括在搜索条件中。

1
2
3
4
5
6
{
"multi_match": {
"query": "结果",
"fields": ["*", "*_title"]
}
}

当这些子字段出现数值类型的时候,就会报异常了,解决方法是加入 lenient 字段。

1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "parse_exception",
"reason": "failed to parse date field [XXX] with format [yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis]"
}

{
"multi_match": {
"query": "结果",
"lenient": "true",
"fields": ["*"]
}
}

利用 multi_match 嵌套全文检索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"include_in_parent": true,
"include_in_root": true,
{
"query": {
"bool": {
"must": [
{
"match": {
"ajztmc": "立案"
}
},
{
"match": {
"zjasl": "刑事"
}
},
{
"range": {
"lasj": {
"gte": "2015-01-01 12:10:10",
"lte": "2016-01-01 12:10:40"
}
}
},
{
"nested": {
"path": [
"rqxxx"
],
"query": {
"bool": {
"must": [
{
"match": {
"rqxx.baqmc": "办案区名称"
}
}
]
}
}
}
},
{
"nested": {
"path": [
"saryxx"
],
"query": {
"bool": {
"must": [
{
"match": {
"abc": "嫌疑人"
}
},
{
"match": {
"def": "女"
}
}
]
}
}
}
},
{
"nested": {
"path": [
"wp"
],
"query": {
"bool": {
"must": [
{
"match": {
"wp.wpzlnc": "赃物"
}
},
{
"match": {
"wp.wpztmc": "物品入库"
}
}
]
}
}
}
},
{
"multi_match": {
"query": "男",
"lenient",
"fields": [
"*"
]
}
}
]
}
},
"from": 0,
"size": 100,
"sort": {
"zxxgsj": {
"order": "desc"
}
}
}

使用查询解析器来解析其内容的查询。下面是一个例子:

1
2
3
4
5
6
7
8
9
GET /_search
{
"query": {
"query_string": {
"default_field": "content",
"query": "this AND that OR thus"
}
}
}

query_string 查询解析输入并在运算符周围分割文本。每个文本部分彼此独立地分析。例如以下查询:

1
2
3
4
5
6
7
8
9
GET /_search
{
"query": {
"query_string": {
"default_field": "content",
"query": "(new york city) OR (big apple)"
}
}
}

将分成 new york citybig apple,然后通过为该字段配置的分析器独立地分析每个部分。

空格不被视为运算符,这意味着 new york city 将 “按原样” 传递给为该字段配置的分析器。如果该字段是关键字字段,则分析器将创建单个术语 new york city,并且查询构建器将在查询中使用此术语。如果要分别查询每个术语,则需要在术语周围添加显式运算符(例如 new AND york AND city)。

当提供多个字段时,也可以修改如何使用类型参数在每个文本部分内组合不同字段查询。这里描述了可能的模式,默认是 Bestfield。query_string 顶级参数包括:

  • query: 要解析的实际查询。参见查询字符串语法
  • default_field: 如果未指定前缀字段,则查询字词的默认字段。默认为 index.query.default_field 索引设置,而索引设置默认为 *.* 提取映射中符合术语查询条件的所有字段,并过滤元数据字段。然后组合所有提取的字段以在没有提供前缀字段时构建查询。
  • default_operator: 如果未显式指定运算符,则使用默认运算符。例如,使用默认运算符 OR,查询 capital of Hungary 将转换为 capital OR of OR Hungary,并且使用默认运算符 AND,将相同的查询转换为 capital AND of AND Hungary。默认值为 OR。
  • analyzer:用于分析查询字符串的分析器名称。
  • quote_analyzer:分析器的名称,用于分析查询字符串中的引用短语。对于这些部件,它将覆盖使用 analyzer 参数或 search_quote_analyzer 设置设置的其他分析器。
  • allow_leading_wildcard: 设置时,* 或 ? 允许作为第一个字符。默认为 true。
  • enable_position_increments:设置为 true 在结果查询中启用位置增量。默认为 true。
  • fuzzy_max_expansions:控制模糊查询将扩展到的术语数。默认为 50
  • fuzziness:设置模糊查询的模糊性。默认为 AUTO。
  • fuzzy_prefix_length:设置模糊查询的前缀长度。默认是 0.
  • fuzzy_transpositions:设置为 false 禁用模糊转置(ab->ba)。默认是 true。
  • phrase_slop: 设置短语的默认斜率。如果为 0,则需要精确的短语匹配。默认值是 0。
  • boost:设置查询的提升值。默认为 1.0。
  • auto_generate_phrase_queries:默认为 false。
  • analyze_wildcard: 默认情况下,不分析查询字符串中的通配符。通过将此值设置为 true,将尽最大努力分析这些值。
  • max_determinized_states: 限制允许创建的 regexp 查询的自动机状态数。这可以防止太难(例如指数级)的 regexp。默认为 10000。
  • minimum_should_match: 一个值,用于控制生成的布尔查询中应该匹配的 "should" 子句的数量。它可以是绝对值(2),百分比(30%)或两者的组合。
  • lenient:如果设置为 true 将导致基于格式的失败(如向数字字段提供文本)将被忽略。
  • time_zone: 时区应用于与日期相关的任何范围查询。
  • quote_field_suffix: 附加到查询字符串的引用部分的字段的后缀。这允许使用具有不同分析链的字段进行精确匹配。
  • auto_generate_synonyms_phrase_query: 是否应为多项同义词自动生成短语查询。默认为 true。
  • all_fields:执行上可以查询映射检测到的所有字段的查询。

Default Field

如果未在查询字符串语法中明确指定要搜索的字段,index.query.default_field 则将使用该字段来派生要搜索的字段。如果 index.query.default_field 未指定,query_string 则将自动尝试确定索引映射中可查询的现有字段,并对这些字段执行搜索。请注意,这不包括嵌套文档,使用嵌套查询来搜索这些文档。

Multi Field

该 query_string 查询还可以指定查询的字段。可以通过 "fields" 参数提供字段:

1
field1: query_term OR field2: query_term | ...

query_string 针对多个字段运行查询的想法是将每个查询字词扩展为 OR 子句,如下所示:

例如,以下查询:

1
2
3
4
5
6
7
8
9
GET /_search
{
"query": {
"query_string": {
"fields": ["content", "name"],
"query": "this AND that"
}
}
}

匹配相同的单词

1
2
3
4
5
6
GET /_search
{
"query": {
"query_string": "(content:this OR name:this) AND (content:that OR name:that)"
}
}

由于从单个搜索项生成了多个查询,因此使用 dis_max 带有 tie_breaker 的查询自动组合它们。例如(name 使用 ^5 符号表示增强 5):

1
2
3
4
5
6
7
8
9
10
GET /_search
{
"query": {
"query_string": {
"fields": ["content", "name^5"],
"query": "this AND that OR thus",
"tie_breaker": 0
}
}
}

简单通配符也可以用于搜索文档的特定内部元素。例如,如果我们有一个 city 包含多个字段(或带有字段的内部对象)的对象,我们可以自动搜索所有 “城市” 字段:

1
2
3
4
5
6
7
8
9
GET /_search
{
"query": {
"query_string": {
"fields": ["city.*"],
"query": "this AND that OR thus"
}
}
}

另一种选择是在查询字符串本身中提供通配符字段搜索(正确转义*符号),例如 city.\*:something

1
2
3
4
5
6
7
8
GET /_search
{
"query": {
"query_string": {
"query": "city.\\*:(this AND that OR thus)"
}
}
}

由于 (反斜杠)是json字符串中的特殊字符,因此需要对其进行转义,因此上面的两个反斜杠 query_string。

query_string 对多个字段运行查询时,允许使用以下附加参数:

  • type: 应如何组合字段以构建文本查询。

fields 参数还可以包括基于通配符字段名称,允许自动扩展到相关字段(包括动态引入的字段)。例如:

1
2
3
4
5
6
7
8
9
GET /_search
{
"query": {
"query_string": {
"fields": ["content", "name.*^5"],
"query": "this AND that OR thus"
}
}
}

Query stirng 语法

查询字符串被解析为一些列术语和运算符。术语可以是单个单词 - quick 或 brown - 或短语,由双引号括起来 - "quick brown" 以相同的顺序搜索短语中的所有单词。

operator 允许你自定义搜索 - 可用选项如下:

Field names

如 query_string 查询中所述 default_field,搜索搜索词,但可以在查询语法中指定其他字段:

  • 其中 status 字段包含 active:
    • status:active
  • 其中 title 字段包含 quick 或 brown。如果省略 OR 运算符,将使用默认运算符:
    • title:(quick OR brown)
    • title:(quick brown)
  • 其中 author 字段包含精确短语 "john smith"
    • author:"john smith"
  • book 中任何字段包含 quick 或 brown(注意我们需要对 * 使用反斜杠转义)
    • book.\*:(quick brown)
  • 该字段 title 具有任何非 null 值:
    • _exists_:title

Wildcards 通配符

通配符可以在单个术语上运行,使用 ? 替换单个字符,使用 * 替换零个或多个字符:

qu?ck bro *

  • 请注意,通配符查询可能使用大量的内存,并且执行得很糟糕,只要想想需要查询多少项来匹配查询字符串 a*b*c*
  • 纯通配符 * 被重写为 exists。因此,通配符 "field:*" 将匹配具有空值的文档,如下所示:{"field": ""},如果字段丢失或使用显式空值设置则不匹配:{"field": null}
  • 允许在单词的开头(例如 "*ing")使用通配符特别重,因为需要检查索引中的所有术语,以防它们匹配。可以通过设置 allow_leading_wildcard 为禁用前导通配符 false。

Regular expressions 正则表达式

正则表达式模式可以通过将它们包装在 forward-slashes("/") 中嵌入查询字符串中:

name:/jo?n(ath[oa]n)/

正则表达式语法中解释了受支持的正则表达式语法。

allow_leading_wildcard 参数对正则表达式没有任何控制权。如下所示的查询字符串将强制 Elasticsearch 访问索引中的每个术语:/.*n/ 谨慎使用!

Fuzziness 模糊

我们可以使用 "fuzzy" 运算符搜索与我们的搜索字词类似但不完全相同的字词:

1
quikc~ brwn~ foks~

Ranges 范围

可以为日期,数字或字符串字段指定范围。包含范围用方括号指定,[min TO max] 排他范围用大括号指定 {min TO max}

  • All days in 2012:
    • date:[2012-01-01 TO 2012-12-31]
  • Numbers 1..5
    • count:[1 TO 5]
  • Tags between alpha and omega,excluding alpha and omega:
    • tag:{alpha TO omega}
  • Numbers from 10 upwards
    • count:[10 TO *]
  • Dates before 2012
    • date:{* TO 2012-01-01}

可以组合使用大括号和方括号:

  • 数字从 1 到 5 但不包括 5:
    • count:[1 TO 5}
  • 一边无范围的范围可以使用如下语法:
    • age:>10
    • age:>=10
    • age:<10
    • age:<=10

要将上限和下限与简化语法结合使用,你需要将两个子句与 AND 运算符连接:

1
2
age:(>=10 AND <20)
age:(+>=10 +<20)

使用 Boost 提升权重

使用 boost 运算符 ^ 使一个术语比另一个术语更相关。例如,如果我们想要找到关于 foxes 的所有文档,但我们对 quick foxes 特别感兴趣:

1
quick^2 fox

默认 boost 值为 1,但可以是任何正浮点数。0 到 1 之间的提升会降低相关性。

提升也可以应用于短语或群组:

1
"john smith"^2 (foo bar)^4

Boolean operators 布尔运算符

默认情况下,只要一个术语匹配,所有术语都是可选的。搜索 foo bar baz 将查找包含一个或多个 foo 或 bar 或 baz 的任何文档。我们已经讨论了 default_operator 上面的内容,它允许你强制要求所有的术语,但也有一些布尔运算符可以在查询字符串本身中使用,以提供更多的控制。

首选运算符+(此术语必须存在)和-(此术语不得出现)。所有其他条款都是可选的。例如,这个查询:

1
quick brown +fox -news

说明:

  • fox 必须存在
  • news 一定不能存在
  • quick 和 brown 是可选的 - 它们的存在增加了相关性

熟悉的布尔运算符 AND,OR 以及 NOT (也写作 &&, || 和 !)也支持,但要小心,他们不遵守通常的优先级规则,所以每当多个运算符一起使用时,应使用括号。例如,以前的查询可以重写为:

1
((quick AND fox) OR (brown AND fox) OR fox) AND NOT news

此表单现在可以正确复制原始查询中的逻辑,但相关性评分与原始查询几乎没有相似之处。

相反,使用查询 bool 查询重写相同 match 查询将如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"bool": {
"must": {
"match": "fox"
},
"should": {
"match": "quick brown"
},
"must_not": {
"match": "news"
}
}
}

Grouping 分组

可以将多个术语或子句与括号组合在一起,以形成子查询:

1
(quick OR brown) AND fox

组可用于定位特定字段,或用于提升子查询的结果:

1
status:(active OR pending) title:(full text search)^2

Reserved characters 保留字符

如果你需要在查询本身中使用任何作为运算符的字符(而不是运算符),那么你应该使用前导反斜杠来转义它们。例如,要搜索 (1+1)=2,你需要将查询编写为 \(1\+1\)\=2

保留的字符是:+ - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /

无法正确转义这些特殊字符可能会导致语法错误,从而阻止你的查询运行。

<and> 根本无法转义。阻止它们尝试创建范围查询的唯一方法是安全从查询字符串中删除它们。

Empty Query 空查询

如果查询字符串为空或仅包含空格,则查询将生成空结果集。

elasticsearch 中的查询请求有两种方式,一种是简易版的查询,另外一种是使用 JSON 完整的请求体,叫做结构化查询(DSL)。由于 DSL 查询更为直观也更为简易,所以大都使用这种方式。DSL 查询是 POST 过去一个 json,由于 post 的请求是 json 格式的,所以存在很多灵活性,也有很多形式。这里有一个地方注意的是官方文档里面给的例子的 json 结构只是一部分,并不是可以直接复制粘贴进去使用的。一般要在外面加个 query 为 key 的结构。

match

最简单的一个 match 例子:

查询和 “我的宝马多少马力” 这个查询语句匹配的文档。

1
2
3
4
5
6
7
8
9
{
"query": {
"match": {
"content": {
"query": "我的宝马多少马力"
}
}
}
}

上面的查询匹配就会进行分词,比如 “宝马多少马力” 会被分词为 “宝马 多少 马力”,所有有关 “宝马 多少 马力”,那么所有包含这三个词中的一个或多个的文档就会被搜索出来。并且根据 lucene 的评分机制(TF/IDF)来进行评分。

match_phrase

比如上面一个例子,一个文档 “我的保时捷马力也不错” 也会被搜索出来,那么想要精确匹配所有同时包含 “宝马 多少 马力” 的文档怎么做?就要使用 match_phrase 了。

1
2
3
4
5
6
7
8
9
{
"query": {
"match_phrase": {
"content": {
"query": "我的宝马多少马力"
}
}
}
}

完全匹配可能比较严,我们会希望有个可调节因子,少匹配一个也满足,那就需要使用到 slop。

1
2
3
4
5
6
7
8
9
10
{
"query": {
"match_phrase": {
"content": {
"query": "我的宝马多少马力",
"slop": 1
}
}
}
}

multi_match

如果我们希望两个字段进行匹配,其中一个字段有这个文档就满足的话,使用 multi_match

1
2
3
4
5
6
7
8
{
"query": {
"multi_match": {
"query": "我的宝马多少马力",
"fields": ["title", "content"]
}
}
}

但是 multi_match 就涉及到匹配评分的问题了。

我们希望完全匹配的文档占的评分比较高,则需要使用 best_fields

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"query": {
"multi_match": {
"query": "我的宝马发动机多少",
"type": "best_fields",
"fields": [
"tag",
"content"
],
"tie_breaker": 0.3
}
}
}

意思就是完全匹配 “宝马 发动机” 的文档评分会比较靠前,如果只匹配宝马的文档评分乘以 0.3 的系数。

我们希望越多字段匹配的文档评分越高,就要使用 most_fields

1
2
3
4
5
6
7
8
9
10
11
12
{
"query": {
"multi_match": {
"query": "我的宝马发动机多少",
"type": "most_fields",
"fields": [
"tag",
"content"
]
}
}
}

我们希望这个词条的分词词汇是分配到不同字段中的,那么就使用 cross_fields

1
2
3
4
5
6
7
8
9
10
11
12
{
"query": {
"multi_match": {
"query": "我的宝马发动机多少",
"type": "cross_fields",
"fields": [
"tag",
"content"
]
}
}
}

term

term 是代表完全匹配,即不进行分词器分析,文档中必须包含整个搜索的词汇

1
2
3
4
5
6
7
{
"query": {
"term": {
"content": "汽车保养"
}
}
}

查询出的所有文档都包含 “汽车保养” 这个词组的词汇。

使用 term 要确定的是这个字段是否 “被分析(analyzed)”,默认的字符串是被分析的。

拿官网上的例子举例:

mapping 是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PUT my_index
{
"mappings": {
"my_type": {
"properties": {
"full_text": {
"type": "string"
},
"exact_value": {
"type": "string",
"index": "not_analyzed"
}
}
}
}
}

PUT my_index/my_type/1
{
"full_text": "Quick Foxes!",
"exact_value": "Quick Foxes!"
}

其中的 full_text 是被分析过的,所以 full_text 的索引中存的就是 [quick, foxes],而 extra_value 中存的是 [Quick Foxes!]。

那下面的几个请求:

1
2
3
4
5
6
7
8
GET my_index/my_type/_search
{
"query": {
"term": {
"exact_value": "Quick Foxes!"
}
}
}

能请求得的数据,因为完全匹配。

1
2
3
4
5
6
7
8
GET my_index/my_type/_search
{
"query": {
"term": {
"full_text": "Quick Foxes!"
}
}
}

请求不出数据,因为 full_text 分词后的结果中没有 [Quick Foxes!] 这个分词。

bool 联合查询:must,should,must_not

如果我们想要请求 “content 中带宝马,但是 tag 中不带宝马” 类似这样的需求,就需要用到 bool 联合查询。联合查询就会使用到 must,should,must_not 三种关键词。

这三个可以这么理解:

  • must:文档必须完全匹配条件
  • should:should 下面会带有一个以上的条件,至少满足一个条件,这个文档就符合 should
  • must_not:文档必须不匹配条件

比如上面那个需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"query": {
"bool": {
"must": {
"term": {
"content": "宝马"
}
},
"must_not": {
"term": {
"tags": "宝马"
}
}
}
}
}