Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions sandbox/plugins/dsl-query-executor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,43 @@ _search request
→ SearchResponseBuilder (builds SearchResponse)
```

### Bool Query
Converts to Calcite logical expressions with full support for all clauses and `minimum_should_match`.

**Clauses:**
- `must` - Required clauses (AND logic)
- `should` - Optional clauses (OR logic)
- `must_not` - Exclusion clauses (NOT logic)
- `filter` - Filtering clauses (AND logic, no scoring)

**minimum_should_match formats:**
- Non-negative integer: `"2"` - exactly 2 clauses must match
- Negative integer: `"-1"` - total minus 1 must match
- Non-negative percentage: `"70%"` - 70% of clauses (rounded down)
- Negative percentage: `"-30%"` - can miss 30% of clauses
- Single combination: `"2<75%"` - if total ≤ 2 match all, else 75%
- Multiple combinations: `"3<-1 5<50%"` - threshold-based rules

**Example:**
```json
{
"bool": {
"must": [
{"term": {"status": "active"}}
],
"should": [
{"term": {"priority": "high"}},
{"term": {"priority": "medium"}},
{"term": {"priority": "low"}}
],
"must_not": [
{"term": {"deleted": "true"}}
],
"minimum_should_match": "2"
}
}
```

## Dependencies

- `analytics-engine` — provides `QueryPlanExecutor` and `EngineContext` via Guice (declared as `extendedPlugins`)
Expand All @@ -42,8 +79,11 @@ _search request

```bash
# Unit tests
./gradlew :sandbox:plugins:dsl-query-executor:test
./gradlew :sandbox:plugins:dsl-query-executor:test -Dsandbox.enabled=true

# Integration tests
./gradlew :sandbox:plugins:dsl-query-executor:internalClusterTest
./gradlew :sandbox:plugins:dsl-query-executor:internalClusterTest -Dsandbox.enabled=true

# Specific test class
./gradlew :sandbox:plugins:dsl-query-executor:test --tests "BoolQueryTranslatorTests" -Dsandbox.enabled=true
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.dsl;

import org.apache.lucene.tests.util.LuceneTestCase.AwaitsFix;
import org.opensearch.common.xcontent.XContentType;
import org.opensearch.index.query.QueryBuilders;
import org.opensearch.search.builder.SearchSourceBuilder;

/**
* Integration tests for bool query with minimum_should_match parameter.
*/
@AwaitsFix(bugUrl = "DSL query pipeline not fully wired E2E: fragment conversion, shard execution and Arrow Flight drain pending")
public class DslBoolQueryIT extends DslIntegTestBase {

@Override
protected void createTestIndex() {
createIndex(INDEX);
ensureGreen();

// Index multiple documents with different field values
client().prepareIndex(INDEX)
.setId("1")
.setSource(
"{\"name\":\"laptop\",\"price\":1200,\"brand\":\"brandX\",\"rating\":4.5,\"tag\":\"electronics\"}",
XContentType.JSON
)
.get();
client().prepareIndex(INDEX)
.setId("2")
.setSource("{\"name\":\"phone\",\"price\":800,\"brand\":\"brandY\",\"rating\":4.0,\"tag\":\"electronics\"}", XContentType.JSON)
.get();
client().prepareIndex(INDEX)
.setId("3")
.setSource("{\"name\":\"tablet\",\"price\":600,\"brand\":\"brandX\",\"rating\":3.5,\"tag\":\"electronics\"}", XContentType.JSON)
.get();
client().prepareIndex(INDEX)
.setId("4")
.setSource(
"{\"name\":\"monitor\",\"price\":400,\"brand\":\"brandZ\",\"rating\":4.2,\"tag\":\"accessories\"}",
XContentType.JSON
)
.get();
refresh(INDEX);
}

// Basic bool query tests

public void testBoolQueryWithMust() {
createTestIndex();
assertOk(search(new SearchSourceBuilder().query(QueryBuilders.boolQuery().must(QueryBuilders.termQuery("tag", "electronics")))));
}

public void testBoolQueryWithShould() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery("brand", "brandX"))
.should(QueryBuilders.termQuery("brand", "brandY"))
)
)
);
}

public void testBoolQueryWithMustNot() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("tag", "electronics"))
.mustNot(QueryBuilders.termQuery("brand", "brandZ"))
)
)
);
}

