这是继上一篇文章 “Elasticsearch:Elasticsearch 查询示例 - 动手练习(一)” 的续篇。
Compound Queries
到目前为止,在本教程中,我们已经看到我们触发了单个查询,例如查找文本匹配或查找年龄范围等。但在现实世界中,我们更经常需要检查多个条件并根据返回的文档在哪。 此外,我们可能需要修改查询的相关性或分数参数或更改单个查询的行为等。复合查询是帮助我们实现上述场景的查询。 在本节中,让我们看看一些最有用的复合查询。
Bool query
布尔查询提供了一种以布尔方式组合多个查询的方法。 例如,如果我们要检索 “position” 字段中关键字为 “researcher” 的所有文档以及具有12年以上 experience 的文档,我们需要使用匹配查询和范围查询的组合 . 可以使用 bool 查询来制定这种查询。 bool 查询主要定义了 4 种类型的出现:
| must | 其中的条件或查询必须出现在文档中才能将它们视为匹配项。 此外,这有助于分值。 |
| should | 条件/查询应该匹配。 |
| filter | 与 must 子句相同,但分数将被忽略 |
| must_not | 文件中不得出现指定的条件/查询。 得分被忽略并保持为 0,因为结果被忽略。 |
典型的 bool 查询结构如下所示:
1. POST _search
2. {
3. "query": {
4. "bool" : {
5. "must" : [],
6. "filter": [],
7. "must_not" : [],
8. "should" : []
9. }
10. }
11. }
现在让我们探索如何将 bool 查询用于不同的用例。
Bool query 例子 1 - must
在我们的示例中,假设我们需要找到所有具有 12 年或以上 experienece 并且在 “position” 字段中也有 “manager” 字样的员工。 我们可以使用以下 bool 查询来做到这一点:
1. POST employees/_search
2. {
3. "query": {
4. "bool": {
5. "must": [
6. {
7. "match": {
8. "position": "manager"
9. }
10. },
11. {
12. "range": {
13. "experience": {
14. "gte": 12
15. }
16. }
17. }
18. ]
19. }
20. }
21. }
上述查询的响应将包含与 “must” 数组中的两个查询匹配的文档,如下所示:
Bool Query 例子 2 - filter
前面的示例演示了 bool 查询中的 “must” 参数。 你可以在前面示例的结果中看到,结果在 “_score” 字段中有值。 现在让我们使用相同的查询,但这次让我们用 “filter” 替换 “must”,看看会发生什么:
1. POST employees/_search
2. {
3. "query": {
4. "bool": {
5. "filter": [
6. {
7. "match": {
8. "position": "manager"
9. }
10. },
11. {
12. "range": {
13. "experience": {
14. "gte": 12
15. }
16. }
17. }
18. ]
19. }
20. }
21. }
从上面的截图可以看出,搜索结果的分值为零。 这是因为在使用过滤器上下文时,Elasticsearch 不会计算分数以加快搜索速度。
如果我们将 must 条件与过滤条件一起使用,则会为 must 中的子句计算分数,但不会为过滤条件计算分数。
Bool Query 例子 3 - should
现在,让我们看看 bool 查询中 “should” 部分的效果。 让我们在上面示例的查询中添加一个 should 子句。 这个 “should” 条件是匹配在文档的 “phrase” 字段中包含文本 “versatile” 的文档。 对此的查询如下所示:
`
1. POST employees/_search
2. {
3. "query": {
4. "bool": {
5. "must": [
6. {
7. "match": {
8. "position": "manager"
9. }
10. },
11. {
12. "range": {
13. "experience": {
14. "gte": 12
15. }
16. }
17. }
18. ],
19. "should": [
20. {
21. "match": {
22. "phrase": "versatile"
23. }
24. }
25. ]
26. }
27. }
28. }
`
现在结果将与我们在上一个示例中收到的 2 个文档相同,但是显示为最后一个结果的 id=3 的文档显示为第一个结果。 这是因为 “should” 数组中的子句出现在该文档中,因此分数增加了,因此它被提升为第一个文档。
Bool Query 例子 4 - 多个条件
bool 查询的真实示例可能比上述简单查询更复杂。 如果用户想要获得可能来自 “Yamaha” 或“ Telane” 公司、头衔为 “manager” 或 “associate”、年薪超过 100,000 的员工,该怎么办?
上述条件,简而言之,可简写如下:
(company = Yamaha OR company = Yozio ) AND (position = manager OR position = associate ) AND (salary>=100000)
这可以在单个 must 子句中使用多个 bool 查询来实现,如下面的查询所示:
`
1. POST employees/_search
2. {
3. "query": {
4. "bool": {
5. "must": [
6. {
7. "bool": {
8. "should": [
9. {
10. "match": {
11. "company": "Talane"
12. }
13. },
14. {
15. "match": {
16. "company": "Yamaha"
17. }
18. }
19. ]
20. }
21. },
22. {
23. "bool": {
24. "should": [
25. {
26. "match": {
27. "position": "manager"
28. }
29. },
30. {
31. "match": {
32. "position": "Associate"
33. }
34. }
35. ]
36. }
37. },
38. {
39. "bool": {
40. "must": [
41. {
42. "range": {
43. "salary": {
44. "gte": 100000
45. }
46. }
47. }
48. ]
49. }
50. }
51. ]
52. }
53. }
54. }
`
Boosting Queries
有时,搜索条件中有一些要求,我们需要将某些搜索结果降级,但又不想完全从搜索结果中忽略它们。 在这种情况下,提升查询会变得很方便。 让我们通过一个简单的例子来证明这一点。
让我们搜索所有来自中国的员工,然后在搜索结果中将员工从 “Telane” 公司降级。 我们可以像下面这样使用提升查询:
1. POST employees/_search
2. {
3. "query": {
4. "boosting": {
5. "positive": {
6. "match": {
7. "country": "china"
8. }
9. },
10. "negative": {
11. "match": {
12. "company": "Talane"
13. }
14. },
15. "negative_boost": 0.5
16. }
17. }
18. }
现在上述查询的响应如下所示,您可以看到公司 “Talane” 的员工排名最后,与之前的结果相差 0.5。
我们可以将任何查询应用于提升查询的 “positive” 和 “negative” 部分。 当我们需要使用布尔查询应用多个条件时,这很好。 下面给出了此类查询的示例:
`
1. GET employees/_search
2. {
3. "query": {
4. "boosting": {
5. "positive": {
6. "bool": {
7. "should": [
8. {
9. "match": {
10. "country": {
11. "query": "china"
12. }
13. }
14. },
15. {
16. "range": {
17. "experience": {
18. "gte": 10
19. }
20. }
21. }
22. ]
23. }
24. },
25. "negative": {
26. "match": {
27. "gender": "female"
28. }
29. },
30. "negative_boost": 0.5
31. }
32. }
33. }
`
Function Score Queries
function_score 查询使我们能够更改查询返回的文档的分数。 function_score 查询需要一个查询和一个或多个函数来计算分数。 如果未提及任何函数,则查询将正常执行。
函数得分最简单的情况,没有任何函数,如下所示:
function_score: weight
如前几节所述,我们可以在 “function_score” 查询的 “functions” 数组中使用一个或多个评分函数。 最简单但重要的函数之一是 “weight” 评分函数。
根据官方文档,权重分数允许你将分数乘以提供的权重。 可以在函数数组(上面的示例)中为每个函数定义权重,并乘以相应函数计算的分数。
让我们使用对上述查询的简单修改来演示该示例。 让我们在查询的 “functions” 部分包含两个过滤器。 第一个子句将在文档的“phrase” 字段中搜索术语 “coherent”,如果找到,则将分数提高 2。第二个子句将在 “phrase” 字段中搜索术语 “emulation” 对于此类文件,将增加 10 倍。 这是相同的查询:
`
1. GET employees/_search
2. {
3. "_source": [
4. "position",
5. "phrase"
6. ],
7. "query": {
8. "function_score": {
9. "query": {
10. "match": {
11. "position": "manager"
12. }
13. },
14. "functions": [
15. {
16. "filter": {
17. "match": {
18. "phrase": "coherent"
19. }
20. },
21. "weight": 2
22. },
23. {
24. "filter": {
25. "match": {
26. "phrase": "emulation"
27. }
28. },
29. "weight": 10
30. }
31. ],
32. "score_mode": "multiply",
33. "boost": "5",
34. "boost_mode": "multiply"
35. }
36. }
37. }
`
上述查询的响应如下:
`
1. {
2. "took" : 8,
3. "timed_out" : false,
4. "_shards" : {
5. "total" : 1,
6. "successful" : 1,
7. "skipped" : 0,
8. "failed" : 0
9. },
10. "hits" : {
11. "total" : {
12. "value" : 2,
13. "relation" : "eq"
14. },
15. "max_score" : 72.61542,
16. "hits" : [
17. {
18. "_index" : "employees",
19. "_type" : "_doc",
20. "_id" : "4",
21. "_score" : 72.61542,
22. "_source" : {
23. "phrase" : "Emulation of roots heuristic coherent systems",
24. "position" : "Resources Manager"
25. }
26. },
27. {
28. "_index" : "employees",
29. "_type" : "_doc",
30. "_id" : "3",
31. "_score" : 30.498476,
32. "_source" : {
33. "phrase" : "Versatile object-oriented emulation",
34. "position" : "Human Resources Manager"
35. }
36. }
37. ]
38. }
39. }
`
对 position 字段的查询的简单匹配部分对两个文档产生了 3.63 和 3.04 的分数。 当函数数组中的第一个函数被应用时(匹配“coherent” 关键字),只有一个匹配,那就是 id = 4 的文档。
该文档的当前分数乘以匹配 “coherent” 的权重因子,即 2。现在该文档的新分数变为 3.63*2 = 7.2
之后,两个文档的第二个条件(“emulation” 匹配)匹配。
所以 id=4 的文档的当前得分是 7.2*10 = 72,其中 10 是第二个子句的权重因子。
id=3 的文档只匹配第二个子句,因此它的分数 = 3.0*10 = 30。
function_score: script_score
我们经常需要根据一个或多个字段/字段来计算分数,而默认的评分机制是不够的。 Elasticsearch 为我们提供了 “script_score” 评分功能,可以根据自定义要求计算评分。 在这里,我们可以提供一个脚本,它将根据字段上的自定义逻辑返回每个文档的分数。
比如说,我们需要将分数计算为工资和经验的函数,即工资与经验比率最高的员工应该得分更多。 我们可以使用以下 function_score 查询:
1. GET employees/_search
2. {
3. "_source": [
4. "name",
5. "experience",
6. "salary"
7. ],
8. "query": {
9. "function_score": {
10. "query": {
11. "match_all": {}
12. },
13. "functions": [
14. {
15. "script_score": {
16. "script": {
17. "source": "(doc['salary'].value/doc['experience'].value)/1000"
18. }
19. }
20. }
21. ],
22. "boost_mode": "replace"
23. }
24. }
25. }
在上面的查询中,脚本部分:
(doc['salary'].value/doc['experience'].value)/1000
上面的脚本部分将为搜索结果生成分数。 例如,对于薪水 = 180025 且经验 = 7 的员工,生成的分数将是:
(180025/7)/1000 = 25
由于我们使用的是 boost_mode:replace 脚本计算的分数,该脚本与每个文档的分数完全相同。 上述查询的结果在下面的屏幕截图中给出:
function_score: field_value_factor
我们可以使用 “field_value_factor” 函数利用文档中的字段来影响分数。 这在某些方面是 “script_score” 的简单替代方案。 在我们的示例中,让我们利用 “experience” 字段值来影响我们的分数,如下所示:
1. GET employees/_search
2. {
3. "_source": [
4. "name",
5. "experience"
6. ],
7. "query": {
8. "function_score": {
9. "field_value_factor": {
10. "field": "experience",
11. "factor": 0.5,
12. "modifier": "square",
13. "missing": 1
14. }
15. }
16. }
17. }
上述查询的响应如下所示:
`
1. {
2. "took" : 1,
3. "timed_out" : false,
4. "_shards" : {
5. "total" : 1,
6. "successful" : 1,
7. "skipped" : 0,
8. "failed" : 0
9. },
10. "hits" : {
11. "total" : {
12. "value" : 4,
13. "relation" : "eq"
14. },
15. "max_score" : 36.0,
16. "hits" : [
17. {
18. "_index" : "employees",
19. "_type" : "_doc",
20. "_id" : "3",
21. "_score" : 36.0,
22. "_source" : {
23. "name" : "Winston Waren",
24. "experience" : 12
25. }
26. },
27. {
28. "_index" : "employees",
29. "_type" : "_doc",
30. "_id" : "4",
31. "_score" : 36.0,
32. "_source" : {
33. "name" : "Alan Thomas",
34. "experience" : 12
35. }
36. },
37. {
38. "_index" : "employees",
39. "_type" : "_doc",
40. "_id" : "2",
41. "_score" : 30.25,
42. "_source" : {
43. "name" : "Othilia Cathel",
44. "experience" : 11
45. }
46. },
47. {
48. "_index" : "employees",
49. "_type" : "_doc",
50. "_id" : "1",
51. "_score" : 12.25,
52. "_source" : {
53. "name" : "Huntlee Dargavel",
54. "experience" : 7
55. }
56. }
57. ]
58. }
59. }
`
上面的分数计算如下:
Square of (factor*doc[experience].value)
对于包含值为 12 的 “experience” 的文档,分数将为:
square of (0.5*12) = square of (6) = 36
function_score: Decay Functions
考虑搜索某个位置附近的酒店的用例。对于这个用例,酒店越近,搜索结果的相关性越高,但当距离越远,搜索就变得无关紧要。或者进一步细化,如果酒店距离比该位置更远,例如步行距离为 1 公里,则搜索结果应显示得分迅速下降。而 1km 半径内的得分应该更高。
对于这种用例,评分的衰减模式是最好的选择,即分数将从兴趣点开始衰减。为此,我们在 Elasticsearch 中有评分函数,它们被称为衰减函数。衰减函数分为三种类型,即 “高斯”、“线性” 和 “指数” 或 “exp”。
让我们从我们的场景中举一个用例的例子。我们需要根据员工的薪水给他们打分。接近 200000 和介于 170000 到 230000 之间的分数应该更高,低于和高于该范围的分数应该显着降低。
1. GET employees/_search
2. {
3. "_source": [
4. "name",
5. "salary"
6. ],
7. "query": {
8. "function_score": {
9. "query": {
10. "match_all": {}
11. },
12. "functions": [
13. {
14. "gauss": {
15. "salary": {
16. "origin": 200000,
17. "scale": 30000
18. }
19. }
20. }
21. ],
22. "boost_mode": "replace"
23. }
24. }
25. }
这里的 “origin” 代表开始计算距离的点。 标度表示距原点的距离,应优先考虑到该距离。 还有一些额外的参数是可选的,可以在 Elastic 的文档中查看。
以上查询结果如下图所示:
Parent - Child Queries
可以使用 Elasticsearch 中的父子方法(现在称为连接操作)处理一对多关系。 让我们通过一个示例场景来演示这一点。 考虑我们有一个论坛,任何人都可以在其中发布任何主题(比如帖子)。 用户可以对个别帖子发表评论。 因此,在这种情况下,我们可以将单个帖子视为父文档,将对其的评论视为其子文档。 这在下图中得到了最好的解释:
对于此操作,我们将创建一个单独的索引,并应用特殊的映射(模式)。使用以下请求创建具有连接数据类型的索引:
1. PUT post-comments
2. {
3. "mappings": {
4. "properties": {
5. "document_type": {
6. "type": "join",
7. "relations": {
8. "post": "comment"
9. }
10. }
11. }
12. }
13. }
在上面的模式中,你可以看到有一个名为 “join” 的类型,它表示这个索引将有父子相关的文档。 此外,“relations” 对象定义了父标识符和子标识符的名称。
即 post:comment 指的是父子关系。 每个文档都包含一个名为 “document_type” 的字段,其值为 “post” 或 “comment”。 值 “post” 将指示文档是父文档,值 “comment” 将指示文档是 “子”。
让我们为此索引一些文档:
1. PUT post-comments/_doc/1
2. {
3. "document_type": {
4. "name": "post"
5. },
6. "post_title" : "Angel Has Fallen"
7. }
1. PUT post-comments/_doc/2
2. {
3. "document_type": {
4. "name": "post"
5. },
6. "post_title" : "Beauty and the beast - a nice movie"
7. }
为 id=1 的文档索引子文档:
1. PUT post-comments/_doc/A?routing=1
2. {
3. "document_type": {
4. "name": "comment",
5. "parent": "1"
6. },
7. "comment_author": "Neil Soans",
8. "comment_description": "'Angel has Fallen' has some redeeming qualities, but they're too few and far in between to justify its existence"
9. }
1. PUT post-comments/_doc/B?routing=1
2. {
3. "document_type": {
4. "name": "comment",
5. "parent": "1"
6. },
7. "comment_author": "Exiled Universe",
8. "comment_description": "Best in the trilogy! This movie wasn't better than the Rambo movie but it was very very close."
9. }
为 id=2 的文档索引子文档:
1. PUT post-comments/_doc/D?routing=1
2. {
3. "document_type": {
4. "name": "comment",
5. "parent": "2"
6. },
7. "comment_author": "Emma Cochrane",
8. "comment_description": "There's the sublime beauty of a forgotten world and the promise of happily-ever-after to draw you to one of your favourite fairy tales, once again. Give it an encore."
9. }
1. PUT post-comments/_doc/E?routing=1
2. {
3. "document_type": {
4. "name": "comment",
5. "parent": "2"
6. },
7. "comment_author": "Common Sense Media Editors",
8. "comment_description": "Stellar music, brisk storytelling, delightful animation, and compelling characters make this both a great animated feature for kids and a great movie for anyone"
9. }
has_child Query
这将查询孩子的文档,然后返回与他们关联的父母作为结果。 假设我们需要在子文档中的 “comments_description” 字段中查询 “music” 这个词,并得到与搜索结果对应的父文档,我们可以使用 has_child 查询如下:
1. GET post-comments/_search
2. {
3. "query": {
4. "has_child": {
5. "type": "comment",
6. "query": {
7. "match": {
8. "comment_description": "music"
9. }
10. }
11. }
12. }
13. }
对于上面的查询,匹配到搜索的子文档只是 id=E 的文档,而父文档是id=2的文档。 搜索结果将为我们提供如下父文档:
has_parent Query
has_parent 查询将执行与 has_child 查询相反的操作,即它将返回与查询匹配的父文档的子文档。
让我们在父文档中搜索单词 “Beauty” 并返回匹配父文档的子文档。 我们可以使用下面的查询:
1. GET post-comments/_search
2. {
3. "query": {
4. "has_parent": {
5. "parent_type": "post",
6. "query": {
7. "match": {
8. "post_title": "Beauty"
9. }
10. }
11. }
12. }
13. }
上述查询的匹配父文档是文档 id = 1 的文档。 从下面的响应中可以看出,上面的查询返回了 id=1 文档对应的子文档:
使用 parents 获取 child 文件
有时,我们需要在搜索结果中同时包含父文档和子文档。 例如,如果我们列出帖子,那么在它下面显示一些评论也会很好,因为它会更吸引眼球。
Elasticsearch 也允许这样做。 让我们使用 has_child 查询返回父母,这一次,我们也将获取相应的子文档。
下面的查询包含一个名为 “inner_hits” 的参数,它允许我们做同样的事情。
其它 Query 例子
query_string query
“query_string” 查询是一种特殊的多用途查询,它可以组合使用其他几个查询,如 “match”、“multi-match”、“wildcard”、regexp” 等。“query_string” 查询遵循严格的格式 违反它会输出错误信息。 因此,即使具有它的功能,它也很少用于实现面向用户的搜索框。
让我们看看一个示例查询:
1. POST employees/_search
2. {
3. "query": {
4. "query_string": {
5. "query": "(roots heuristic systems) OR (enigneer~) OR (salary:(>=10000 AND <=52000)) ",
6. "fields": [
7. "position",
8. "phrase^3"
9. ]
10. }
11. }
12. }
上述查询将在 “position” 和 “phrase” 字段中搜索 “roots” 或 “heuristic” 或 “systems” 或 “engineer”(查询中使用〜表示使用模糊查询)和 返回结果。 “phrase^3” 表示在 “phrase” 字段中找到的匹配项应提升3倍。salary:(>10000 AND <=52000),表示获取具有 “salary” 字段值的文档 ”,落在 10000 到 52000 之间。
simple_query_string query
“simple_query_string” 查询是 query_string_query 的简化形式,有两个主要区别:
它更具容错性,这意味着如果语法错误,它不会返回错误。 相反,它忽略了查询的错误部分。 这使得它对用户界面搜索框更加友好。
运算符 AND/OR/NOT 等被替换为 +/|/-
一个简单的例子是:
1. POST employees/_search
2. {
3. "query": {
4. "simple_query_string": {
5. "query": "(roots) | (resources manager) + (male) ",
6. "fields": [
7. "gender",
8. "position",
9. "phrase^3"
10. ]
11. }
12. }
13. }
上述查询将在 “fields” 数组中提到的所有字段中搜索 “roots” 或 “resources” 或 “manager” 和 “male”。
Named queries
顾名思义,命名查询都是关于查询的命名。 在某些情况下,它可以帮助我们识别查询的哪些部分/部分与文档匹配。 Elasticsearch 通过允许我们命名查询或查询的部分来为我们提供确切的功能,以便在匹配文档中查看这些名称。
让我们看看这个在行动中,在图像中的下面的例子:
在上面的示例中,匹配查询提供了一个 “_name” 参数,该参数的查询名称为 “phrase_field_name”。 在结果中,我们有匹配结果的文档,其中包含一个名为 “matched_queries” 的数组字段,其中包含匹配查询/查询的名称(此处为“phrase_field_name”)。
下面的示例显示了命名查询在 bool 查询中的用法,这是命名查询最常见的用例之一。