NEST

Wesley13
• 阅读 576

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(波动拳) 缩进的练习

NEST

Operator overloading

由于这个原因,NEST 引入了运算符重载,使得更容易去编写复杂的 bool 查询。这些重载的运算符是:

我们会示例来演示这几个运算符

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 了解更多信息。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
Java获得今日零时零分零秒的时间(Date型)
publicDatezeroTime()throwsParseException{    DatetimenewDate();    SimpleDateFormatsimpnewSimpleDateFormat("yyyyMMdd00:00:00");    SimpleDateFormatsimp2newS
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这