Elasticsearch.Nest 教程系列 7-2 搜索:Writing bool queries | 编写布尔查询

  • 本系列博文是“伪”官方文档翻译(更加本土化),并非完全将官方文档进行翻译,而是在查阅、测试原始文档并转换为自己真知灼见后的“准”翻译。有不同见解 / 说明不周的地方,还请海涵、不吝拍砖 :)

  • 官方文档见此:https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/introduction.html

  • 本系列对应的版本环境:ElasticSearch@7.3.1,NEST@7.3.1,IDE 和开发平台默认为 VS2019,.NET CORE 2.1


在使用查询 DSL 的时候,编写布尔查询命令会很冗长,如使用带有 2 个 should 子句的单个布尔查询:

1
2
3
4
5
6
7
8
9
10
var searchResults = this.Client.Search<Project>(s => s
.Query(q => q
.Bool(b => b
.Should(
bs => bs.Term(p => p.Name, "x"),
bs => bs.Term(p => p.Name, "y")
)
)
)
);

可以相像,如果有多个嵌套布尔查询,最终代码久会长成这样:

为了解决代码冗长的问题,Nest 重写了一元操作符(“!”和“+”)以及二元操作符(“||”和“&&”)

运算符 对应的操作
&& must
|| should
+ filter
! must_not

重写二元操作符:“||”

使用重写的二元运算符“||”,可以让你更加简洁的在 should 子句中使用布尔查询。

基于最上面的例子,改写如下:

1
2
3
4
var firstSearchResponse = client.Search<Project>(s => s
.Query(q => q.Term(p => p.Name, "x") || q.Term(p => p.Name, "y")
)
);

同样,如果使用的是对象初始化语法的,同样可以使用“||”:

1
2
3
4
5
var secondSearchResponse = client.Search<Project>(new SearchRequest<Project>
{
Query = new TermQuery { Field = Field<Project>(p => p.Name), Value = "x" }
|| new TermQuery { Field = Field<Project>(p => p.Name), Value = "y" }
});

以上两种方式生成的命令相同,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"query": {
"bool": {
"should": [
{
"term": {
"name": {
"value": "x"
}
}
},
{
"term": {
"name": {
"value": "y"
}
}
}
]
}
}
}

重写二元操作符:“&&”

重写的“&&”二元运算符可用于简化 must 子句的布尔查询:

1
2
3
4
5
6
7
8
9
10
11
var firstSearchResponse = client.Search<Project>(s => s
.Query(q => q.Term(p => p.Name, "x") && q.Term(p => p.Name, "y")
)
);

//以下用法等效于上面
var secondSearchResponse = client.Search<Project>(new SearchRequest<Project>
{
Query = new TermQuery { Field = Field<Project>(p => p.Name), Value = "x" }
&& new TermQuery { Field = Field<Project>(p => p.Name), Value = "y" }
});

生成的命令均为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"query": {
"bool": {
"must": [
{
"term": {
"name": {
"value": "x"
}
}
},
{
"term": {
"name": {
"value": "y"
}
}
}
]
}
}
}

NEST 客户端会对像上面这样的请求合并为一次 bool 查询。

重写一元操作符:“!”

重写的一元操作附“!”主要面向含有 must_not 子句的查询。

1
2
3
4
5
6
7
8
9
10
11
var firstSearchResponse = client.Search<Project>(s => s
.Query(q => !q
.Term(p => p.Name, "x")
)
);

//等效于
var secondSearchResponse = client.Search<Project>(new SearchRequest<Project>
{
Query = !new TermQuery { Field = Field<Project>(p => p.Name), Value = "x" }
});

以上两种方式最终生成的命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"query": {
"bool": {
"must_not": [
{
"term": {
"name": {
"value": "x"
}
}
}
]
}
}
}

两个含有“!”运算符的查询可以用“&&”进行合并,以形成带有 2 个 must_not 子句的单个布尔查询。

1
2
3
4
//伪代码:
!q.Query() && !q.Query()
或者
!Query && !Query

重写一元操作符:“+”

通过使用一元“+”运算符可以将查询转换为带有过滤子句的布尔查询

1
2
3
4
5
6
7
8
9
10
var firstSearchResponse = client.Search<Project>(s => s
.Query(q => +q //+运算符
.Term(p => p.Name, "x")
)
);
//或者以下方式
var secondSearchResponse = client.Search<Project>(new SearchRequest<Project>
{
Query = +new TermQuery { Field = Field<Project>(p => p.Name), Value = "x" }
});

最终生成的命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"query": {
"bool": {
"filter": [
{
"term": {
"name": {
"value": "x"
}
}
}
]
}
}}

