NEST - 編寫布爾查詢
Writing bool queries
Version:5.x
英文原文地址:Writing bool queries
在使用查詢 DSL 時,編寫 bool
查詢會很容易把代碼變得冗長。舉個栗子,使用一個包含兩個 should
子句的 bool
查詢
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") ) ) ) );
現在設想多層嵌套的 bool
查詢,你會意識到這很快就會成為一個 hadouken(波動拳) 縮進的練習
Operator overloading
由於這個原因,NEST 引入了運算符重載,使得更容易去編寫復雜的 bool
查詢。這些重載的運算符是:
- Binary
||
operator 或 - Binary
&&
operator 與 - Unary
!
operator 非 - Unary
+
operator 篩選
我們會示例來演示這幾個運算符
Binary || operator
使用重載的二元 ||
運算符,可以更簡潔地表達含有 should
子句的 bool
查詢
之前哈杜根的栗子現在變成了 Fluent API 的樣子
var firstSearchResponse = client.Search<Project>(s => s
.Query(q => q
.Term(p => p.Name, "x") || q
.Term(p => p.Name, "y")
)
);
使用 Object Initializer 語法
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" } });
兩者都會生成如下 JSON 查詢 DSL
{
"query": {
"bool": {
"should": [
{
"term": {
"name": {
"value": "x"
}
}
},
{
"term": {
"name": {
"value": "y"
}
}
}
]
}
}
}
Binary && operator
重載的二元 &&
運算符用於將多個查詢組合在一起。當要組合的查詢沒有應用任何一元運算符時,生成的查詢是一個包含 must
子句的 bool
查詢
var firstSearchResponse = client.Search<Project>(s => s
.Query(q => q
.Term(p => p.Name, "x") && q
.Term(p => p.Name, "y")
)
);
使用 Object Initializer 語法
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" }
});
兩者都會生成如下 JSON 查詢 DSL
{
"query": {
"bool": {
"must": [
{
"term": {
"name": {
"value": "x"
}
}
},
{
"term": {
"name": {
"value": "y"
}
}
}
]
}
}
}
運算符重載會重寫原生的實現
term && term && term
會轉換成
bool
|___must
|___term
|___bool
|___must
|___term
|___term
可以想象,隨著查詢變得越來越復雜,結果很快就會變得笨拙。NEST 是很聰明的,它會把多個 &&
查詢聯合成一個 bool
查詢
bool
|___must
|___term
|___term
|___term
如下所示
Assert(
q => q.Query() && q.Query() && q.Query(), (1)
Query && Query && Query, (2)
c => c.Bool.Must.Should().HaveCount(3) (3)
);
(1) 使用 Fluent API 將三個查詢 &&
在一起
(2) 使用 Object Initializer 語法將三個查詢 &&
在一起
(3) 斷言最終的 bool
查詢會包含 3 個 must
子句
Unary ! operator
NEST 使用一元 !
運算符創建包含 must_not
子句的 bool
查詢
var firstSearchResponse = client.Search<Project>(s => s
.Query(q => !q
.Term(p => p.Name, "x")
)
);
使用 Object Initializer 語法
var secondSearchResponse = client.Search<Project>(new SearchRequest<Project>
{
Query = !new TermQuery { Field = Field<Project>(p => p.Name), Value = "x" }
});
兩者都會生成如下 JSON 查詢 DSL
{
"query": {
"bool": {
"must_not": [
{
"term": {
"name": {
"value": "x"
}
}
}
]
}
}
}
用一元 !
運算符標記的兩個查詢可以使用 and
運算符組合起來,從而形成一個包含兩個 must_not
子句的 bool
查詢
Assert(
q => !q.Query() && !q.Query(),
!Query && !Query,
c => c.Bool.MustNot.Should().HaveCount(2));
Unary + operator
可以使用一元 +
運算符將查詢轉換為帶有 filter
子句的 bool
查詢
var firstSearchResponse = client.Search<Project>(s => s
.Query(q => +q
.Term(p => p.Name, "x")
)
);
使用 Object Initializer 語法
var secondSearchResponse = client.Search<Project>(new SearchRequest<Project>
{
Query = +new TermQuery { Field = Field<Project>(p => p.Name), Value = "x" }
});
兩者都會生成如下 JSON 查詢 DSL
{
"query": {
"bool": {
"filter": [
{
"term": {
"name": {
"value": "x"
}
}
}
]
}
}
}
在篩選上下文中運行查詢,這在提高性能方面很有用。因為不需要計算查詢的相關性評分來影響結果的順序。
同樣的,使用一元 +
運算符標記的查詢可以和 &&
運算符組合在一起,構成一個包含兩個 filter
子句的 bool
查詢
Assert(
q => +q.Query() && +q.Query(),
+Query && +Query,
c => c.Bool.Filter.Should().HaveCount(2));
Combining bool queries
在使用二元 &&
運算符組合多個查詢時,如果某些或者全部的查詢都應用了一元運算符,NEST 仍然可以把它們合並成一個 bool
查詢
參考下面這個 bool
查詢
bool
|___must
| |___term
| |___term
| |___term
|
|___must_not
|___term
NEST 中可以這樣構建
Assert(
q => q.Query() && q.Query() && q.Query() && !q.Query(),
Query && Query && Query && !Query,
c=>
{
c.Bool.Must.Should().HaveCount(3);
c.Bool.MustNot.Should().HaveCount(1);
});
一個更復雜的栗子
term && term && term && !term && +term && +term
依然會生成下面這個結構的單個 bool
查詢
bool
|___must
| |___term
| |___term
| |___term
|
|___must_not
| |___term
|
|___filter
|___term
|___term
Assert(
q => q.Query() && q.Query() && q.Query() && !q.Query() && +q.Query() && +q.Query(),
Query && Query && Query && !Query && +Query && +Query,
c =>
{
c.Bool.Must.Should().HaveCount(3);
c.Bool.MustNot.Should().HaveCount(1);
c.Bool.Filter.Should().HaveCount(2);
});
你也可以將使用重載運算符的查詢和真正的 bool
查詢混合在一起
bool(must=term, term, term) && !term
仍然會合並為一個 bool
查詢
Assert(
q => q.Bool(b => b.Must(mq => mq.Query(), mq => mq.Query(), mq => mq.Query())) && !q.Query(),
new BoolQuery { Must = new QueryContainer[] { Query, Query, Query } } && !Query,
c =>
{
c.Bool.Must.Should().HaveCount(3);
c.Bool.MustNot.Should().HaveCount(1);
});
Combining queries with || or should clauses
就像之前的栗子,NEST 會把多個 should
或者 ||
查詢合並成一個包含多個 should
子句的 bool
查詢。
總而言之,這個
term || term || term
會變成
bool
|___should
|___term
|___term
|___term
但是,bool
查詢不會完全遵循你從編程語言所期望的布爾邏輯
term1 && (term2 || term3 || term4)
不會變成
bool
|___must
| |___term1
|
|___should
|___term2
|___term3
|___term4
為什麽會這樣?當一個 bool
查詢中只包含 should
子句時,至少會匹配一個。但是,當這個 bool
查詢還包含一個 must
子句時,應該將 should
子句當作一個 boost
因子,這意味著他們都不是必需匹配的。但是如果匹配,文檔的相關性評分會得到提高,從而在結果中顯示更高的值。should
子句的行為會因為 must
的存在而發生改變。
因此,再看看前面那個示例,你只能得到包含 term1
的結果。這顯然不是使用運算符重載的目的。
為此,NEST 將之前的查詢重寫成了:
bool
|___must
|___term1
|___bool
|___should
|___term2
|___term3
|___term4
Assert(
q => q.Query() && (q.Query() || q.Query() || q.Query()),
Query && (Query || Query || Query),
c =>
{
c.Bool.Must.Should().HaveCount(2);
var lastMustClause = (IQueryContainer)c.Bool.Must.Last();
lastMustClause.Should().NotBeNull();
lastMustClause.Bool.Should().NotBeNull();
lastMustClause.Bool.Should.Should().HaveCount(3);
});
添加圓括號,強制改變運算順序
在構建搜索查詢時,使用 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
的文檔也應該被命中。
Assert(
q => q.Bool(b => b
.Should(mq => mq.Query(), mq => mq.Query(), mq => mq.Query(), mq => mq.Query())
.MinimumShouldMatch(2)
)
|| !q.Query() || q.Query(),
new BoolQuery
{
Should = new QueryContainer[] { Query, Query, Query, Query },
MinimumShouldMatch = 2
} || !Query || Query,
c =>
{
c.Bool.Should.Should().HaveCount(3);
var nestedBool = c.Bool.Should.First() as IQueryContainer;
nestedBool.Bool.Should.Should().HaveCount(4);
});
Locked bool queries
如果設置了任何一個查詢元數據,NEST 將不會合並 bool
查詢。舉個栗子,如果設置了 boost
或者 name
,NEST 會視其為已被鎖定。
在這裏,我們演示兩個鎖定的 bool
查詢
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"));
Performance considerations
如果你需要使用 bool
DSL 組合多個查詢,請考慮一下內容。
你可以在循環中使用按位賦值來將多個查詢合並為一個更大的查詢。
本例中,我們使用 &=
賦值運算符創建一個含有 1000 個 must
子句的 bool
查詢。
var c = new QueryContainer();
var q = new TermQuery { Field = "x", Value = "x" };
for (var i = 0; i < 1000; i++)
{
c &= q;
}
| Median| StdDev| Gen 0| Gen 1| Gen 2| Bytes Allocated/Op
| 1.8507 ms| 0.1878 ms| 1,793.00| 21.00| -| 1.872.672,28
可以看到,因為每次叠代我們都需要重新評估 bool
查詢的合並能力,所以導致了大量的分配的產生。
由於我們事先已經知道了 bool
查詢的形狀,所以下面這個栗子要快的多
QueryContainer q = new TermQuery { Field = "x", Value = "x" };
var x = Enumerable.Range(0, 1000).Select(f => q).ToArray();
var boolQuery = new BoolQuery
{
Must = x
};
| Median| StdDev| Gen 0| Gen 1| Gen 2| Bytes Allocated/Op
| 31.4610 μs| 0.9495 μs| 439.00| -| -| 7.912,95
在性能和分配上的下降是巨大的!
如果你使用的是 NEST 2.4.6 之前的版本,通過循環把很多
bool
查詢分配給了一個更大的bool
查詢,客戶端沒有做好以最優化的方式合並結果的工作,並且在執行大約 2000 次叠代時可能會引發異常。這僅適用於按位分配許多bool
查詢,其他查詢不受影響。從 NEST 2.4.6 開始,你可以隨意組合大量的
bool
查詢。查閱 PR #2335 on github 了解更多信息。
NEST - 編寫布爾查詢