NEST - 編寫布爾查詢

Writing bool queries

Version:5.xhtml

英文原文地址:Writing bool queriesgit

在使用查詢 DSL 時,編寫 bool 查詢會很容易把代碼變得冗長。舉個栗子,使用一個包含兩個 should 子句的 bool 查詢github

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(波動拳) 縮進的練習編程

img

Operator overloading

因爲這個緣由,NEST 引入了運算符重載,使得更容易去編寫複雜的 bool 查詢。這些重載的運算符是:json

咱們會示例來演示這幾個運算符c#

Binary || operator

使用重載的二元 || 運算符,能夠更簡潔地表達含有 should 子句的 bool 查詢api

以前哈杜根的栗子如今變成了 Fluent API 的樣子elasticsearch

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 查詢 DSLide

{
  "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 瞭解更多信息。

相關文章
相關標籤/搜索