Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -2143,16 +2143,13 @@ private void collectTermsFromAndClause(SearchParser.AndClauseContext ctx, List<T
QsOccur defaultOccur, boolean introducedByOr) {
List<SearchParser.NotClauseContext> notClauses = ctx.notClause();

// Determine how to handle implicit operators
String defaultOperator = options.getDefaultOperator();
boolean useAndForImplicit = "AND".equalsIgnoreCase(defaultOperator);

for (int i = 0; i < notClauses.size(); i++) {
boolean introducedByAnd;
if (i > 0) {
// Check if there's an explicit AND before this notClause
// by walking ctx.children and finding the token immediately before this notClause
introducedByAnd = hasExplicitAndBefore(ctx, notClauses.get(i), useAndForImplicit);
// Check if there's an explicit AND token before this notClause.
// Implicit conjunction (no AND token) returns false - only explicit AND
// should trigger the "introduced by AND" logic that modifies preceding terms.
introducedByAnd = hasExplicitAndBefore(ctx, notClauses.get(i));
} else {
introducedByAnd = false;
}
Expand All @@ -2166,13 +2163,18 @@ private void collectTermsFromAndClause(SearchParser.AndClauseContext ctx, List<T
/**
* Check if there's an explicit AND token before the target notClause.
* Walks ctx.children to find the position of target and checks the preceding token.
*
* IMPORTANT: Returns false for implicit conjunction (no explicit AND token).
* In Lucene's QueryParserBase.addClause(), only explicit CONJ_AND modifies the
* preceding term. CONJ_NONE (implicit conjunction) only affects the current term's
* occur via the default_operator, without modifying the preceding term.
*
* @param ctx The AndClauseContext containing the children
* @param target The target NotClauseContext to check
* @param implicitDefault Value to return if no explicit AND (use default_operator)
* @return true if explicit AND before target, implicitDefault if no explicit AND
* @return true only if there's an explicit AND token before target
*/
private boolean hasExplicitAndBefore(SearchParser.AndClauseContext ctx,
SearchParser.NotClauseContext target, boolean implicitDefault) {
SearchParser.NotClauseContext target) {
for (int j = 0; j < ctx.getChildCount(); j++) {
if (ctx.getChild(j) == target) {
// Found the target - check if the preceding sibling is an AND token
Expand All @@ -2181,12 +2183,12 @@ private boolean hasExplicitAndBefore(SearchParser.AndClauseContext ctx,
(org.antlr.v4.runtime.tree.TerminalNode) ctx.getChild(j - 1);
return terminal.getSymbol().getType() == SearchParser.AND;
}
// No explicit AND before this term - use default
return implicitDefault;
// No explicit AND before this term
return false;
}
}
// Target not found (should not happen) - use default
return implicitDefault;
// Target not found (should not happen)
return false;
}

private void collectTermsFromNotClause(SearchParser.NotClauseContext ctx, List<TermWithOccur> terms,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,114 @@ public void testLuceneModeEmptyOptions() {
Assertions.assertEquals(QsClauseType.AND, plan.getRoot().getType());
}

// ============ Tests for Implicit Conjunction (CONJ_NONE) ============

@Test
public void testLuceneModeImplicitConjunctionAndOperator() {
// Test: "a OR b c" with default_operator=AND
// In Lucene, implicit conjunction (CONJ_NONE) does NOT modify the preceding term.
// Only explicit AND/OR conjunctions modify the preceding term.
// a(CONJ_NONE)→MUST, b(CONJ_OR)→prev(a) SHOULD, b SHOULD,
// c(CONJ_NONE)→MUST (no modification to prev b)
// Result: [SHOULD(a), SHOULD(b), MUST(c)]
// This matches ES query_string: "a OR b c" with default_operator=AND
String dsl = "field:a OR field:b field:c";
String options = "{\"mode\":\"lucene\",\"default_operator\":\"AND\",\"minimum_should_match\":0}";
QsPlan plan = SearchDslParser.parseDsl(dsl, options);

Assertions.assertNotNull(plan);
Assertions.assertEquals(QsClauseType.OCCUR_BOOLEAN, plan.getRoot().getType());
Assertions.assertEquals(3, plan.getRoot().getChildren().size());

QsNode nodeA = plan.getRoot().getChildren().get(0);
Assertions.assertEquals("a", nodeA.getValue());
Assertions.assertEquals(SearchDslParser.QsOccur.SHOULD, nodeA.getOccur());

QsNode nodeB = plan.getRoot().getChildren().get(1);
Assertions.assertEquals("b", nodeB.getValue());
Assertions.assertEquals(SearchDslParser.QsOccur.SHOULD, nodeB.getOccur());

QsNode nodeC = plan.getRoot().getChildren().get(2);
Assertions.assertEquals("c", nodeC.getValue());
Assertions.assertEquals(SearchDslParser.QsOccur.MUST, nodeC.getOccur());
}

@Test
public void testLuceneModeImplicitConjunctionNotAndOperator() {
// Test: "a OR b NOT c" with default_operator=AND
// In Lucene, implicit NOT conjunction (CONJ_NONE + MOD_NOT) does NOT modify preceding term.
// a(CONJ_NONE)→MUST, b(CONJ_OR)→prev(a) SHOULD, b SHOULD,
// NOT c(CONJ_NONE, MOD_NOT)→MUST_NOT (no modification to prev b)
// Result: [SHOULD(a), SHOULD(b), MUST_NOT(c)]
// This matches ES query_string: "a OR b NOT c" with default_operator=AND
String dsl = "field:a OR field:b NOT field:c";
String options = "{\"mode\":\"lucene\",\"default_operator\":\"AND\",\"minimum_should_match\":0}";
QsPlan plan = SearchDslParser.parseDsl(dsl, options);

Assertions.assertNotNull(plan);
Assertions.assertEquals(QsClauseType.OCCUR_BOOLEAN, plan.getRoot().getType());
Assertions.assertEquals(3, plan.getRoot().getChildren().size());

QsNode nodeA = plan.getRoot().getChildren().get(0);
Assertions.assertEquals("a", nodeA.getValue());
Assertions.assertEquals(SearchDslParser.QsOccur.SHOULD, nodeA.getOccur());

QsNode nodeB = plan.getRoot().getChildren().get(1);
Assertions.assertEquals("b", nodeB.getValue());
Assertions.assertEquals(SearchDslParser.QsOccur.SHOULD, nodeB.getOccur());

QsNode nodeC = plan.getRoot().getChildren().get(2);
Assertions.assertEquals("c", nodeC.getValue());
Assertions.assertEquals(SearchDslParser.QsOccur.MUST_NOT, nodeC.getOccur());
}

@Test
public void testLuceneModeImplicitConjunctionOrOperator() {
// Test: "a OR b c" with default_operator=OR
// With OR_OPERATOR, implicit conjunction gives SHOULD to current term.
// a(CONJ_NONE)→SHOULD, b(CONJ_OR)→SHOULD, c(CONJ_NONE)→SHOULD
// Result: [SHOULD(a), SHOULD(b), SHOULD(c)]
String dsl = "field:a OR field:b field:c";
String options = "{\"mode\":\"lucene\",\"minimum_should_match\":0}";
QsPlan plan = SearchDslParser.parseDsl(dsl, options);

Assertions.assertNotNull(plan);
Assertions.assertEquals(QsClauseType.OCCUR_BOOLEAN, plan.getRoot().getType());
Assertions.assertEquals(3, plan.getRoot().getChildren().size());

for (QsNode child : plan.getRoot().getChildren()) {
Assertions.assertEquals(SearchDslParser.QsOccur.SHOULD, child.getOccur());
}
}

@Test
public void testLuceneModeExplicitAndStillModifiesPrev() {
// Test: "a OR b AND c" with default_operator=AND
// Explicit AND SHOULD modify the preceding term, unlike implicit conjunction.
// a(CONJ_NONE)→MUST, b(CONJ_OR)→prev(a) SHOULD, b SHOULD,
// c(CONJ_AND)→prev(b) MUST, c MUST
// Result: [SHOULD(a), MUST(b), MUST(c)]
String dsl = "field:a OR field:b AND field:c";
String options = "{\"mode\":\"lucene\",\"default_operator\":\"AND\",\"minimum_should_match\":0}";
QsPlan plan = SearchDslParser.parseDsl(dsl, options);

Assertions.assertNotNull(plan);
Assertions.assertEquals(QsClauseType.OCCUR_BOOLEAN, plan.getRoot().getType());
Assertions.assertEquals(3, plan.getRoot().getChildren().size());

QsNode nodeA = plan.getRoot().getChildren().get(0);
Assertions.assertEquals("a", nodeA.getValue());
Assertions.assertEquals(SearchDslParser.QsOccur.SHOULD, nodeA.getOccur());

QsNode nodeB = plan.getRoot().getChildren().get(1);
Assertions.assertEquals("b", nodeB.getValue());
Assertions.assertEquals(SearchDslParser.QsOccur.MUST, nodeB.getOccur());

QsNode nodeC = plan.getRoot().getChildren().get(2);
Assertions.assertEquals("c", nodeC.getValue());
Assertions.assertEquals(SearchDslParser.QsOccur.MUST, nodeC.getOccur());
}

// ============ Tests for Escape Handling ============

@Test
Expand Down
Loading