1. 程式人生 > >NEST - 編寫布爾查詢

NEST - 編寫布爾查詢

快的 ren 上下文 dia 而在 client 就會 運算符重載 構建

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 - 編寫布爾查詢