NEST编写bool查询

1381 发布于: 2021-05-18 读完约需 6 分钟

概述

在NEST中使用查询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 indenting

操作符重载

为此,NEST引入了运算符重载,使复杂的bool查询更容易编写。重载操作符如下:

  • 二元或(|| )操作符
  • 二元与(&&)操作符
  • 一元非(!)操作符
  • 一元加(+)操作符

二元或(|| )操作符

使用重载的二元或(||)操作符,可以更简洁地表达带有should子句的bool查询。

上面的示例可以使用NEST的二元或操作符简写成如下语句:

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

两者都会产生以下JSON查询DSL:

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

二元与(&&)操作符

重载的二元与(&&)操作符可用于将查询组合在一起。如果要组合的查询没有应用任何一元运算符,则结果查询是带有must子句的bool查询:

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

两者都会产生以下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

一元非操作符

NEST还提供了一元非操作符,你可以用它创建带有must_not子句的bool查询:

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

两者都会产生以下JSON查询DSL:

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

两个一元非(!)操作符的查询可以与二元操作符(&&)组合,形成一个带有两个must_not子句的bool查询:

Assert(
    q => !q.Query() && !q.Query(), 
    !Query && !Query, 
    c => c.Bool.MustNot.Should().HaveCount(2));

一元加(+)操作符

在NEST中,可以使用一元加操作符(+)将查询转换为带有过滤子句的bool查询:

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

两者都会产生以下JSON查询DSL:

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

这将在筛选器上下文中运行查询,这对于提高性能非常有用,因为查询的相关度评分不需要影响结果的顺序。

与一元非操作符(!)相似,用一元加操作符(+)标记的查询可以与二元与操作符(&&)组合,形成一个带有两个筛选子句的bool查询:

Assert(
    q => +q.Query() && +q.Query(),
    +Query && +Query,
    c => c.Bool.Filter.Should().HaveCount(2));

组合bool查询

当使用二元操作符(&&)组合多个查询(其中一些或所有查询都使用一元运算符)时,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

使用NEST构建单个bool查询的代码如下:

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
|
|___must_not
|   |___term
|
|___filter
   |___term
   |___term

同时,你也可以使用混合查询:

bool(must=term, term, term) && !term

NEST构建代码如下:

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

将查询与二元或操作符(||)或should子句组合

根据前面的例子,当NEST遇到正在运行的bool查询只包含should子句时,它将把多个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子句现在就充当了一个增强因子,这意味着它们都不必须匹配,但如果匹配,该文档的相关性得分就会提高,从而在结果中出现得更高。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子句作为增强因子是一个非常强大的构造。你可以将一个实际的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查询会破坏原始查询的语义,因为只有在term5term6上匹配才会成功。

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

锁定bool查询

如果设置了任何查询元数据,NEST将不会组合bool查询,例如,如果设置了boostname等元数据,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"));

两个右查询都不被锁定的bool查询:

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

版权声明:本作品系原创,版权归码友网所有,如未经许可,禁止任何形式转载,违者必究。

发表评论

登录用户才能发表评论, 请 登 录 或者 注册