public void testBoolQueryWithFilter() {
createTestIndex();
assertOk(search(new SearchSourceBuilder().query(QueryBuilders.boolQuery().filter(QueryBuilders.termQuery("tag", "electronics")))));
}

public void testBoolQueryWithAllClauses() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("tag", "electronics"))
.should(QueryBuilders.termQuery("brand", "brandX"))
.mustNot(QueryBuilders.termQuery("name", "phone"))
.filter(QueryBuilders.termQuery("rating", 4.5))
)
)
);
}

// minimum_should_match: Integer tests

public void testMinimumShouldMatchPositiveInteger() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery("brand", "brandX"))
.should(QueryBuilders.termQuery("brand", "brandY"))
.should(QueryBuilders.termQuery("brand", "brandZ"))
.minimumShouldMatch("2")
)
)
);
}

public void testMinimumShouldMatchNegativeInteger() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery("brand", "brandX"))
.should(QueryBuilders.termQuery("brand", "brandY"))
.should(QueryBuilders.termQuery("brand", "brandZ"))
.minimumShouldMatch("-1")
)
)
);
}

// minimum_should_match: Percentage tests

public void testMinimumShouldMatchPositivePercentage() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery("tag", "electronics"))
.should(QueryBuilders.termQuery("brand", "brandX"))
.should(QueryBuilders.termQuery("name", "laptop"))
.should(QueryBuilders.termQuery("rating", 4.5))
.minimumShouldMatch("75%")
)
)
);
}

public void testMinimumShouldMatchNegativePercentage() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery("tag", "electronics"))
.should(QueryBuilders.termQuery("brand", "brandX"))
.should(QueryBuilders.termQuery("name", "laptop"))
.should(QueryBuilders.termQuery("rating", 4.5))
.minimumShouldMatch("-25%")
)
)
);
}

// minimum_should_match: Combination tests

public void testMinimumShouldMatchSingleCombination() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery("brand", "brandX"))
.should(QueryBuilders.termQuery("brand", "brandY"))
.minimumShouldMatch("2<75%")
)
)
);
}

public void testMinimumShouldMatchMultipleCombinations() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery("tag", "electronics"))
.should(QueryBuilders.termQuery("brand", "brandX"))
.should(QueryBuilders.termQuery("name", "laptop"))
.should(QueryBuilders.termQuery("rating", 4.5))
.minimumShouldMatch("3<-1 5<50%")
)
)
);
}

// minimum_should_match with must/filter

public void testMinimumShouldMatchWithMust() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("tag", "electronics"))
.should(QueryBuilders.termQuery("brand", "brandX"))
.should(QueryBuilders.termQuery("brand", "brandY"))
.minimumShouldMatch("1")
)
)
);
}

public void testMinimumShouldMatchWithFilter() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.filter(QueryBuilders.termQuery("tag", "electronics"))
.should(QueryBuilders.termQuery("brand", "brandX"))
.should(QueryBuilders.termQuery("brand", "brandY"))
.minimumShouldMatch("1")
)
)
);
}

// Nested bool queries

public void testNestedBoolQuery() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("tag", "electronics"))
.must(
QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery("brand", "brandX"))
.should(QueryBuilders.termQuery("brand", "brandY"))
)
)
)
);
}

public void testNestedBoolQueryWithMinimumShouldMatch() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("tag", "electronics"))
.must(
QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery("brand", "brandX"))
.should(QueryBuilders.termQuery("brand", "brandY"))
.should(QueryBuilders.termQuery("brand", "brandZ"))
.minimumShouldMatch("2")
)
)
)
);
}

// Edge cases

public void testBoolQueryWithOnlyShouldClause() {
createTestIndex();
assertOk(search(new SearchSourceBuilder().query(QueryBuilders.boolQuery().should(QueryBuilders.termQuery("brand", "brandX")))));
}

public void testBoolQueryWithMinimumShouldMatchOne() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery("brand", "brandX"))
.should(QueryBuilders.termQuery("brand", "brandY"))
.minimumShouldMatch("1")
)
)
);
}

public void testBoolQueryWithMinimumShouldMatchAll() {
createTestIndex();
assertOk(
search(
new SearchSourceBuilder().query(
QueryBuilders.boolQuery()
.should(QueryBuilders.termQuery("tag", "electronics"))
.should(QueryBuilders.termQuery("brand", "brandX"))
.minimumShouldMatch("2")
)
)
);
}
}
Loading
Loading