Writing bool queries
Version:5.x
英文原文地址:Writing bool queries
在使用查询 DSL 时,编写 bool 查询会很容易把代码变得冗长。举个栗子,使用一个包含两个 should 子句的 bool 查询
1var searchResults = this.Client.Search<Project>(s => s 2 .Query(q => q 3 .Bool(b => b 4 .Should( 5 bs => bs.Term(p => p.Name, "x"), 6 bs => bs.Term(p => p.Name, "y") 7 ) 8 ) 9 ) 10);
现在设想多层嵌套的 bool 查询,你会意识到这很快就会成为一个 hadouken(波动拳) 缩进的练习

Operator overloading
由于这个原因,NEST 引入了运算符重载,使得更容易去编写复杂的 bool 查询。这些重载的运算符是:
我们会示例来演示这几个运算符
Binary || operator
使用重载的二元 || 运算符,可以更简洁地表达含有 should 子句的 bool 查询
之前哈杜根的栗子现在变成了 Fluent API 的样子
1var firstSearchResponse = client.Search<Project>(s => s 2 .Query(q => q 3 .Term(p => p.Name, "x") || q 4 .Term(p => p.Name, "y") 5 ) 6);
使用 Object Initializer 语法
1var secondSearchResponse = client.Search<Project>(new SearchRequest<Project> 2{ 3 Query = new TermQuery { Field = Field<Project>(p => p.Name), Value = "x" } || 4 new TermQuery { Field = Field<Project>(p => p.Name), Value = "y" } 5});
两者都会生成如下 JSON 查询 DSL
1{ 2 "query": { 3 "bool": { 4 "should": [ 5 { 6 "term": { 7 "name": { 8 "value": "x" 9 } 10 } 11 }, 12 { 13 "term": { 14 "name": { 15 "value": "y" 16 } 17 } 18 } 19 ] 20 } 21 } 22}
Binary && operator
重载的二元 && 运算符用于将多个查询组合在一起。当要组合的查询没有应用任何一元运算符时,生成的查询是一个包含 must 子句的 bool 查询
1var firstSearchResponse = client.Search<Project>(s => s 2 .Query(q => q 3 .Term(p => p.Name, "x") && q 4 .Term(p => p.Name, "y") 5 ) 6);
使用 Object Initializer 语法
1var secondSearchResponse = client.Search<Project>(new SearchRequest<Project> 2{ 3 Query = new TermQuery { Field = Field<Project>(p => p.Name), Value = "x" } && 4 new TermQuery { Field = Field<Project>(p => p.Name), Value = "y" } 5});
两者都会生成如下 JSON 查询 DSL
1{ 2 "query": { 3 "bool": { 4 "must": [ 5 { 6 "term": { 7 "name": { 8 "value": "x" 9 } 10 } 11 }, 12 { 13 "term": { 14 "name": { 15 "value": "y" 16 } 17 } 18 } 19 ] 20 } 21 } 22}
运算符重载会重写原生的实现
term && term && term
会转换成
1bool 2|___must 3 |___term 4 |___bool 5 |___must 6 |___term 7 |___term
可以想象,随着查询变得越来越复杂,结果很快就会变得笨拙。NEST 是很聪明的,它会把多个 && 查询联合成一个 bool 查询
1bool 2|___must 3 |___term 4 |___term 5 |___term
如下所示
1Assert( 2 q => q.Query() && q.Query() && q.Query(), (1) 3 Query && Query && Query, (2) 4 c => c.Bool.Must.Should().HaveCount(3) (3) 5);
(1) 使用 Fluent API 将三个查询 && 在一起
(2) 使用 Object Initializer 语法将三个查询 && 在一起
(3) 断言最终的 bool 查询会包含 3 个 must 子句
Unary ! operator
NEST 使用一元 ! 运算符创建包含 must_not 子句的 bool 查询
1var firstSearchResponse = client.Search<Project>(s => s 2 .Query(q => !q 3 .Term(p => p.Name, "x") 4 ) 5);
使用 Object Initializer 语法
1var secondSearchResponse = client.Search<Project>(new SearchRequest<Project> 2{ 3 Query = !new TermQuery { Field = Field<Project>(p => p.Name), Value = "x" } 4});
两者都会生成如下 JSON 查询 DSL
1{ 2 "query": { 3 "bool": { 4 "must_not": [ 5 { 6 "term": { 7 "name": { 8 "value": "x" 9 } 10 } 11 } 12 ] 13 } 14 } 15}
用一元 ! 运算符标记的两个查询可以使用 and 运算符组合起来,从而形成一个包含两个 must_not 子句的 bool 查询
1Assert( 2 q => !q.Query() && !q.Query(), 3 !Query && !Query, 4 c => c.Bool.MustNot.Should().HaveCount(2));
Unary + operator
可以使用一元 + 运算符将查询转换为带有 filter 子句的 bool 查询
1var firstSearchResponse = client.Search<Project>(s => s 2 .Query(q => +q 3 .Term(p => p.Name, "x") 4 ) 5);
使用 Object Initializer 语法
1var secondSearchResponse = client.Search<Project>(new SearchRequest<Project> 2{ 3 Query = +new TermQuery { Field = Field<Project>(p => p.Name), Value = "x" } 4});
两者都会生成如下 JSON 查询 DSL
1{ 2 "query": { 3 "bool": { 4 "filter": [ 5 { 6 "term": { 7 "name": { 8 "value": "x" 9 } 10 } 11 } 12 ] 13 } 14 } 15}
在筛选上下文中运行查询,这在提高性能方面很有用。因为不需要计算查询的相关性评分来影响结果的顺序。
同样的,使用一元 + 运算符标记的查询可以和 && 运算符组合在一起,构成一个包含两个 filter 子句的 bool 查询
1Assert( 2 q => +q.Query() && +q.Query(), 3 +Query && +Query, 4 c => c.Bool.Filter.Should().HaveCount(2));
Combining bool queries
在使用二元 && 运算符组合多个查询时,如果某些或者全部的查询都应用了一元运算符,NEST 仍然可以把它们合并成一个 bool 查询
参考下面这个 bool 查询
1bool 2|___must 3| |___term 4| |___term 5| |___term 6| 7|___must_not 8 |___term
NEST 中可以这样构建
1Assert( 2 q => q.Query() && q.Query() && q.Query() && !q.Query(), 3 Query && Query && Query && !Query, 4 c=> 5 { 6 c.Bool.Must.Should().HaveCount(3); 7 c.Bool.MustNot.Should().HaveCount(1); 8 });
一个更复杂的栗子
term && term && term && !term && +term && +term
依然会生成下面这个结构的单个 bool 查询
1bool 2|___must 3| |___term 4| |___term 5| |___term 6| 7|___must_not 8| |___term 9| 10|___filter 11 |___term 12 |___term 13 14 15Assert( 16 q => q.Query() && q.Query() && q.Query() && !q.Query() && +q.Query() && +q.Query(), 17 Query && Query && Query && !Query && +Query && +Query, 18 c => 19 { 20 c.Bool.Must.Should().HaveCount(3); 21 c.Bool.MustNot.Should().HaveCount(1); 22 c.Bool.Filter.Should().HaveCount(2); 23 });
你也可以将使用重载运算符的查询和真正的 bool 查询混合在一起
bool(must=term, term, term) && !term
仍然会合并为一个 bool 查询
1Assert( 2 q => q.Bool(b => b.Must(mq => mq.Query(), mq => mq.Query(), mq => mq.Query())) && !q.Query(), 3 new BoolQuery { Must = new QueryContainer[] { Query, Query, Query } } && !Query, 4 c => 5 { 6 c.Bool.Must.Should().HaveCount(3); 7 c.Bool.MustNot.Should().HaveCount(1); 8 });
Combining queries with || or should clauses
就像之前的栗子,NEST 会把多个 should 或者 || 查询合并成一个包含多个 should 子句的 bool 查询。
总而言之,这个
term || term || term
会变成
1bool 2|___should 3 |___term 4 |___term 5 |___term
但是,bool 查询不会完全遵循你从编程语言所期望的布尔逻辑
term1 && (term2 || term3 || term4)
不会变成
1bool 2|___must 3| |___term1 4| 5|___should 6 |___term2 7 |___term3 8 |___term4
为什么会这样?当一个 bool 查询中只包含 should 子句时,至少会匹配一个。但是,当这个 bool 查询还包含一个 must 子句时,应该将 should 子句当作一个 boost 因子,这意味着他们都不是必需匹配的。但是如果匹配,文档的相关性评分会得到提高,从而在结果中显示更高的值。should 子句的行为会因为 must 的存在而发生改变。
因此,再看看前面那个示例,你只能得到包含 term1 的结果。这显然不是使用运算符重载的目的。
为此,NEST 将之前的查询重写成了:
1bool 2|___must 3 |___term1 4 |___bool 5 |___should 6 |___term2 7 |___term3 8 |___term4 9 10 11 12Assert( 13 q => q.Query() && (q.Query() || q.Query() || q.Query()), 14 Query && (Query || Query || Query), 15 c => 16 { 17 c.Bool.Must.Should().HaveCount(2); 18 var lastMustClause = (IQueryContainer)c.Bool.Must.Last(); 19 lastMustClause.Should().NotBeNull(); 20 lastMustClause.Bool.Should().NotBeNull(); 21 lastMustClause.Bool.Should.Should().HaveCount(3); 22 });
添加圆括号,强制改变运算顺序
在构建搜索查询时,使用 should 子句作为 boost 因子可能是一个非常强大的构造方式。另外需要记住,你可以将实际的 bool 查询和 NEST 的重载运算符混合使用
还有一个微妙的情况,NEST 不会盲目地合并两个只包含 should 子句的 bool 查询。考虑下面这个查询
bool(should=term1, term2, term3, term4, minimum_should_match=2) || term5 || term6
如果 NEST 确定二元 || 运算符两边的查询只包含 should 子句,并把它们合并在了一起。这将给第一个 bool 查询中的 minimum_should_match 参数赋予不同的含义。将其改写为包含 5 个 should 子句的 bool 查询会破坏原始查询的语义,因为只匹配了 term5 或者 term6 的文档也应该被命中。
1Assert( 2 q => q.Bool(b => b 3 .Should(mq => mq.Query(), mq => mq.Query(), mq => mq.Query(), mq => mq.Query()) 4 .MinimumShouldMatch(2) 5 ) 6 || !q.Query() || q.Query(), 7 new BoolQuery 8 { 9 Should = new QueryContainer[] { Query, Query, Query, Query }, 10 MinimumShouldMatch = 2 11 } || !Query || Query, 12 c => 13 { 14 c.Bool.Should.Should().HaveCount(3); 15 var nestedBool = c.Bool.Should.First() as IQueryContainer; 16 nestedBool.Bool.Should.Should().HaveCount(4); 17 });
Locked bool queries
如果设置了任何一个查询元数据,NEST 将不会合并 bool 查询。举个栗子,如果设置了 boost 或者 name ,NEST 会视其为已被锁定。
在这里,我们演示两个锁定的 bool 查询
1Assert( 2 q => q.Bool(b => b.Name("leftBool").Should(mq => mq.Query())) 3 || q.Bool(b => b.Name("rightBool").Should(mq => mq.Query())), 4 new BoolQuery { Name = "leftBool", Should = new QueryContainer[] { Query } } 5 || new BoolQuery { Name = "rightBool", Should = new QueryContainer[] { Query } }, 6 c => AssertDoesNotJoinOntoLockedBool(c, "leftBool"));
锁定右边的查询
1Assert( 2 q => q.Bool(b => b.Should(mq => mq.Query())) 3 || q.Bool(b => b.Name("rightBool").Should(mq => mq.Query())), 4 new BoolQuery { Should = new QueryContainer[] { Query } } 5 || new BoolQuery { Name = "rightBool", Should = new QueryContainer[] { Query } }, 6 c => AssertDoesNotJoinOntoLockedBool(c, "rightBool"));
锁定左边的查询
1Assert( 2 q => q.Bool(b => b.Name("leftBool").Should(mq => mq.Query())) 3 || q.Bool(b => b.Should(mq => mq.Query())), 4 new BoolQuery { Name = "leftBool", Should = new QueryContainer[] { Query } } 5 || new BoolQuery { Should = new QueryContainer[] { Query } }, 6 c => AssertDoesNotJoinOntoLockedBool(c, "leftBool"));
Performance considerations
如果你需要使用 bool DSL 组合多个查询,请考虑一下内容。
你可以在循环中使用按位赋值来将多个查询合并为一个更大的查询。
本例中,我们使用 &= 赋值运算符创建一个含有 1000 个 must 子句的 bool 查询。
1var c = new QueryContainer(); 2var q = new TermQuery { Field = "x", Value = "x" }; 3 4for (var i = 0; i < 1000; i++) 5{ 6 c &= q; 7} 8 9 10| Median| StdDev| Gen 0| Gen 1| Gen 2| Bytes Allocated/Op 11| 1.8507 ms| 0.1878 ms| 1,793.00| 21.00| -| 1.872.672,28 12
可以看到,因为每次迭代我们都需要重新评估 bool 查询的合并能力,所以导致了大量的分配的产生。
由于我们事先已经知道了 bool 查询的形状,所以下面这个栗子要快的多
1QueryContainer q = new TermQuery { Field = "x", Value = "x" }; 2var x = Enumerable.Range(0, 1000).Select(f => q).ToArray(); 3var boolQuery = new BoolQuery 4{ 5 Must = x 6}; 7 8 9| Median| StdDev| Gen 0| Gen 1| Gen 2| Bytes Allocated/Op 10| 31.4610 μs| 0.9495 μs| 439.00| -| -| 7.912,95 11
在性能和分配上的下降是巨大的!
如果你使用的是 NEST 2.4.6 之前的版本,通过循环把很多
bool查询分配给了一个更大的bool查询,客户端没有做好以最优化的方式合并结果的工作,并且在执行大约 2000 次迭代时可能会引发异常。这仅适用于按位分配许多bool查询,其他查询不受影响。从 NEST 2.4.6 开始,你可以随意组合大量的
bool查询。查阅 PR #2335 on github 了解更多信息。