以上命令将再过滤器上下文中运行,当你不需要使用 _score 来排序的时候,使用这种方式可以提高性能。

跟一元运算符“!”类似,使用“+”的 2 个子句同样可以使用“&&”进行合并,形成带有 2 个 filter 子句的单个布尔查询。

1
2
3
4
//伪代码:
+q.Query() && +q.Query()
或者
+Query && +Query

使用“&&”合并查询

1.当将多个 Term 级别的查询用二元运算符“&&”(其中某些或所有查询都应用了一元运算符)进行连接,如:

1
term && term && term

NEST 会形成如下的嵌套顺序:

  • Nest 会自动将 && 连接的条件限制在单个 bool 查询中。

使用“||”合并查询或 should 子句

在上面的示例(描述“||”的小节)中,NEST将合并多个“||”或 should 子句到单个布尔查询中 。

像下面这样的组合:

1
term || term || term

最终会被解析为:

不过,布尔查询并非完全遵循编程语言中的布尔逻辑,如:

1
term1 && (term2 || term3 || term4)

需要特别注意,以上组合并不会被解析为以下关系:

原因:

  • 当布尔查询中仅包含 should 子句时,表示的是必须要有一个被匹配上。

  • 但当该布尔查询还具有 must 子句时,should 子句则会充当提升因子,这意味着它们不必匹配,但是如果匹配了,则该文档的相关性得分将得到提升。也就是说,should 子句的行为语义会根据 must 子句的存在而发生改变。

因此,对于“term1 && (term2 || term3 || term4)”这样的组合,你将得到包含 term1 的结果,这跟使用重载运算符时的预期是不相符

在构建搜索查询时,将 should 子句用作提升因子非常有用,你可以将实际的 bool 查询与NEST的运算符重载混合后进行匹配。

另外需要注意下,当 2 个布尔查询仅包含 should 子句的时候,NEST 不会进行合并,如下:

1
bool(should=term1, term2, term3, term4, minimum_should_match=2) || term5 || term6
  • 如果 NEST 发现运算符“||”左右两边仅包含 should 子句,那么此时 NEST 会改变第一个布尔查询中的 minimum_should_match 参数的原意。

    • 像上面的的示例 仅会在 term5 和 term6 上进行匹配

不会合并处理的情况

如果设置了任何查询元数据,如设置了 boost 或 name 等元数据,则 NEST 不会合并布尔查询,而是会将其视为已锁定,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Assert(
q => q.Bool(b => b.Name("leftBool").Should(mq => mq.Query()))
|| q.Bool(b => b.Name("rightBool").Should(mq => mq.Query())),
new BoolQuery { Name = "leftBool", Should = new QueryContainer[] { Query } }
|| new BoolQuery { Name = "rightBool", Should = new QueryContainer[] { Query } },
c => AssertDoesNotJoinOntoLockedBool(c, "leftBool"));
//或者下面这种
Assert(
q => q.Bool(b => b.Should(mq => mq.Query()))
|| q.Bool(b => b.Name("rightBool").Should(mq => mq.Query())),
new BoolQuery { Should = new QueryContainer[] { Query } }
|| new BoolQuery { Name = "rightBool", Should = new QueryContainer[] { Query } },
c => AssertDoesNotJoinOntoLockedBool(c, "rightBool"));

//或者下面这种
Assert(
q => q.Bool(b => b.Name("leftBool").Should(mq => mq.Query()))
|| q.Bool(b => b.Should(mq => mq.Query())),
new BoolQuery { Name = "leftBool", Should = new QueryContainer[] { Query } }
|| new BoolQuery { Should = new QueryContainer[] { Query } },
c => AssertDoesNotJoinOntoLockedBool(c, "leftBool"));

性能优化建议

假设你有很多 must 子句,使用“&=”进行合并

1
2
3
4
5
6
7
var c = new QueryContainer();
var q = new TermQuery { Field = "x", Value = "x" };

for (var i = 0; i < 1000; i++)
{
c &= q;
}

性能分析结果如下:

  • 分配了过多的对象,这会导致 GC 压力。

因为已经知道了布尔查询的大致形态,因此可以通过如下方式来进行优化:

  • 因为知道最终是一个布尔查询,所以这里可以直接用布尔查询来代替 Term 查询合并。

1
2
3
4
5
6
QueryContainer q = new TermQuery { Field = "x", Value = "x" };
var x = Enumerable.Range(0, 1000).Select(f => q).ToArray();
var boolQuery = new BoolQuery
{
Must = x
};

性能分析结果如下: