Elasticsearch.Nest 系列 7-2 搜索:Writing bool queries | 编写布尔查询


在使用查询 DSL 的时候,编写布尔查询命令会很冗长,如使用带有 2 个 should 子句的单个布尔查询:

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")
            )
        )
    )
);

可以相像,如果有多个嵌套布尔查询,最终代码久会长成这样:

为了解决代码冗长的问题,Nest 重写了一元操作符(“!”和“+”)以及二元操作符(“||”和“&&”)

运算符 对应的操作
&& must
\ \ should
+ filter
! must_not

重写二元操作符:”||”

使用重写的二元运算符“||”,可以让你更加简洁的在 should 子句中使用布尔查询。

基于最上面的例子,改写如下:

var firstSearchResponse = client.Search<Project>(s => s
    .Query(q => q.Term(p => p.Name, "x") || q.Term(p => p.Name, "y")
    )
);

同样,如果使用的是对象初始化语法的,同样可以使用“||”:

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" }
});

以上两种方式生成的命令相同,如下:

{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "name": {
              "value": "x"
            }
          }
        },
        {
          "term": {
            "name": {
              "value": "y"
            }
          }
        }
      ]
    }
  }
}

重写二元操作符:”&&”

重写的“&&”二元运算符可用于简化 must 子句的布尔查询:

var firstSearchResponse = client.Search<Project>(s => s
    .Query(q => q.Term(p => p.Name, "x") && q.Term(p => p.Name, "y")
    )
);

//以下用法等效于上面
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" }
});

生成的命令均为:

{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "name": {
              "value": "x"
            }
          }
        },
        {
          "term": {
            "name": {
              "value": "y"
            }
          }
        }
      ]
    }
  }
}

NEST 客户端会对像上面这样的请求合并为一次 bool 查询。

重写一元操作符:”!”

重写的一元操作附“!”主要面向含有 must_not 子句的查询。

var firstSearchResponse = client.Search<Project>(s => s
    .Query(q => !q
        .Term(p => p.Name, "x")
    )
);

//等效于
var secondSearchResponse = client.Search<Project>(new SearchRequest<Project>
{
    Query = !new TermQuery { Field = Field<Project>(p => p.Name), Value = "x" }
});

以上两种方式最终生成的命令如下:

{
  "query": {
    "bool": {
      "must_not": [
        {
          "term": {
            "name": {
              "value": "x"
            }
          }
        }
      ]
    }
  }
}

两个含有“!”运算符的查询可以用“&&”进行合并,以形成带有 2 个 must_not 子句的单个布尔查询。

//伪代码:
!q.Query() && !q.Query()
或者
!Query && !Query

重写一元操作符:”+”

通过使用一元“+”运算符可以将查询转换为带有过滤子句的布尔查询

var firstSearchResponse = client.Search<Project>(s => s
    .Query(q => +q  //+运算符
        .Term(p => p.Name, "x")
    )
);
//或者以下方式
var secondSearchResponse = client.Search<Project>(new SearchRequest<Project>
{
    Query = +new TermQuery { Field = Field<Project>(p => p.Name), Value = "x" }
});

最终生成的命令如下:

{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "name": {
              "value": "x"
            }
          }
        }
      ]
    }
  }}

以上命令将再过滤器上下文中运行,当你不需要使用 _score 来排序的时候,使用这种方式可以提高性能。

跟一元运算符“!”类似,使用“+”的 2 个子句同样可以使用“&&”进行合并,形成带有 2 个 filter 子句的单个布尔查询。

//伪代码:
+q.Query() && +q.Query()
或者
 +Query && +Query

使用“&&”合并查询

1.当将多个 Term 级别的查询用二元运算符“&&”(其中某些或所有查询都应用了一元运算符)进行连接,如:

term && term && term

NEST 会形成如下的嵌套顺序:

  • Nest 会自动将 && 连接的条件限制在单个 bool 查询中。

使用“||”合并查询或 should 子句

在上面的示例(描述“||”的小节)中,NEST将合并多个“||”或 should 子句到单个布尔查询中 。

像下面这样的组合:

term || term || term

最终会被解析为:

不过,布尔查询并非完全遵循编程语言中的布尔逻辑,如:

term1 && (term2 || term3 || term4)

需要特别注意,以上组合并不会被解析为以下关系:

原因:

  • 当布尔查询中仅包含 should 子句时,表示的是必须要有一个被匹配上。
  • 但当该布尔查询还具有 must 子句时,should 子句则会充当提升因子,这意味着它们不必匹配,但是如果匹配了,则该文档的相关性得分将得到提升。也就是说,should 子句的行为语义会根据 must 子句的存在而发生改变。

因此,对于“term1 && (term2 || term3 || term4)”这样的组合,你将得到包含 term1 的结果,这跟使用重载运算符时的预期是不相符

在构建搜索查询时,将 should 子句用作提升因子非常有用,你可以将实际的 bool 查询与NEST的运算符重载混合后进行匹配。

另外需要注意下,当 2 个布尔查询仅包含 should 子句的时候,NEST 不会进行合并,如下:

bool(should=term1, term2, term3, term4, minimum_should_match=2) || term5 || term6
  • 如果 NEST 发现运算符“||”左右两边仅包含 should 子句,那么此时 NEST 会改变第一个布尔查询中的 minimum_should_match 参数的原意。
    • 像上面的的示例 仅会在 term5 和 term6 上进行匹配

不会合并处理的情况

如果设置了任何查询元数据,如设置了 boost 或 name 等元数据,则 NEST 不会合并布尔查询,而是会将其视为已锁定,如下:

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"));

性能优化建议

假设你有很多 must 子句,使用“&=”进行合并

var c = new QueryContainer();
var q = new TermQuery { Field = "x", Value = "x" };

for (var i = 0; i < 1000; i++)
{
    c &= q;
}

性能分析结果如下:

  • 分配了过多的对象,这会导致 GC 压力。

因为已经知道了布尔查询的大致形态,因此可以通过如下方式来进行优化:

  • 因为知道最终是一个布尔查询,所以这里可以直接用布尔查询来代替 Term 查询合并。

    QueryContainer q = new TermQuery { Field = "x", Value = "x" };
    var x = Enumerable.Range(0, 1000).Select(f => q).ToArray();
    var boolQuery = new BoolQuery
    {
      Must = x
    };
    

性能分析结果如下:

常言道:学然后知不足,教然后知困。

我知道你的焦虑,一起共进加油:P

关不关注都无所谓,会根据生活节奏紧凑度定期分享些开发经验、搬砖生涯、痛点、感悟。

欢迎关注我的订阅号:P