Browse Source

[ES|QL] Implement new syntax for RERANK and COMPLETION (#129716)

Aurélien FOUCRET 2 months ago
parent
commit
d56a4762ed
24 changed files with 583 additions and 678 deletions
  1. 22 6
      docs/reference/query-languages/esql/_snippets/commands/layout/completion.md
  2. 9 1
      x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/MapExpression.java
  3. 5 5
      x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestRerankTestCase.java
  4. 4 4
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/completion.csv-spec
  5. 3 3
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/fork.csv-spec
  6. 8 8
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/rerank.csv-spec
  7. 16 24
      x-pack/plugin/esql/src/main/antlr/EsqlBaseParser.g4
  8. 1 1
      x-pack/plugin/esql/src/main/antlr/parser/Expression.g4
  9. 1 1
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java
  10. 3 5
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.interp
  11. 175 176
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.java
  12. 20 44
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseListener.java
  13. 11 25
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseVisitor.java
  14. 26 46
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserListener.java
  15. 14 26
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserVisitor.java
  16. 14 13
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java
  17. 61 91
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java
  18. 9 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/inference/Completion.java
  19. 8 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/inference/InferencePlan.java
  20. 13 36
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/inference/Rerank.java
  21. 40 63
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java
  22. 2 2
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java
  23. 7 4
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java
  24. 111 94
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java

+ 22 - 6
docs/reference/query-languages/esql/_snippets/commands/layout/completion.md

@@ -9,10 +9,26 @@ The `COMPLETION` command allows you to send prompts and context to a Large Langu
 
 **Syntax**
 
+::::{tab-set}
+
+:::{tab-item} 9.2.0+
+
 ```esql
-COMPLETION [column =] prompt WITH inference_id
+COMPLETION [column =] prompt WITH { "inference_id" : "my_inference_endpoint" }
 ```
 
+:::
+
+:::{tab-item} 9.1.x only
+
+```esql
+COMPLETION [column =] prompt WITH my_inference_endpoint
+```
+
+:::
+
+::::
+
 **Parameters**
 
 `column`
@@ -24,7 +40,7 @@ COMPLETION [column =] prompt WITH inference_id
 :   The input text or expression used to prompt the LLM.
     This can be a string literal or a reference to a column containing text.
 
-`inference_id`
+`my_inference_endpoint`
 :   The ID of the [inference endpoint](docs-content://explore-analyze/elastic-inference/inference-api.md) to use for the task.
     The inference endpoint must be configured with the `completion` task type.
 
@@ -75,7 +91,7 @@ How you increase the timeout depends on your deployment type:
 If you don't want to increase the timeout limit, try the following:
 
 * Reduce data volume with `LIMIT` or more selective filters before the `COMPLETION` command
-* Split complex operations into multiple simpler queries 
+* Split complex operations into multiple simpler queries
 * Configure your HTTP client's response timeout (Refer to [HTTP client configuration](/reference/elasticsearch/configuration-reference/networking-settings.md#_http_client_configuration))
 
 
@@ -85,7 +101,7 @@ Use the default column name (results stored in `completion` column):
 
 ```esql
 ROW question = "What is Elasticsearch?"
-| COMPLETION question WITH test_completion_model
+| COMPLETION question WITH { "inference_id" : "my_inference_endpoint" }
 | KEEP question, completion
 ```
 
@@ -97,7 +113,7 @@ Specify the output column (results stored in `answer` column):
 
 ```esql
 ROW question = "What is Elasticsearch?"
-| COMPLETION answer = question WITH test_completion_model
+| COMPLETION answer = question WITH { "inference_id" : "my_inference_endpoint" }
 | KEEP question, answer
 ```
 
@@ -117,7 +133,7 @@ FROM movies
    "Synopsis: ", synopsis, "\n",
    "Actors: ", MV_CONCAT(actors, ", "), "\n",
   )
-| COMPLETION summary = prompt WITH test_completion_model
+| COMPLETION summary = prompt WITH { "inference_id" : "my_inference_endpoint" }
 | KEEP title, summary, rating
 ```
 

+ 9 - 1
x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/MapExpression.java

@@ -120,10 +120,18 @@ public class MapExpression extends Expression {
             return map.get(key);
         } else {
             // the key(literal) could be converted to BytesRef by ConvertStringToByteRef
-            return keyFoldedMap.containsKey(key) ? keyFoldedMap.get(key) : keyFoldedMap.get(new BytesRef(key.toString()));
+            return keyFoldedMap.containsKey(key) ? keyFoldedMap.get(key) : keyFoldedMap.get(foldKey(key));
         }
     }
 
+    public boolean containsKey(Object key) {
+        return keyFoldedMap.containsKey(key) || keyFoldedMap.containsKey(foldKey(key));
+    }
+
+    private BytesRef foldKey(Object key) {
+        return new BytesRef(key.toString());
+    }
+
     @Override
     public boolean equals(Object obj) {
         if (this == obj) {

+ 5 - 5
x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestRerankTestCase.java

@@ -93,7 +93,7 @@ public class RestRerankTestCase extends ESRestTestCase {
         String query = """
             FROM rerank-test-index
             | WHERE match(title, "exploration")
-            | RERANK "exploration" ON title WITH test_reranker
+            | RERANK "exploration" ON title WITH { "inference_id" : "test_reranker" }
             | EVAL _score = ROUND(_score, 5)
             """;
 
@@ -112,7 +112,7 @@ public class RestRerankTestCase extends ESRestTestCase {
         String query = """
             FROM rerank-test-index
             | WHERE match(title, "exploration")
-            | RERANK "exploration" ON title, author WITH test_reranker
+            | RERANK "exploration" ON title, author WITH { "inference_id" : "test_reranker" }
             | EVAL _score = ROUND(_score, 5)
             """;
 
@@ -131,7 +131,7 @@ public class RestRerankTestCase extends ESRestTestCase {
         String query = """
             FROM rerank-test-index
             | WHERE match(title, "exploration")
-            | RERANK ? ON title WITH ?
+            | RERANK ? ON title WITH { "inference_id" : ? }
             | EVAL _score = ROUND(_score, 5)
             """;
 
@@ -150,7 +150,7 @@ public class RestRerankTestCase extends ESRestTestCase {
         String query = """
             FROM rerank-test-index
             | WHERE match(title, ?queryText)
-            | RERANK ?queryText ON title WITH ?inferenceId
+            | RERANK ?queryText ON title WITH { "inference_id" : ?inferenceId }
             | EVAL _score = ROUND(_score, 5)
             """;
 
@@ -169,7 +169,7 @@ public class RestRerankTestCase extends ESRestTestCase {
         String query = """
             FROM rerank-test-index
             | WHERE match(title, "exploration")
-            | RERANK "exploration" ON title WITH test_missing
+            | RERANK "exploration" ON title WITH { "inference_id" : "test_missing" }
             | EVAL _score = ROUND(_score, 5)
             """;
 

+ 4 - 4
x-pack/plugin/esql/qa/testFixtures/src/main/resources/completion.csv-spec

@@ -6,7 +6,7 @@ completion using a ROW source operator
 required_capability: completion
 
 ROW prompt="Who is Victor Hugo?"
-| COMPLETION completion_output = prompt WITH test_completion
+| COMPLETION completion_output = prompt WITH { "inference_id" : "test_completion" }
 ;
 
 prompt:keyword      | completion_output:keyword
@@ -18,7 +18,7 @@ completion using a ROW source operator and prompt is a multi-valued field
 required_capability: completion
 
 ROW prompt=["Answer the following question:", "Who is Victor Hugo?"]
-| COMPLETION completion_output = prompt WITH test_completion
+| COMPLETION completion_output = prompt WITH { "inference_id" : "test_completion" }
 ;
 
 prompt:keyword                                        | completion_output:keyword
@@ -34,7 +34,7 @@ FROM books METADATA _score
 | WHERE title:"war and peace" AND author:"Tolstoy"
 | SORT _score DESC
 | LIMIT 2
-| COMPLETION title WITH test_completion
+| COMPLETION title WITH { "inference_id" : "test_completion" }
 | KEEP title, completion
 ;
 
@@ -51,7 +51,7 @@ FROM books METADATA _score
 | WHERE title:"war and peace" AND author:"Tolstoy"
 | SORT _score DESC
 | LIMIT 2
-| COMPLETION CONCAT("This is a prompt: ", title) WITH test_completion
+| COMPLETION CONCAT("This is a prompt: ", title) WITH { "inference_id" : "test_completion" }
 | KEEP title, completion
 ;
 

+ 3 - 3
x-pack/plugin/esql/qa/testFixtures/src/main/resources/fork.csv-spec

@@ -809,7 +809,7 @@ FROM employees
 | KEEP emp_no, first_name, last_name
 | FORK (WHERE emp_no == 10048 OR emp_no == 10081)
        (WHERE emp_no == 10081 OR emp_no == 10087)
-| COMPLETION x = CONCAT(first_name, " ", last_name) WITH test_completion
+| COMPLETION x=CONCAT(first_name, " ", last_name) WITH { "inference_id" : "test_completion" }
 | SORT _fork, emp_no
 ;
 
@@ -827,7 +827,7 @@ required_capability: completion
 FROM employees
 | KEEP emp_no, first_name, last_name
 | FORK (WHERE emp_no == 10048 OR emp_no == 10081
-       | COMPLETION x = CONCAT(first_name, " ", last_name) WITH test_completion)
+       | COMPLETION x=CONCAT(first_name, " ", last_name) WITH { "inference_id" : "test_completion" })
        (WHERE emp_no == 10081 OR emp_no == 10087)
 | SORT _fork, emp_no
 ;
@@ -845,7 +845,7 @@ required_capability: completion
 
 FROM employees
 | KEEP emp_no, first_name, last_name
-| COMPLETION x = CONCAT(first_name, " ", last_name) WITH test_completion
+| COMPLETION x=CONCAT(first_name, " ", last_name) WITH { "inference_id" : "test_completion" }
 | FORK (WHERE emp_no == 10048 OR emp_no == 10081)
        (WHERE emp_no == 10081 OR emp_no == 10087)
 | SORT _fork, emp_no

+ 8 - 8
x-pack/plugin/esql/qa/testFixtures/src/main/resources/rerank.csv-spec

@@ -10,7 +10,7 @@ required_capability: match_operator_colon
 FROM books METADATA _score
 | WHERE title:"war and peace" AND author:"Tolstoy"
 | SORT _score DESC, book_no ASC
-| RERANK "war and peace" ON title WITH inferenceId=test_reranker
+| RERANK "war and peace" ON title WITH { "inference_id" : "test_reranker" }
 | EVAL _score=ROUND(_score, 2)
 | KEEP book_no, title, author, _score
 ;
@@ -29,7 +29,7 @@ required_capability: match_operator_colon
 FROM books METADATA _score
 | WHERE title:"war and peace" AND author:"Tolstoy"
 | SORT _score DESC, book_no ASC
-| RERANK "war and peace" ON title WITH inferenceId=test_reranker, scoreColumn=rerank_score
+| RERANK rerank_score="war and peace" ON title WITH { "inference_id" : "test_reranker" }
 | EVAL _score=ROUND(_score, 2), rerank_score=ROUND(rerank_score, 2)
 | KEEP book_no, title, author, rerank_score
 ;
@@ -48,7 +48,7 @@ required_capability: match_operator_colon
 FROM books METADATA _score
 | WHERE title:"war and peace" AND author:"Tolstoy"
 | SORT _score DESC
-| RERANK "war and peace" ON title WITH inferenceId=test_reranker, scoreColumn=rerank_score
+| RERANK rerank_score="war and peace" ON title WITH { "inference_id" : "test_reranker" }
 | EVAL _score=ROUND(_score, 2), rerank_score=ROUND(rerank_score, 2)
 | SORT rerank_score, _score ASC, book_no ASC
 | KEEP book_no, title, author, rerank_score
@@ -68,7 +68,7 @@ required_capability: match_operator_colon
 
 FROM books METADATA _score
 | WHERE title:"war and peace" AND author:"Tolstoy"
-| RERANK "war and peace" ON title, author WITH inferenceId=test_reranker
+| RERANK "war and peace" ON title, author WITH { "inference_id" : "test_reranker" }
 | EVAL _score=ROUND(_score, 2)
 | SORT _score DESC, book_no ASC
 | KEEP book_no, title, author, _score
@@ -90,7 +90,7 @@ FROM books METADATA _score
 | WHERE title:"war and peace" AND author:"Tolstoy"
 | SORT _score DESC, book_no ASC
 | LIMIT 3
-| RERANK "war and peace" ON title WITH inferenceId=test_reranker
+| RERANK "war and peace" ON title WITH { "inference_id" : "test_reranker" }
 | EVAL _score=ROUND(_score, 2)
 | SORT _score DESC, book_no ASC
 | KEEP book_no, title, author, _score
@@ -109,7 +109,7 @@ required_capability: match_operator_colon
 
 FROM books METADATA _score
 | WHERE title:"war and peace" AND author:"Tolstoy"
-| RERANK "war and peace" ON title WITH inferenceId=test_reranker
+| RERANK "war and peace" ON title WITH { "inference_id" : "test_reranker" }
 | EVAL _score=ROUND(_score, 2)
 | SORT _score DESC, book_no ASC
 | KEEP book_no, title, author, _score
@@ -129,7 +129,7 @@ required_capability: match_operator_colon
 
 FROM books
 | WHERE title:"war and peace" AND author:"Tolstoy"
-| RERANK "war and peace" ON title WITH inferenceId=test_reranker
+| RERANK "war and peace" ON title WITH { "inference_id" : "test_reranker" }
 | EVAL _score=ROUND(_score, 2)
 | KEEP book_no, title, author, _score
 | SORT author, title 
@@ -153,7 +153,7 @@ FROM books METADATA _id, _index, _score
 | FORK ( WHERE title:"Tolkien" | SORT _score, _id DESC | LIMIT 3 )
        ( WHERE author:"Tolkien" | SORT _score, _id DESC | LIMIT 3 )
 | FUSE
-| RERANK "Tolkien" ON title WITH inferenceId=test_reranker
+| RERANK "Tolkien" ON title WITH { "inference_id" : "test_reranker" }
 | EVAL _score=ROUND(_score, 2)
 | SORT _score DESC, book_no ASC
 | LIMIT 2

+ 16 - 24
x-pack/plugin/esql/src/main/antlr/EsqlBaseParser.g4

@@ -219,23 +219,28 @@ renameClause:
     ;
 
 dissectCommand
-    : DISSECT primaryExpression string commandOptions?
+    : DISSECT primaryExpression string dissectCommandOptions?
     ;
 
-grokCommand
-    : GROK primaryExpression string
+dissectCommandOptions
+    : dissectCommandOption (COMMA dissectCommandOption)*
     ;
 
-mvExpandCommand
-    : MV_EXPAND qualifiedName
+dissectCommandOption
+    : identifier ASSIGN constant
     ;
 
-commandOptions
-    : commandOption (COMMA commandOption)*
+
+commandNamedParameters
+    : (WITH mapExpression)?
     ;
 
-commandOption
-    : identifier ASSIGN constant
+grokCommand
+    : GROK primaryExpression string
+    ;
+
+mvExpandCommand
+    : MV_EXPAND qualifiedName
     ;
 
 explainCommand
@@ -293,7 +298,7 @@ forkSubQueryProcessingCommand
     ;
 
 completionCommand
-    : COMPLETION (targetField=qualifiedName ASSIGN)? prompt=primaryExpression WITH inferenceId=identifierOrParameter
+    : COMPLETION (targetField=qualifiedName ASSIGN)? prompt=primaryExpression commandNamedParameters
     ;
 
 //
@@ -315,19 +320,6 @@ fuseCommand
     : DEV_FUSE
     ;
 
-inferenceCommandOptions
-    : inferenceCommandOption (COMMA inferenceCommandOption)*
-    ;
-
-inferenceCommandOption
-    : identifier ASSIGN inferenceCommandOptionValue
-    ;
-
-inferenceCommandOptionValue
-    : constant
-    | identifier
-    ;
-
 rerankCommand
-    : DEV_RERANK queryText=constant ON rerankFields (WITH inferenceCommandOptions)?
+    : DEV_RERANK (targetField=qualifiedName ASSIGN)? queryText=constant ON rerankFields commandNamedParameters
     ;

+ 1 - 1
x-pack/plugin/esql/src/main/antlr/parser/Expression.g4

@@ -57,7 +57,7 @@ functionName
     ;
 
 mapExpression
-    : LEFT_BRACES entryExpression (COMMA entryExpression)* RIGHT_BRACES
+    : LEFT_BRACES (entryExpression (COMMA entryExpression)*)? RIGHT_BRACES
     ;
 
 entryExpression

+ 1 - 1
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java

@@ -1405,7 +1405,7 @@ public class Analyzer extends ParameterizedRuleExecutor<LogicalPlan, AnalyzerCon
     private static class ImplicitCasting extends ParameterizedRule<LogicalPlan, LogicalPlan, AnalyzerContext> {
         @Override
         public LogicalPlan apply(LogicalPlan plan, AnalyzerContext context) {
-            // do implicit casting for function arguments
+            // do implicit casting for named parameters
             return plan.transformExpressionsUp(
                 org.elasticsearch.xpack.esql.core.expression.function.Function.class,
                 e -> ImplicitCasting.cast(e, context.functionRegistry().snapshotRegistry())

File diff suppressed because it is too large
+ 3 - 5
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.interp


File diff suppressed because it is too large
+ 175 - 176
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParser.java


+ 20 - 44
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseListener.java

@@ -541,49 +541,61 @@ public class EsqlBaseParserBaseListener implements EsqlBaseParserListener {
    *
    * <p>The default implementation does nothing.</p>
    */
-  @Override public void enterGrokCommand(EsqlBaseParser.GrokCommandContext ctx) { }
+  @Override public void enterDissectCommandOptions(EsqlBaseParser.DissectCommandOptionsContext ctx) { }
   /**
    * {@inheritDoc}
    *
    * <p>The default implementation does nothing.</p>
    */
-  @Override public void exitGrokCommand(EsqlBaseParser.GrokCommandContext ctx) { }
+  @Override public void exitDissectCommandOptions(EsqlBaseParser.DissectCommandOptionsContext ctx) { }
   /**
    * {@inheritDoc}
    *
    * <p>The default implementation does nothing.</p>
    */
-  @Override public void enterMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx) { }
+  @Override public void enterDissectCommandOption(EsqlBaseParser.DissectCommandOptionContext ctx) { }
   /**
    * {@inheritDoc}
    *
    * <p>The default implementation does nothing.</p>
    */
-  @Override public void exitMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx) { }
+  @Override public void exitDissectCommandOption(EsqlBaseParser.DissectCommandOptionContext ctx) { }
+  /**
+   * {@inheritDoc}
+   *
+   * <p>The default implementation does nothing.</p>
+   */
+  @Override public void enterCommandNamedParameters(EsqlBaseParser.CommandNamedParametersContext ctx) { }
+  /**
+   * {@inheritDoc}
+   *
+   * <p>The default implementation does nothing.</p>
+   */
+  @Override public void exitCommandNamedParameters(EsqlBaseParser.CommandNamedParametersContext ctx) { }
   /**
    * {@inheritDoc}
    *
    * <p>The default implementation does nothing.</p>
    */
-  @Override public void enterCommandOptions(EsqlBaseParser.CommandOptionsContext ctx) { }
+  @Override public void enterGrokCommand(EsqlBaseParser.GrokCommandContext ctx) { }
   /**
    * {@inheritDoc}
    *
    * <p>The default implementation does nothing.</p>
    */
-  @Override public void exitCommandOptions(EsqlBaseParser.CommandOptionsContext ctx) { }
+  @Override public void exitGrokCommand(EsqlBaseParser.GrokCommandContext ctx) { }
   /**
    * {@inheritDoc}
    *
    * <p>The default implementation does nothing.</p>
    */
-  @Override public void enterCommandOption(EsqlBaseParser.CommandOptionContext ctx) { }
+  @Override public void enterMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx) { }
   /**
    * {@inheritDoc}
    *
    * <p>The default implementation does nothing.</p>
    */
-  @Override public void exitCommandOption(EsqlBaseParser.CommandOptionContext ctx) { }
+  @Override public void exitMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx) { }
   /**
    * {@inheritDoc}
    *
@@ -812,42 +824,6 @@ public class EsqlBaseParserBaseListener implements EsqlBaseParserListener {
    * <p>The default implementation does nothing.</p>
    */
   @Override public void exitFuseCommand(EsqlBaseParser.FuseCommandContext ctx) { }
-  /**
-   * {@inheritDoc}
-   *
-   * <p>The default implementation does nothing.</p>
-   */
-  @Override public void enterInferenceCommandOptions(EsqlBaseParser.InferenceCommandOptionsContext ctx) { }
-  /**
-   * {@inheritDoc}
-   *
-   * <p>The default implementation does nothing.</p>
-   */
-  @Override public void exitInferenceCommandOptions(EsqlBaseParser.InferenceCommandOptionsContext ctx) { }
-  /**
-   * {@inheritDoc}
-   *
-   * <p>The default implementation does nothing.</p>
-   */
-  @Override public void enterInferenceCommandOption(EsqlBaseParser.InferenceCommandOptionContext ctx) { }
-  /**
-   * {@inheritDoc}
-   *
-   * <p>The default implementation does nothing.</p>
-   */
-  @Override public void exitInferenceCommandOption(EsqlBaseParser.InferenceCommandOptionContext ctx) { }
-  /**
-   * {@inheritDoc}
-   *
-   * <p>The default implementation does nothing.</p>
-   */
-  @Override public void enterInferenceCommandOptionValue(EsqlBaseParser.InferenceCommandOptionValueContext ctx) { }
-  /**
-   * {@inheritDoc}
-   *
-   * <p>The default implementation does nothing.</p>
-   */
-  @Override public void exitInferenceCommandOptionValue(EsqlBaseParser.InferenceCommandOptionValueContext ctx) { }
   /**
    * {@inheritDoc}
    *

+ 11 - 25
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserBaseVisitor.java

@@ -327,28 +327,35 @@ public class EsqlBaseParserBaseVisitor<T> extends AbstractParseTreeVisitor<T> im
    * <p>The default implementation returns the result of calling
    * {@link #visitChildren} on {@code ctx}.</p>
    */
-  @Override public T visitGrokCommand(EsqlBaseParser.GrokCommandContext ctx) { return visitChildren(ctx); }
+  @Override public T visitDissectCommandOptions(EsqlBaseParser.DissectCommandOptionsContext ctx) { return visitChildren(ctx); }
   /**
    * {@inheritDoc}
    *
    * <p>The default implementation returns the result of calling
    * {@link #visitChildren} on {@code ctx}.</p>
    */
-  @Override public T visitMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx) { return visitChildren(ctx); }
+  @Override public T visitDissectCommandOption(EsqlBaseParser.DissectCommandOptionContext ctx) { return visitChildren(ctx); }
+  /**
+   * {@inheritDoc}
+   *
+   * <p>The default implementation returns the result of calling
+   * {@link #visitChildren} on {@code ctx}.</p>
+   */
+  @Override public T visitCommandNamedParameters(EsqlBaseParser.CommandNamedParametersContext ctx) { return visitChildren(ctx); }
   /**
    * {@inheritDoc}
    *
    * <p>The default implementation returns the result of calling
    * {@link #visitChildren} on {@code ctx}.</p>
    */
-  @Override public T visitCommandOptions(EsqlBaseParser.CommandOptionsContext ctx) { return visitChildren(ctx); }
+  @Override public T visitGrokCommand(EsqlBaseParser.GrokCommandContext ctx) { return visitChildren(ctx); }
   /**
    * {@inheritDoc}
    *
    * <p>The default implementation returns the result of calling
    * {@link #visitChildren} on {@code ctx}.</p>
    */
-  @Override public T visitCommandOption(EsqlBaseParser.CommandOptionContext ctx) { return visitChildren(ctx); }
+  @Override public T visitMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx) { return visitChildren(ctx); }
   /**
    * {@inheritDoc}
    *
@@ -482,27 +489,6 @@ public class EsqlBaseParserBaseVisitor<T> extends AbstractParseTreeVisitor<T> im
    * {@link #visitChildren} on {@code ctx}.</p>
    */
   @Override public T visitFuseCommand(EsqlBaseParser.FuseCommandContext ctx) { return visitChildren(ctx); }
-  /**
-   * {@inheritDoc}
-   *
-   * <p>The default implementation returns the result of calling
-   * {@link #visitChildren} on {@code ctx}.</p>
-   */
-  @Override public T visitInferenceCommandOptions(EsqlBaseParser.InferenceCommandOptionsContext ctx) { return visitChildren(ctx); }
-  /**
-   * {@inheritDoc}
-   *
-   * <p>The default implementation returns the result of calling
-   * {@link #visitChildren} on {@code ctx}.</p>
-   */
-  @Override public T visitInferenceCommandOption(EsqlBaseParser.InferenceCommandOptionContext ctx) { return visitChildren(ctx); }
-  /**
-   * {@inheritDoc}
-   *
-   * <p>The default implementation returns the result of calling
-   * {@link #visitChildren} on {@code ctx}.</p>
-   */
-  @Override public T visitInferenceCommandOptionValue(EsqlBaseParser.InferenceCommandOptionValueContext ctx) { return visitChildren(ctx); }
   /**
    * {@inheritDoc}
    *

+ 26 - 46
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserListener.java

@@ -460,45 +460,55 @@ public interface EsqlBaseParserListener extends ParseTreeListener {
    */
   void exitDissectCommand(EsqlBaseParser.DissectCommandContext ctx);
   /**
-   * Enter a parse tree produced by {@link EsqlBaseParser#grokCommand}.
+   * Enter a parse tree produced by {@link EsqlBaseParser#dissectCommandOptions}.
    * @param ctx the parse tree
    */
-  void enterGrokCommand(EsqlBaseParser.GrokCommandContext ctx);
+  void enterDissectCommandOptions(EsqlBaseParser.DissectCommandOptionsContext ctx);
   /**
-   * Exit a parse tree produced by {@link EsqlBaseParser#grokCommand}.
+   * Exit a parse tree produced by {@link EsqlBaseParser#dissectCommandOptions}.
    * @param ctx the parse tree
    */
-  void exitGrokCommand(EsqlBaseParser.GrokCommandContext ctx);
+  void exitDissectCommandOptions(EsqlBaseParser.DissectCommandOptionsContext ctx);
   /**
-   * Enter a parse tree produced by {@link EsqlBaseParser#mvExpandCommand}.
+   * Enter a parse tree produced by {@link EsqlBaseParser#dissectCommandOption}.
    * @param ctx the parse tree
    */
-  void enterMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx);
+  void enterDissectCommandOption(EsqlBaseParser.DissectCommandOptionContext ctx);
   /**
-   * Exit a parse tree produced by {@link EsqlBaseParser#mvExpandCommand}.
+   * Exit a parse tree produced by {@link EsqlBaseParser#dissectCommandOption}.
    * @param ctx the parse tree
    */
-  void exitMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx);
+  void exitDissectCommandOption(EsqlBaseParser.DissectCommandOptionContext ctx);
+  /**
+   * Enter a parse tree produced by {@link EsqlBaseParser#commandNamedParameters}.
+   * @param ctx the parse tree
+   */
+  void enterCommandNamedParameters(EsqlBaseParser.CommandNamedParametersContext ctx);
   /**
-   * Enter a parse tree produced by {@link EsqlBaseParser#commandOptions}.
+   * Exit a parse tree produced by {@link EsqlBaseParser#commandNamedParameters}.
+   * @param ctx the parse tree
+   */
+  void exitCommandNamedParameters(EsqlBaseParser.CommandNamedParametersContext ctx);
+  /**
+   * Enter a parse tree produced by {@link EsqlBaseParser#grokCommand}.
    * @param ctx the parse tree
    */
-  void enterCommandOptions(EsqlBaseParser.CommandOptionsContext ctx);
+  void enterGrokCommand(EsqlBaseParser.GrokCommandContext ctx);
   /**
-   * Exit a parse tree produced by {@link EsqlBaseParser#commandOptions}.
+   * Exit a parse tree produced by {@link EsqlBaseParser#grokCommand}.
    * @param ctx the parse tree
    */
-  void exitCommandOptions(EsqlBaseParser.CommandOptionsContext ctx);
+  void exitGrokCommand(EsqlBaseParser.GrokCommandContext ctx);
   /**
-   * Enter a parse tree produced by {@link EsqlBaseParser#commandOption}.
+   * Enter a parse tree produced by {@link EsqlBaseParser#mvExpandCommand}.
    * @param ctx the parse tree
    */
-  void enterCommandOption(EsqlBaseParser.CommandOptionContext ctx);
+  void enterMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx);
   /**
-   * Exit a parse tree produced by {@link EsqlBaseParser#commandOption}.
+   * Exit a parse tree produced by {@link EsqlBaseParser#mvExpandCommand}.
    * @param ctx the parse tree
    */
-  void exitCommandOption(EsqlBaseParser.CommandOptionContext ctx);
+  void exitMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx);
   /**
    * Enter a parse tree produced by {@link EsqlBaseParser#explainCommand}.
    * @param ctx the parse tree
@@ -695,36 +705,6 @@ public interface EsqlBaseParserListener extends ParseTreeListener {
    * @param ctx the parse tree
    */
   void exitFuseCommand(EsqlBaseParser.FuseCommandContext ctx);
-  /**
-   * Enter a parse tree produced by {@link EsqlBaseParser#inferenceCommandOptions}.
-   * @param ctx the parse tree
-   */
-  void enterInferenceCommandOptions(EsqlBaseParser.InferenceCommandOptionsContext ctx);
-  /**
-   * Exit a parse tree produced by {@link EsqlBaseParser#inferenceCommandOptions}.
-   * @param ctx the parse tree
-   */
-  void exitInferenceCommandOptions(EsqlBaseParser.InferenceCommandOptionsContext ctx);
-  /**
-   * Enter a parse tree produced by {@link EsqlBaseParser#inferenceCommandOption}.
-   * @param ctx the parse tree
-   */
-  void enterInferenceCommandOption(EsqlBaseParser.InferenceCommandOptionContext ctx);
-  /**
-   * Exit a parse tree produced by {@link EsqlBaseParser#inferenceCommandOption}.
-   * @param ctx the parse tree
-   */
-  void exitInferenceCommandOption(EsqlBaseParser.InferenceCommandOptionContext ctx);
-  /**
-   * Enter a parse tree produced by {@link EsqlBaseParser#inferenceCommandOptionValue}.
-   * @param ctx the parse tree
-   */
-  void enterInferenceCommandOptionValue(EsqlBaseParser.InferenceCommandOptionValueContext ctx);
-  /**
-   * Exit a parse tree produced by {@link EsqlBaseParser#inferenceCommandOptionValue}.
-   * @param ctx the parse tree
-   */
-  void exitInferenceCommandOptionValue(EsqlBaseParser.InferenceCommandOptionValueContext ctx);
   /**
    * Enter a parse tree produced by {@link EsqlBaseParser#rerankCommand}.
    * @param ctx the parse tree

+ 14 - 26
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlBaseParserVisitor.java

@@ -284,29 +284,35 @@ public interface EsqlBaseParserVisitor<T> extends ParseTreeVisitor<T> {
    */
   T visitDissectCommand(EsqlBaseParser.DissectCommandContext ctx);
   /**
-   * Visit a parse tree produced by {@link EsqlBaseParser#grokCommand}.
+   * Visit a parse tree produced by {@link EsqlBaseParser#dissectCommandOptions}.
    * @param ctx the parse tree
    * @return the visitor result
    */
-  T visitGrokCommand(EsqlBaseParser.GrokCommandContext ctx);
+  T visitDissectCommandOptions(EsqlBaseParser.DissectCommandOptionsContext ctx);
   /**
-   * Visit a parse tree produced by {@link EsqlBaseParser#mvExpandCommand}.
+   * Visit a parse tree produced by {@link EsqlBaseParser#dissectCommandOption}.
    * @param ctx the parse tree
    * @return the visitor result
    */
-  T visitMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx);
+  T visitDissectCommandOption(EsqlBaseParser.DissectCommandOptionContext ctx);
+  /**
+   * Visit a parse tree produced by {@link EsqlBaseParser#commandNamedParameters}.
+   * @param ctx the parse tree
+   * @return the visitor result
+   */
+  T visitCommandNamedParameters(EsqlBaseParser.CommandNamedParametersContext ctx);
   /**
-   * Visit a parse tree produced by {@link EsqlBaseParser#commandOptions}.
+   * Visit a parse tree produced by {@link EsqlBaseParser#grokCommand}.
    * @param ctx the parse tree
    * @return the visitor result
    */
-  T visitCommandOptions(EsqlBaseParser.CommandOptionsContext ctx);
+  T visitGrokCommand(EsqlBaseParser.GrokCommandContext ctx);
   /**
-   * Visit a parse tree produced by {@link EsqlBaseParser#commandOption}.
+   * Visit a parse tree produced by {@link EsqlBaseParser#mvExpandCommand}.
    * @param ctx the parse tree
    * @return the visitor result
    */
-  T visitCommandOption(EsqlBaseParser.CommandOptionContext ctx);
+  T visitMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx);
   /**
    * Visit a parse tree produced by {@link EsqlBaseParser#explainCommand}.
    * @param ctx the parse tree
@@ -424,24 +430,6 @@ public interface EsqlBaseParserVisitor<T> extends ParseTreeVisitor<T> {
    * @return the visitor result
    */
   T visitFuseCommand(EsqlBaseParser.FuseCommandContext ctx);
-  /**
-   * Visit a parse tree produced by {@link EsqlBaseParser#inferenceCommandOptions}.
-   * @param ctx the parse tree
-   * @return the visitor result
-   */
-  T visitInferenceCommandOptions(EsqlBaseParser.InferenceCommandOptionsContext ctx);
-  /**
-   * Visit a parse tree produced by {@link EsqlBaseParser#inferenceCommandOption}.
-   * @param ctx the parse tree
-   * @return the visitor result
-   */
-  T visitInferenceCommandOption(EsqlBaseParser.InferenceCommandOptionContext ctx);
-  /**
-   * Visit a parse tree produced by {@link EsqlBaseParser#inferenceCommandOptionValue}.
-   * @param ctx the parse tree
-   * @return the visitor result
-   */
-  T visitInferenceCommandOptionValue(EsqlBaseParser.InferenceCommandOptionValueContext ctx);
   /**
    * Visit a parse tree produced by {@link EsqlBaseParser#rerankCommand}.
    * @param ctx the parse tree

+ 14 - 13
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java

@@ -261,8 +261,12 @@ public abstract class ExpressionBuilder extends IdentifierBuilder {
 
     @Override
     public UnresolvedAttribute visitQualifiedName(EsqlBaseParser.QualifiedNameContext ctx) {
+        return visitQualifiedName(ctx, null);
+    }
+
+    public UnresolvedAttribute visitQualifiedName(EsqlBaseParser.QualifiedNameContext ctx, UnresolvedAttribute defaultValue) {
         if (ctx == null) {
-            return null;
+            return defaultValue;
         }
         List<Object> items = visitList(this, ctx.identifierOrParameter(), Object.class);
         List<String> strings = new ArrayList<>(items.size());
@@ -647,35 +651,32 @@ public abstract class ExpressionBuilder extends IdentifierBuilder {
             EsqlBaseParser.StringContext stringCtx = entry.string();
             String key = unquote(stringCtx.QUOTED_STRING().getText()); // key is case-sensitive
             if (key.isBlank()) {
-                throw new ParsingException(
-                    source(ctx),
-                    "Invalid named function argument [{}], empty key is not supported",
-                    entry.getText()
-                );
+                throw new ParsingException(source(ctx), "Invalid named parameter [{}], empty key is not supported", entry.getText());
             }
             if (names.contains(key)) {
-                throw new ParsingException(source(ctx), "Duplicated function arguments with the same name [{}] is not supported", key);
+                throw new ParsingException(source(ctx), "Duplicated named parameters with the same name [{}] is not supported", key);
             }
             Expression value = expression(entry.constant());
             String entryText = entry.getText();
             if (value instanceof Literal l) {
                 if (l.dataType() == NULL) {
-                    throw new ParsingException(source(ctx), "Invalid named function argument [{}], NULL is not supported", entryText);
+                    throw new ParsingException(source(ctx), "Invalid named parameter [{}], NULL is not supported", entryText);
                 }
                 namedArgs.add(Literal.keyword(source(stringCtx), key));
                 namedArgs.add(l);
                 names.add(key);
             } else {
-                throw new ParsingException(
-                    source(ctx),
-                    "Invalid named function argument [{}], only constant value is supported",
-                    entryText
-                );
+                throw new ParsingException(source(ctx), "Invalid named parameter [{}], only constant value is supported", entryText);
             }
         }
         return new MapExpression(Source.EMPTY, namedArgs);
     }
 
+    @Override
+    public MapExpression visitCommandNamedParameters(EsqlBaseParser.CommandNamedParametersContext ctx) {
+        return ctx == null || ctx.mapExpression() == null ? null : visitMapExpression(ctx.mapExpression());
+    }
+
     @Override
     public String visitIdentifierOrParameter(EsqlBaseParser.IdentifierOrParameterContext ctx) {
         if (ctx.identifier() != null) {

+ 61 - 91
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java

@@ -31,6 +31,7 @@ import org.elasticsearch.xpack.esql.core.expression.Expression;
 import org.elasticsearch.xpack.esql.core.expression.Expressions;
 import org.elasticsearch.xpack.esql.core.expression.FoldContext;
 import org.elasticsearch.xpack.esql.core.expression.Literal;
+import org.elasticsearch.xpack.esql.core.expression.MapExpression;
 import org.elasticsearch.xpack.esql.core.expression.MetadataAttribute;
 import org.elasticsearch.xpack.esql.core.expression.NameId;
 import org.elasticsearch.xpack.esql.core.expression.NamedExpression;
@@ -71,6 +72,7 @@ import org.elasticsearch.xpack.esql.plan.logical.Sample;
 import org.elasticsearch.xpack.esql.plan.logical.TimeSeriesAggregate;
 import org.elasticsearch.xpack.esql.plan.logical.UnresolvedRelation;
 import org.elasticsearch.xpack.esql.plan.logical.inference.Completion;
+import org.elasticsearch.xpack.esql.plan.logical.inference.InferencePlan;
 import org.elasticsearch.xpack.esql.plan.logical.inference.Rerank;
 import org.elasticsearch.xpack.esql.plan.logical.join.LookupJoin;
 import org.elasticsearch.xpack.esql.plan.logical.show.ShowInfo;
@@ -89,7 +91,6 @@ import java.util.Set;
 import java.util.function.Function;
 
 import static java.util.Collections.emptyList;
-import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD;
 import static org.elasticsearch.xpack.esql.core.util.StringUtils.WILDCARD;
 import static org.elasticsearch.xpack.esql.expression.NamedExpressions.mergeOutputExpressions;
 import static org.elasticsearch.xpack.esql.parser.ParserUtils.source;
@@ -212,7 +213,7 @@ public class LogicalPlanBuilder extends ExpressionBuilder {
     public PlanFactory visitDissectCommand(EsqlBaseParser.DissectCommandContext ctx) {
         return p -> {
             String pattern = BytesRefs.toString(visitString(ctx.string()).fold(FoldContext.small() /* TODO remove me */));
-            Map<String, Object> options = visitCommandOptions(ctx.commandOptions());
+            Map<String, Object> options = visitDissectCommandOptions(ctx.dissectCommandOptions());
             String appendSeparator = "";
             for (Map.Entry<String, Object> item : options.entrySet()) {
                 if (item.getKey().equalsIgnoreCase("append_separator") == false) {
@@ -260,12 +261,12 @@ public class LogicalPlanBuilder extends ExpressionBuilder {
     }
 
     @Override
-    public Map<String, Object> visitCommandOptions(EsqlBaseParser.CommandOptionsContext ctx) {
+    public Map<String, Object> visitDissectCommandOptions(EsqlBaseParser.DissectCommandOptionsContext ctx) {
         if (ctx == null) {
             return Map.of();
         }
         Map<String, Object> result = new HashMap<>();
-        for (EsqlBaseParser.CommandOptionContext option : ctx.commandOption()) {
+        for (EsqlBaseParser.DissectCommandOptionContext option : ctx.dissectCommandOption()) {
             result.put(visitIdentifier(option.identifier()), expression(option.constant()).fold(FoldContext.small() /* TODO remove me */));
         }
         return result;
@@ -755,137 +756,106 @@ public class LogicalPlanBuilder extends ExpressionBuilder {
         Source source = source(ctx);
         List<Alias> rerankFields = visitRerankFields(ctx.rerankFields());
         Expression queryText = expression(ctx.queryText);
+        Attribute scoreAttribute = visitQualifiedName(ctx.targetField, new UnresolvedAttribute(source, MetadataAttribute.SCORE));
 
         if (queryText instanceof Literal queryTextLiteral && DataType.isString(queryText.dataType())) {
             if (queryTextLiteral.value() == null) {
-                throw new ParsingException(
-                    source(ctx.queryText),
-                    "Query text cannot be null or undefined in RERANK",
-                    ctx.queryText.getText()
-                );
+                throw new ParsingException(source(ctx.queryText), "Query cannot be null or undefined in RERANK", ctx.queryText.getText());
             }
         } else {
             throw new ParsingException(
                 source(ctx.queryText),
-                "RERANK only support string as query text but [{}] cannot be used as string",
+                "Query must be a valid string in RERANK, found [{}]",
                 ctx.queryText.getText()
             );
         }
 
         return p -> {
             checkForRemoteClusters(p, source, "RERANK");
-            return visitRerankOptions(new Rerank(source, p, queryText, rerankFields), ctx.inferenceCommandOptions());
+            return applyRerankOptions(new Rerank(source, p, queryText, rerankFields, scoreAttribute), ctx.commandNamedParameters());
         };
     }
 
-    private Rerank visitRerankOptions(Rerank rerank, EsqlBaseParser.InferenceCommandOptionsContext ctx) {
-        if (ctx == null) {
-            return rerank;
-        }
-
-        Rerank.Builder rerankBuilder = new Rerank.Builder(rerank);
+    private Rerank applyRerankOptions(Rerank rerank, EsqlBaseParser.CommandNamedParametersContext ctx) {
+        MapExpression optionExpression = visitCommandNamedParameters(ctx);
 
-        for (var option : ctx.inferenceCommandOption()) {
-            String optionName = visitIdentifier(option.identifier());
-            EsqlBaseParser.InferenceCommandOptionValueContext optionValue = option.inferenceCommandOptionValue();
-            if (optionName.equals(Rerank.INFERENCE_ID_OPTION_NAME)) {
-                rerankBuilder.withInferenceId(visitInferenceId(optionValue));
-            } else if (optionName.equals(Rerank.SCORE_COLUMN_OPTION_NAME)) {
-                rerankBuilder.withScoreAttribute(visitRerankScoreAttribute(optionName, optionValue));
-            } else {
-                throw new ParsingException(
-                    source(option.identifier()),
-                    "Unknowm parameter [{}] in RERANK command",
-                    option.identifier().getText()
-                );
-            }
+        if (optionExpression == null) {
+            return rerank;
         }
 
-        return rerankBuilder.build();
-    }
+        Map<String, Expression> optionsMap = optionExpression.keyFoldedMap();
+        Expression inferenceId = optionsMap.remove(Rerank.INFERENCE_ID_OPTION_NAME);
 
-    private UnresolvedAttribute visitRerankScoreAttribute(String optionName, EsqlBaseParser.InferenceCommandOptionValueContext ctx) {
-        if (ctx.constant() == null && ctx.identifier() == null) {
-            throw new ParsingException(source(ctx), "Parameter [{}] is null or undefined", optionName);
+        if (inferenceId != null) {
+            rerank = applyInferenceId(rerank, inferenceId);
         }
 
-        Expression optionValue = ctx.identifier() != null
-            ? Literal.keyword(source(ctx.identifier()), visitIdentifier(ctx.identifier()))
-            : expression(ctx.constant());
-
-        if (optionValue instanceof UnresolvedAttribute scoreAttribute) {
-            return scoreAttribute;
-        } else if (optionValue instanceof Literal literal) {
-            if (literal.value() == null) {
-                throw new ParsingException(optionValue.source(), "Parameter [{}] is null or undefined", optionName);
-            }
-
-            if (literal.value() instanceof BytesRef attributeName) {
-                return new UnresolvedAttribute(literal.source(), BytesRefs.toString(attributeName));
-            }
+        if (optionsMap.isEmpty() == false) {
+            throw new ParsingException(
+                source(ctx),
+                "Inavalid option [{}] in RERANK, expected one of [{}]",
+                optionsMap.keySet().stream().findAny().get(),
+                rerank.validOptionNames()
+            );
         }
 
-        throw new ParsingException(
-            source(ctx),
-            "Option [{}] expects a valid attribute in RERANK command. [{}] provided.",
-            optionName,
-            ctx.constant().getText()
-        );
+        return rerank;
     }
 
-    @Override
     public PlanFactory visitCompletionCommand(EsqlBaseParser.CompletionCommandContext ctx) {
         Source source = source(ctx);
         Expression prompt = expression(ctx.prompt);
-        Literal inferenceId = visitInferenceId(ctx.inferenceId);
-        Attribute targetField = ctx.targetField == null
-            ? new UnresolvedAttribute(source, Completion.DEFAULT_OUTPUT_FIELD_NAME)
-            : visitQualifiedName(ctx.targetField);
+        Attribute targetField = visitQualifiedName(ctx.targetField, new UnresolvedAttribute(source, Completion.DEFAULT_OUTPUT_FIELD_NAME));
 
         return p -> {
             checkForRemoteClusters(p, source, "COMPLETION");
-            return new Completion(source, p, inferenceId, prompt, targetField);
+            return applyCompletionOptions(new Completion(source, p, prompt, targetField), ctx.commandNamedParameters());
         };
     }
 
-    private Literal visitInferenceId(EsqlBaseParser.IdentifierOrParameterContext ctx) {
-        if (ctx.identifier() != null) {
-            return Literal.keyword(source(ctx), visitIdentifier(ctx.identifier()));
+    private Completion applyCompletionOptions(Completion completion, EsqlBaseParser.CommandNamedParametersContext ctx) {
+        MapExpression optionsExpresion = visitCommandNamedParameters(ctx);
+
+        if (optionsExpresion == null || optionsExpresion.containsKey(Completion.INFERENCE_ID_OPTION_NAME) == false) {
+            // Having a mandatory named parameter for inference_id is an antipattern, but it will be optional in the future when we have a
+            // default LLM. It is better to keep inference_id as a named parameter and relax the syntax when it will become optional than
+            // completely change the syntax in the future.
+            throw new ParsingException(source(ctx), "Missing mandatory option [{}] in COMPLETION", Completion.INFERENCE_ID_OPTION_NAME);
         }
 
-        return visitInferenceId(expression(ctx.parameter()));
-    }
+        Map<String, Expression> optionsMap = visitCommandNamedParameters(ctx).keyFoldedMap();
 
-    private Literal visitInferenceId(EsqlBaseParser.InferenceCommandOptionValueContext ctx) {
-        if (ctx.identifier() != null) {
-            return Literal.keyword(source(ctx), visitIdentifier(ctx.identifier()));
+        Expression inferenceId = optionsMap.remove(Completion.INFERENCE_ID_OPTION_NAME);
+        if (inferenceId != null) {
+            completion = applyInferenceId(completion, inferenceId);
         }
 
-        return visitInferenceId(expression(ctx.constant()));
-    }
+        if (optionsMap.isEmpty() == false) {
+            throw new ParsingException(
+                source(ctx),
+                "Inavalid option [{}] in COMPLETION, expected one of [{}]",
+                optionsMap.keySet().stream().findAny().get(),
+                completion.validOptionNames()
+            );
+        }
 
-    private Literal visitInferenceId(Expression expression) {
-        if (expression instanceof Literal literal) {
-            if (literal.value() == null) {
-                throw new ParsingException(
-                    expression.source(),
-                    "Parameter [{}] is null or undefined and cannot be used as inference id",
-                    expression.source().text()
-                );
-            }
+        return completion;
+    }
 
-            return literal;
-        } else if (expression instanceof UnresolvedAttribute attribute) {
-            // Support for unquoted inference id
-            return new Literal(expression.source(), attribute.name(), KEYWORD);
+    private <InferencePlanType extends InferencePlan<InferencePlanType>> InferencePlanType applyInferenceId(
+        InferencePlanType inferencePlan,
+        Expression inferenceId
+    ) {
+        if ((inferenceId instanceof Literal && DataType.isString(inferenceId.dataType())) == false) {
+            throw new ParsingException(
+                inferenceId.source(),
+                "Option [{}] must be a valid string, found [{}]",
+                Completion.INFERENCE_ID_OPTION_NAME,
+                inferenceId.source().text()
+            );
         }
 
-        throw new ParsingException(
-            expression.source(),
-            "Query parameter [{}] is not a string and cannot be used as inference id [{}]",
-            expression.source().text(),
-            expression.getClass()
-        );
+        return inferencePlan.withInferenceId(inferenceId);
     }
 
     public PlanFactory visitSampleCommand(EsqlBaseParser.SampleCommandContext ctx) {

+ 9 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/inference/Completion.java

@@ -17,6 +17,7 @@ import org.elasticsearch.xpack.esql.common.Failures;
 import org.elasticsearch.xpack.esql.core.expression.Attribute;
 import org.elasticsearch.xpack.esql.core.expression.AttributeSet;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.Literal;
 import org.elasticsearch.xpack.esql.core.expression.NameId;
 import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
 import org.elasticsearch.xpack.esql.core.tree.Source;
@@ -45,6 +46,10 @@ public class Completion extends InferencePlan<Completion> implements TelemetryAw
     private final Attribute targetField;
     private List<Attribute> lazyOutput;
 
+    public Completion(Source source, LogicalPlan p, Expression prompt, Attribute targetField) {
+        this(source, p, Literal.keyword(Source.EMPTY, DEFAULT_OUTPUT_FIELD_NAME), prompt, targetField);
+    }
+
     public Completion(Source source, LogicalPlan child, Expression inferenceId, Expression prompt, Attribute targetField) {
         super(source, child, inferenceId);
         this.prompt = prompt;
@@ -78,6 +83,10 @@ public class Completion extends InferencePlan<Completion> implements TelemetryAw
 
     @Override
     public Completion withInferenceId(Expression newInferenceId) {
+        if (inferenceId().equals(newInferenceId)) {
+            return this;
+        }
+
         return new Completion(source(), child(), newInferenceId, prompt, targetField);
     }
 

+ 8 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/inference/InferencePlan.java

@@ -18,6 +18,7 @@ import org.elasticsearch.xpack.esql.plan.logical.SortAgnostic;
 import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan;
 
 import java.io.IOException;
+import java.util.List;
 import java.util.Objects;
 
 public abstract class InferencePlan<PlanType extends InferencePlan<PlanType>> extends UnaryPlan
@@ -25,6 +26,9 @@ public abstract class InferencePlan<PlanType extends InferencePlan<PlanType>> ex
         SortAgnostic,
         GeneratingPlan<InferencePlan<PlanType>> {
 
+    public static final String INFERENCE_ID_OPTION_NAME = "inference_id";
+    public static final List<String> VALID_INFERENCE_OPTION_NAMES = List.of(INFERENCE_ID_OPTION_NAME);
+
     private final Expression inferenceId;
 
     protected InferencePlan(Source source, LogicalPlan child, Expression inferenceId) {
@@ -69,4 +73,8 @@ public abstract class InferencePlan<PlanType extends InferencePlan<PlanType>> ex
     public PlanType withInferenceResolutionError(String inferenceId, String error) {
         return withInferenceId(new UnresolvedAttribute(inferenceId().source(), inferenceId, error));
     }
+
+    public List<String> validOptionNames() {
+        return VALID_INFERENCE_OPTION_NAMES;
+    }
 }

+ 13 - 36
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/inference/Rerank.java

@@ -19,9 +19,7 @@ import org.elasticsearch.xpack.esql.core.expression.AttributeSet;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
 import org.elasticsearch.xpack.esql.core.expression.Expressions;
 import org.elasticsearch.xpack.esql.core.expression.Literal;
-import org.elasticsearch.xpack.esql.core.expression.MetadataAttribute;
 import org.elasticsearch.xpack.esql.core.expression.NameId;
-import org.elasticsearch.xpack.esql.core.expression.UnresolvedAttribute;
 import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
 import org.elasticsearch.xpack.esql.core.tree.Source;
 import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
@@ -34,29 +32,19 @@ import java.util.Objects;
 
 import static org.elasticsearch.xpack.esql.core.expression.Expressions.asAttributes;
 import static org.elasticsearch.xpack.esql.expression.NamedExpressions.mergeOutputAttributes;
-import static org.elasticsearch.xpack.esql.parser.ParserUtils.source;
 
 public class Rerank extends InferencePlan<Rerank> implements TelemetryAware {
 
     public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(LogicalPlan.class, "Rerank", Rerank::new);
     public static final String DEFAULT_INFERENCE_ID = ".rerank-v1-elasticsearch";
-    public static final String INFERENCE_ID_OPTION_NAME = "inferenceId";
-    public static final String SCORE_COLUMN_OPTION_NAME = "scoreColumn";
 
     private final Attribute scoreAttribute;
     private final Expression queryText;
     private final List<Alias> rerankFields;
     private List<Attribute> lazyOutput;
 
-    public Rerank(Source source, LogicalPlan child, Expression queryText, List<Alias> rerankFields) {
-        this(
-            source,
-            child,
-            Literal.keyword(Source.EMPTY, DEFAULT_INFERENCE_ID),
-            queryText,
-            rerankFields,
-            new UnresolvedAttribute(Source.EMPTY, MetadataAttribute.SCORE)
-        );
+    public Rerank(Source source, LogicalPlan child, Expression queryText, List<Alias> rerankFields, Attribute scoreAttribute) {
+        this(source, child, Literal.keyword(Source.EMPTY, DEFAULT_INFERENCE_ID), queryText, rerankFields, scoreAttribute);
     }
 
     public Rerank(
@@ -111,14 +99,25 @@ public class Rerank extends InferencePlan<Rerank> implements TelemetryAware {
 
     @Override
     public Rerank withInferenceId(Expression newInferenceId) {
+        if (inferenceId().equals(newInferenceId)) {
+            return this;
+        }
         return new Rerank(source(), child(), newInferenceId, queryText, rerankFields, scoreAttribute);
     }
 
     public Rerank withRerankFields(List<Alias> newRerankFields) {
+        if (rerankFields.equals(newRerankFields)) {
+            return this;
+        }
+
         return new Rerank(source(), child(), inferenceId(), queryText, newRerankFields, scoreAttribute);
     }
 
     public Rerank withScoreAttribute(Attribute newScoreAttribute) {
+        if (scoreAttribute.equals(newScoreAttribute)) {
+            return this;
+        }
+
         return new Rerank(source(), child(), inferenceId(), queryText, rerankFields, newScoreAttribute);
     }
 
@@ -193,26 +192,4 @@ public class Rerank extends InferencePlan<Rerank> implements TelemetryAware {
         }
         return lazyOutput;
     }
-
-    public static class Builder {
-        private Rerank rerank;
-
-        public Builder(Rerank rerank) {
-            this.rerank = rerank;
-        }
-
-        public Rerank build() {
-            return rerank;
-        }
-
-        public Builder withInferenceId(Expression inferenceId) {
-            this.rerank = this.rerank.withInferenceId(inferenceId);
-            return this;
-        }
-
-        public Builder withScoreAttribute(Attribute scoreAttribute) {
-            this.rerank = this.rerank.withScoreAttribute(scoreAttribute);
-            return this;
-        }
-    }
 }

+ 40 - 63
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java

@@ -3549,23 +3549,20 @@ public class AnalyzerTests extends ESTestCase {
         assumeTrue("Requires RERANK command", EsqlCapabilities.Cap.RERANK.isEnabled());
 
         {
-            LogicalPlan plan = analyze(
-                "FROM books METADATA _score | RERANK \"italian food recipe\" ON title WITH inferenceId=`reranking-inference-id`",
-                "mapping-books.json"
-            );
+            LogicalPlan plan = analyze("""
+                FROM books METADATA _score
+                | RERANK "italian food recipe" ON title WITH { "inference_id" : "reranking-inference-id" }
+                """, "mapping-books.json");
             Rerank rerank = as(as(plan, Limit.class).child(), Rerank.class);
             assertThat(rerank.inferenceId(), equalTo(string("reranking-inference-id")));
         }
 
         {
-            VerificationException ve = expectThrows(
-                VerificationException.class,
-                () -> analyze(
-                    "FROM books METADATA _score | RERANK \"italian food recipe\" ON title WITH inferenceId=`completion-inference-id`",
-                    "mapping-books.json"
-                )
+            VerificationException ve = expectThrows(VerificationException.class, () -> analyze("""
+                FROM books METADATA _score
+                | RERANK "italian food recipe" ON title WITH { "inference_id" : "completion-inference-id" }
+                """, "mapping-books.json"));
 
-            );
             assertThat(
                 ve.getMessage(),
                 containsString(
@@ -3576,26 +3573,19 @@ public class AnalyzerTests extends ESTestCase {
         }
 
         {
-            VerificationException ve = expectThrows(
-                VerificationException.class,
-                () -> analyze(
-                    "FROM books METADATA _score | RERANK \"italian food recipe\" ON title WITH inferenceId=`error-inference-id`",
-                    "mapping-books.json"
-                )
+            VerificationException ve = expectThrows(VerificationException.class, () -> analyze("""
+                FROM books METADATA _score
+                | RERANK "italian food recipe" ON title WITH { "inference_id" : "error-inference-id" }
+                """, "mapping-books.json"));
 
-            );
             assertThat(ve.getMessage(), containsString("error with inference resolution"));
         }
 
         {
-            VerificationException ve = expectThrows(
-                VerificationException.class,
-                () -> analyze(
-                    "FROM books  METADATA _score | RERANK \"italian food recipe\" ON title WITH inferenceId=`unknown-inference-id`",
-                    "mapping-books.json"
-                )
-
-            );
+            VerificationException ve = expectThrows(VerificationException.class, () -> analyze("""
+                FROM books  METADATA _score
+                | RERANK "italian food recipe" ON title WITH { "inference_id" : "unknown-inference-id" }
+                """, "mapping-books.json"));
             assertThat(ve.getMessage(), containsString("unresolved inference [unknown-inference-id]"));
         }
     }
@@ -3610,7 +3600,7 @@ public class AnalyzerTests extends ESTestCase {
                 | WHERE title:"italian food recipe" OR description:"italian food recipe"
                 | KEEP description, title, year, _score
                 | DROP description
-                | RERANK "italian food recipe" ON title WITH inferenceId=`reranking-inference-id`
+                | RERANK "italian food recipe" ON title WITH { "inference_id" : "reranking-inference-id" }
                 """, "mapping-books.json");
 
             Limit limit = as(plan, Limit.class); // Implicit limit added by AddImplicitLimit rule.
@@ -3635,7 +3625,7 @@ public class AnalyzerTests extends ESTestCase {
                 FROM books METADATA _score
                 | WHERE title:"food"
                 | RERANK "food" ON title, description=SUBSTRING(description, 0, 100), yearRenamed=year
-                  WITH inferenceId=`reranking-inference-id`
+                  WITH { "inference_id" : "reranking-inference-id" }
                 """, "mapping-books.json");
 
             Limit limit = as(plan, Limit.class); // Implicit limit added by AddImplicitLimit rule.
@@ -3667,29 +3657,13 @@ public class AnalyzerTests extends ESTestCase {
             assertThat(rerank.scoreAttribute(), equalTo(getAttributeByName(relation.output(), MetadataAttribute.SCORE)));
         }
 
-        {
-            // Unnamed field.
-            try {
-                LogicalPlan plan = analyze("""
-                    FROM books METADATA _score
-                    | WHERE title:"food"
-                    | RERANK "food" ON title, SUBSTRING(description, 0, 100), yearRenamed=year WITH inferenceId=`reranking-inference-id`
-                    """, "mapping-books.json");
-            } catch (ParsingException ex) {
-                assertThat(
-                    ex.getMessage(),
-                    containsString("line 3:36: mismatched input '(' expecting {<EOF>, '|', '=', ',', '.', 'with'}")
-                );
-            }
-        }
-
         {
             VerificationException ve = expectThrows(
                 VerificationException.class,
-                () -> analyze(
-                    "FROM books METADATA _score | RERANK \"italian food recipe\" ON missingField WITH inferenceId=`reranking-inference-id`",
-                    "mapping-books.json"
-                )
+                () -> analyze("""
+                    FROM books METADATA _score
+                    | RERANK \"italian food recipe\" ON missingField WITH { "inference_id" : "reranking-inference-id" }
+                    """, "mapping-books.json")
 
             );
             assertThat(ve.getMessage(), containsString("Unknown column [missingField]"));
@@ -3704,7 +3678,7 @@ public class AnalyzerTests extends ESTestCase {
             LogicalPlan plan = analyze("""
                 FROM books METADATA _score
                 | WHERE title:"italian food recipe" OR description:"italian food recipe"
-                | RERANK "italian food recipe" ON title WITH inferenceId=`reranking-inference-id`
+                | RERANK "italian food recipe" ON title WITH { "inference_id" : "reranking-inference-id" }
                 """, "mapping-books.json");
 
             Limit limit = as(plan, Limit.class); // Implicit limit added by AddImplicitLimit rule.
@@ -3722,7 +3696,7 @@ public class AnalyzerTests extends ESTestCase {
             LogicalPlan plan = analyze("""
                 FROM books
                 | WHERE title:"italian food recipe" OR description:"italian food recipe"
-                | RERANK "italian food recipe" ON title WITH inferenceId=`reranking-inference-id`
+                | RERANK "italian food recipe" ON title WITH { "inference_id" : "reranking-inference-id" }
                 """, "mapping-books.json");
 
             Limit limit = as(plan, Limit.class); // Implicit limit added by AddImplicitLimit rule.
@@ -3740,7 +3714,7 @@ public class AnalyzerTests extends ESTestCase {
             LogicalPlan plan = analyze("""
                 FROM books METADATA _score
                 | WHERE title:"italian food recipe" OR description:"italian food recipe"
-                | RERANK "italian food recipe" ON title WITH inferenceId=`reranking-inference-id`, scoreColumn=rerank_score
+                | RERANK rerank_score = "italian food recipe" ON title WITH { "inference_id" : "reranking-inference-id" }
                 """, "mapping-books.json");
 
             Limit limit = as(plan, Limit.class); // Implicit limit added by AddImplicitLimit rule.
@@ -3758,7 +3732,7 @@ public class AnalyzerTests extends ESTestCase {
                 FROM books METADATA _score
                 | WHERE title:"italian food recipe" OR description:"italian food recipe"
                 | EVAL rerank_score = _score
-                | RERANK "italian food recipe" ON title WITH inferenceId=`reranking-inference-id`, scoreColumn=rerank_score
+                | RERANK rerank_score = "italian food recipe" ON title WITH { "inference_id" : "reranking-inference-id" }
                 """, "mapping-books.json");
 
             Limit limit = as(plan, Limit.class); // Implicit limit added by AddImplicitLimit rule.
@@ -3775,8 +3749,9 @@ public class AnalyzerTests extends ESTestCase {
     public void testResolveCompletionInferenceId() {
         LogicalPlan plan = analyze("""
             FROM books METADATA _score
-            | COMPLETION CONCAT("Translate the following text in French\\n", description) WITH `completion-inference-id`
+            | COMPLETION CONCAT("Translate this text in French\\n", description) WITH { "inference_id" : "completion-inference-id" }
             """, "mapping-books.json");
+
         Completion completion = as(as(plan, Limit.class).child(), Completion.class);
         assertThat(completion.inferenceId(), equalTo(string("completion-inference-id")));
     }
@@ -3785,7 +3760,7 @@ public class AnalyzerTests extends ESTestCase {
         assertError(
             """
                 FROM books METADATA _score
-                | COMPLETION CONCAT("Translate the following text in French\\n", description) WITH `reranking-inference-id`
+                | COMPLETION CONCAT("Translate this text in French\\n", description) WITH { "inference_id" : "reranking-inference-id" }
                 """,
             "mapping-books.json",
             new QueryParams(),
@@ -3797,21 +3772,22 @@ public class AnalyzerTests extends ESTestCase {
     public void testResolveCompletionInferenceMissingInferenceId() {
         assertError("""
             FROM books METADATA _score
-            | COMPLETION CONCAT("Translate the following text in French\\n", description) WITH `unknown-inference-id`
+            | COMPLETION CONCAT("Translate the following text in French\\n", description) WITH { "inference_id" : "unknown-inference-id" }
             """, "mapping-books.json", new QueryParams(), "unresolved inference [unknown-inference-id]");
     }
 
     public void testResolveCompletionInferenceIdResolutionError() {
         assertError("""
             FROM books METADATA _score
-            | COMPLETION CONCAT("Translate the following text in French\\n", description) WITH `error-inference-id`
+            | COMPLETION CONCAT("Translate the following text in French\\n", description) WITH { "inference_id" : "error-inference-id" }
             """, "mapping-books.json", new QueryParams(), "error with inference resolution");
     }
 
     public void testResolveCompletionTargetField() {
         LogicalPlan plan = analyze("""
             FROM books METADATA _score
-            | COMPLETION translation=CONCAT("Translate the following text in French\\n", description) WITH `completion-inference-id`
+            | COMPLETION translation = CONCAT("Translate the following text in French\\n", description)
+              WITH { "inference_id" : "completion-inference-id" }
             """, "mapping-books.json");
 
         Completion completion = as(as(plan, Limit.class).child(), Completion.class);
@@ -3821,7 +3797,7 @@ public class AnalyzerTests extends ESTestCase {
     public void testResolveCompletionDefaultTargetField() {
         LogicalPlan plan = analyze("""
             FROM books METADATA _score
-            | COMPLETION CONCAT("Translate the following text in French\\n", description) WITH `completion-inference-id`
+            | COMPLETION CONCAT("Translate this text in French\\n", description) WITH { "inference_id" : "completion-inference-id" }
             """, "mapping-books.json");
 
         Completion completion = as(as(plan, Limit.class).child(), Completion.class);
@@ -3831,7 +3807,7 @@ public class AnalyzerTests extends ESTestCase {
     public void testResolveCompletionPrompt() {
         LogicalPlan plan = analyze("""
             FROM books METADATA _score
-            | COMPLETION CONCAT("Translate the following text in French\\n", description) WITH `completion-inference-id`
+            | COMPLETION CONCAT("Translate this text in French\\n", description) WITH { "inference_id" : "completion-inference-id" }
             """, "mapping-books.json");
 
         Completion completion = as(as(plan, Limit.class).child(), Completion.class);
@@ -3839,21 +3815,22 @@ public class AnalyzerTests extends ESTestCase {
 
         assertThat(
             as(completion.prompt(), Concat.class).children(),
-            equalTo(List.of(string("Translate the following text in French\n"), getAttributeByName(esRelation.output(), "description")))
+            equalTo(List.of(string("Translate this text in French\n"), getAttributeByName(esRelation.output(), "description")))
         );
     }
 
     public void testResolveCompletionPromptInvalidType() {
         assertError("""
             FROM books METADATA _score
-            | COMPLETION LENGTH(description) WITH `completion-inference-id`
+            | COMPLETION LENGTH(description) WITH { "inference_id" : "completion-inference-id" }
             """, "mapping-books.json", new QueryParams(), "prompt must be of type [text] but is [integer]");
     }
 
-    public void testResolveCompletionOutputField() {
+    public void testResolveCompletionOutputFieldOverwriteInputField() {
         LogicalPlan plan = analyze("""
             FROM books METADATA _score
-            | COMPLETION description=CONCAT("Translate the following text in French\\n", description) WITH `completion-inference-id`
+            | COMPLETION description = CONCAT("Translate the following text in French\\n", description)
+              WITH { "inference_id" : "completion-inference-id" }
             """, "mapping-books.json");
 
         Completion completion = as(as(plan, Limit.class).child(), Completion.class);

+ 2 - 2
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java

@@ -5515,7 +5515,7 @@ public class LogicalPlanOptimizerTests extends AbstractLogicalPlanOptimizerTests
             ),
             new PushDownEnrich()
         ),
-        // | COMPLETION y=CONCAT(some text, x) WITH inferenceID
+        // | COMPLETION y =CONCAT(some text, x) WITH { "inference_id" : "inferenceID" }
         new PushdownShadowingGeneratingPlanTestCase(
             (plan, attr) -> new Completion(
                 EMPTY,
@@ -5526,7 +5526,7 @@ public class LogicalPlanOptimizerTests extends AbstractLogicalPlanOptimizerTests
             ),
             new PushDownInferencePlan()
         ),
-        // | RERANK "some text" ON x WITH inferenceID=inferenceID, scoreColumn=y
+        // | RERANK "some text" ON x INTO y WITH { "inference_id" : "inferenceID" }
         new PushdownShadowingGeneratingPlanTestCase(
             (plan, attr) -> new Rerank(
                 EMPTY,

+ 7 - 4
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java

@@ -247,8 +247,11 @@ public class PushDownAndCombineFiltersTests extends ESTestCase {
         assertEquals(expected, new PushDownAndCombineFilters().apply(fb));
     }
 
-    // from ... | where a > 1 | COMPLETION completion="some prompt" WITH inferenceId | where b < 2 and match(completion, some text)
-    // => ... | where a > 1 AND b < 2| COMPLETION completion="some prompt" WITH inferenceId | where match(completion, some text)
+    // from ... | where a > 1 | COMPLETION completion = "some prompt" WITH { "inferenceId' : "inferenceId" } | where b < 2 and
+    // match(completion, some text)
+    // => ... | where a > 1 AND b < 2| COMPLETION completion = "some prompt" WITH { "inferenceId' : "inferenceId" } | where
+    // match(completion,
+    // some text)
     public void testPushDownFilterPastCompletion() {
         FieldAttribute a = getFieldAttribute("a");
         FieldAttribute b = getFieldAttribute("b");
@@ -284,8 +287,8 @@ public class PushDownAndCombineFiltersTests extends ESTestCase {
         assertEquals(expectedOptimizedPlan, new PushDownAndCombineFilters().apply(filterB));
     }
 
-    // from ... | where a > 1 | RERANK "query" ON title WITH inferenceId | where b < 2 and _score > 1
-    // => ... | where a > 1 AND b < 2| RERANK "query" ON title WITH inferenceId | where _score > 1
+    // from ... | where a > 1 | RERANK "query" ON title WITH { "inference_id" : "inferenceId" } | where b < 2 and _score > 1
+    // => ... | where a > 1 AND b < 2| RERANK "query" ON title WITH { "inference_id" : "inferenceId" } | where _score > 1
     public void testPushDownFilterPastRerank() {
         FieldAttribute a = getFieldAttribute("a");
         FieldAttribute b = getFieldAttribute("b");

+ 111 - 94
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java

@@ -2604,11 +2604,11 @@ public class StatementParserTests extends AbstractStatementParserTests {
         );
     }
 
-    public void testNamedFunctionArgumentInMap() {
+    public void testFunctionNamedParameterInMap() {
         // functions can be scalar, grouping and aggregation
         // functions can be in eval/where/stats/sort/dissect/grok commands, commands in snapshot are not covered
         // positive
-        // In eval and where clause as function arguments
+        // In eval and where clause as function named parameters
         LinkedHashMap<String, Object> expectedMap1 = new LinkedHashMap<>(4);
         expectedMap1.put("option1", "string");
         expectedMap1.put("option2", 1);
@@ -2652,7 +2652,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
                 """)
         );
 
-        // In stats, by and sort as function arguments
+        // In stats, by and sort as function named parameters
         assertEquals(
             new OrderBy(
                 EMPTY,
@@ -2688,7 +2688,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
                 """)
         );
 
-        // In dissect and grok as function arguments
+        // In dissect and grok as function named parameter
         LogicalPlan plan = statement("""
             from test
             | dissect fn1(f1, f2, {"option1":"string", "option2":1,"option3":[2.0,3.0,4.0],"option4":[true,false]}) "%{bar}"
@@ -2707,7 +2707,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
         assertEquals(ur, relation("test"));
     }
 
-    public void testNamedFunctionArgumentInMapWithNamedParameters() {
+    public void testFunctionNamedParameterInMapWithNamedParameters() {
         // map entry values provided in named parameter, arrays are not supported by named parameters yet
         LinkedHashMap<String, Object> expectedMap1 = new LinkedHashMap<>(4);
         expectedMap1.put("option1", "string");
@@ -2850,7 +2850,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
         assertEquals(ur, relation("test"));
     }
 
-    public void testNamedFunctionArgumentWithCaseSensitiveKeys() {
+    public void testFunctionNamedParameterWithCaseSensitiveKeys() {
         LinkedHashMap<String, Object> expectedMap1 = new LinkedHashMap<>(3);
         expectedMap1.put("option", "string");
         expectedMap1.put("Option", 1);
@@ -2888,7 +2888,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
         );
     }
 
-    public void testMultipleNamedFunctionArgumentsNotAllowed() {
+    public void testMultipleFunctionNamedParametersNotAllowed() {
         Map<String, String> commands = Map.ofEntries(
             Map.entry("eval x = {}", "41"),
             Map.entry("where {}", "38"),
@@ -2912,7 +2912,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
         }
     }
 
-    public void testNamedFunctionArgumentNotInMap() {
+    public void testFunctionNamedParameterNotInMap() {
         Map<String, String> commands = Map.ofEntries(
             Map.entry("eval x = {}", "38"),
             Map.entry("where {}", "35"),
@@ -2936,7 +2936,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
         }
     }
 
-    public void testNamedFunctionArgumentNotConstant() {
+    public void testFunctionNamedParameterNotConstant() {
         Map<String, String[]> commands = Map.ofEntries(
             Map.entry("eval x = {}", new String[] { "31", "35" }),
             Map.entry("where {}", new String[] { "28", "32" }),
@@ -2952,7 +2952,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
             String error1 = command.getValue()[0];
             String error2 = command.getValue()[1];
             String errorMessage1 = cmd.startsWith("dissect") || cmd.startsWith("grok")
-                ? "mismatched input '1' expecting QUOTED_STRING"
+                ? "mismatched input '1' expecting {QUOTED_STRING"
                 : "no viable alternative at input 'fn(f1, { 1'";
             String errorMessage2 = cmd.startsWith("dissect") || cmd.startsWith("grok")
                 ? "mismatched input 'string' expecting {QUOTED_STRING"
@@ -2968,7 +2968,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
         }
     }
 
-    public void testNamedFunctionArgumentEmptyMap() {
+    public void testNamedFunctionNamedParametersEmptyMap() {
         Map<String, String> commands = Map.ofEntries(
             Map.entry("eval x = {}", "30"),
             Map.entry("where {}", "27"),
@@ -2982,17 +2982,12 @@ public class StatementParserTests extends AbstractStatementParserTests {
         for (Map.Entry<String, String> command : commands.entrySet()) {
             String cmd = command.getKey();
             String error = command.getValue();
-            String errorMessage = cmd.startsWith("dissect") || cmd.startsWith("grok")
-                ? "mismatched input '}' expecting QUOTED_STRING"
-                : "no viable alternative at input 'fn(f1, {}'";
-            expectError(
-                LoggerMessageFormat.format(null, "from test | " + cmd, "fn(f1, {}})"),
-                LoggerMessageFormat.format(null, "line 1:{}: {}", error, errorMessage)
-            );
+
+            statement(LoggerMessageFormat.format(null, "from test | " + cmd, "fn(f1, {})"));
         }
     }
 
-    public void testNamedFunctionArgumentMapWithNULL() {
+    public void testNamedFunctionNamedParametersMapWithNULL() {
         Map<String, String> commands = Map.ofEntries(
             Map.entry("eval x = {}", "29"),
             Map.entry("where {}", "26"),
@@ -3008,17 +3003,12 @@ public class StatementParserTests extends AbstractStatementParserTests {
             String error = command.getValue();
             expectError(
                 LoggerMessageFormat.format(null, "from test | " + cmd, "fn(f1, {\"option\":null})"),
-                LoggerMessageFormat.format(
-                    null,
-                    "line 1:{}: {}",
-                    error,
-                    "Invalid named function argument [\"option\":null], NULL is not supported"
-                )
+                LoggerMessageFormat.format(null, "line 1:{}: {}", error, "Invalid named parameter [\"option\":null], NULL is not supported")
             );
         }
     }
 
-    public void testNamedFunctionArgumentMapWithEmptyKey() {
+    public void testNamedFunctionNamedParametersMapWithEmptyKey() {
         Map<String, String> commands = Map.ofEntries(
             Map.entry("eval x = {}", "29"),
             Map.entry("where {}", "26"),
@@ -3034,26 +3024,16 @@ public class StatementParserTests extends AbstractStatementParserTests {
             String error = command.getValue();
             expectError(
                 LoggerMessageFormat.format(null, "from test | " + cmd, "fn(f1, {\"\":1})"),
-                LoggerMessageFormat.format(
-                    null,
-                    "line 1:{}: {}",
-                    error,
-                    "Invalid named function argument [\"\":1], empty key is not supported"
-                )
+                LoggerMessageFormat.format(null, "line 1:{}: {}", error, "Invalid named parameter [\"\":1], empty key is not supported")
             );
             expectError(
                 LoggerMessageFormat.format(null, "from test | " + cmd, "fn(f1, {\"  \":1})"),
-                LoggerMessageFormat.format(
-                    null,
-                    "line 1:{}: {}",
-                    error,
-                    "Invalid named function argument [\"  \":1], empty key is not supported"
-                )
+                LoggerMessageFormat.format(null, "line 1:{}: {}", error, "Invalid named parameter [\"  \":1], empty key is not supported")
             );
         }
     }
 
-    public void testNamedFunctionArgumentMapWithDuplicatedKey() {
+    public void testNamedFunctionNamedParametersMapWithDuplicatedKey() {
         Map<String, String> commands = Map.ofEntries(
             Map.entry("eval x = {}", "29"),
             Map.entry("where {}", "26"),
@@ -3073,13 +3053,13 @@ public class StatementParserTests extends AbstractStatementParserTests {
                     null,
                     "line 1:{}: {}",
                     error,
-                    "Duplicated function arguments with the same name [dup] is not supported"
+                    "Duplicated named parameters with the same name [dup] is not supported"
                 )
             );
         }
     }
 
-    public void testNamedFunctionArgumentInInvalidPositions() {
+    public void testNamedFunctionNamedParametersInInvalidPositions() {
         // negative, named arguments are not supported outside of a functionExpression where booleanExpression or indexPattern is supported
         String map = "{\"option1\":\"string\", \"option2\":1}";
 
@@ -3108,7 +3088,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
         }
     }
 
-    public void testNamedFunctionArgumentWithUnsupportedNamedParameterTypes() {
+    public void testNamedFunctionNamedParametersWithUnsupportedNamedParameterTypes() {
         Map<String, String> commands = Map.ofEntries(
             Map.entry("eval x = {}", "29"),
             Map.entry("where {}", "26"),
@@ -3129,7 +3109,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
                     null,
                     "line 1:{}: {}",
                     error,
-                    "Invalid named function argument [\"option1\":?n1], only constant value is supported"
+                    "Invalid named parameter [\"option1\":?n1], only constant value is supported"
                 )
             );
             expectError(
@@ -3139,7 +3119,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
                     null,
                     "line 1:{}: {}",
                     error,
-                    "Invalid named function argument [\"option1\":?n1], only constant value is supported"
+                    "Invalid named parameter [\"option1\":?n1], only constant value is supported"
                 )
             );
         }
@@ -3577,7 +3557,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
                ( LOOKUP JOIN idx2 ON f1 )
                ( ENRICH idx2 on f1 with f2 = f3 )
                ( FORK ( WHERE a:"baz" ) ( EVAL x = [ 1, 2, 3 ] ) )
-               ( COMPLETION a = b WITH c )
+               ( COMPLETION a=b WITH { "inference_id": "c" } )
             | KEEP a
             """;
 
@@ -3614,7 +3594,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
                ( LOOKUP JOIN idx2 ON f1 )
                ( ENRICH idx2 on f1 with f2 = f3 )
                ( FORK ( WHERE a:"baz" ) ( EVAL x = [ 1, 2, 3 ] ) )
-               ( COMPLETION a = b WITH c )
+               ( COMPLETION a=b WITH { "inference_id": "c" } )
                ( SAMPLE 0.99 )
             | KEEP a
             """;
@@ -3719,22 +3699,22 @@ public class StatementParserTests extends AbstractStatementParserTests {
         assertThat(rerank.rerankFields(), equalTo(List.of(alias("title", attribute("title")))));
     }
 
-    public void testRerankInferenceId() {
+    public void testRerankEmptyOptions() {
         assumeTrue("RERANK requires corresponding capability", EsqlCapabilities.Cap.RERANK.isEnabled());
 
-        var plan = processingCommand("RERANK \"query text\" ON title WITH inferenceId=inferenceId");
+        var plan = processingCommand("RERANK \"query text\" ON title WITH {}");
         var rerank = as(plan, Rerank.class);
 
-        assertThat(rerank.inferenceId(), equalTo(literalString("inferenceId")));
+        assertThat(rerank.inferenceId(), equalTo(literalString(".rerank-v1-elasticsearch")));
+        assertThat(rerank.scoreAttribute(), equalTo(attribute("_score")));
         assertThat(rerank.queryText(), equalTo(literalString("query text")));
         assertThat(rerank.rerankFields(), equalTo(List.of(alias("title", attribute("title")))));
-        assertThat(rerank.scoreAttribute(), equalTo(attribute("_score")));
     }
 
-    public void testRerankQuotedInferenceId() {
+    public void testRerankInferenceId() {
         assumeTrue("RERANK requires corresponding capability", EsqlCapabilities.Cap.RERANK.isEnabled());
 
-        var plan = processingCommand("RERANK \"query text\" ON title WITH inferenceId=\"inferenceId\"");
+        var plan = processingCommand("RERANK \"query text\" ON title WITH { \"inference_id\" : \"inferenceId\" }");
         var rerank = as(plan, Rerank.class);
 
         assertThat(rerank.inferenceId(), equalTo(literalString("inferenceId")));
@@ -3746,19 +3726,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
     public void testRerankScoreAttribute() {
         assumeTrue("RERANK requires corresponding capability", EsqlCapabilities.Cap.RERANK.isEnabled());
 
-        var plan = processingCommand("RERANK \"query text\" ON title WITH scoreColumn=rerank_score");
-        var rerank = as(plan, Rerank.class);
-
-        assertThat(rerank.inferenceId(), equalTo(literalString(".rerank-v1-elasticsearch")));
-        assertThat(rerank.scoreAttribute(), equalTo(attribute("rerank_score")));
-        assertThat(rerank.queryText(), equalTo(literalString("query text")));
-        assertThat(rerank.rerankFields(), equalTo(List.of(alias("title", attribute("title")))));
-    }
-
-    public void testRerankQuotedScoreAttribute() {
-        assumeTrue("RERANK requires corresponding capability", EsqlCapabilities.Cap.RERANK.isEnabled());
-
-        var plan = processingCommand("RERANK \"query text\" ON title WITH scoreColumn=\"rerank_score\"");
+        var plan = processingCommand("RERANK rerank_score=\"query text\" ON title");
         var rerank = as(plan, Rerank.class);
 
         assertThat(rerank.inferenceId(), equalTo(literalString(".rerank-v1-elasticsearch")));
@@ -3770,7 +3738,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
     public void testRerankInferenceIdAnddScoreAttribute() {
         assumeTrue("RERANK requires corresponding capability", EsqlCapabilities.Cap.RERANK.isEnabled());
 
-        var plan = processingCommand("RERANK \"query text\" ON title WITH inferenceId=inferenceId, scoreColumn=rerank_score");
+        var plan = processingCommand("RERANK rerank_score=\"query text\" ON title WITH { \"inference_id\" : \"inferenceId\" }");
         var rerank = as(plan, Rerank.class);
 
         assertThat(rerank.inferenceId(), equalTo(literalString("inferenceId")));
@@ -3782,7 +3750,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
     public void testRerankSingleField() {
         assumeTrue("RERANK requires corresponding capability", EsqlCapabilities.Cap.RERANK.isEnabled());
 
-        var plan = processingCommand("RERANK \"query text\" ON title WITH inferenceId=inferenceID");
+        var plan = processingCommand("RERANK \"query text\" ON title WITH { \"inference_id\" : \"inferenceID\" }");
         var rerank = as(plan, Rerank.class);
 
         assertThat(rerank.queryText(), equalTo(literalString("query text")));
@@ -3794,7 +3762,9 @@ public class StatementParserTests extends AbstractStatementParserTests {
     public void testRerankMultipleFields() {
         assumeTrue("RERANK requires corresponding capability", EsqlCapabilities.Cap.RERANK.isEnabled());
 
-        var plan = processingCommand("RERANK \"query text\" ON title, description, authors_renamed=authors WITH inferenceId=inferenceID");
+        var plan = processingCommand(
+            "RERANK \"query text\" ON title, description, authors_renamed=authors WITH { \"inference_id\" : \"inferenceID\" }"
+        );
         var rerank = as(plan, Rerank.class);
 
         assertThat(rerank.queryText(), equalTo(literalString("query text")));
@@ -3815,9 +3785,9 @@ public class StatementParserTests extends AbstractStatementParserTests {
     public void testRerankComputedFields() {
         assumeTrue("RERANK requires corresponding capability", EsqlCapabilities.Cap.RERANK.isEnabled());
 
-        var plan = processingCommand(
-            "RERANK \"query text\" ON title, short_description = SUBSTRING(description, 0, 100) WITH inferenceId=inferenceID"
-        );
+        var plan = processingCommand("""
+            RERANK "query text" ON title, short_description = SUBSTRING(description, 0, 100) WITH { "inference_id": "inferenceID" }
+            """);
         var rerank = as(plan, Rerank.class);
 
         assertThat(rerank.queryText(), equalTo(literalString("query text")));
@@ -3834,14 +3804,25 @@ public class StatementParserTests extends AbstractStatementParserTests {
         assertThat(rerank.scoreAttribute(), equalTo(attribute("_score")));
     }
 
+    public void testRerankComputedFieldsWithoutName() {
+        assumeTrue("RERANK requires corresponding capability", EsqlCapabilities.Cap.RERANK.isEnabled());
+        // Unnamed alias are forbidden
+        expectError(
+            "FROM books METADATA _score | RERANK \"food\" ON title, SUBSTRING(description, 0, 100), yearRenamed=year`",
+            "line 1:63: mismatched input '(' expecting {<EOF>, '|', '=', ',', '.', 'with'}"
+        );
+    }
+
     public void testRerankWithPositionalParameters() {
         assumeTrue("RERANK requires corresponding capability", EsqlCapabilities.Cap.RERANK.isEnabled());
 
-        var queryParams = new QueryParams(
-            List.of(paramAsConstant(null, "query text"), paramAsConstant(null, "reranker"), paramAsConstant(null, "rerank_score"))
-        );
+        var queryParams = new QueryParams(List.of(paramAsConstant(null, "query text"), paramAsConstant(null, "reranker")));
         var rerank = as(
-            parser.createStatement("row a = 1 | RERANK ? ON title WITH inferenceId=?, scoreColumn=? ", queryParams, EsqlTestUtils.TEST_CFG),
+            parser.createStatement(
+                "row a = 1 | RERANK rerank_score = ? ON title WITH { \"inference_id\" : ? }",
+                queryParams,
+                EsqlTestUtils.TEST_CFG
+            ),
             Rerank.class
         );
 
@@ -3854,16 +3835,10 @@ public class StatementParserTests extends AbstractStatementParserTests {
     public void testRerankWithNamedParameters() {
         assumeTrue("RERANK requires corresponding capability", EsqlCapabilities.Cap.RERANK.isEnabled());
 
-        var queryParams = new QueryParams(
-            List.of(
-                paramAsConstant("queryText", "query text"),
-                paramAsConstant("inferenceId", "reranker"),
-                paramAsConstant("scoreColumnName", "rerank_score")
-            )
-        );
+        var queryParams = new QueryParams(List.of(paramAsConstant("queryText", "query text"), paramAsConstant("inferenceId", "reranker")));
         var rerank = as(
             parser.createStatement(
-                "row a = 1 | RERANK ?queryText ON title WITH inferenceId=?inferenceId, scoreColumn=?scoreColumnName",
+                "row a = 1 | RERANK rerank_score=?queryText ON title WITH { \"inference_id\": ?inferenceId }",
                 queryParams,
                 EsqlTestUtils.TEST_CFG
             ),
@@ -3873,22 +3848,46 @@ public class StatementParserTests extends AbstractStatementParserTests {
         assertThat(rerank.queryText(), equalTo(literalString("query text")));
         assertThat(rerank.inferenceId(), equalTo(literalString("reranker")));
         assertThat(rerank.rerankFields(), equalTo(List.of(alias("title", attribute("title")))));
+        assertThat(rerank.scoreAttribute(), equalTo(attribute("rerank_score")));
     }
 
     public void testInvalidRerank() {
         assumeTrue("RERANK requires corresponding capability", EsqlCapabilities.Cap.RERANK.isEnabled());
-        expectError("FROM foo* | RERANK ON title WITH inferenceId", "line 1:20: mismatched input 'ON' expecting {QUOTED_STRING");
+        expectError(
+            "FROM foo* | RERANK \"query text\" ON title WITH { \"inference_id\": 3 }",
+            "line 1:65: Option [inference_id] must be a valid string, found [3]"
+        );
+        expectError(
+            "FROM foo* | RERANK \"query text\" ON title WITH { \"inference_id\": \"inferenceId\", \"unknown_option\": 3 }",
+            "line 1:42: Inavalid option [unknown_option] in RERANK, expected one of [[inference_id]]"
+        );
+        expectError("FROM foo* | RERANK 45 ON title", "Query must be a valid string in RERANK, found [45]");
+        expectError("FROM foo* | RERANK ON title WITH inferenceId", "line 1:20: extraneous input 'ON' expecting {QUOTED_STRING");
         expectError("FROM foo* | RERANK \"query text\" WITH inferenceId", "line 1:33: mismatched input 'WITH' expecting 'on'");
 
         var fromPatterns = randomIndexPatterns(CROSS_CLUSTER);
         expectError(
-            "FROM " + fromPatterns + " | RERANK \"query text\" ON title WITH inferenceId=inferenceId",
+            "FROM " + fromPatterns + " | RERANK \"query text\" ON title WITH { \"inference_id\" : \"inference_id\" }",
             "invalid index pattern [" + unquoteIndexPattern(fromPatterns) + "], remote clusters are not supported with RERANK"
         );
     }
 
+    public void testCompletionMissingOptions() {
+        expectError("FROM foo* | COMPLETION targetField = prompt", "line 1:44: Missing mandatory option [inference_id] in COMPLETION");
+    }
+
+    public void testCompletionEmptyOptions() {
+        expectError(
+            "FROM foo* | COMPLETION targetField = prompt WITH { }",
+            "line 1:45: Missing mandatory option [inference_id] in COMPLETION"
+        );
+    }
+
     public void testCompletionUsingFieldAsPrompt() {
-        var plan = as(processingCommand("COMPLETION targetField=prompt_field WITH inferenceID"), Completion.class);
+        var plan = as(
+            processingCommand("COMPLETION targetField=prompt_field WITH{ \"inference_id\" : \"inferenceID\" }"),
+            Completion.class
+        );
 
         assertThat(plan.prompt(), equalTo(attribute("prompt_field")));
         assertThat(plan.inferenceId(), equalTo(literalString("inferenceID")));
@@ -3896,7 +3895,10 @@ public class StatementParserTests extends AbstractStatementParserTests {
     }
 
     public void testCompletionUsingFunctionAsPrompt() {
-        var plan = as(processingCommand("COMPLETION targetField=CONCAT(fieldA, fieldB) WITH inferenceID"), Completion.class);
+        var plan = as(
+            processingCommand("COMPLETION targetField=CONCAT(fieldA, fieldB) WITH { \"inference_id\" : \"inferenceID\" }"),
+            Completion.class
+        );
 
         assertThat(plan.prompt(), equalTo(function("CONCAT", List.of(attribute("fieldA"), attribute("fieldB")))));
         assertThat(plan.inferenceId(), equalTo(literalString("inferenceID")));
@@ -3904,7 +3906,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
     }
 
     public void testCompletionDefaultFieldName() {
-        var plan = as(processingCommand("COMPLETION prompt_field WITH inferenceID"), Completion.class);
+        var plan = as(processingCommand("COMPLETION prompt_field WITH{ \"inference_id\" : \"inferenceID\" }"), Completion.class);
 
         assertThat(plan.prompt(), equalTo(attribute("prompt_field")));
         assertThat(plan.inferenceId(), equalTo(literalString("inferenceID")));
@@ -3914,7 +3916,11 @@ public class StatementParserTests extends AbstractStatementParserTests {
     public void testCompletionWithPositionalParameters() {
         var queryParams = new QueryParams(List.of(paramAsConstant(null, "inferenceId")));
         var plan = as(
-            parser.createStatement("row a = 1 | COMPLETION prompt_field WITH ?", queryParams, EsqlTestUtils.TEST_CFG),
+            parser.createStatement(
+                "row a = 1 | COMPLETION prompt_field WITH { \"inference_id\" : ? }",
+                queryParams,
+                EsqlTestUtils.TEST_CFG
+            ),
             Completion.class
         );
 
@@ -3926,7 +3932,11 @@ public class StatementParserTests extends AbstractStatementParserTests {
     public void testCompletionWithNamedParameters() {
         var queryParams = new QueryParams(List.of(paramAsConstant("inferenceId", "myInference")));
         var plan = as(
-            parser.createStatement("row a = 1 | COMPLETION prompt_field WITH ?inferenceId", queryParams, EsqlTestUtils.TEST_CFG),
+            parser.createStatement(
+                "row a = 1 | COMPLETION prompt_field WITH { \"inference_id\" : ?inferenceId }",
+                queryParams,
+                EsqlTestUtils.TEST_CFG
+            ),
             Completion.class
         );
 
@@ -3936,15 +3946,22 @@ public class StatementParserTests extends AbstractStatementParserTests {
     }
 
     public void testInvalidCompletion() {
-        expectError("FROM foo* | COMPLETION WITH inferenceId", "line 1:24: extraneous input 'WITH' expecting {");
+        expectError(
+            "FROM foo* | COMPLETION prompt WITH { \"inference_id\": 3 }",
+            "line 1:54: Option [inference_id] must be a valid string, found [3]"
+        );
+        expectError(
+            "FROM foo* | COMPLETION prompt WITH { \"inference_id\": \"inferenceId\", \"unknown_option\": 3 }",
+            "line 1:31: Inavalid option [unknown_option] in COMPLETION, expected one of [[inference_id]]"
+        );
 
-        expectError("FROM foo* | COMPLETION completion=prompt WITH", "line 1:46: mismatched input '<EOF>' expecting {");
+        expectError("FROM foo* | COMPLETION WITH inferenceId", "line 1:24: extraneous input 'WITH' expecting {");
 
-        expectError("FROM foo* | COMPLETION completion=prompt", "line 1:41: mismatched input '<EOF>' expecting {");
+        expectError("FROM foo* | COMPLETION completion=prompt WITH", "ine 1:46: mismatched input '<EOF>' expecting '{'");
 
         var fromPatterns = randomIndexPatterns(CROSS_CLUSTER);
         expectError(
-            "FROM " + fromPatterns + " | COMPLETION prompt_field WITH inferenceId",
+            "FROM " + fromPatterns + " | COMPLETION prompt_field WITH { \"inference_id\" : \"inference_id\" }",
             "invalid index pattern [" + unquoteIndexPattern(fromPatterns) + "], remote clusters are not supported with COMPLETION"
         );
     }

Some files were not shown because too many files changed in this diff