Skip to content

Commit c9b4c36

Browse files
committed
Fix regression: preserve not(not X) in BooleanFlatteningRewriter Resolve #19266 by fixing double-negation under MUST_NOT introduced by #19060 When parent is must_not and nested bool has only must_not clauses, rewrite to a positive OR:
not(bool(must_not:[X1..Xn])) => filter(bool(should:[X1..Xn], minimum_should_match=1)) Use FILTER (not MUST) to preserve non-scoring semantics of must_not Leave other flattenings unchanged; only trigger on pure must_not nested bool Add unit test: testDoubleNegationConvertedToPositiveMustShould Signed-off-by: Atri Sharma <[email protected]>
1 parent 1df543e commit c9b4c36

File tree

2 files changed

+37
-3
lines changed

2 files changed

+37
-3
lines changed

server/src/main/java/org/opensearch/search/query/rewriters/BooleanFlatteningRewriter.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,24 @@ private void flattenClauses(List<QueryBuilder> clauses, BoolQueryBuilder target,
129129
if (clause instanceof BoolQueryBuilder) {
130130
BoolQueryBuilder nestedBool = (BoolQueryBuilder) clause;
131131

132-
if (canFlatten(nestedBool, clauseType)) {
133-
// Flatten the nested bool query by extracting its clauses
132+
// Special handling for double negation: NOT over a bool that is ONLY NOTs
133+
if (clauseType == ClauseType.MUST_NOT && canFlatten(nestedBool, ClauseType.MUST_NOT)) {
134+
// not( bool( must_not: [X1..Xn] ) ) ==> must( bool( should: [X1..Xn], minimum_should_match: 1 ) )
135+
// This preserves the logical equivalence not(not(X)) == X and generalizes to multiple X via OR.
136+
BoolQueryBuilder orQuery = new BoolQueryBuilder();
137+
orQuery.minimumShouldMatch(1);
138+
for (QueryBuilder nestedClause : nestedBool.mustNot()) {
139+
if (nestedClause instanceof BoolQueryBuilder) {
140+
nestedClause = flattenBoolQuery((BoolQueryBuilder) nestedClause);
141+
}
142+
orQuery.should(nestedClause);
143+
}
144+
// Use FILTER to preserve scoring semantics (must_not does not contribute to score)
145+
target.filter(orQuery);
146+
} else if (canFlatten(nestedBool, clauseType)) {
147+
// General flattening for same-clause-type nesting
134148
List<QueryBuilder> nestedClauses = getClausesForType(nestedBool, clauseType);
135149
for (QueryBuilder nestedClause : nestedClauses) {
136-
// Recursively flatten if needed
137150
if (nestedClause instanceof BoolQueryBuilder) {
138151
nestedClause = flattenBoolQuery((BoolQueryBuilder) nestedClause);
139152
}

server/src/test/java/org/opensearch/search/query/rewriters/BooleanFlatteningRewriterTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,27 @@ public void testMustNotClauseNoFlattening() {
102102
assertThat(rewrittenBool.mustNot().get(0), instanceOf(BoolQueryBuilder.class));
103103
}
104104

105+
public void testDoubleNegationConvertedToPositiveMustShould() {
106+
// not( bool( must_not: [ term ] ) ) => must( bool( should: [ term ], msm=1 ) )
107+
QueryBuilder inner = QueryBuilders.boolQuery().mustNot(QueryBuilders.termQuery("product", "Oranges"));
108+
QueryBuilder query = QueryBuilders.boolQuery().mustNot(inner);
109+
110+
QueryBuilder rewritten = rewriter.rewrite(query, context);
111+
assertThat(rewritten, instanceOf(BoolQueryBuilder.class));
112+
BoolQueryBuilder rewrittenBool = (BoolQueryBuilder) rewritten;
113+
114+
// No must_not remains at top level
115+
assertThat(rewrittenBool.mustNot().size(), equalTo(0));
116+
// One MUST that is an OR over the inner negatives
117+
assertThat(rewrittenBool.must().size(), equalTo(1));
118+
assertThat(rewrittenBool.must().get(0), instanceOf(BoolQueryBuilder.class));
119+
120+
BoolQueryBuilder mustBool = (BoolQueryBuilder) rewrittenBool.must().get(0);
121+
assertThat(mustBool.should().size(), equalTo(1));
122+
assertThat(mustBool.minimumShouldMatch(), equalTo("1"));
123+
assertThat(mustBool.should().get(0), instanceOf(QueryBuilders.termQuery("product", "Oranges").getClass()));
124+
}
125+
105126
@AwaitsFix(bugUrl = "https://github.com/opensearch-project/OpenSearch/issues/18906")
106127
public void testDeepNesting() {
107128
// TODO: This test expects complete flattening of deeply nested bool queries

0 commit comments

Comments
 (0)