This is an automated email from the ASF dual-hosted git repository.

airborne pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 6cd22c9fdcc [feat](search) Support field-grouped query syntax 
field:(term1 OR term2) (#60786)
6cd22c9fdcc is described below

commit 6cd22c9fdcc52ff803e6c2add348d0f634188c9f
Author: Jack <[email protected]>
AuthorDate: Sat Feb 21 12:05:23 2026 +0800

    [feat](search) Support field-grouped query syntax field:(term1 OR term2) 
(#60786)
    
    ### What problem does this PR solve?
    
    Issue Number: close #N/A
    
    Problem Summary:
    
    The `search()` function did not support ES `query_string` field-grouped
    syntax where all terms inside parentheses inherit the field prefix:
    
    ```sql
    -- Previously failed with syntax error
    SELECT * FROM t WHERE search('title:(rock OR jazz)', 
'{"fields":["title","content"]}');
    ```
    
    ES semantics:
    | Input | Expansion |
    |-------|-----------|
    | `title:(rock OR jazz)` | `(title:rock OR title:jazz)` |
    | `title:(rock jazz)` with `default_operator:AND` | `(+title:rock
    +title:jazz)` |
    | `title:(rock OR jazz) AND music` with `fields:[title,content]` |
    `(title:rock OR title:jazz) AND (title:music OR content:music)` |
    | `title:("rock and roll" OR jazz)` | `(title:"rock and roll" OR
    title:jazz)` |
    
    ### Root cause
    
    The ANTLR grammar `SearchParser.g4` defined `fieldQuery : fieldPath
    COLON searchValue` where `searchValue` only accepts leaf values (TERM,
    QUOTED, etc.), not a parenthesized sub-clause. So `title:(` caused a
    syntax error.
    
    ### Solution
    
    **Grammar** (`SearchParser.g4`):
    - Add `fieldGroupQuery : fieldPath COLON LPAREN clause RPAREN` rule
    - Add it as alternative in `atomClause` before `fieldQuery`
    
    **Visitor** (`SearchDslParser.java`):
    - Add `markExplicitFieldRecursive()` helper — marks all leaf nodes in a
    group as `explicitField=true` to prevent `MultiFieldExpander` from
    re-expanding them across unintended fields
    - Modify `visitBareQuery()` in both `QsAstBuilder` and
    `QsLuceneModeAstBuilder` to use `currentFieldName` as field group
    context when set
    - Add `visitFieldGroupQuery()` to both AST builders: sets field context,
    visits inner clause, marks all leaves explicit
    - Update `visitAtomClause()` and `collectTermsFromNotClause()` to handle
---
 .../apache/doris/nereids/search/SearchParser.g4    |   9 +-
 .../functions/scalar/SearchDslParser.java          | 167 ++++++++++-
 .../functions/scalar/SearchDslParserTest.java      | 321 ++++++++++++++++++++-
 .../search/test_search_boundary_cases.groovy       |   3 +
 .../test_search_default_field_operator.groovy      |   3 +
 .../suites/search/test_search_dsl_operators.groovy |   3 +
 .../suites/search/test_search_escape.groovy        |   3 +
 .../suites/search/test_search_exact_basic.groovy   |   3 +
 .../search/test_search_exact_lowercase.groovy      |   3 +
 .../suites/search/test_search_exact_match.groovy   |   3 +
 .../search/test_search_exact_multi_index.groovy    |   3 +
 .../search/test_search_field_group_query.groovy    | 205 +++++++++++++
 .../suites/search/test_search_lucene_mode.groovy   |   3 +
 .../suites/search/test_search_multi_field.groovy   |   3 +
 .../search/test_search_null_regression.groovy      |   3 +
 .../search/test_search_null_semantics.groovy       |   3 +
 .../search/test_search_regexp_lowercase.groovy     |   3 +
 .../test_search_variant_dual_index_reader.groovy   |   2 +
 .../test_search_variant_subcolumn_analyzer.groovy  |   5 +
 .../search/test_search_vs_match_consistency.groovy |   3 +
 20 files changed, 738 insertions(+), 13 deletions(-)

diff --git 
a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/search/SearchParser.g4 
b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/search/SearchParser.g4
index cc5f6082cd6..3ff445ea1d6 100644
--- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/search/SearchParser.g4
+++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/search/SearchParser.g4
@@ -25,9 +25,14 @@ orClause   : andClause (OR andClause)* ;
 // AND is optional - space-separated terms use default_operator
 andClause  : notClause (AND? notClause)* ;
 notClause  : NOT atomClause | atomClause ;
-// Note: fieldQuery is listed before bareQuery so ANTLR prioritizes 
field:value over bare value.
+// Note: fieldGroupQuery is listed before fieldQuery so ANTLR prioritizes 
field:(group) over field:value.
+// fieldQuery is listed before bareQuery so ANTLR prioritizes field:value over 
bare value.
 // This ensures "field:term" is parsed as fieldQuery, not bareQuery with 
"field" as term.
-atomClause : LPAREN clause RPAREN | fieldQuery | bareQuery ;
+atomClause : LPAREN clause RPAREN | fieldGroupQuery | fieldQuery | bareQuery ;
+
+// Support for field:(grouped query) syntax, e.g., title:(rock OR jazz)
+// All terms inside the parentheses inherit the field prefix.
+fieldGroupQuery : fieldPath COLON LPAREN clause RPAREN ;
 
 // Support for variant subcolumn paths (e.g., field.subcolumn, field.sub1.sub2)
 fieldQuery : fieldPath COLON searchValue ;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SearchDslParser.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SearchDslParser.java
index 14e55fce15c..db12e715bed 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SearchDslParser.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SearchDslParser.java
@@ -62,6 +62,7 @@ import javax.annotation.Nullable;
 public class SearchDslParser {
     private static final Logger LOG = 
LogManager.getLogger(SearchDslParser.class);
     private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
+    private static final int MAX_FIELD_GROUP_DEPTH = 32;
 
     /**
      * Exception for search DSL syntax errors.
@@ -306,6 +307,43 @@ public class SearchDslParser {
         }
     }
 
+    /**
+     * Recursively mark leaf nodes with the given field name and set 
explicitField=true.
+     * Used for field-grouped queries like title:(rock OR jazz) to ensure 
inner leaf nodes
+     * are bound to the group's field and are not re-expanded by 
MultiFieldExpander.
+     *
+     * Skips nodes already marked as explicitField to preserve inner explicit 
bindings,
+     * e.g., title:(content:foo OR bar) keeps content:foo intact and only sets 
title on bar.
+     *
+     * @param depth current recursion depth to prevent StackOverflow from 
malicious input
+     */
+    private static void markExplicitFieldRecursive(QsNode node, String field) {
+        markExplicitFieldRecursive(node, field, 0);
+    }
+
+    private static void markExplicitFieldRecursive(QsNode node, String field, 
int depth) {
+        if (node == null) {
+            return;
+        }
+        if (depth > MAX_FIELD_GROUP_DEPTH) {
+            throw new SearchDslSyntaxException(
+                    "Field group query nesting too deep (max " + 
MAX_FIELD_GROUP_DEPTH + ")");
+        }
+        // Skip nodes already explicitly bound to a field (e.g., inner 
field:term inside a group)
+        if (node.isExplicitField()) {
+            return;
+        }
+        if (node.getChildren() != null && !node.getChildren().isEmpty()) {
+            for (QsNode child : node.getChildren()) {
+                markExplicitFieldRecursive(child, field, depth + 1);
+            }
+        } else {
+            // Leaf node - set field and mark as explicit
+            node.setField(field);
+            node.setExplicitField(true);
+        }
+    }
+
     /**
      * Common ANTLR parsing helper with visitor pattern.
      * Reduces code duplication across parsing methods.
@@ -753,6 +791,13 @@ public class SearchDslParser {
                 }
                 return result;
             }
+            if (ctx.fieldGroupQuery() != null) {
+                QsNode result = visit(ctx.fieldGroupQuery());
+                if (result == null) {
+                    throw new SearchDslSyntaxException("Invalid field group 
query");
+                }
+                return result;
+            }
             if (ctx.fieldQuery() != null) {
                 QsNode result = visit(ctx.fieldQuery());
                 if (result == null) {
@@ -772,18 +817,21 @@ public class SearchDslParser {
 
         @Override
         public QsNode visitBareQuery(SearchParser.BareQueryContext ctx) {
-            // Bare query - uses default field
-            if (defaultField == null || defaultField.isEmpty()) {
+            // Use currentFieldName if inside a field group context (set by 
visitFieldGroupQuery),
+            // otherwise fall back to the configured defaultField.
+            String effectiveField = (currentFieldName != null && 
!currentFieldName.isEmpty())
+                    ? currentFieldName : defaultField;
+            if (effectiveField == null || effectiveField.isEmpty()) {
                 throw new SearchDslSyntaxException(
                     "No field specified and no default_field configured. "
                     + "Either use field:value syntax or set default_field in 
options.");
             }
 
-            fieldNames.add(defaultField);
+            fieldNames.add(effectiveField);
 
-            // Set current field context to default field before visiting 
search value
+            // Set current field context before visiting search value
             String previousFieldName = currentFieldName;
-            currentFieldName = defaultField;
+            currentFieldName = effectiveField;
 
             try {
                 if (ctx.searchValue() == null) {
@@ -846,6 +894,50 @@ public class SearchDslParser {
             }
         }
 
+        @Override
+        public QsNode visitFieldGroupQuery(SearchParser.FieldGroupQueryContext 
ctx) {
+            if (ctx.fieldPath() == null) {
+                throw new SearchDslSyntaxException("Invalid field group query: 
missing field path");
+            }
+
+            // Build complete field path from segments (support 
field.subcolumn syntax)
+            StringBuilder fullPath = new StringBuilder();
+            List<SearchParser.FieldSegmentContext> segments = 
ctx.fieldPath().fieldSegment();
+            for (int i = 0; i < segments.size(); i++) {
+                if (i > 0) {
+                    fullPath.append('.');
+                }
+                String segment = segments.get(i).getText();
+                if (segment.startsWith("\"") && segment.endsWith("\"")) {
+                    segment = segment.substring(1, segment.length() - 1);
+                }
+                fullPath.append(segment);
+            }
+
+            String fieldPath = fullPath.toString();
+            fieldNames.add(fieldPath);
+
+            // Set field group context so bare terms inside use this field
+            String previousFieldName = currentFieldName;
+            currentFieldName = fieldPath;
+
+            try {
+                if (ctx.clause() == null) {
+                    throw new SearchDslSyntaxException("Invalid field group 
query: missing inner clause");
+                }
+                QsNode result = visit(ctx.clause());
+                if (result == null) {
+                    throw new SearchDslSyntaxException("Invalid field group 
query: inner clause returned null");
+                }
+                // Mark all leaf nodes as explicitly bound to this field.
+                // This prevents MultiFieldExpander from re-expanding them 
across other fields.
+                markExplicitFieldRecursive(result, fieldPath);
+                return result;
+            } finally {
+                currentFieldName = previousFieldName;
+            }
+        }
+
         @Override
         public QsNode visitSearchValue(SearchParser.SearchValueContext ctx) {
             String fieldName = getCurrentFieldName();
@@ -2102,6 +2194,9 @@ public class SearchDslParser {
                 } finally {
                     nestingLevel--;
                 }
+            } else if (atomCtx.fieldGroupQuery() != null) {
+                // Field group query (e.g., title:(rock OR jazz))
+                node = visit(atomCtx.fieldGroupQuery());
             } else if (atomCtx.fieldQuery() != null) {
                 // Field query with explicit field prefix
                 node = visit(atomCtx.fieldQuery());
@@ -2245,6 +2340,9 @@ public class SearchDslParser {
             if (ctx.clause() != null) {
                 return visit(ctx.clause());
             }
+            if (ctx.fieldGroupQuery() != null) {
+                return visit(ctx.fieldGroupQuery());
+            }
             if (ctx.fieldQuery() != null) {
                 return visit(ctx.fieldQuery());
             }
@@ -2256,19 +2354,22 @@ public class SearchDslParser {
 
         @Override
         public QsNode visitBareQuery(SearchParser.BareQueryContext ctx) {
-            // Bare query - uses effective default field (considering override)
+            // Use currentFieldName if inside a field group context (set by 
visitFieldGroupQuery),
+            // otherwise fall back to the effective default field.
             String defaultField = getEffectiveDefaultField();
-            if (defaultField == null || defaultField.isEmpty()) {
+            String effectiveField = (currentFieldName != null && 
!currentFieldName.isEmpty())
+                    ? currentFieldName : defaultField;
+            if (effectiveField == null || effectiveField.isEmpty()) {
                 throw new SearchDslSyntaxException(
                     "No field specified and no default_field configured. "
                     + "Either use field:value syntax or set default_field in 
options.");
             }
 
-            fieldNames.add(defaultField);
+            fieldNames.add(effectiveField);
 
-            // Set current field context to default field before visiting 
search value
+            // Set current field context before visiting search value
             String previousFieldName = currentFieldName;
-            currentFieldName = defaultField;
+            currentFieldName = effectiveField;
 
             try {
                 if (ctx.searchValue() == null) {
@@ -2318,6 +2419,52 @@ public class SearchDslParser {
             }
         }
 
+        @Override
+        public QsNode visitFieldGroupQuery(SearchParser.FieldGroupQueryContext 
ctx) {
+            if (ctx.fieldPath() == null) {
+                throw new SearchDslSyntaxException("Invalid field group query: 
missing field path");
+            }
+
+            // Build complete field path from segments (support 
field.subcolumn syntax)
+            StringBuilder fullPath = new StringBuilder();
+            List<SearchParser.FieldSegmentContext> segments = 
ctx.fieldPath().fieldSegment();
+            for (int i = 0; i < segments.size(); i++) {
+                if (i > 0) {
+                    fullPath.append('.');
+                }
+                String segment = segments.get(i).getText();
+                if (segment.startsWith("\"") && segment.endsWith("\"")) {
+                    segment = segment.substring(1, segment.length() - 1);
+                }
+                fullPath.append(segment);
+            }
+
+            String fieldPath = fullPath.toString();
+            fieldNames.add(fieldPath);
+
+            // Set field group context so bare terms inside use this field
+            String previousFieldName = currentFieldName;
+            currentFieldName = fieldPath;
+            nestingLevel++;
+
+            try {
+                if (ctx.clause() == null) {
+                    throw new SearchDslSyntaxException("Invalid field group 
query: missing inner clause");
+                }
+                QsNode result = visit(ctx.clause());
+                if (result == null) {
+                    throw new SearchDslSyntaxException("Invalid field group 
query: inner clause returned null");
+                }
+                // Mark all leaf nodes as explicitly bound to this field.
+                // This prevents MultiFieldExpander from re-expanding them 
across other fields.
+                markExplicitFieldRecursive(result, fieldPath);
+                return result;
+            } finally {
+                nestingLevel--;
+                currentFieldName = previousFieldName;
+            }
+        }
+
         @Override
         public QsNode visitSearchValue(SearchParser.SearchValueContext ctx) {
             String fieldName = currentFieldName;
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SearchDslParserTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SearchDslParserTest.java
index 704874fa0d9..9b40b05305e 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SearchDslParserTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SearchDslParserTest.java
@@ -1541,6 +1541,60 @@ public class SearchDslParserTest {
         Assertions.assertEquals(QsClauseType.REGEXP, 
regexpGroup.getChildren().get(1).getType());
     }
 
+    @Test
+    public void testMultiFieldExplicitFieldNotExpanded() {
+        // Bug #1: explicit field prefix (field:term) should NOT be expanded 
across fields,
+        // even when the field is in the fields list. Matches ES query_string 
behavior.
+        // "title:music AND content:history" → +title:music +content:history 
(no expansion)
+        String dsl = "title:music AND content:history";
+        String options = 
"{\"fields\":[\"title\",\"content\"],\"default_operator\":\"and\",\"mode\":\"lucene\",\"type\":\"best_fields\"}";
+        QsPlan plan = SearchDslParser.parseDsl(dsl, options);
+
+        Assertions.assertNotNull(plan);
+        QsNode root = plan.getRoot();
+        Assertions.assertEquals(QsClauseType.OCCUR_BOOLEAN, root.getType());
+        Assertions.assertEquals(2, root.getChildren().size());
+
+        // First child: title:music - should be a TERM pinned to "title", NOT 
expanded
+        QsNode musicNode = root.getChildren().get(0);
+        Assertions.assertEquals(QsClauseType.TERM, musicNode.getType());
+        Assertions.assertEquals("title", musicNode.getField());
+        Assertions.assertEquals("music", musicNode.getValue());
+
+        // Second child: content:history - should be a TERM pinned to 
"content", NOT expanded
+        QsNode historyNode = root.getChildren().get(1);
+        Assertions.assertEquals(QsClauseType.TERM, historyNode.getType());
+        Assertions.assertEquals("content", historyNode.getField());
+        Assertions.assertEquals("history", historyNode.getValue());
+    }
+
+    @Test
+    public void testMultiFieldMixedExplicitAndBareTerms() {
+        // "title:football AND american" → +title:football +(title:american | 
content:american)
+        // Explicit field pinned, bare term expanded
+        String dsl = "title:football AND american";
+        String options = 
"{\"fields\":[\"title\",\"content\"],\"default_operator\":\"and\",\"mode\":\"lucene\",\"type\":\"best_fields\"}";
+        QsPlan plan = SearchDslParser.parseDsl(dsl, options);
+
+        Assertions.assertNotNull(plan);
+        QsNode root = plan.getRoot();
+        Assertions.assertEquals(QsClauseType.OCCUR_BOOLEAN, root.getType());
+        Assertions.assertEquals(2, root.getChildren().size());
+
+        // First child: title:football - pinned to "title"
+        QsNode footballNode = root.getChildren().get(0);
+        Assertions.assertEquals(QsClauseType.TERM, footballNode.getType());
+        Assertions.assertEquals("title", footballNode.getField());
+        Assertions.assertEquals("football", footballNode.getValue());
+
+        // Second child: american - expanded across [title, content]
+        QsNode americanGroup = root.getChildren().get(1);
+        Assertions.assertEquals(QsClauseType.OCCUR_BOOLEAN, 
americanGroup.getType());
+        Assertions.assertEquals(2, americanGroup.getChildren().size());
+        Assertions.assertEquals("title", 
americanGroup.getChildren().get(0).getField());
+        Assertions.assertEquals("content", 
americanGroup.getChildren().get(1).getField());
+    }
+
     @Test
     public void testMultiFieldCrossFieldsLuceneMode() {
         // Test: cross_fields with Lucene mode
@@ -1950,8 +2004,273 @@ public class SearchDslParserTest {
         Assertions.assertTrue(hasMatchAll, "Should contain MATCH_ALL_DOCS node 
for '*'");
     }
 
-    // ============ Tests for MATCH_ALL_DOCS in multi-field mode ============
+    // ===== Field-Grouped Query Tests =====
+    @Test
+    public void testFieldGroupQuerySimpleOr() {
+        // title:(rock OR jazz) → OR(TERM(title,rock), TERM(title,jazz))
+        // ES semantics: field prefix applies to all terms inside parentheses
+        String dsl = "title:(rock OR jazz)";
+        QsPlan plan = SearchDslParser.parseDsl(dsl);
+
+        Assertions.assertNotNull(plan);
+        QsNode root = plan.getRoot();
+        Assertions.assertEquals(QsClauseType.OR, root.getType());
+        Assertions.assertEquals(2, root.getChildren().size());
+
+        QsNode child0 = root.getChildren().get(0);
+        QsNode child1 = root.getChildren().get(1);
+
+        Assertions.assertEquals(QsClauseType.TERM, child0.getType());
+        Assertions.assertEquals("title", child0.getField());
+        Assertions.assertEquals("rock", child0.getValue());
+        Assertions.assertTrue(child0.isExplicitField(), "term should be marked 
explicit");
+
+        Assertions.assertEquals(QsClauseType.TERM, child1.getType());
+        Assertions.assertEquals("title", child1.getField());
+        Assertions.assertEquals("jazz", child1.getValue());
+        Assertions.assertTrue(child1.isExplicitField(), "term should be marked 
explicit");
+
+        // Field bindings should include title
+        Assertions.assertEquals(1, plan.getFieldBindings().size());
+        Assertions.assertEquals("title", 
plan.getFieldBindings().get(0).getFieldName());
+    }
+
+    @Test
+    public void testFieldGroupQueryWithAndOperator() {
+        // title:(rock jazz) with default_operator:AND → AND(TERM(title,rock), 
TERM(title,jazz))
+        String dsl = "title:(rock jazz)";
+        String options = "{\"default_operator\":\"and\"}";
+        QsPlan plan = SearchDslParser.parseDsl(dsl, options);
+
+        Assertions.assertNotNull(plan);
+        QsNode root = plan.getRoot();
+        Assertions.assertEquals(QsClauseType.AND, root.getType());
+        Assertions.assertEquals(2, root.getChildren().size());
+
+        for (QsNode child : root.getChildren()) {
+            Assertions.assertEquals(QsClauseType.TERM, child.getType());
+            Assertions.assertEquals("title", child.getField());
+            Assertions.assertTrue(child.isExplicitField(), "child should be 
marked explicit");
+        }
+        Assertions.assertEquals("rock", root.getChildren().get(0).getValue());
+        Assertions.assertEquals("jazz", root.getChildren().get(1).getValue());
+    }
+
+    @Test
+    public void testFieldGroupQueryWithPhrase() {
+        // title:("rock and roll" OR jazz) → OR(PHRASE(title,"rock and roll"), 
TERM(title,jazz))
+        String dsl = "title:(\"rock and roll\" OR jazz)";
+        QsPlan plan = SearchDslParser.parseDsl(dsl);
+
+        Assertions.assertNotNull(plan);
+        QsNode root = plan.getRoot();
+        Assertions.assertEquals(QsClauseType.OR, root.getType());
+        Assertions.assertEquals(2, root.getChildren().size());
+
+        QsNode phrase = root.getChildren().get(0);
+        QsNode term = root.getChildren().get(1);
+
+        Assertions.assertEquals(QsClauseType.PHRASE, phrase.getType());
+        Assertions.assertEquals("title", phrase.getField());
+        Assertions.assertEquals("rock and roll", phrase.getValue());
+        Assertions.assertTrue(phrase.isExplicitField());
+
+        Assertions.assertEquals(QsClauseType.TERM, term.getType());
+        Assertions.assertEquals("title", term.getField());
+        Assertions.assertEquals("jazz", term.getValue());
+        Assertions.assertTrue(term.isExplicitField());
+    }
+
+    @Test
+    public void testFieldGroupQueryWithWildcardAndRegexp() {
+        // title:(roc* OR /ja../) → OR(PREFIX(title,roc*), REGEXP(title,ja..))
+        String dsl = "title:(roc* OR /ja../)";
+        QsPlan plan = SearchDslParser.parseDsl(dsl);
+
+        Assertions.assertNotNull(plan);
+        QsNode root = plan.getRoot();
+        Assertions.assertEquals(QsClauseType.OR, root.getType());
+        Assertions.assertEquals(2, root.getChildren().size());
+
+        QsNode prefix = root.getChildren().get(0);
+        Assertions.assertEquals(QsClauseType.PREFIX, prefix.getType());
+        Assertions.assertEquals("title", prefix.getField());
+        Assertions.assertTrue(prefix.isExplicitField());
+
+        QsNode regexp = root.getChildren().get(1);
+        Assertions.assertEquals(QsClauseType.REGEXP, regexp.getType());
+        Assertions.assertEquals("title", regexp.getField());
+        Assertions.assertEquals("ja..", regexp.getValue());
+        Assertions.assertTrue(regexp.isExplicitField());
+    }
+
+    @Test
+    public void testFieldGroupQueryCombinedWithBareQuery() {
+        // title:(rock OR jazz) AND music → combined query
+        // In standard mode with default_field=content: explicit title terms + 
expanded music
+        String dsl = "title:(rock OR jazz) AND music";
+        String options = "{\"default_field\":\"content\"}";
+        QsPlan plan = SearchDslParser.parseDsl(dsl, options);
+
+        Assertions.assertNotNull(plan);
+        QsNode root = plan.getRoot();
+        // Root is AND
+        Assertions.assertEquals(QsClauseType.AND, root.getType());
+        Assertions.assertEquals(2, root.getChildren().size());
 
+        // First child is the OR group from title:(rock OR jazz)
+        QsNode orGroup = root.getChildren().get(0);
+        Assertions.assertEquals(QsClauseType.OR, orGroup.getType());
+        Assertions.assertEquals(2, orGroup.getChildren().size());
+        for (QsNode child : orGroup.getChildren()) {
+            Assertions.assertEquals("title", child.getField());
+            Assertions.assertTrue(child.isExplicitField());
+        }
+
+        // Second child is bare "music" → uses default_field "content"
+        QsNode musicNode = root.getChildren().get(1);
+        Assertions.assertEquals(QsClauseType.TERM, musicNode.getType());
+        Assertions.assertEquals("content", musicNode.getField());
+        Assertions.assertFalse(musicNode.isExplicitField());
+    }
+
+    @Test
+    public void testFieldGroupQueryMultiFieldExplicitNotExpanded() {
+        // title:(rock OR jazz) with fields=[title,content] in cross_fields 
mode
+        // Explicit title:(rock OR jazz) should NOT be expanded to content
+        String dsl = "title:(rock OR jazz)";
+        String options = 
"{\"fields\":[\"title\",\"content\"],\"type\":\"cross_fields\"}";
+        QsPlan plan = SearchDslParser.parseDsl(dsl, options);
+
+        Assertions.assertNotNull(plan);
+        // Result should preserve title field for rock and jazz (not expand to 
content)
+        // We verify that "content" is not a field used in the plan
+        boolean hasContentBinding = plan.getFieldBindings().stream()
+                .anyMatch(b -> "content".equals(b.getFieldName()));
+        Assertions.assertFalse(hasContentBinding,
+                "Explicit title:(rock OR jazz) should not expand to content 
field");
+    }
+
+    @Test
+    public void testFieldGroupQueryLuceneMode() {
+        // title:(rock OR jazz) in lucene mode → OR(SHOULD(title:rock), 
SHOULD(title:jazz))
+        String dsl = "title:(rock OR jazz)";
+        String options = "{\"mode\":\"lucene\"}";
+        QsPlan plan = SearchDslParser.parseDsl(dsl, options);
+
+        Assertions.assertNotNull(plan);
+        QsNode root = plan.getRoot();
+        Assertions.assertNotNull(root);
+
+        // In lucene mode, the inner clause should be an OCCUR_BOOLEAN with 
SHOULD children
+        Assertions.assertEquals(QsClauseType.OCCUR_BOOLEAN, root.getType());
+        Assertions.assertEquals(2, root.getChildren().size());
+
+        for (QsNode child : root.getChildren()) {
+            Assertions.assertEquals("title", child.getField());
+            Assertions.assertEquals(QsOccur.SHOULD, child.getOccur());
+        }
+    }
+
+    @Test
+    public void testFieldGroupQueryLuceneModeAndOperator() {
+        // title:(rock AND jazz) in lucene mode → 
OCCUR_BOOLEAN(MUST(title:rock), MUST(title:jazz))
+        String dsl = "title:(rock AND jazz)";
+        String options = "{\"mode\":\"lucene\"}";
+        QsPlan plan = SearchDslParser.parseDsl(dsl, options);
+
+        Assertions.assertNotNull(plan);
+        QsNode root = plan.getRoot();
+        Assertions.assertNotNull(root);
+
+        Assertions.assertEquals(QsClauseType.OCCUR_BOOLEAN, root.getType());
+        Assertions.assertEquals(2, root.getChildren().size());
+
+        for (QsNode child : root.getChildren()) {
+            Assertions.assertEquals("title", child.getField());
+            Assertions.assertEquals(QsOccur.MUST, child.getOccur());
+        }
+    }
+
+    @Test
+    public void testFieldGroupQueryLuceneModeMultiField() {
+        // title:(rock OR jazz) AND music with fields=[title,content], 
mode=lucene
+        // title terms are explicit, music expands to both fields
+        String dsl = "title:(rock OR jazz) AND music";
+        String options = 
"{\"fields\":[\"title\",\"content\"],\"mode\":\"lucene\"}";
+        QsPlan plan = SearchDslParser.parseDsl(dsl, options);
+
+        Assertions.assertNotNull(plan);
+        Assertions.assertNotNull(plan.getRoot());
+        // Should parse without error and produce a plan
+        Assertions.assertFalse(plan.getFieldBindings().isEmpty());
+    }
+
+    @Test
+    public void testFieldGroupQuerySubcolumnPath() {
+        // attrs.color:(red OR blue) - field group with dot-notation path
+        String dsl = "attrs.color:(red OR blue)";
+        QsPlan plan = SearchDslParser.parseDsl(dsl);
+
+        Assertions.assertNotNull(plan);
+        QsNode root = plan.getRoot();
+        Assertions.assertEquals(QsClauseType.OR, root.getType());
+        Assertions.assertEquals(2, root.getChildren().size());
+
+        for (QsNode child : root.getChildren()) {
+            Assertions.assertEquals("attrs.color", child.getField());
+            Assertions.assertTrue(child.isExplicitField());
+        }
+    }
+
+    @Test
+    public void testFieldGroupQueryInnerExplicitFieldPreserved() {
+        // title:(content:foo OR bar) → content:foo stays pinned to "content", 
bar gets "title"
+        // Inner explicit field binding must NOT be overridden by outer group 
field
+        String dsl = "title:(content:foo OR bar)";
+        QsPlan plan = SearchDslParser.parseDsl(dsl);
+
+        Assertions.assertNotNull(plan);
+        QsNode root = plan.getRoot();
+        Assertions.assertEquals(QsClauseType.OR, root.getType());
+        Assertions.assertEquals(2, root.getChildren().size());
+
+        // First child: content:foo - should keep "content" (inner explicit 
binding)
+        QsNode fooNode = root.getChildren().get(0);
+        Assertions.assertEquals(QsClauseType.TERM, fooNode.getType());
+        Assertions.assertEquals("content", fooNode.getField());
+        Assertions.assertEquals("foo", fooNode.getValue());
+        Assertions.assertTrue(fooNode.isExplicitField());
+
+        // Second child: bar - should get "title" (outer group field)
+        QsNode barNode = root.getChildren().get(1);
+        Assertions.assertEquals(QsClauseType.TERM, barNode.getType());
+        Assertions.assertEquals("title", barNode.getField());
+        Assertions.assertEquals("bar", barNode.getValue());
+        Assertions.assertTrue(barNode.isExplicitField());
+    }
+
+    @Test
+    public void testFieldGroupQueryNotOperatorInside() {
+        // title:(rock OR NOT jazz) → OR(title:rock, NOT(title:jazz))
+        String dsl = "title:(rock OR NOT jazz)";
+        QsPlan plan = SearchDslParser.parseDsl(dsl);
+
+        Assertions.assertNotNull(plan);
+        QsNode root = plan.getRoot();
+        Assertions.assertEquals(QsClauseType.OR, root.getType());
+        Assertions.assertEquals(2, root.getChildren().size());
+
+        QsNode rockNode = root.getChildren().get(0);
+        Assertions.assertEquals("title", rockNode.getField());
+        Assertions.assertEquals("rock", rockNode.getValue());
+        Assertions.assertTrue(rockNode.isExplicitField());
+
+        QsNode notNode = root.getChildren().get(1);
+        Assertions.assertEquals(QsClauseType.NOT, notNode.getType());
+    }
+
+    // ============ Tests for MATCH_ALL_DOCS in multi-field mode ============
     @Test
     public void testMultiFieldMatchAllDocsBestFieldsLuceneMode() {
         // Test: "*" with best_fields + lucene mode should produce 
MATCH_ALL_DOCS
diff --git a/regression-test/suites/search/test_search_boundary_cases.groovy 
b/regression-test/suites/search/test_search_boundary_cases.groovy
index e29b5decb30..b7c3a931252 100644
--- a/regression-test/suites/search/test_search_boundary_cases.groovy
+++ b/regression-test/suites/search/test_search_boundary_cases.groovy
@@ -18,6 +18,9 @@
 suite("test_search_boundary_cases") {
     def tableName = "search_boundary_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // Create test table for boundary and edge cases
diff --git 
a/regression-test/suites/search/test_search_default_field_operator.groovy 
b/regression-test/suites/search/test_search_default_field_operator.groovy
index 23082586235..89d07a794be 100644
--- a/regression-test/suites/search/test_search_default_field_operator.groovy
+++ b/regression-test/suites/search/test_search_default_field_operator.groovy
@@ -18,6 +18,9 @@
 suite("test_search_default_field_operator") {
     def tableName = "search_enhanced_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // Create table with inverted indexes
diff --git a/regression-test/suites/search/test_search_dsl_operators.groovy 
b/regression-test/suites/search/test_search_dsl_operators.groovy
index 37ac49abf55..7415dfab10a 100644
--- a/regression-test/suites/search/test_search_dsl_operators.groovy
+++ b/regression-test/suites/search/test_search_dsl_operators.groovy
@@ -38,6 +38,9 @@
 suite("test_search_dsl_operators") {
     def tableName = "search_dsl_operators_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // Create table with inverted indexes
diff --git a/regression-test/suites/search/test_search_escape.groovy 
b/regression-test/suites/search/test_search_escape.groovy
index 5414455d9d9..18e999e2767 100644
--- a/regression-test/suites/search/test_search_escape.groovy
+++ b/regression-test/suites/search/test_search_escape.groovy
@@ -32,6 +32,9 @@
 suite("test_search_escape") {
     def tableName = "search_escape_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // Create table with inverted indexes
diff --git a/regression-test/suites/search/test_search_exact_basic.groovy 
b/regression-test/suites/search/test_search_exact_basic.groovy
index cf5701ac98f..0c9c368c340 100644
--- a/regression-test/suites/search/test_search_exact_basic.groovy
+++ b/regression-test/suites/search/test_search_exact_basic.groovy
@@ -18,6 +18,9 @@
 suite("test_search_exact_basic") {
     def tableName = "exact_basic_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // Simple table with basic index
diff --git a/regression-test/suites/search/test_search_exact_lowercase.groovy 
b/regression-test/suites/search/test_search_exact_lowercase.groovy
index 9d1b3756cbc..8389d7e72ed 100644
--- a/regression-test/suites/search/test_search_exact_lowercase.groovy
+++ b/regression-test/suites/search/test_search_exact_lowercase.groovy
@@ -18,6 +18,9 @@
 suite("test_search_exact_lowercase") {
     def tableName = "exact_lowercase_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // EXACT on mixed indexes: prefers untokenized, but untokenized index 
doesn't support lowercase
diff --git a/regression-test/suites/search/test_search_exact_match.groovy 
b/regression-test/suites/search/test_search_exact_match.groovy
index 307963b19b1..caf55679375 100644
--- a/regression-test/suites/search/test_search_exact_match.groovy
+++ b/regression-test/suites/search/test_search_exact_match.groovy
@@ -18,6 +18,9 @@
 suite("test_search_exact_match") {
     def tableName = "search_exact_test_table"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // Create test table with different index configurations
diff --git a/regression-test/suites/search/test_search_exact_multi_index.groovy 
b/regression-test/suites/search/test_search_exact_multi_index.groovy
index ab361dc45c0..1230b53aa4d 100644
--- a/regression-test/suites/search/test_search_exact_multi_index.groovy
+++ b/regression-test/suites/search/test_search_exact_multi_index.groovy
@@ -18,6 +18,9 @@
 suite("test_search_exact_multi_index") {
     def tableName = "exact_multi_index_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // Table with multiple indexes on the same column
diff --git a/regression-test/suites/search/test_search_field_group_query.groovy 
b/regression-test/suites/search/test_search_field_group_query.groovy
new file mode 100644
index 00000000000..b352d6d1cc7
--- /dev/null
+++ b/regression-test/suites/search/test_search_field_group_query.groovy
@@ -0,0 +1,205 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/**
+ * Tests for field-grouped query syntax in search() function.
+ *
+ * Supports ES query_string field-grouped syntax: field:(term1 OR term2)
+ * All terms inside the parentheses inherit the field prefix.
+ *
+ * Equivalent transformations:
+ *   title:(rock OR jazz)          → (title:rock OR title:jazz)
+ *   title:(rock jazz) AND:and     → (+title:rock +title:jazz)
+ *   title:(rock OR jazz) AND music → (title:rock OR title:jazz) AND music
+ */
+suite("test_search_field_group_query") {
+    def tableName = "search_field_group_test"
+
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    // When false, search() expressions are not pushed to the inverted index 
evaluation
+    // path, causing "SearchExpr should not be executed without inverted 
index" errors.
+    sql """ set enable_common_expr_pushdown = true """
+
+    sql "DROP TABLE IF EXISTS ${tableName}"
+
+    sql """
+        CREATE TABLE ${tableName} (
+            id INT,
+            title VARCHAR(255),
+            content TEXT,
+            category VARCHAR(100),
+            INDEX idx_title (title) USING INVERTED PROPERTIES("parser" = 
"english"),
+            INDEX idx_content (content) USING INVERTED PROPERTIES("parser" = 
"english"),
+            INDEX idx_category (category) USING INVERTED
+        ) ENGINE=OLAP
+        DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_allocation" = "tag.location.default: 1")
+    """
+
+    sql """INSERT INTO ${tableName} VALUES
+        (1, 'rock music history',    'The history of rock and roll music',     
     'Music'),
+        (2, 'jazz music theory',     'Jazz harmony and improvisation theory',  
      'Music'),
+        (3, 'classical music guide', 'Guide to classical music composers',     
      'Music'),
+        (4, 'python programming',    'Python language tutorial for beginners', 
      'Tech'),
+        (5, 'rock climbing tips',    'Tips and techniques for rock climbing',  
      'Sports'),
+        (6, 'jazz and blues fusion', 'The fusion of jazz and blues in modern 
music', 'Music'),
+        (7, 'machine learning',      'Introduction to machine learning 
algorithms',  'Tech'),
+        (8, 'rock and jazz review',  'Review of rock and jazz music 
festivals',     'Music')
+    """
+
+    sql "sync"
+
+    // === Basic field-grouped OR query ===
+
+    // title:(rock OR jazz) should match rows with "rock" or "jazz" in title
+    def res1 = sql """
+        SELECT id FROM ${tableName}
+        WHERE search('title:(rock OR jazz)', '{"default_operator":"or"}')
+        ORDER BY id
+    """
+    // rows 1 (rock music history), 2 (jazz music theory), 5 (rock climbing 
tips),
+    // 6 (jazz and blues fusion), 8 (rock and jazz review)
+    assertEquals([[1], [2], [5], [6], [8]], res1)
+
+    // === Field-grouped query vs equivalent expanded query ===
+
+    // title:(rock OR jazz) should give same results as explicit (title:rock 
OR title:jazz)
+    def res2a = sql """
+        SELECT id FROM ${tableName}
+        WHERE search('title:(rock OR jazz)', '{"default_operator":"or"}')
+        ORDER BY id
+    """
+    def res2b = sql """
+        SELECT id FROM ${tableName}
+        WHERE search('title:rock OR title:jazz', '{"default_operator":"or"}')
+        ORDER BY id
+    """
+    assertEquals(res2a, res2b)
+
+    // === Field-grouped AND (implicit) ===
+
+    // title:(rock jazz) with default_operator:AND → both "rock" AND "jazz" 
must be in title
+    def res3 = sql """
+        SELECT id FROM ${tableName}
+        WHERE search('title:(rock jazz)', '{"default_operator":"and"}')
+        ORDER BY id
+    """
+    // Only row 8 has both "rock" and "jazz" in title
+    assertEquals([[8]], res3)
+
+    // === Field-grouped query combined with bare query ===
+
+    // title:(rock OR jazz) AND music - explicit title terms + bare "music" on 
default field
+    def res4 = sql """
+        SELECT id FROM ${tableName}
+        WHERE search('title:(rock OR jazz) AND music', 
'{"default_field":"title","default_operator":"and"}')
+        ORDER BY id
+    """
+    // Must have "rock" or "jazz" in title AND "music" in title
+    // Row 1: title="rock music history" → has "rock" and "music" ✓
+    // Row 2: title="jazz music theory" → has "jazz" and "music" ✓
+    // Row 8: title="rock and jazz review" → has "rock" and "jazz" but no 
"music" in title ✗
+    // Row 5: title="rock climbing tips" → has "rock" but no "music" ✗
+    assertEquals([[1], [2]], res4)
+
+    // === Field-grouped query in multi-field mode ===
+
+    // title:(rock OR jazz) with fields=[title,content]
+    // Explicit title:(rock OR jazz) should NOT expand to content field
+    def res5a = sql """
+        SELECT id FROM ${tableName}
+        WHERE search('title:(rock OR jazz)', 
'{"fields":["title","content"],"type":"cross_fields"}')
+        ORDER BY id
+    """
+    // Only rows where title contains "rock" or "jazz"
+    // NOT rows where only content has those terms (row 3 has "rock and roll" 
in content)
+    def res5b = sql """
+        SELECT id FROM ${tableName}
+        WHERE search('title:rock OR title:jazz', 
'{"fields":["title","content"],"type":"cross_fields"}')
+        ORDER BY id
+    """
+    assertEquals(res5a, res5b)
+
+    // Rows 1,2,5,6,8 have rock/jazz in title; content-only matches should not 
appear
+    assertEquals(true, res5a.size() >= 1)
+    // Row 4 has "python" in title, so it should NOT appear
+    assert !res5a.collect { it[0] }.contains(4)
+    // Row 7 has "machine learning", so it should NOT appear
+    assert !res5a.collect { it[0] }.contains(7)
+
+    // === Phrase inside field group ===
+
+    // title:("rock and") - phrase query inside group
+    def res6 = sql """
+        SELECT id FROM ${tableName}
+        WHERE search('title:("rock and")', '{"default_operator":"or"}')
+        ORDER BY id
+    """
+    // Row 8: "rock and jazz review" → contains "rock and" ✓
+    assert res6.collect { it[0] }.contains(8)
+
+    // === Field-grouped query in Lucene mode ===
+
+    // title:(rock OR jazz) in lucene mode
+    def res7 = sql """
+        SELECT id FROM ${tableName}
+        WHERE search('title:(rock OR jazz)', '{"mode":"lucene"}')
+        ORDER BY id
+    """
+    // Should match rows with "rock" or "jazz" in title (SHOULD semantics)
+    assertEquals([[1], [2], [5], [6], [8]], res7)
+
+    // title:(rock AND jazz) in lucene mode
+    def res8 = sql """
+        SELECT id FROM ${tableName}
+        WHERE search('title:(rock AND jazz)', '{"mode":"lucene"}')
+        ORDER BY id
+    """
+    // Must have both "rock" AND "jazz" in title
+    assertEquals([[8]], res8)
+
+    // === Field-grouped combined with explicit field query ===
+
+    // title:(rock OR jazz) AND category:Music
+    def res9 = sql """
+        SELECT id FROM ${tableName}
+        WHERE search('title:(rock OR jazz) AND category:Music', 
'{"default_operator":"and"}')
+        ORDER BY id
+    """
+    // Must have (rock or jazz in title) AND category=Music
+    // Row 1: rock music history, Music ✓
+    // Row 2: jazz music theory, Music ✓
+    // Row 5: rock climbing tips, Sports ✗
+    // Row 6: jazz and blues fusion, Music ✓
+    // Row 8: rock and jazz review, Music ✓
+    assertEquals([[1], [2], [6], [8]], res9)
+
+    // === Verify it was previously broken (would have been a syntax error) ===
+    // This verifies the fix: parsing title:(rock OR jazz) should not throw
+    try {
+        def resSyntax = sql """
+            SELECT COUNT(*) FROM ${tableName}
+            WHERE search('title:(rock OR jazz)', '{"default_operator":"or"}')
+        """
+        assert resSyntax[0][0] >= 0 : "Should parse and execute without error"
+    } catch (Exception e) {
+        throw new AssertionError("title:(rock OR jazz) syntax should be 
supported: " + e.message)
+    }
+
+    sql "DROP TABLE IF EXISTS ${tableName}"
+}
diff --git a/regression-test/suites/search/test_search_lucene_mode.groovy 
b/regression-test/suites/search/test_search_lucene_mode.groovy
index 8e95a27a377..f971d8a9729 100644
--- a/regression-test/suites/search/test_search_lucene_mode.groovy
+++ b/regression-test/suites/search/test_search_lucene_mode.groovy
@@ -33,6 +33,9 @@
 suite("test_search_lucene_mode") {
     def tableName = "search_lucene_mode_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // Create table with inverted indexes
diff --git a/regression-test/suites/search/test_search_multi_field.groovy 
b/regression-test/suites/search/test_search_multi_field.groovy
index bd55a874f41..ca1c97eef5a 100644
--- a/regression-test/suites/search/test_search_multi_field.groovy
+++ b/regression-test/suites/search/test_search_multi_field.groovy
@@ -33,6 +33,9 @@
 suite("test_search_multi_field") {
     def tableName = "search_multi_field_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // Create table with inverted indexes on multiple fields
diff --git a/regression-test/suites/search/test_search_null_regression.groovy 
b/regression-test/suites/search/test_search_null_regression.groovy
index 742f86959a1..70666341662 100644
--- a/regression-test/suites/search/test_search_null_regression.groovy
+++ b/regression-test/suites/search/test_search_null_regression.groovy
@@ -18,6 +18,9 @@
 suite("test_search_null_regression") {
     def tableName = "search_null_regression_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // Create test table that reproduces the original bug scenarios
diff --git a/regression-test/suites/search/test_search_null_semantics.groovy 
b/regression-test/suites/search/test_search_null_semantics.groovy
index 9e18e063cf9..1c49e350bf3 100644
--- a/regression-test/suites/search/test_search_null_semantics.groovy
+++ b/regression-test/suites/search/test_search_null_semantics.groovy
@@ -18,6 +18,9 @@
 suite("test_search_null_semantics") {
     def tableName = "search_null_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // Create test table with inverted index and NULL values
diff --git a/regression-test/suites/search/test_search_regexp_lowercase.groovy 
b/regression-test/suites/search/test_search_regexp_lowercase.groovy
index 957027c2610..7151ac1c10b 100644
--- a/regression-test/suites/search/test_search_regexp_lowercase.groovy
+++ b/regression-test/suites/search/test_search_regexp_lowercase.groovy
@@ -22,6 +22,9 @@
 suite("test_search_regexp_lowercase") {
     def tableName = "search_regexp_lowercase_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     sql """
diff --git 
a/regression-test/suites/search/test_search_variant_dual_index_reader.groovy 
b/regression-test/suites/search/test_search_variant_dual_index_reader.groovy
index 6e1e297af92..6e53370381f 100644
--- a/regression-test/suites/search/test_search_variant_dual_index_reader.groovy
+++ b/regression-test/suites/search/test_search_variant_dual_index_reader.groovy
@@ -39,6 +39,8 @@ suite("test_search_variant_dual_index_reader") {
     sql """ set enable_match_without_inverted_index = false """
     sql """ set enable_common_expr_pushdown = true """
     sql """ set default_variant_enable_typed_paths_to_sparse = false """
+    // Pin doc_mode to false to prevent CI flakiness from fuzzy testing.
+    sql """ set default_variant_enable_doc_mode = false """
 
     sql "DROP TABLE IF EXISTS ${tableName}"
 
diff --git 
a/regression-test/suites/search/test_search_variant_subcolumn_analyzer.groovy 
b/regression-test/suites/search/test_search_variant_subcolumn_analyzer.groovy
index d14cf15f7a3..61c7c34c6e9 100644
--- 
a/regression-test/suites/search/test_search_variant_subcolumn_analyzer.groovy
+++ 
b/regression-test/suites/search/test_search_variant_subcolumn_analyzer.groovy
@@ -34,6 +34,11 @@ suite("test_search_variant_subcolumn_analyzer") {
     sql """ set enable_match_without_inverted_index = false """
     sql """ set enable_common_expr_pushdown = true """
     sql """ set default_variant_enable_typed_paths_to_sparse = false """
+    // Pin doc_mode to false to prevent CI flakiness from fuzzy testing.
+    // When default_variant_enable_doc_mode=true (randomly set by fuzzy 
testing),
+    // variant subcolumns are stored in document mode, causing inverted index
+    // iterators to be unavailable in BE for search() evaluation.
+    sql """ set default_variant_enable_doc_mode = false """
 
     sql "DROP TABLE IF EXISTS ${tableName}"
 
diff --git 
a/regression-test/suites/search/test_search_vs_match_consistency.groovy 
b/regression-test/suites/search/test_search_vs_match_consistency.groovy
index e39268838e5..ac4cd1f802b 100644
--- a/regression-test/suites/search/test_search_vs_match_consistency.groovy
+++ b/regression-test/suites/search/test_search_vs_match_consistency.groovy
@@ -18,6 +18,9 @@
 suite("test_search_vs_match_consistency") {
     def tableName = "search_match_consistency_test"
 
+    // Pin enable_common_expr_pushdown to prevent CI flakiness from fuzzy 
testing.
+    sql """ set enable_common_expr_pushdown = true """
+
     sql "DROP TABLE IF EXISTS ${tableName}"
 
     // Create test table similar to wikipedia structure


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to