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

eldenmoon 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 c914dc3cefa [refactor](variant) normalize nested search predicate 
field resolution (#61548)
c914dc3cefa is described below

commit c914dc3cefad1b8a1121c12f7152ad8cf5a44503
Author: lihangyu <[email protected]>
AuthorDate: Mon Mar 23 11:33:56 2026 +0800

    [refactor](variant) normalize nested search predicate field resolution 
(#61548)
    
    Problem Summary:
    Nested search predicates were parsed inconsistently across code paths.
    Queries inside `NESTED(path, ...)` had to repeat the full nested prefix,
    unsupported nested forms were validated late, and normalized field
    bindings could diverge from the field paths pushed down to thrift.
    
    This change centralizes nested field path construction and normalizes
    child predicates against the active nested path during parsing. It
    applies the same validation rules in standard and lucene modes, rejects
    unsupported nested forms earlier, and keeps normalized field bindings
    aligned with generated thrift search params. The added FE tests cover
    standard mode, lucene mode, invalid nested syntax, and thrift
    serialization of normalized nested fields.
    
    Normalize FE handling of nested search predicates for Variant search
    DSL. Fields inside `NESTED(path, ...)` must now be written relative to
    the nested path, and unsupported forms such as absolute nested field
    references, bare queries, nested `NESTED(...)`, and non-top-level
    `NESTED` clauses now fail with explicit syntax errors.
    
    - Test: Not run in this session (message-only amend; the code change
    adds FE test coverage)
    - Behavior changed: Yes (nested predicates now require relative field
    references inside `NESTED(path, ...)`)
    - Does this need documentation: No
---
 be/src/exec/common/variant_util.cpp                |   5 -
 .../org/apache/doris/analysis/SearchDslParser.java | 199 ++++++++-------------
 .../apache/doris/analysis/SearchPredicateTest.java |  29 +++
 .../functions/scalar/SearchDslParserTest.java      | 100 +++++++++--
 4 files changed, 198 insertions(+), 135 deletions(-)

diff --git a/be/src/exec/common/variant_util.cpp 
b/be/src/exec/common/variant_util.cpp
index 5a0a978ece8..dd0c15a0025 100644
--- a/be/src/exec/common/variant_util.cpp
+++ b/be/src/exec/common/variant_util.cpp
@@ -988,11 +988,6 @@ Status VariantCompactionUtil::check_path_stats(const 
std::vector<RowsetSharedPtr
             return Status::OK();
         }
     }
-    for (const auto& column : output->tablet_schema()->columns()) {
-        if (!column->is_variant_type()) {
-            continue;
-        }
-    }
     std::unordered_map<int32_t, PathToNoneNullValues> 
original_uid_to_path_stats;
     for (const auto& rs : intputs) {
         RETURN_IF_ERROR(aggregate_path_to_stats(rs, 
&original_uid_to_path_stats));
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/analysis/SearchDslParser.java 
b/fe/fe-core/src/main/java/org/apache/doris/analysis/SearchDslParser.java
index 832ec5a37f3..d31c52c9155 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/SearchDslParser.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/SearchDslParser.java
@@ -277,6 +277,37 @@ public class SearchDslParser {
         }
     }
 
+    private static String buildFieldPath(SearchParser.FieldPathContext ctx) {
+        if (ctx == null) {
+            throw new RuntimeException("Invalid field query: missing field 
path");
+        }
+
+        StringBuilder fullPath = new StringBuilder();
+        List<SearchParser.FieldSegmentContext> segments = ctx.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);
+        }
+        return fullPath.toString();
+    }
+
+    private static String normalizeNestedFieldPath(String fieldPath, @Nullable 
String nestedPath) {
+        if (nestedPath == null || nestedPath.isEmpty()) {
+            return fieldPath;
+        }
+        if (fieldPath.equals(nestedPath) || fieldPath.startsWith(nestedPath + 
".")) {
+            throw new SearchDslSyntaxException("Fields in NESTED predicates 
must be relative to nested path: "
+                    + nestedPath + ", but got: " + fieldPath);
+        }
+        return nestedPath + "." + fieldPath;
+    }
+
     /**
      * Collect all field names from an AST node recursively.
      * @param node The AST node to collect from
@@ -472,6 +503,7 @@ public class SearchDslParser {
             // Build AST using first field as placeholder for bare queries, 
with default operator
             QsAstBuilder visitor = new QsAstBuilder(fields.get(0), 
defaultOperator);
             QsNode root = visitor.visit(tree);
+            validateNestedTopLevelOnly(root);
 
             // Apply multi-field expansion based on type
             QsNode expandedRoot;
@@ -563,6 +595,7 @@ public class SearchDslParser {
             // Use constructor with override to avoid mutating shared options 
object (thread-safety)
             QsLuceneModeAstBuilder visitor = new 
QsLuceneModeAstBuilder(effectiveOptions, fields.get(0));
             QsNode root = visitor.visit(tree);
+            validateNestedTopLevelOnly(root);
 
             // In ES query_string, both best_fields and cross_fields use 
per-clause expansion
             // (each clause is independently expanded across fields). The 
difference is only
@@ -646,6 +679,8 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure NOT 
query rewriting)
         private final Set<String> fieldNames = new LinkedHashSet<>();
         // Context stack to track current field name during parsing
         private String currentFieldName = null;
+        // Current nested path when visiting NESTED(path, predicates)
+        private String currentNestedPath = null;
         // Default field for bare queries (without field: prefix)
         private final String defaultField;
         // Default operator for implicit conjunction (space-separated terms): 
"AND" or "OR"
@@ -822,6 +857,9 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure NOT 
query rewriting)
 
         @Override
         public QsNode visitBareQuery(SearchParser.BareQueryContext ctx) {
+            if (currentNestedPath != null && (currentFieldName == null || 
currentFieldName.isEmpty())) {
+                throw new SearchDslSyntaxException("Bare queries are not 
supported inside NESTED predicates");
+            }
             // Use currentFieldName if inside a field group context (set by 
visitFieldGroupQuery),
             // otherwise fall back to the configured defaultField.
             String effectiveField = (currentFieldName != null && 
!currentFieldName.isEmpty())
@@ -858,60 +896,29 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure 
NOT query rewriting)
             if (ctx.NESTED_PATH() == null) {
                 throw new RuntimeException("Invalid NESTED clause: missing 
path");
             }
-            String nestedPath = ctx.NESTED_PATH().getText();
-            QsNode innerQuery = visit(ctx.clause());
-            if (innerQuery == null) {
-                throw new RuntimeException("Invalid NESTED clause: missing 
inner query");
+            if (currentNestedPath != null) {
+                throw new SearchDslSyntaxException("Nested NESTED() is not 
supported");
             }
-
-            validateNestedFieldPaths(innerQuery, nestedPath);
-
-            QsNode node = new QsNode(QsClauseType.NESTED, 
Collections.singletonList(innerQuery));
-            node.nestedPath = nestedPath;
-            return node;
-        }
-
-        private void validateNestedFieldPaths(QsNode node, String nestedPath) {
-            if (node == null) {
-                return;
-            }
-            if (node.type == QsClauseType.NESTED) {
-                throw new RuntimeException("Nested NESTED() is not supported: 
" + nestedPath);
-            }
-            if (node.field != null && !node.field.startsWith(nestedPath + 
".")) {
-                throw new RuntimeException("Fields in NESTED query must start 
with nested path: "
-                        + nestedPath + ", but got: " + node.field);
-            }
-            if (node.children != null) {
-                for (QsNode child : node.children) {
-                    validateNestedFieldPaths(child, nestedPath);
+            String nestedPath = ctx.NESTED_PATH().getText();
+            String previousNestedPath = currentNestedPath;
+            currentNestedPath = nestedPath;
+            try {
+                QsNode innerQuery = visit(ctx.clause());
+                if (innerQuery == null) {
+                    throw new RuntimeException("Invalid NESTED clause: missing 
inner query");
                 }
+
+                QsNode node = new QsNode(QsClauseType.NESTED, 
Collections.singletonList(innerQuery));
+                node.nestedPath = nestedPath;
+                return node;
+            } finally {
+                currentNestedPath = previousNestedPath;
             }
         }
 
         @Override
         public QsNode visitFieldQuery(SearchParser.FieldQueryContext ctx) {
-            if (ctx.fieldPath() == null) {
-                throw new RuntimeException("Invalid field 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();
-                // Remove quotes if present
-                if (segment.startsWith("\"") && segment.endsWith("\"")) {
-                    segment = segment.substring(1, segment.length() - 1);
-                }
-                fullPath.append(segment);
-            }
-
-            String fieldPath = fullPath.toString();
+            String fieldPath = 
normalizeNestedFieldPath(buildFieldPath(ctx.fieldPath()), currentNestedPath);
             fieldNames.add(fieldPath);
 
             // Set current field context before visiting search value
@@ -941,21 +948,7 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure 
NOT query rewriting)
                 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();
+            String fieldPath = 
normalizeNestedFieldPath(buildFieldPath(ctx.fieldPath()), currentNestedPath);
             fieldNames.add(fieldPath);
 
             // Set field group context so bare terms inside use this field
@@ -2075,6 +2068,7 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure 
NOT query rewriting)
         private final Set<String> fieldNames = new LinkedHashSet<>();
         private final SearchOptions options;
         private String currentFieldName = null;
+        private String currentNestedPath = null;
         // Override for default field - used in multi-field mode to avoid 
mutating options
         private final String overrideDefaultField;
         private int nestingLevel = 0;
@@ -2301,6 +2295,8 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure 
NOT query rewriting)
                 } finally {
                     nestingLevel--;
                 }
+            } else if (atomCtx.nestedQuery() != null) {
+                node = visit(atomCtx.nestedQuery());
             } else if (atomCtx.fieldGroupQuery() != null) {
                 // Field group query (e.g., title:(rock OR jazz))
                 node = visit(atomCtx.fieldGroupQuery());
@@ -2464,6 +2460,9 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure 
NOT query rewriting)
 
         @Override
         public QsNode visitBareQuery(SearchParser.BareQueryContext ctx) {
+            if (currentNestedPath != null && (currentFieldName == null || 
currentFieldName.isEmpty())) {
+                throw new SearchDslSyntaxException("Bare queries are not 
supported inside NESTED predicates");
+            }
             // Use currentFieldName if inside a field group context (set by 
visitFieldGroupQuery),
             // otherwise fall back to the effective default field.
             String defaultField = getEffectiveDefaultField();
@@ -2501,55 +2500,29 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure 
NOT query rewriting)
             if (ctx.NESTED_PATH() == null) {
                 throw new RuntimeException("Invalid NESTED clause: missing 
path");
             }
-            String nestedPath = ctx.NESTED_PATH().getText();
-            QsNode innerQuery = visit(ctx.clause());
-            if (innerQuery == null) {
-                throw new RuntimeException("Invalid NESTED clause: missing 
inner query");
-            }
-
-            validateNestedFieldPaths(innerQuery, nestedPath);
-
-            QsNode node = new QsNode(QsClauseType.NESTED, 
Collections.singletonList(innerQuery));
-            node.nestedPath = nestedPath;
-            return node;
-        }
-
-        private void validateNestedFieldPaths(QsNode node, String nestedPath) {
-            if (node == null) {
-                return;
-            }
-            if (node.type == QsClauseType.NESTED) {
-                throw new RuntimeException("Nested NESTED() is not supported: 
" + nestedPath);
+            if (currentNestedPath != null) {
+                throw new SearchDslSyntaxException("Nested NESTED() is not 
supported");
             }
-            if (node.field != null && !node.field.startsWith(nestedPath + 
".")) {
-                throw new RuntimeException("Fields in NESTED query must start 
with nested path: "
-                        + nestedPath + ", but got: " + node.field);
-            }
-            if (node.children != null) {
-                for (QsNode child : node.children) {
-                    validateNestedFieldPaths(child, nestedPath);
+            String nestedPath = ctx.NESTED_PATH().getText();
+            String previousNestedPath = currentNestedPath;
+            currentNestedPath = nestedPath;
+            try {
+                QsNode innerQuery = visit(ctx.clause());
+                if (innerQuery == null) {
+                    throw new RuntimeException("Invalid NESTED clause: missing 
inner query");
                 }
+
+                QsNode node = new QsNode(QsClauseType.NESTED, 
Collections.singletonList(innerQuery));
+                node.nestedPath = nestedPath;
+                return node;
+            } finally {
+                currentNestedPath = previousNestedPath;
             }
         }
 
         @Override
         public QsNode visitFieldQuery(SearchParser.FieldQueryContext ctx) {
-            // Build complete field path
-            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();
+            String fieldPath = 
normalizeNestedFieldPath(buildFieldPath(ctx.fieldPath()), currentNestedPath);
             fieldNames.add(fieldPath);
 
             String previousFieldName = currentFieldName;
@@ -2571,21 +2544,7 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure 
NOT query rewriting)
                 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();
+            String fieldPath = 
normalizeNestedFieldPath(buildFieldPath(ctx.fieldPath()), currentNestedPath);
             fieldNames.add(fieldPath);
 
             // Set field group context so bare terms inside use this field
@@ -2724,7 +2683,7 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure 
NOT query rewriting)
             return;
         }
         if (node.type == QsClauseType.NESTED && !isRoot) {
-            throw new RuntimeException("NESTED clause must be evaluated at top 
level");
+            throw new SearchDslSyntaxException("NESTED clause must be 
evaluated at top level");
         }
         if (node.children == null || node.children.isEmpty()) {
             return;
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/analysis/SearchPredicateTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/analysis/SearchPredicateTest.java
index 8a5602c3317..c1e82b894e6 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/analysis/SearchPredicateTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/analysis/SearchPredicateTest.java
@@ -153,6 +153,35 @@ public class SearchPredicateTest {
         Assertions.assertEquals("content", 
param.field_bindings.get(1).field_name);
     }
 
+    @Test
+    public void testNestedRelativeFieldsAreNormalizedBeforeThrift() {
+        String dsl = "NESTED(data.items, msg:hello AND meta.channel:action)";
+        SearchDslParser.QsPlan plan = SearchDslParser.parseDsl(dsl, 
"{\"mode\":\"standard\"}");
+        List<Expr> children = Arrays.asList(createTestSlotRef("data"), 
createTestSlotRef("data"));
+
+        SearchPredicate predicate = new SearchPredicate(dsl, plan, children, 
true);
+
+        TExprNode thriftNode = new TExprNode();
+        predicate.accept(ExprToThriftVisitor.INSTANCE, thriftNode);
+
+        TSearchParam param = thriftNode.search_param;
+        Assertions.assertNotNull(param);
+        Assertions.assertEquals("NESTED", param.root.clause_type);
+        Assertions.assertEquals("data.items", param.root.nested_path);
+        Assertions.assertEquals(1, param.root.children.size());
+        Assertions.assertEquals("AND", param.root.children.get(0).clause_type);
+        Assertions.assertEquals("data.items.msg", 
param.root.children.get(0).children.get(0).field_name);
+        Assertions.assertEquals("data.items.meta.channel", 
param.root.children.get(0).children.get(1).field_name);
+
+        Assertions.assertEquals(2, param.field_bindings.size());
+        Assertions.assertEquals("data.items.msg", 
param.field_bindings.get(0).field_name);
+        Assertions.assertEquals("data", 
param.field_bindings.get(0).parent_field_name);
+        Assertions.assertEquals("items.msg", 
param.field_bindings.get(0).subcolumn_path);
+        Assertions.assertEquals("data.items.meta.channel", 
param.field_bindings.get(1).field_name);
+        Assertions.assertEquals("data", 
param.field_bindings.get(1).parent_field_name);
+        Assertions.assertEquals("items.meta.channel", 
param.field_bindings.get(1).subcolumn_path);
+    }
+
     @Test
     public void testClone() {
         String dsl = "title:hello";
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 c5f228cf118..6dc16a1da7a 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
@@ -2531,7 +2531,7 @@ public class SearchDslParserTest {
 
     @Test
     public void testNestedQuerySimple() {
-        String dsl = "NESTED(data, data.msg:hello)";
+        String dsl = "NESTED(data, msg:hello)";
         QsPlan plan = SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
 
         Assertions.assertNotNull(plan);
@@ -2545,7 +2545,7 @@ public class SearchDslParserTest {
 
     @Test
     public void testNestedQueryAnd() {
-        String dsl = "NESTED(data, data.msg:hello AND data.title:news)";
+        String dsl = "NESTED(data, msg:hello AND title:news)";
         QsPlan plan = SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
 
         Assertions.assertNotNull(plan);
@@ -2558,29 +2558,109 @@ public class SearchDslParserTest {
     }
 
     @Test
-    public void testNestedQueryFieldValidation() {
-        String dsl = "NESTED(data, other.msg:hello)";
+    public void testNestedQueryAbsolutePathRejected() {
+        String dsl = "NESTED(data, data.msg:hello)";
         RuntimeException exception = 
Assertions.assertThrows(RuntimeException.class, () -> {
             SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
         });
-        Assertions.assertTrue(exception.getMessage().contains("Fields in 
NESTED query must start with nested path"));
+        Assertions.assertTrue(exception.getMessage().contains("Fields in 
NESTED predicates must be relative"));
     }
 
     @Test
     public void testNestedQueryPathWithDot() {
-        String dsl = "NESTED(data.items, data.items.msg:hello)";
+        String dsl = "NESTED(data.items, meta.channel:action)";
         QsPlan plan = SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
 
         Assertions.assertNotNull(plan);
         Assertions.assertEquals(QsClauseType.NESTED, plan.getRoot().getType());
         Assertions.assertEquals("data.items", plan.getRoot().getNestedPath());
         Assertions.assertTrue(plan.getFieldBindings().stream()
-                .anyMatch(b -> "data.items.msg".equals(b.getFieldName())));
+                .anyMatch(b -> 
"data.items.meta.channel".equals(b.getFieldName())));
+    }
+
+    @Test
+    public void testNestedQuerySimpleLuceneMode() {
+        String dsl = "NESTED(data, msg:hello)";
+        QsPlan plan = SearchDslParser.parseDsl(dsl,
+                
"{\"mode\":\"lucene\",\"default_operator\":\"AND\",\"minimum_should_match\":0}");
+
+        Assertions.assertNotNull(plan);
+        Assertions.assertEquals(QsClauseType.NESTED, plan.getRoot().getType());
+        Assertions.assertEquals("data", plan.getRoot().getNestedPath());
+        Assertions.assertEquals(1, plan.getRoot().getChildren().size());
+        Assertions.assertEquals(QsClauseType.TERM, 
plan.getRoot().getChildren().get(0).getType());
+        Assertions.assertEquals("data.msg", 
plan.getRoot().getChildren().get(0).getField());
+        Assertions.assertTrue(plan.getFieldBindings().stream().anyMatch(b -> 
"data.msg".equals(b.getFieldName())));
+    }
+
+    @Test
+    public void testNestedQueryAndLuceneMode() {
+        String dsl = "NESTED(data, msg:hello AND title:news)";
+        QsPlan plan = SearchDslParser.parseDsl(dsl,
+                
"{\"mode\":\"lucene\",\"default_operator\":\"AND\",\"minimum_should_match\":0}");
+
+        Assertions.assertNotNull(plan);
+        Assertions.assertEquals(QsClauseType.NESTED, plan.getRoot().getType());
+        Assertions.assertEquals("data", plan.getRoot().getNestedPath());
+        Assertions.assertEquals(1, plan.getRoot().getChildren().size());
+        Assertions.assertEquals(QsClauseType.OCCUR_BOOLEAN, 
plan.getRoot().getChildren().get(0).getType());
+        Assertions.assertTrue(plan.getFieldBindings().stream().anyMatch(b -> 
"data.msg".equals(b.getFieldName())));
+        Assertions.assertTrue(plan.getFieldBindings().stream().anyMatch(b -> 
"data.title".equals(b.getFieldName())));
+    }
+
+    @Test
+    public void testNestedQueryDescendantFieldLuceneMode() {
+        String dsl = "NESTED(data.items, input.display_text:selforigin)";
+        QsPlan plan = SearchDslParser.parseDsl(dsl,
+                
"{\"mode\":\"lucene\",\"default_operator\":\"AND\",\"minimum_should_match\":0}");
+
+        Assertions.assertNotNull(plan);
+        Assertions.assertEquals(QsClauseType.NESTED, plan.getRoot().getType());
+        Assertions.assertEquals("data.items", plan.getRoot().getNestedPath());
+        Assertions.assertTrue(plan.getFieldBindings().stream()
+                .anyMatch(b -> 
"data.items.input.display_text".equals(b.getFieldName())));
+    }
+
+    @Test
+    public void testNestedQueryMustBeTopLevelInAndLuceneMode() {
+        String dsl = "title:hello AND NESTED(data, msg:hello)";
+        RuntimeException exception = 
Assertions.assertThrows(RuntimeException.class, () -> {
+            SearchDslParser.parseDsl(dsl,
+                    
"{\"mode\":\"lucene\",\"default_operator\":\"AND\",\"minimum_should_match\":0}");
+        });
+        Assertions.assertTrue(exception.getMessage().contains("NESTED clause 
must be evaluated at top level"));
+    }
+
+    @Test
+    public void testNestedQueryMixedRelativeAndAbsoluteRejected() {
+        String dsl = "NESTED(data.items, msg:hello AND data.items.title:news)";
+        RuntimeException exception = 
Assertions.assertThrows(RuntimeException.class, () -> {
+            SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
+        });
+        Assertions.assertTrue(exception.getMessage().contains("Fields in 
NESTED predicates must be relative"));
+    }
+
+    @Test
+    public void testNestedQueryBareQueryRejected() {
+        String dsl = "NESTED(data.items, hello)";
+        RuntimeException exception = 
Assertions.assertThrows(RuntimeException.class, () -> {
+            SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
+        });
+        Assertions.assertTrue(exception.getMessage().contains("Bare queries 
are not supported inside NESTED predicates"));
+    }
+
+    @Test
+    public void testNestedQueryNestedNestedRejected() {
+        String dsl = "NESTED(data, NESTED(data.items, msg:hello))";
+        RuntimeException exception = 
Assertions.assertThrows(RuntimeException.class, () -> {
+            SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
+        });
+        Assertions.assertTrue(exception.getMessage().contains("Nested NESTED() 
is not supported"));
     }
 
     @Test
     public void testNestedQueryMustBeTopLevelInAnd() {
-        String dsl = "title:hello AND NESTED(data, data.msg:hello)";
+        String dsl = "title:hello AND NESTED(data, msg:hello)";
         RuntimeException exception = 
Assertions.assertThrows(RuntimeException.class, () -> {
             SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
         });
@@ -2589,7 +2669,7 @@ public class SearchDslParserTest {
 
     @Test
     public void testNestedQueryMustBeTopLevelInOr() {
-        String dsl = "NESTED(data, data.msg:hello) OR title:hello";
+        String dsl = "NESTED(data, msg:hello) OR title:hello";
         RuntimeException exception = 
Assertions.assertThrows(RuntimeException.class, () -> {
             SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
         });
@@ -2598,7 +2678,7 @@ public class SearchDslParserTest {
 
     @Test
     public void testNestedQueryMustBeTopLevelInNot() {
-        String dsl = "NOT NESTED(data, data.msg:hello)";
+        String dsl = "NOT NESTED(data, msg:hello)";
         RuntimeException exception = 
Assertions.assertThrows(RuntimeException.class, () -> {
             SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
         });


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

Reply via email to