فهرست منبع

[8.x] Added optional parameters to QSTR ES|QL function (#121787) (#125112)

* Added optional parameters to QSTR ES|QL function (#121787)

Adds options to QSTR function.

#118619 added named function parameters. This PR uses this mechanism for allowing query string function parameters, so query string parameters can be used in ES|QL.

Closes #120933

(cherry picked from commit ee4bcac1db0e9e0732d8de5a9c05ee7b13bdf887)

# Conflicts:
#	x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java
#	x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java

* [CI] Auto commit changes from spotless

* Fix compilation error

* Fix missing import

* getFirst() -> get(0)

* Make method public

* Make asBuilder public in subclasses

* Revert "Make asBuilder public in subclasses"

This reverts commit f444aa6001efe74f261ec077a4021ef0bcf84b28.

* Revert "Make method public"

This reverts commit a1d9f56079be4507be86ebaeab34f66d20e1c32c.

* .asQueryBuilder() -> .toQueryBuilder()

* Remove extraneous change which sneaked in during backport

---------

Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co>
Svilen Mihaylov 6 ماه پیش
والد
کامیت
3e9bc9b05e
17فایلهای تغییر یافته به همراه767 افزوده شده و 102 حذف شده
  1. 6 0
      docs/changelog/121787.yaml
  2. 29 0
      docs/reference/esql/functions/functionNamedParams/qstr.asciidoc
  3. 4 0
      docs/reference/esql/functions/kibana/definition/qstr.json
  4. 26 24
      server/src/main/java/org/elasticsearch/index/query/QueryStringQueryBuilder.java
  5. 52 28
      x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/querydsl/query/QueryStringQuery.java
  6. 20 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec
  7. 5 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
  8. 1 1
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  9. 315 11
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java
  10. 17 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java
  11. 86 2
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java
  12. 7 3
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/NoneFieldFullTextFunctionTestCase.java
  13. 38 2
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryStringErrorTests.java
  14. 72 2
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryStringTests.java
  15. 38 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java
  16. 3 3
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/QueryStringQueryTests.java
  17. 48 26
      x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/querydsl/query/QueryStringQuery.java

+ 6 - 0
docs/changelog/121787.yaml

@@ -0,0 +1,6 @@
+pr: 121787
+summary: Added optional parameters to QSTR ES|QL function
+area: Search
+type: feature
+issues:
+ - 120933

+ 29 - 0
docs/reference/esql/functions/functionNamedParams/qstr.asciidoc

@@ -0,0 +1,29 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported function named parameters*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+name | types | description
+max_determinized_states | [integer] | Maximum number of automaton states required for the query. Default is 10000.
+fuzziness | [keyword] | Maximum edit distance allowed for matching.
+auto_generate_synonyms_phrase_query | [boolean] | If true, match phrase queries are automatically created for multi-term synonyms.
+phrase_slop | [integer] | Maximum number of positions allowed between matching tokens for phrases.
+default_field | [keyword] | Default field to search if no field is provided in the query string. Supports wildcards (*).
+allow_leading_wildcard | [boolean] | If true, the wildcard characters * and ? are allowed as the first character of the query string.
+minimum_should_match | [string] | Minimum number of clauses that must match for a document to be returned.
+fuzzy_transpositions | [boolean] | If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba).
+fuzzy_prefix_length | [integer] | Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.
+time_zone | [keyword] | Coordinated Universal Time (UTC) offset or IANA time zone used to convert date values in the query string to UTC.
+lenient | [boolean] | If false, format-based errors, such as providing a text query value for a numeric field, are returned.
+rewrite | [keyword] | Method used to rewrite the query.
+default_operator | [keyword] | Default boolean logic used to interpret text in the query string if no operators are specified.
+analyzer | [keyword] | Analyzer used to convert the text in the query value into token.
+fuzzy_max_expansions | [integer] | Maximum number of terms to which the query expands for fuzzy matching. Defaults to 50.
+quote_analyzer | [keyword] | Analyzer used to convert quoted text in the query string into tokens.
+allow_wildcard | [boolean] | If true, the query attempts to analyze wildcard terms in the query string.
+boost | [float] | Floating point number used to decrease or increase the relevance scores of the query.
+quote_field_suffix | [keyword] | Suffix appended to quoted text in the query string.
+enable_position_increments | [boolean] | If true, enable position increments in queries constructed from a query_string search. Defaults to true.
+fields | [keyword] | Array of fields to search. Supports wildcards (*).
+|===

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4 - 0
docs/reference/esql/functions/kibana/definition/qstr.json


+ 26 - 24
server/src/main/java/org/elasticsearch/index/query/QueryStringQueryBuilder.java

@@ -61,30 +61,32 @@ public final class QueryStringQueryBuilder extends AbstractQueryBuilder<QueryStr
     public static final MultiMatchQueryBuilder.Type DEFAULT_TYPE = MultiMatchQueryBuilder.Type.BEST_FIELDS;
     public static final boolean DEFAULT_FUZZY_TRANSPOSITIONS = FuzzyQuery.defaultTranspositions;
 
-    private static final ParseField QUERY_FIELD = new ParseField("query");
-    private static final ParseField FIELDS_FIELD = new ParseField("fields");
-    private static final ParseField DEFAULT_FIELD_FIELD = new ParseField("default_field");
-    private static final ParseField DEFAULT_OPERATOR_FIELD = new ParseField("default_operator");
-    private static final ParseField ANALYZER_FIELD = new ParseField("analyzer");
-    private static final ParseField QUOTE_ANALYZER_FIELD = new ParseField("quote_analyzer");
-    private static final ParseField ALLOW_LEADING_WILDCARD_FIELD = new ParseField("allow_leading_wildcard");
-    private static final ParseField MAX_DETERMINIZED_STATES_FIELD = new ParseField("max_determinized_states");
-    private static final ParseField ENABLE_POSITION_INCREMENTS_FIELD = new ParseField("enable_position_increments");
-    private static final ParseField ESCAPE_FIELD = new ParseField("escape");
-    private static final ParseField FUZZY_PREFIX_LENGTH_FIELD = new ParseField("fuzzy_prefix_length");
-    private static final ParseField FUZZY_MAX_EXPANSIONS_FIELD = new ParseField("fuzzy_max_expansions");
-    private static final ParseField FUZZY_REWRITE_FIELD = new ParseField("fuzzy_rewrite");
-    private static final ParseField PHRASE_SLOP_FIELD = new ParseField("phrase_slop");
-    private static final ParseField TIE_BREAKER_FIELD = new ParseField("tie_breaker");
-    private static final ParseField ANALYZE_WILDCARD_FIELD = new ParseField("analyze_wildcard");
-    private static final ParseField REWRITE_FIELD = new ParseField("rewrite");
-    private static final ParseField MINIMUM_SHOULD_MATCH_FIELD = new ParseField("minimum_should_match");
-    private static final ParseField QUOTE_FIELD_SUFFIX_FIELD = new ParseField("quote_field_suffix");
-    private static final ParseField LENIENT_FIELD = new ParseField("lenient");
-    private static final ParseField TIME_ZONE_FIELD = new ParseField("time_zone");
-    private static final ParseField TYPE_FIELD = new ParseField("type");
-    private static final ParseField GENERATE_SYNONYMS_PHRASE_QUERY = new ParseField("auto_generate_synonyms_phrase_query");
-    private static final ParseField FUZZY_TRANSPOSITIONS_FIELD = new ParseField("fuzzy_transpositions");
+    public static final ParseField QUERY_FIELD = new ParseField("query");
+    public static final ParseField BOOST_FIELD = new ParseField("boost");
+    public static final ParseField FIELDS_FIELD = new ParseField("fields");
+    public static final ParseField DEFAULT_FIELD_FIELD = new ParseField("default_field");
+    public static final ParseField DEFAULT_OPERATOR_FIELD = new ParseField("default_operator");
+    public static final ParseField ANALYZER_FIELD = new ParseField("analyzer");
+    public static final ParseField QUOTE_ANALYZER_FIELD = new ParseField("quote_analyzer");
+    public static final ParseField ALLOW_LEADING_WILDCARD_FIELD = new ParseField("allow_leading_wildcard");
+    public static final ParseField MAX_DETERMINIZED_STATES_FIELD = new ParseField("max_determinized_states");
+    public static final ParseField ENABLE_POSITION_INCREMENTS_FIELD = new ParseField("enable_position_increments");
+    public static final ParseField ESCAPE_FIELD = new ParseField("escape");
+    public static final ParseField FUZZINESS_FIELD = new ParseField("fuzziness");
+    public static final ParseField FUZZY_PREFIX_LENGTH_FIELD = new ParseField("fuzzy_prefix_length");
+    public static final ParseField FUZZY_MAX_EXPANSIONS_FIELD = new ParseField("fuzzy_max_expansions");
+    public static final ParseField FUZZY_REWRITE_FIELD = new ParseField("fuzzy_rewrite");
+    public static final ParseField PHRASE_SLOP_FIELD = new ParseField("phrase_slop");
+    public static final ParseField TIE_BREAKER_FIELD = new ParseField("tie_breaker");
+    public static final ParseField ANALYZE_WILDCARD_FIELD = new ParseField("analyze_wildcard");
+    public static final ParseField REWRITE_FIELD = new ParseField("rewrite");
+    public static final ParseField MINIMUM_SHOULD_MATCH_FIELD = new ParseField("minimum_should_match");
+    public static final ParseField QUOTE_FIELD_SUFFIX_FIELD = new ParseField("quote_field_suffix");
+    public static final ParseField LENIENT_FIELD = new ParseField("lenient");
+    public static final ParseField TIME_ZONE_FIELD = new ParseField("time_zone");
+    public static final ParseField TYPE_FIELD = new ParseField("type");
+    public static final ParseField GENERATE_SYNONYMS_PHRASE_QUERY = new ParseField("auto_generate_synonyms_phrase_query");
+    public static final ParseField FUZZY_TRANSPOSITIONS_FIELD = new ParseField("fuzzy_transpositions");
 
     private final String queryString;
 

+ 52 - 28
x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/querydsl/query/QueryStringQuery.java

@@ -8,7 +8,6 @@ package org.elasticsearch.xpack.esql.core.querydsl.query;
 
 import org.elasticsearch.common.unit.Fuzziness;
 import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
-import org.elasticsearch.core.Booleans;
 import org.elasticsearch.index.query.MultiMatchQueryBuilder;
 import org.elasticsearch.index.query.Operator;
 import org.elasticsearch.index.query.QueryBuilder;
@@ -22,41 +21,66 @@ import java.util.Objects;
 import java.util.function.BiConsumer;
 
 import static java.util.Map.entry;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ALLOW_LEADING_WILDCARD_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ANALYZER_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ANALYZE_WILDCARD_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.DEFAULT_FIELD_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.DEFAULT_OPERATOR_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ENABLE_POSITION_INCREMENTS_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ESCAPE_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZINESS_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZY_MAX_EXPANSIONS_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZY_PREFIX_LENGTH_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZY_REWRITE_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZY_TRANSPOSITIONS_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.GENERATE_SYNONYMS_PHRASE_QUERY;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.LENIENT_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.MAX_DETERMINIZED_STATES_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.MINIMUM_SHOULD_MATCH_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.PHRASE_SLOP_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.QUOTE_ANALYZER_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.QUOTE_FIELD_SUFFIX_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.REWRITE_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.TIE_BREAKER_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.TIME_ZONE_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.TYPE_FIELD;
 
 public class QueryStringQuery extends Query {
 
-    // TODO: it'd be great if these could be constants instead of Strings, needs a core change to make the fields public first
-    private static final Map<String, BiConsumer<QueryStringQueryBuilder, String>> BUILDER_APPLIERS = Map.ofEntries(
-        entry("allow_leading_wildcard", (qb, s) -> qb.allowLeadingWildcard(Booleans.parseBoolean(s))),
-        entry("analyze_wildcard", (qb, s) -> qb.analyzeWildcard(Booleans.parseBoolean(s))),
-        entry("analyzer", QueryStringQueryBuilder::analyzer),
-        entry("auto_generate_synonyms_phrase_query", (qb, s) -> qb.autoGenerateSynonymsPhraseQuery(Booleans.parseBoolean(s))),
-        entry("default_field", QueryStringQueryBuilder::defaultField),
-        entry("default_operator", (qb, s) -> qb.defaultOperator(Operator.fromString(s))),
-        entry("enable_position_increments", (qb, s) -> qb.enablePositionIncrements(Booleans.parseBoolean(s))),
-        entry("escape", (qb, s) -> qb.escape(Booleans.parseBoolean(s))),
-        entry("fuzziness", (qb, s) -> qb.fuzziness(Fuzziness.fromString(s))),
-        entry("fuzzy_max_expansions", (qb, s) -> qb.fuzzyMaxExpansions(Integer.valueOf(s))),
-        entry("fuzzy_prefix_length", (qb, s) -> qb.fuzzyPrefixLength(Integer.valueOf(s))),
-        entry("fuzzy_rewrite", QueryStringQueryBuilder::fuzzyRewrite),
-        entry("fuzzy_transpositions", (qb, s) -> qb.fuzzyTranspositions(Booleans.parseBoolean(s))),
-        entry("lenient", (qb, s) -> qb.lenient(Booleans.parseBoolean(s))),
-        entry("max_determinized_states", (qb, s) -> qb.maxDeterminizedStates(Integer.valueOf(s))),
-        entry("minimum_should_match", QueryStringQueryBuilder::minimumShouldMatch),
-        entry("phrase_slop", (qb, s) -> qb.phraseSlop(Integer.valueOf(s))),
-        entry("rewrite", QueryStringQueryBuilder::rewrite),
-        entry("quote_analyzer", QueryStringQueryBuilder::quoteAnalyzer),
-        entry("quote_field_suffix", QueryStringQueryBuilder::quoteFieldSuffix),
-        entry("tie_breaker", (qb, s) -> qb.tieBreaker(Float.valueOf(s))),
-        entry("time_zone", QueryStringQueryBuilder::timeZone),
-        entry("type", (qb, s) -> qb.type(MultiMatchQueryBuilder.Type.parse(s, LoggingDeprecationHandler.INSTANCE)))
+    private static final Map<String, BiConsumer<QueryStringQueryBuilder, Object>> BUILDER_APPLIERS = Map.ofEntries(
+        entry(ALLOW_LEADING_WILDCARD_FIELD.getPreferredName(), (qb, obj) -> qb.allowLeadingWildcard((Boolean) obj)),
+        entry(ANALYZE_WILDCARD_FIELD.getPreferredName(), (qb, obj) -> qb.analyzeWildcard((Boolean) obj)),
+        entry(ANALYZER_FIELD.getPreferredName(), (qb, obj) -> qb.analyzer((String) obj)),
+        entry(GENERATE_SYNONYMS_PHRASE_QUERY.getPreferredName(), (qb, obj) -> qb.autoGenerateSynonymsPhraseQuery((Boolean) obj)),
+        entry(DEFAULT_FIELD_FIELD.getPreferredName(), (qb, obj) -> qb.defaultField((String) obj)),
+        entry(DEFAULT_OPERATOR_FIELD.getPreferredName(), (qb, obj) -> qb.defaultOperator(Operator.fromString((String) obj))),
+        entry(ENABLE_POSITION_INCREMENTS_FIELD.getPreferredName(), (qb, obj) -> qb.enablePositionIncrements((Boolean) obj)),
+        entry(ESCAPE_FIELD.getPreferredName(), (qb, obj) -> qb.escape((Boolean) obj)),
+        entry(FUZZINESS_FIELD.getPreferredName(), (qb, obj) -> qb.fuzziness(Fuzziness.fromString((String) obj))),
+        entry(FUZZY_MAX_EXPANSIONS_FIELD.getPreferredName(), (qb, obj) -> qb.fuzzyMaxExpansions((Integer) obj)),
+        entry(FUZZY_PREFIX_LENGTH_FIELD.getPreferredName(), (qb, obj) -> qb.fuzzyPrefixLength((Integer) obj)),
+        entry(FUZZY_REWRITE_FIELD.getPreferredName(), (qb, obj) -> qb.fuzzyRewrite((String) obj)),
+        entry(FUZZY_TRANSPOSITIONS_FIELD.getPreferredName(), (qb, obj) -> qb.fuzzyTranspositions((Boolean) obj)),
+        entry(LENIENT_FIELD.getPreferredName(), (qb, obj) -> qb.lenient((Boolean) obj)),
+        entry(MAX_DETERMINIZED_STATES_FIELD.getPreferredName(), (qb, obj) -> qb.maxDeterminizedStates((Integer) obj)),
+        entry(MINIMUM_SHOULD_MATCH_FIELD.getPreferredName(), (qb, obj) -> qb.minimumShouldMatch((String) obj)),
+        entry(PHRASE_SLOP_FIELD.getPreferredName(), (qb, obj) -> qb.phraseSlop((Integer) obj)),
+        entry(REWRITE_FIELD.getPreferredName(), (qb, obj) -> qb.rewrite((String) obj)),
+        entry(QUOTE_ANALYZER_FIELD.getPreferredName(), (qb, obj) -> qb.quoteAnalyzer((String) obj)),
+        entry(QUOTE_FIELD_SUFFIX_FIELD.getPreferredName(), (qb, obj) -> qb.quoteFieldSuffix((String) obj)),
+        entry(TIE_BREAKER_FIELD.getPreferredName(), (qb, obj) -> qb.tieBreaker((Float) obj)),
+        entry(TIME_ZONE_FIELD.getPreferredName(), (qb, obj) -> qb.timeZone((String) obj)),
+        entry(
+            TYPE_FIELD.getPreferredName(),
+            (qb, obj) -> qb.type(MultiMatchQueryBuilder.Type.parse((String) obj, LoggingDeprecationHandler.INSTANCE))
+        )
     );
 
     private final String query;
     private final Map<String, Float> fields;
-    private final Map<String, String> options;
+    private final Map<String, Object> options;
 
-    public QueryStringQuery(Source source, String query, Map<String, Float> fields, Map<String, String> options) {
+    public QueryStringQuery(Source source, String query, Map<String, Float> fields, Map<String, Object> options) {
         super(source);
         this.query = query;
         this.fields = fields;

+ 20 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec

@@ -189,3 +189,23 @@ book_no:keyword
 7140
 2714
 ;
+
+qstrWithFieldAndOptions
+required_capability: qstr_function
+required_capability: query_string_function_options
+
+// tag::qstr-with-options[]
+FROM books 
+| WHERE QSTR("title: Hobbjt~", {"fuzziness": 2})
+| KEEP book_no, title 
+| SORT book_no 
+| LIMIT 5
+// end::qstr-with-options[]
+;
+ignoreOrder: true
+
+book_no:keyword | title:text                        
+4289            | Poems from the Hobbit             
+6405            | The Hobbit or There and Back Again
+7480            | The Hobbit                                     
+;

+ 5 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

@@ -678,6 +678,11 @@ public class EsqlCapabilities {
          */
         MATCH_FUNCTION_OPTIONS,
 
+        /**
+         * Support options in the query string function.
+         */
+        QUERY_STRING_FUNCTION_OPTIONS,
+
         /**
          * Support for aggregate_metric_double type
          */

+ 1 - 1
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java

@@ -419,7 +419,7 @@ public class EsqlFunctionRegistry {
             new FunctionDefinition[] {
                 def(Kql.class, uni(Kql::new), "kql"),
                 def(Match.class, tri(Match::new), "match"),
-                def(QueryString.class, uni(QueryString::new), "qstr") } };
+                def(QueryString.class, bi(QueryString::new), "qstr") } };
 
     }
 

+ 315 - 11
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java

@@ -7,31 +7,101 @@
 
 package org.elasticsearch.xpack.esql.expression.function.fulltext;
 
+import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.TransportVersions;
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.xpack.esql.core.InvalidArgumentException;
+import org.elasticsearch.xpack.esql.core.expression.EntryExpression;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.Literal;
+import org.elasticsearch.xpack.esql.core.expression.MapExpression;
 import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
 import org.elasticsearch.xpack.esql.core.querydsl.query.QueryStringQuery;
 import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
 import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.core.type.DataTypeConverter;
 import org.elasticsearch.xpack.esql.expression.function.Example;
 import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.MapParam;
+import org.elasticsearch.xpack.esql.expression.function.OptionalArgument;
 import org.elasticsearch.xpack.esql.expression.function.Param;
 import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
 import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
 
 import java.io.IOException;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
+
+import static java.util.Map.entry;
+import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ALLOW_LEADING_WILDCARD_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ANALYZER_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ANALYZE_WILDCARD_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.BOOST_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.DEFAULT_FIELD_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.DEFAULT_OPERATOR_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ENABLE_POSITION_INCREMENTS_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZINESS_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZY_MAX_EXPANSIONS_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZY_PREFIX_LENGTH_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZY_TRANSPOSITIONS_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.GENERATE_SYNONYMS_PHRASE_QUERY;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.LENIENT_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.MAX_DETERMINIZED_STATES_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.MINIMUM_SHOULD_MATCH_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.PHRASE_SLOP_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.QUOTE_ANALYZER_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.QUOTE_FIELD_SUFFIX_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.REWRITE_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.TIME_ZONE_FIELD;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isFoldable;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isMapExpression;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNull;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable;
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType;
+import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN;
+import static org.elasticsearch.xpack.esql.core.type.DataType.FLOAT;
+import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER;
+import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD;
+import static org.elasticsearch.xpack.esql.core.type.DataType.SEMANTIC_TEXT;
+import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT;
 
 /**
  * Full text function that performs a {@link QueryStringQuery} .
  */
-public class QueryString extends FullTextFunction {
+public class QueryString extends FullTextFunction implements OptionalArgument {
+
+    public static final Map<String, DataType> ALLOWED_OPTIONS = Map.ofEntries(
+        entry(BOOST_FIELD.getPreferredName(), FLOAT),
+        entry(ALLOW_LEADING_WILDCARD_FIELD.getPreferredName(), BOOLEAN),
+        entry(ANALYZE_WILDCARD_FIELD.getPreferredName(), BOOLEAN),
+        entry(ANALYZER_FIELD.getPreferredName(), KEYWORD),
+        entry(GENERATE_SYNONYMS_PHRASE_QUERY.getPreferredName(), BOOLEAN),
+        entry(DEFAULT_FIELD_FIELD.getPreferredName(), KEYWORD),
+        entry(DEFAULT_OPERATOR_FIELD.getPreferredName(), KEYWORD),
+        entry(ENABLE_POSITION_INCREMENTS_FIELD.getPreferredName(), BOOLEAN),
+        entry(FUZZINESS_FIELD.getPreferredName(), KEYWORD),
+        entry(FUZZY_MAX_EXPANSIONS_FIELD.getPreferredName(), INTEGER),
+        entry(FUZZY_PREFIX_LENGTH_FIELD.getPreferredName(), INTEGER),
+        entry(FUZZY_TRANSPOSITIONS_FIELD.getPreferredName(), BOOLEAN),
+        entry(LENIENT_FIELD.getPreferredName(), BOOLEAN),
+        entry(MAX_DETERMINIZED_STATES_FIELD.getPreferredName(), INTEGER),
+        entry(MINIMUM_SHOULD_MATCH_FIELD.getPreferredName(), KEYWORD),
+        entry(QUOTE_ANALYZER_FIELD.getPreferredName(), KEYWORD),
+        entry(QUOTE_FIELD_SUFFIX_FIELD.getPreferredName(), KEYWORD),
+        entry(PHRASE_SLOP_FIELD.getPreferredName(), INTEGER),
+        entry(REWRITE_FIELD.getPreferredName(), KEYWORD),
+        entry(TIME_ZONE_FIELD.getPreferredName(), KEYWORD)
+    );
 
     public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
         Expression.class,
@@ -44,7 +114,9 @@ public class QueryString extends FullTextFunction {
         preview = true,
         description = "Performs a <<query-dsl-query-string-query,query string query>>. "
             + "Returns true if the provided query string matches the row.",
-        examples = { @Example(file = "qstr-function", tag = "qstr-with-field") }
+        examples = {
+            @Example(file = "qstr-function", tag = "qstr-with-field"),
+            @Example(file = "qstr-function", tag = "qstr-with-options") }
     )
     public QueryString(
         Source source,
@@ -52,13 +124,157 @@ public class QueryString extends FullTextFunction {
             name = "query",
             type = { "keyword", "text" },
             description = "Query string in Lucene query string format."
-        ) Expression queryString
+        ) Expression queryString,
+        @MapParam(
+            name = "options",
+            params = {
+                @MapParam.MapParamEntry(
+                    name = "default_field",
+                    type = "keyword",
+                    valueHint = { "standard" },
+                    description = "Default field to search if no field is provided in the query string. Supports wildcards (*)."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "allow_leading_wildcard",
+                    type = "boolean",
+                    valueHint = { "true", "false" },
+                    description = "If true, the wildcard characters * and ? are allowed as the first character of the query string. "
+                        + "Defaults to true."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "allow_wildcard",
+                    type = "boolean",
+                    valueHint = { "false", "true" },
+                    description = "If true, the query attempts to analyze wildcard terms in the query string. Defaults to false. "
+                ),
+                @MapParam.MapParamEntry(
+                    name = "analyzer",
+                    type = "keyword",
+                    valueHint = { "standard" },
+                    description = "Analyzer used to convert the text in the query value into token. "
+                        + "Defaults to the index-time analyzer mapped for the default_field."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "auto_generate_synonyms_phrase_query",
+                    type = "boolean",
+                    valueHint = { "true", "false" },
+                    description = "If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "fuzziness",
+                    type = "keyword",
+                    valueHint = { "AUTO", "1", "2" },
+                    description = "Maximum edit distance allowed for matching."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "boost",
+                    type = "float",
+                    valueHint = { "2.5" },
+                    description = "Floating point number used to decrease or increase the relevance scores of the query."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "default_operator",
+                    type = "keyword",
+                    valueHint = { "OR", "AND" },
+                    description = "Default boolean logic used to interpret text in the query string if no operators are specified."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "enable_position_increments",
+                    type = "boolean",
+                    valueHint = { "true", "false" },
+                    description = "If true, enable position increments in queries constructed from a query_string search. Defaults to true."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "fields",
+                    type = "keyword",
+                    valueHint = { "standard" },
+                    description = "Array of fields to search. Supports wildcards (*)."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "fuzzy_max_expansions",
+                    type = "integer",
+                    valueHint = { "50" },
+                    description = "Maximum number of terms to which the query expands for fuzzy matching. Defaults to 50."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "fuzzy_prefix_length",
+                    type = "integer",
+                    valueHint = { "0" },
+                    description = "Number of beginning characters left unchanged for fuzzy matching. Defaults to 0."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "fuzzy_transpositions",
+                    type = "boolean",
+                    valueHint = { "true", "false" },
+                    description = "If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). "
+                        + "Defaults to true."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "lenient",
+                    type = "boolean",
+                    valueHint = { "true", "false" },
+                    description = "If false, format-based errors, such as providing a text query value for a numeric field, are returned. "
+                        + "Defaults to false."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "max_determinized_states",
+                    type = "integer",
+                    valueHint = { "10000" },
+                    description = "Maximum number of automaton states required for the query. Default is 10000."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "minimum_should_match",
+                    type = "string",
+                    valueHint = { "standard" },
+                    description = "Minimum number of clauses that must match for a document to be returned."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "quote_analyzer",
+                    type = "keyword",
+                    valueHint = { "standard" },
+                    description = "Analyzer used to convert quoted text in the query string into tokens. "
+                        + "Defaults to the search_quote_analyzer mapped for the default_field."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "phrase_slop",
+                    type = "integer",
+                    valueHint = { "0" },
+                    description = "Maximum number of positions allowed between matching tokens for phrases. "
+                        + "Defaults to 0 (which means exact matches are required)."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "quote_field_suffix",
+                    type = "keyword",
+                    valueHint = { "standard" },
+                    description = "Suffix appended to quoted text in the query string."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "rewrite",
+                    type = "keyword",
+                    valueHint = { "standard" },
+                    description = "Method used to rewrite the query."
+                ),
+                @MapParam.MapParamEntry(
+                    name = "time_zone",
+                    type = "keyword",
+                    valueHint = { "standard" },
+                    description = "Coordinated Universal Time (UTC) offset or IANA time zone used to convert date values in the "
+                        + "query string to UTC."
+                ), },
+            description = "(Optional) Additional options for Query String as <<esql-function-named-params,function named parameters>>."
+                + " See <<query-dsl-query-string-query,query string query>> for more information.",
+            optional = true
+        ) Expression options
     ) {
-        super(source, queryString, List.of(queryString), null);
+        this(source, queryString, options, null);
     }
 
-    public QueryString(Source source, Expression queryString, QueryBuilder queryBuilder) {
-        super(source, queryString, List.of(queryString), queryBuilder);
+    // Options for QueryString. They don't need to be serialized as the data nodes will retrieve them from the query builder.
+    private final transient Expression options;
+
+    public QueryString(Source source, Expression queryString, Expression options, QueryBuilder queryBuilder) {
+        super(source, queryString, options == null ? List.of(queryString) : List.of(queryString, options), queryBuilder);
+        this.options = options;
     }
 
     private static QueryString readFrom(StreamInput in) throws IOException {
@@ -68,7 +284,7 @@ public class QueryString extends FullTextFunction {
         if (in.getTransportVersion().onOrAfter(TransportVersions.ESQL_QUERY_BUILDER_IN_SEARCH_FUNCTIONS)) {
             queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class);
         }
-        return new QueryString(source, query, queryBuilder);
+        return new QueryString(source, query, null, queryBuilder);
     }
 
     @Override
@@ -90,23 +306,111 @@ public class QueryString extends FullTextFunction {
         return "QSTR";
     }
 
+    public Expression options() {
+        return options;
+    }
+
+    public static final Set<DataType> QUERY_DATA_TYPES = Set.of(KEYWORD, TEXT, SEMANTIC_TEXT);
+
+    private TypeResolution resolveQuery() {
+        return isType(query(), QUERY_DATA_TYPES::contains, sourceText(), FIRST, "keyword, text, semantic_text").and(
+            isNotNullAndFoldable(query(), sourceText(), FIRST)
+        );
+    }
+
+    private Map<String, Object> queryStringOptions() throws InvalidArgumentException {
+        if (options() == null) {
+            return null;
+        }
+
+        Map<String, Object> matchOptions = new HashMap<>();
+        for (EntryExpression entry : ((MapExpression) options()).entryExpressions()) {
+            Expression optionExpr = entry.key();
+            Expression valueExpr = entry.value();
+            TypeResolution resolution = isFoldable(optionExpr, sourceText(), SECOND).and(isFoldable(valueExpr, sourceText(), SECOND));
+            if (resolution.unresolved()) {
+                throw new InvalidArgumentException(resolution.message());
+            }
+            Object optionExprLiteral = ((Literal) optionExpr).value();
+            Object valueExprLiteral = ((Literal) valueExpr).value();
+            String optionName = optionExprLiteral instanceof BytesRef br ? br.utf8ToString() : optionExprLiteral.toString();
+            String optionValue = valueExprLiteral instanceof BytesRef br ? br.utf8ToString() : valueExprLiteral.toString();
+            // validate the optionExpr is supported
+            DataType dataType = ALLOWED_OPTIONS.get(optionName);
+            if (dataType == null) {
+                throw new InvalidArgumentException(
+                    format(null, "Invalid option [{}] in [{}], expected one of {}", optionName, sourceText(), ALLOWED_OPTIONS.keySet())
+                );
+            }
+            try {
+                matchOptions.put(optionName, DataTypeConverter.convert(optionValue, dataType));
+            } catch (InvalidArgumentException e) {
+                throw new InvalidArgumentException(
+                    format(null, "Invalid option [{}] in [{}], {}", optionName, sourceText(), e.getMessage())
+                );
+            }
+        }
+
+        return matchOptions;
+    }
+
+    private TypeResolution resolveOptions() {
+        if (options() != null) {
+            TypeResolution resolution = isNotNull(options(), sourceText(), SECOND);
+            if (resolution.unresolved()) {
+                return resolution;
+            }
+            // MapExpression does not have a DataType associated with it
+            resolution = isMapExpression(options(), sourceText(), SECOND);
+            if (resolution.unresolved()) {
+                return resolution;
+            }
+
+            try {
+                queryStringOptions();
+            } catch (InvalidArgumentException e) {
+                return new TypeResolution(e.getMessage());
+            }
+        }
+        return TypeResolution.TYPE_RESOLVED;
+    }
+
+    @Override
+    protected TypeResolution resolveParams() {
+        return resolveQuery().and(resolveOptions());
+    }
+
     @Override
     public Expression replaceChildren(List<Expression> newChildren) {
-        return new QueryString(source(), newChildren.get(0), queryBuilder());
+        return new QueryString(source(), newChildren.get(0), newChildren.size() == 1 ? null : newChildren.get(1), queryBuilder());
     }
 
     @Override
     protected NodeInfo<? extends Expression> info() {
-        return NodeInfo.create(this, QueryString::new, query(), queryBuilder());
+        return NodeInfo.create(this, QueryString::new, query(), options(), queryBuilder());
     }
 
     @Override
     protected Query translate(TranslatorHandler handler) {
-        return new QueryStringQuery(source(), Objects.toString(queryAsObject()), Map.of(), Map.of());
+        return new QueryStringQuery(source(), Objects.toString(queryAsObject()), Map.of(), queryStringOptions());
     }
 
     @Override
     public Expression replaceQueryBuilder(QueryBuilder queryBuilder) {
-        return new QueryString(source(), query(), queryBuilder);
+        return new QueryString(source(), query(), options(), queryBuilder);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        // QueryString does not serialize options, as they get included in the query builder. We need to override equals and hashcode to
+        // ignore options when comparing.
+        if (o == null || getClass() != o.getClass()) return false;
+        var qstr = (QueryString) o;
+        return Objects.equals(query(), qstr.query()) && Objects.equals(queryBuilder(), qstr.queryBuilder());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(query(), queryBuilder());
     }
 }

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

@@ -43,6 +43,7 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.Max;
 import org.elasticsearch.xpack.esql.expression.function.aggregate.Min;
 import org.elasticsearch.xpack.esql.expression.function.fulltext.Match;
 import org.elasticsearch.xpack.esql.expression.function.fulltext.MatchOperator;
+import org.elasticsearch.xpack.esql.expression.function.fulltext.QueryString;
 import org.elasticsearch.xpack.esql.index.EsIndex;
 import org.elasticsearch.xpack.esql.index.IndexResolution;
 import org.elasticsearch.xpack.esql.parser.ParsingException;
@@ -2808,6 +2809,22 @@ public class AnalyzerTests extends ESTestCase {
         assertEquals(DataType.DOUBLE, ee.dataType());
     }
 
+    public void testFunctionNamedParamsAsFunctionArgument1() {
+        LogicalPlan plan = analyze("""
+            from test
+            | WHERE QSTR("first_name: Anna", {"minimum_should_match": 3.0})
+            """);
+        Limit limit = as(plan, Limit.class);
+        Filter filter = as(limit.child(), Filter.class);
+        QueryString qstr = as(filter.condition(), QueryString.class);
+        MapExpression me = as(qstr.options(), MapExpression.class);
+        assertEquals(1, me.entryExpressions().size());
+        EntryExpression ee = as(me.entryExpressions().get(0), EntryExpression.class);
+        assertEquals(new Literal(EMPTY, "minimum_should_match", DataType.KEYWORD), ee.key());
+        assertEquals(new Literal(EMPTY, 3.0, DataType.DOUBLE), ee.value());
+        assertEquals(DataType.DOUBLE, ee.dataType());
+    }
+
     private void verifyUnsupported(String query, String errorMessage) {
         verifyUnsupported(query, errorMessage, "mapping-multi-field-variation.json");
     }

+ 86 - 2
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java

@@ -18,6 +18,7 @@ import org.elasticsearch.xpack.esql.core.type.EsField;
 import org.elasticsearch.xpack.esql.core.type.InvalidMappedField;
 import org.elasticsearch.xpack.esql.core.type.UnsupportedEsField;
 import org.elasticsearch.xpack.esql.expression.function.fulltext.Match;
+import org.elasticsearch.xpack.esql.expression.function.fulltext.QueryString;
 import org.elasticsearch.xpack.esql.index.EsIndex;
 import org.elasticsearch.xpack.esql.index.IndexResolution;
 import org.elasticsearch.xpack.esql.parser.EsqlParser;
@@ -35,10 +36,15 @@ import java.util.Set;
 import static org.elasticsearch.xpack.esql.EsqlTestUtils.paramAsConstant;
 import static org.elasticsearch.xpack.esql.EsqlTestUtils.withDefaultLimitWarning;
 import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.loadMapping;
+import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN;
 import static org.elasticsearch.xpack.esql.core.type.DataType.COUNTER_DOUBLE;
 import static org.elasticsearch.xpack.esql.core.type.DataType.COUNTER_INTEGER;
 import static org.elasticsearch.xpack.esql.core.type.DataType.COUNTER_LONG;
+import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE;
+import static org.elasticsearch.xpack.esql.core.type.DataType.FLOAT;
+import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER;
 import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD;
+import static org.elasticsearch.xpack.esql.core.type.DataType.LONG;
 import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
@@ -1367,10 +1373,10 @@ public class VerifierTests extends ESTestCase {
 
     public void testQueryStringFunctionArgNotNullOrConstant() throws Exception {
         assertEquals(
-            "1:19: argument of [qstr(first_name)] must be a constant, received [first_name]",
+            "1:19: first argument of [qstr(first_name)] must be a constant, received [first_name]",
             error("from test | where qstr(first_name)")
         );
-        assertEquals("1:19: argument of [qstr(null)] cannot be null, received [null]", error("from test | where qstr(null)"));
+        assertEquals("1:19: first argument of [qstr(null)] cannot be null, received [null]", error("from test | where qstr(null)"));
         // Other value types are tested in QueryStringFunctionTests
     }
 
@@ -2110,6 +2116,84 @@ public class VerifierTests extends ESTestCase {
         );
     }
 
+    public void testQueryStringOptions() {
+        // Check positive cases
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"analyzer\": \"standard\"})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"allow_leading_wildcard\": false})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"analyze_wildcard\": false})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"auto_generate_synonyms_phrase_query\": true})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"boost\": 2.1})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"default_field\": \"field1\"})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"default_operator\": \"AND\"})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"enable_position_increments\": false})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"fuzziness\": 2})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"fuzziness\": \"AUTO\"})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"fuzzy_prefix_length\": 5})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"fuzzy_transpositions\": false})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"lenient\": false})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"max_determinized_states\": 10})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"minimum_should_match\": \"2\"})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"quote_analyzer\": \"qnalyzer_1\"})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"quote_field_suffix\": \"q_suffix\"})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"phrase_slop\": 10})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"rewrite\": \"r1\"})");
+        query("FROM test | WHERE QSTR(\"first_name: Jean\", {\"time_zone\": \"time_zone\"})");
+
+        // Check all data types for available options
+        DataType[] optionTypes = new DataType[] { INTEGER, LONG, FLOAT, DOUBLE, KEYWORD, BOOLEAN };
+        for (Map.Entry<String, DataType> allowedOptions : QueryString.ALLOWED_OPTIONS.entrySet()) {
+            String optionName = allowedOptions.getKey();
+            DataType optionType = allowedOptions.getValue();
+            // Check every possible type for the option - we'll try to convert it to the expected type
+            for (DataType currentType : optionTypes) {
+                String optionValue = switch (currentType) {
+                    case BOOLEAN -> String.valueOf(randomBoolean());
+                    case INTEGER -> String.valueOf(randomIntBetween(0, 100000));
+                    case LONG -> String.valueOf(randomLong());
+                    case FLOAT -> String.valueOf(randomFloat());
+                    case DOUBLE -> String.valueOf(randomDouble());
+                    case KEYWORD -> randomAlphaOfLength(10);
+                    default -> throw new IllegalArgumentException("Unsupported option type: " + currentType);
+                };
+                String queryOptionValue = optionValue;
+                if (currentType == KEYWORD) {
+                    queryOptionValue = "\"" + optionValue + "\"";
+                }
+
+                String query = "FROM test | WHERE QSTR(\"first_name: Jean\", {\"" + optionName + "\": " + queryOptionValue + "})";
+                try {
+                    // Check conversion is possible
+                    DataTypeConverter.convert(optionValue, optionType);
+                    // If no exception was thrown, conversion is possible and should be done
+                    query(query);
+                } catch (InvalidArgumentException e) {
+                    // Conversion is not possible, query should fail
+                    assertEquals(
+                        "1:19: Invalid option ["
+                            + optionName
+                            + "] in [QSTR(\"first_name: Jean\", {\""
+                            + optionName
+                            + "\": "
+                            + queryOptionValue
+                            + "})], cannot cast ["
+                            + optionValue
+                            + "] to ["
+                            + optionType.typeName()
+                            + "]",
+                        error(query)
+                    );
+                }
+            }
+        }
+
+        assertThat(
+            error("FROM test |  WHERE QSTR(\"first_name: Jean\", {\"unknown_option\": true})"),
+            containsString(
+                "1:20: Invalid option [unknown_option] in [QSTR(\"first_name: Jean\", {\"unknown_option\": true})]," + " expected one of "
+            )
+        );
+    }
+
     private void query(String query) {
         query(query, defaultAnalyzer);
     }

+ 7 - 3
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/NoneFieldFullTextFunctionTestCase.java

@@ -26,12 +26,12 @@ public abstract class NoneFieldFullTextFunctionTestCase extends AbstractFunction
         this.testCase = testCaseSupplier.get();
     }
 
-    public final void testFold() {
+    public void testFold() {
         Expression expression = buildLiteralExpression(testCase);
         assertFalse("expected resolved", expression.typeResolved().unresolved());
     }
 
-    protected static Iterable<Object[]> generateParameters() {
+    protected static List<TestCaseSupplier> getStringTestSupplier() {
         List<TestCaseSupplier> suppliers = new LinkedList<>();
         for (DataType strType : DataType.stringTypes()) {
             suppliers.add(
@@ -42,7 +42,11 @@ public abstract class NoneFieldFullTextFunctionTestCase extends AbstractFunction
                 )
             );
         }
-        return parameterSuppliersFromTypedData(suppliers);
+        return suppliers;
+    }
+
+    protected static Iterable<Object[]> generateParameters() {
+        return parameterSuppliersFromTypedData(getStringTestSupplier());
     }
 
     private static TestCaseSupplier.TestCase testCase(DataType strType, String str, Matcher<Boolean> matcher) {

+ 38 - 2
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryStringErrorTests.java

@@ -8,16 +8,21 @@
 package org.elasticsearch.xpack.esql.expression.function.fulltext;
 
 import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.TypeResolutions;
 import org.elasticsearch.xpack.esql.core.tree.Source;
 import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
 import org.elasticsearch.xpack.esql.expression.function.ErrorsForCasesWithoutExamplesTestCase;
 import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison;
 import org.hamcrest.Matcher;
 
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import java.util.stream.Stream;
 
+import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
 import static org.hamcrest.Matchers.equalTo;
 
 public class QueryStringErrorTests extends ErrorsForCasesWithoutExamplesTestCase {
@@ -34,11 +39,42 @@ public class QueryStringErrorTests extends ErrorsForCasesWithoutExamplesTestCase
 
     @Override
     protected Expression build(Source source, List<Expression> args) {
-        return new QueryString(source, args.get(0));
+        return new QueryString(source, args.get(0), args.size() > 1 ? args.get(1) : null);
     }
 
     @Override
     protected Matcher<String> expectedTypeErrorMatcher(List<Set<DataType>> validPerPosition, List<DataType> signature) {
-        return equalTo(typeErrorMessage(false, validPerPosition, signature, (v, p) -> "string"));
+        return equalTo(errorMessageStringForMatch(validPerPosition, signature, (l, p) -> "keyword, text, semantic_text"));
+    }
+
+    private static String errorMessageStringForMatch(
+        List<Set<DataType>> validPerPosition,
+        List<DataType> signature,
+        AbstractFunctionTestCase.PositionalErrorMessageSupplier positionalErrorMessageSupplier
+    ) {
+        boolean invalid = false;
+        for (int i = 0; i < signature.size() && invalid == false; i++) {
+            // Need to check for nulls and bad parameters in order
+            if (signature.get(i) == DataType.NULL) {
+                return TypeResolutions.ParamOrdinal.fromIndex(i).name().toLowerCase(Locale.ROOT)
+                    + " argument of ["
+                    + sourceForSignature(signature)
+                    + "] cannot be null, received []";
+            }
+            if (validPerPosition.get(i).contains(signature.get(i)) == false) {
+                // Map expressions have different error messages
+                if (i == 1) {
+                    return format(null, "second argument of [{}] must be a map expression, received []", sourceForSignature(signature));
+                }
+                break;
+            }
+        }
+
+        try {
+            return typeErrorMessage(true, validPerPosition, signature, positionalErrorMessageSupplier);
+        } catch (IllegalStateException e) {
+            // This means all the positional args were okay, so the expected error is for nulls or from the combination
+            return EsqlBinaryComparison.formatIncompatibleTypesMessage(signature.get(0), signature.get(1), sourceForSignature(signature));
+        }
     }
 }

+ 72 - 2
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryStringTests.java

@@ -10,14 +10,27 @@ package org.elasticsearch.xpack.esql.expression.function.fulltext;
 import com.carrotsearch.randomizedtesting.annotations.Name;
 import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
 
+import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.Literal;
+import org.elasticsearch.xpack.esql.core.expression.MapExpression;
 import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
 import org.elasticsearch.xpack.esql.expression.function.FunctionName;
 import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.esql.io.stream.PlanStreamOutput;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Supplier;
 
+import static org.elasticsearch.xpack.esql.SerializationTestUtils.serializeDeserialize;
+import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN;
+import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD;
+import static org.elasticsearch.xpack.esql.core.type.DataType.UNSUPPORTED;
+import static org.elasticsearch.xpack.esql.planner.TranslatorHandler.TRANSLATOR_HANDLER;
+import static org.hamcrest.Matchers.equalTo;
+
 @FunctionName("qstr")
 public class QueryStringTests extends NoneFieldFullTextFunctionTestCase {
 
@@ -27,11 +40,68 @@ public class QueryStringTests extends NoneFieldFullTextFunctionTestCase {
 
     @ParametersFactory
     public static Iterable<Object[]> parameters() {
-        return generateParameters();
+        return parameterSuppliersFromTypedData(addFunctionNamedParams(getStringTestSupplier()));
+    }
+
+    /**
+     * Adds function named parameters to all the test case suppliers provided
+     */
+    private static List<TestCaseSupplier> addFunctionNamedParams(List<TestCaseSupplier> suppliers) {
+        List<TestCaseSupplier> result = new ArrayList<>();
+        for (TestCaseSupplier supplier : suppliers) {
+            List<DataType> dataTypes = new ArrayList<>(supplier.types());
+            dataTypes.add(UNSUPPORTED);
+            result.add(new TestCaseSupplier(supplier.name() + ", options", dataTypes, () -> {
+                List<TestCaseSupplier.TypedData> values = new ArrayList<>(supplier.get().getData());
+                values.add(
+                    new TestCaseSupplier.TypedData(
+                        new MapExpression(
+                            Source.EMPTY,
+                            List.of(
+                                new Literal(Source.EMPTY, "default_field", KEYWORD),
+                                new Literal(Source.EMPTY, randomAlphaOfLength(10), KEYWORD)
+                            )
+                        ),
+                        UNSUPPORTED,
+                        "options"
+                    ).forceLiteral()
+                );
+
+                return new TestCaseSupplier.TestCase(values, equalTo("MatchEvaluator"), BOOLEAN, equalTo(true));
+            }));
+        }
+        return result;
     }
 
     @Override
     protected Expression build(Source source, List<Expression> args) {
-        return new QueryString(source, args.get(0));
+        var qstr = new QueryString(source, args.get(0), args.size() > 1 ? args.get(1) : null);
+        // We need to add the QueryBuilder to the match expression, as it is used to implement equals() and hashCode() and
+        // thus test the serialization methods. But we can only do this if the parameters make sense .
+        if (args.get(0).foldable()) {
+            QueryBuilder queryBuilder = TRANSLATOR_HANDLER.asQuery(qstr).toQueryBuilder();
+            qstr.replaceQueryBuilder(queryBuilder);
+        }
+        return qstr;
+    }
+
+    @Override
+    public void testFold() {
+        // Query string cannot be folded.
+    }
+
+    /**
+     * Copy of the overridden method that doesn't check for children size, as the {@code options} child isn't serialized for Query String.
+     */
+    @Override
+    protected Expression serializeDeserializeExpression(Expression expression) {
+        Expression newExpression = serializeDeserialize(
+            expression,
+            PlanStreamOutput::writeNamedWriteable,
+            in -> in.readNamedWriteable(Expression.class),
+            testCase.getConfiguration() // The configuration query should be == to the source text of the function for this to work
+        );
+        // Fields use synthetic sources, which can't be serialized. So we use the originals instead.
+        return newExpression.replaceChildren(expression.children());
     }
 }

+ 38 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java

@@ -86,6 +86,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 
@@ -1569,6 +1570,43 @@ public class LocalPhysicalPlanOptimizerTests extends MapperServiceTestCase {
         assertThat(actualLuceneQuery.toString(), is(expectedLuceneQuery.toString()));
     }
 
+    public void testQStrOptionsPushDown() {
+        String query = """
+            from test
+            | where QSTR("first_name: Anna", {"allow_leading_wildcard": "true", "analyze_wildcard": "true",
+            "analyzer": "auto", "auto_generate_synonyms_phrase_query": "false", "default_field": "test", "default_operator": "AND",
+            "enable_position_increments": "true", "fuzziness": "auto", "fuzzy_max_expansions": 4, "fuzzy_prefix_length": 3,
+            "fuzzy_transpositions": "true", "lenient": "false", "max_determinized_states": 10, "minimum_should_match": 3,
+            "quote_analyzer": "q_analyzer", "quote_field_suffix": "q_field_suffix", "phrase_slop": 20, "rewrite": "fuzzy",
+            "time_zone": "America/Los_Angeles"})
+            """;
+        var plan = plannerOptimizer.plan(query);
+
+        AtomicReference<String> planStr = new AtomicReference<>();
+        plan.forEachDown(EsQueryExec.class, result -> planStr.set(result.query().toString()));
+
+        var expectedQStrQuery = new QueryStringQueryBuilder("first_name: Anna").allowLeadingWildcard(true)
+            .analyzeWildcard(true)
+            .analyzer("auto")
+            .autoGenerateSynonymsPhraseQuery(false)
+            .defaultField("test")
+            .defaultOperator(Operator.fromString("AND"))
+            .enablePositionIncrements(true)
+            .fuzziness(Fuzziness.fromString("auto"))
+            .fuzzyPrefixLength(3)
+            .fuzzyMaxExpansions(4)
+            .fuzzyTranspositions(true)
+            .lenient(false)
+            .maxDeterminizedStates(10)
+            .minimumShouldMatch("3")
+            .quoteAnalyzer("q_analyzer")
+            .quoteFieldSuffix("q_field_suffix")
+            .phraseSlop(20)
+            .rewrite("fuzzy")
+            .timeZone("America/Los_Angeles");
+        assertThat(expectedQStrQuery.toString(), is(planStr.get()));
+    }
+
     /**
      * Expecting
      * LimitExec[1000[INTEGER]]

+ 3 - 3
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/QueryStringQueryTests.java

@@ -22,10 +22,10 @@ import static org.hamcrest.Matchers.equalTo;
 public class QueryStringQueryTests extends ESTestCase {
 
     public void testQueryBuilding() {
-        QueryStringQueryBuilder qb = getBuilder(Map.of("lenient", "true"));
+        QueryStringQueryBuilder qb = getBuilder(Map.of("lenient", true));
         assertThat(qb.lenient(), equalTo(true));
 
-        qb = getBuilder(Map.of("lenient", "true", "default_operator", "AND"));
+        qb = getBuilder(Map.of("lenient", true, "default_operator", "AND"));
         assertThat(qb.lenient(), equalTo(true));
         assertThat(qb.defaultOperator(), equalTo(Operator.AND));
 
@@ -36,7 +36,7 @@ public class QueryStringQueryTests extends ESTestCase {
         assertThat(e.getMessage(), equalTo("failed to parse [multi_match] query type [aoeu]. unknown type."));
     }
 
-    private static QueryStringQueryBuilder getBuilder(Map<String, String> options) {
+    private static QueryStringQueryBuilder getBuilder(Map<String, Object> options) {
         final Source source = new Source(1, 1, StringUtils.EMPTY);
         final QueryStringQuery query = new QueryStringQuery(source, "eggplant", Collections.singletonMap("foo", 1.0f), options);
         return (QueryStringQueryBuilder) query.toQueryBuilder();

+ 48 - 26
x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/querydsl/query/QueryStringQuery.java

@@ -23,44 +23,66 @@ import java.util.Objects;
 import java.util.function.BiConsumer;
 
 import static java.util.Map.entry;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ALLOW_LEADING_WILDCARD_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ANALYZER_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ANALYZE_WILDCARD_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.DEFAULT_FIELD_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.DEFAULT_OPERATOR_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ENABLE_POSITION_INCREMENTS_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.ESCAPE_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZINESS_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZY_MAX_EXPANSIONS_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZY_PREFIX_LENGTH_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZY_REWRITE_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.FUZZY_TRANSPOSITIONS_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.GENERATE_SYNONYMS_PHRASE_QUERY;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.LENIENT_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.MAX_DETERMINIZED_STATES_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.MINIMUM_SHOULD_MATCH_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.PHRASE_SLOP_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.QUOTE_ANALYZER_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.QUOTE_FIELD_SUFFIX_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.REWRITE_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.TIE_BREAKER_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.TIME_ZONE_FIELD;
+import static org.elasticsearch.index.query.QueryStringQueryBuilder.TYPE_FIELD;
 
 public class QueryStringQuery extends LeafQuery {
 
-    // TODO: it'd be great if these could be constants instead of Strings, needs a core change to make the fields public first
     private static final Map<String, BiConsumer<QueryStringQueryBuilder, String>> BUILDER_APPLIERS = Map.ofEntries(
-        entry("allow_leading_wildcard", (qb, s) -> qb.allowLeadingWildcard(Booleans.parseBoolean(s))),
-        entry("analyze_wildcard", (qb, s) -> qb.analyzeWildcard(Booleans.parseBoolean(s))),
-        entry("analyzer", QueryStringQueryBuilder::analyzer),
-        entry("auto_generate_synonyms_phrase_query", (qb, s) -> qb.autoGenerateSynonymsPhraseQuery(Booleans.parseBoolean(s))),
-        entry("default_field", QueryStringQueryBuilder::defaultField),
-        entry("default_operator", (qb, s) -> qb.defaultOperator(Operator.fromString(s))),
-        entry("enable_position_increments", (qb, s) -> qb.enablePositionIncrements(Booleans.parseBoolean(s))),
-        entry("escape", (qb, s) -> qb.escape(Booleans.parseBoolean(s))),
-        entry("fuzziness", (qb, s) -> qb.fuzziness(Fuzziness.fromString(s))),
-        entry("fuzzy_max_expansions", (qb, s) -> qb.fuzzyMaxExpansions(Integer.valueOf(s))),
-        entry("fuzzy_prefix_length", (qb, s) -> qb.fuzzyPrefixLength(Integer.valueOf(s))),
-        entry("fuzzy_rewrite", QueryStringQueryBuilder::fuzzyRewrite),
-        entry("fuzzy_transpositions", (qb, s) -> qb.fuzzyTranspositions(Booleans.parseBoolean(s))),
-        entry("lenient", (qb, s) -> qb.lenient(Booleans.parseBoolean(s))),
-        entry("max_determinized_states", (qb, s) -> qb.maxDeterminizedStates(Integer.valueOf(s))),
-        entry("minimum_should_match", QueryStringQueryBuilder::minimumShouldMatch),
-        entry("phrase_slop", (qb, s) -> qb.phraseSlop(Integer.valueOf(s))),
-        entry("rewrite", QueryStringQueryBuilder::rewrite),
-        entry("quote_analyzer", QueryStringQueryBuilder::quoteAnalyzer),
-        entry("quote_field_suffix", QueryStringQueryBuilder::quoteFieldSuffix),
-        entry("tie_breaker", (qb, s) -> qb.tieBreaker(Float.valueOf(s))),
-        entry("time_zone", QueryStringQueryBuilder::timeZone),
-        entry("type", (qb, s) -> qb.type(MultiMatchQueryBuilder.Type.parse(s, LoggingDeprecationHandler.INSTANCE)))
+        entry(ALLOW_LEADING_WILDCARD_FIELD.getPreferredName(), (qb, s) -> qb.allowLeadingWildcard(Booleans.parseBoolean(s))),
+        entry(ANALYZE_WILDCARD_FIELD.getPreferredName(), (qb, s) -> qb.analyzeWildcard(Booleans.parseBoolean(s))),
+        entry(ANALYZER_FIELD.getPreferredName(), QueryStringQueryBuilder::analyzer),
+        entry(GENERATE_SYNONYMS_PHRASE_QUERY.getPreferredName(), (qb, s) -> qb.autoGenerateSynonymsPhraseQuery(Booleans.parseBoolean(s))),
+        entry(DEFAULT_FIELD_FIELD.getPreferredName(), QueryStringQueryBuilder::defaultField),
+        entry(DEFAULT_OPERATOR_FIELD.getPreferredName(), (qb, s) -> qb.defaultOperator(Operator.fromString(s))),
+        entry(ENABLE_POSITION_INCREMENTS_FIELD.getPreferredName(), (qb, s) -> qb.enablePositionIncrements(Booleans.parseBoolean(s))),
+        entry(ESCAPE_FIELD.getPreferredName(), (qb, s) -> qb.escape(Booleans.parseBoolean(s))),
+        entry(FUZZINESS_FIELD.getPreferredName(), (qb, s) -> qb.fuzziness(Fuzziness.fromString(s))),
+        entry(FUZZY_MAX_EXPANSIONS_FIELD.getPreferredName(), (qb, s) -> qb.fuzzyMaxExpansions(Integer.parseInt(s))),
+        entry(FUZZY_PREFIX_LENGTH_FIELD.getPreferredName(), (qb, s) -> qb.fuzzyPrefixLength(Integer.parseInt(s))),
+        entry(FUZZY_REWRITE_FIELD.getPreferredName(), QueryStringQueryBuilder::fuzzyRewrite),
+        entry(FUZZY_TRANSPOSITIONS_FIELD.getPreferredName(), (qb, s) -> qb.fuzzyTranspositions(Booleans.parseBoolean(s))),
+        entry(LENIENT_FIELD.getPreferredName(), (qb, s) -> qb.lenient(Booleans.parseBoolean(s))),
+        entry(MAX_DETERMINIZED_STATES_FIELD.getPreferredName(), (qb, s) -> qb.maxDeterminizedStates(Integer.parseInt(s))),
+        entry(MINIMUM_SHOULD_MATCH_FIELD.getPreferredName(), QueryStringQueryBuilder::minimumShouldMatch),
+        entry(PHRASE_SLOP_FIELD.getPreferredName(), (qb, s) -> qb.phraseSlop(Integer.parseInt(s))),
+        entry(REWRITE_FIELD.getPreferredName(), QueryStringQueryBuilder::rewrite),
+        entry(QUOTE_ANALYZER_FIELD.getPreferredName(), QueryStringQueryBuilder::quoteAnalyzer),
+        entry(QUOTE_FIELD_SUFFIX_FIELD.getPreferredName(), QueryStringQueryBuilder::quoteFieldSuffix),
+        entry(TIE_BREAKER_FIELD.getPreferredName(), (qb, s) -> qb.tieBreaker(Float.parseFloat(s))),
+        entry(TIME_ZONE_FIELD.getPreferredName(), QueryStringQueryBuilder::timeZone),
+        entry(TYPE_FIELD.getPreferredName(), (qb, s) -> qb.type(MultiMatchQueryBuilder.Type.parse(s, LoggingDeprecationHandler.INSTANCE)))
     );
 
     private final String query;
     private final Map<String, Float> fields;
-    private StringQueryPredicate predicate;
+    private final StringQueryPredicate predicate;
     private final Map<String, String> options;
 
     // dedicated constructor for QueryTranslator
     public QueryStringQuery(Source source, String query, String fieldName) {
-        this(source, query, Collections.singletonMap(fieldName, Float.valueOf(1.0f)), null);
+        this(source, query, Collections.singletonMap(fieldName, 1.0f), null);
     }
 
     public QueryStringQuery(Source source, String query, Map<String, Float> fields, StringQueryPredicate predicate) {

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است