SQL: Parse all multi_match options from request (elastic/x-pack-elasticsearch#3361)
* Parse all multi_match options from request Rather than hardcoding predicates that require more methods for each newly added options, this encapsulates the state into a collection of appliers that can accumulate what's needed in the QueryBuilder itself. For example, the following: ```json GET /_xpack/sql/translate { "query": "SELECT foo,baz from i WHERE MATCH('baz', 'should clause', 'operator=AND;type=cross_fields')" } ``` Then generates the following: ```json { "size" : 1000, "query" : { "multi_match" : { "query" : "should clause", "fields" : [ "baz^1.0" ], "type" : "cross_fields", "operator" : "AND", "slop" : 0, "prefix_length" : 0, "max_expansions" : 50, "zero_terms_query" : "NONE", "auto_generate_synonyms_phrase_query" : true, "fuzzy_transpositions" : true, "boost" : 1.0 } }, "_source" : { "includes" : [ "baz" ], "excludes" : [ ] }, "docvalue_fields" : [ "foo" ] } ``` And when an invalid field value is used: ```json GET /_xpack/sql { "query": "SELECT foo,baz from i WHERE MATCH('baz', 'should clause', 'operator=AND;minimum_should_match=potato')" } ``` We get what ES would usually send back: ```json { "error" : { "root_cause" : [ { "type" : "query_shard_exception", "reason" : "failed to create query: {\n \"multi_match\" : {\n \"query\" : \"should clause\",\n \"fields\" : [\n \"baz^1.0\"\n ],\n \"type\" : \"best_fields\",\n \"operator\" : \"AND\",\n \"slop\" : 0,\n \"prefix_length\" : 0,\n \"max_expansions\" : 50,\n \"minimum_should_match\" : \"potato\",\n \"zero_terms_query\" : \"NONE\",\n \"auto_generate_synonyms_phrase_query\" : true,\n \"fuzzy_transpositions\" : true,\n \"boost\" : 1.0\n }\n}", "index_uuid" : "ef3MWf8FTUe2Qjz2FLbhoQ", "index" : "i" } ], "type" : "search_phase_execution_exception", "reason" : "all shards failed", "phase" : "query", "grouped" : true, "failed_shards" : [ { "shard" : 0, "index" : "i", "node" : "VfG9zfk9TDWdWvEZu0a4Rw", "reason" : { "type" : "query_shard_exception", "reason" : "failed to create query: {\n \"multi_match\" : {\n \"query\" : \"should clause\",\n \"fields\" : [\n \"baz^1.0\"\n ],\n \"type\" : \"best_fields\",\n \"operator\" : \"AND\",\n \"slop\" : 0,\n \"prefix_length\" : 0,\n \"max_expansions\" : 50,\n \"minimum_should_match\" : \"potato\",\n \"zero_terms_query\" : \"NONE\",\n \"auto_generate_synonyms_phrase_query\" : true,\n \"fuzzy_transpositions\" : true,\n \"boost\" : 1.0\n }\n}", "index_uuid" : "ef3MWf8FTUe2Qjz2FLbhoQ", "index" : "i", "caused_by" : { "type" : "number_format_exception", "reason" : "For input string: \"potato\"" } } } ] }, "status" : 400 } ``` It even includes the validation that ES already does for things like `type`: ```json GET /_xpack/sql { "query": "SELECT foo,baz from i WHERE MATCH('baz', 'should clause', 'operator=AND;type=eggplant')" } ``` ```json { "error" : { "root_cause" : [ { "type" : "parse_exception", "reason" : "failed to parse [multi_match] query type [eggplant]. unknown type." } ], "type" : "parse_exception", "reason" : "failed to parse [multi_match] query type [eggplant]. unknown type." }, "status" : 400 } ``` Resolves elastic/x-pack-elasticsearch#3257 Original commit: elastic/x-pack-elasticsearch@59f518af4a
This commit is contained in:
parent
9765058e5c
commit
38cee807b9
|
@ -24,7 +24,14 @@ SELECT emp_no, first_name, gender, last_name FROM test_emp WHERE MATCH(first_nam
|
|||
;
|
||||
|
||||
multiMatchQuery
|
||||
SELECT emp_no, first_name, gender, last_name FROM test_emp WHERE MATCH('first_name,last_name', 'Morton', 'type=best_fields;default_operator=OR');
|
||||
SELECT emp_no, first_name, gender, last_name FROM test_emp WHERE MATCH('first_name,last_name', 'Morton', 'type=best_fields;operator=OR');
|
||||
|
||||
emp_no:i | first_name:s | gender:s | last_name:s
|
||||
10095 |Hilari |M |Morton
|
||||
;
|
||||
|
||||
multiMatchQueryAllOptions
|
||||
SELECT emp_no, first_name, gender, last_name FROM test_emp WHERE MATCH('first_name,last_name', 'Morton', 'slop=1;lenient=true;cutoff_frequency=2;tie_breaker=0.1;use_dis_max=true;fuzzy_rewrite=scoring_boolean;minimum_should_match=1;operator=AND;max_expansions=30;prefix_length=1;analyzer=english;type=best_fields;auto_generate_synonyms_phrase_query=true;fuzzy_transpositions=true');
|
||||
|
||||
emp_no:i | first_name:s | gender:s | last_name:s
|
||||
10095 |Hilari |M |Morton
|
||||
|
|
|
@ -16,15 +16,12 @@ public class MultiMatchQueryPredicate extends FullTextPredicate {
|
|||
|
||||
private final String fieldString;
|
||||
private final Map<String, Float> fields;
|
||||
private final Operator operator;
|
||||
|
||||
public MultiMatchQueryPredicate(Location location, String fieldString, String query, String options) {
|
||||
super(location, query, options, emptyList());
|
||||
this.fieldString = fieldString;
|
||||
|
||||
// inferred
|
||||
this.fields = FullTextUtils.parseFields(fieldString, location);
|
||||
this.operator = FullTextUtils.operator(optionMap(), "operator");
|
||||
}
|
||||
|
||||
public String fieldString() {
|
||||
|
@ -35,10 +32,6 @@ public class MultiMatchQueryPredicate extends FullTextPredicate {
|
|||
return fields;
|
||||
}
|
||||
|
||||
public Operator operator() {
|
||||
return operator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(fieldString, super.hashCode());
|
||||
|
|
|
@ -5,20 +5,49 @@
|
|||
*/
|
||||
package org.elasticsearch.xpack.sql.querydsl.query;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.elasticsearch.common.Booleans;
|
||||
import org.elasticsearch.index.query.Operator;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
|
||||
import org.elasticsearch.index.query.QueryBuilder;
|
||||
import org.elasticsearch.xpack.sql.expression.predicate.fulltext.MultiMatchQueryPredicate;
|
||||
import org.elasticsearch.xpack.sql.tree.Location;
|
||||
|
||||
import static org.elasticsearch.index.query.QueryBuilders.multiMatchQuery;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public class MultiMatchQuery extends LeafQuery {
|
||||
|
||||
private static final Map<String, BiConsumer<MultiMatchQueryBuilder, String>> BUILDER_APPLIERS;
|
||||
|
||||
static {
|
||||
HashMap<String, BiConsumer<MultiMatchQueryBuilder, String>> appliers = new HashMap<>(14);
|
||||
// TODO: it'd be great if these could be constants instead of Strings, needs a core change to make the fields public first
|
||||
appliers.put("slop", (qb, s) -> qb.slop(Integer.valueOf(s)));
|
||||
// TODO: add zero terms query support, I'm not sure the best way to parse it yet...
|
||||
// appliers.put("zero_terms_query", (qb, s) -> qb.zeroTermsQuery(s));
|
||||
appliers.put("lenient", (qb, s) -> qb.lenient(Booleans.parseBoolean(s)));
|
||||
appliers.put("cutoff_frequency", (qb, s) -> qb.cutoffFrequency(Float.valueOf(s)));
|
||||
appliers.put("tie_breaker", (qb, s) -> qb.tieBreaker(Float.valueOf(s)));
|
||||
appliers.put("use_dis_max", (qb, s) -> qb.useDisMax(Booleans.parseBoolean(s)));
|
||||
appliers.put("fuzzy_rewrite", (qb, s) -> qb.fuzzyRewrite(s));
|
||||
appliers.put("minimum_should_match", (qb, s) -> qb.minimumShouldMatch(s));
|
||||
appliers.put("operator", (qb, s) -> qb.operator(Operator.fromString(s)));
|
||||
appliers.put("max_expansions", (qb, s) -> qb.maxExpansions(Integer.valueOf(s)));
|
||||
appliers.put("prefix_length", (qb, s) -> qb.prefixLength(Integer.valueOf(s)));
|
||||
appliers.put("analyzer", (qb, s) -> qb.analyzer(s));
|
||||
appliers.put("type", (qb, s) -> qb.type(s));
|
||||
appliers.put("auto_generate_synonyms_phrase_query", (qb, s) -> qb.autoGenerateSynonymsPhraseQuery(Booleans.parseBoolean(s)));
|
||||
appliers.put("fuzzy_transpositions", (qb, s) -> qb.fuzzyTranspositions(Booleans.parseBoolean(s)));
|
||||
BUILDER_APPLIERS = Collections.unmodifiableMap(appliers);
|
||||
}
|
||||
|
||||
private final String query;
|
||||
private final Map<String, Float> fields;
|
||||
private final Map<String, String> options;
|
||||
private final MultiMatchQueryPredicate predicate;
|
||||
|
||||
public MultiMatchQuery(Location location, String query, Map<String, Float> fields, MultiMatchQueryPredicate predicate) {
|
||||
|
@ -26,16 +55,21 @@ public class MultiMatchQuery extends LeafQuery {
|
|||
this.query = query;
|
||||
this.fields = fields;
|
||||
this.predicate = predicate;
|
||||
this.options = predicate.optionMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryBuilder asBuilder() {
|
||||
MultiMatchQueryBuilder queryBuilder = multiMatchQuery(query);
|
||||
final MultiMatchQueryBuilder queryBuilder = QueryBuilders.multiMatchQuery(query);
|
||||
queryBuilder.fields(fields);
|
||||
queryBuilder.analyzer(predicate.analyzer());
|
||||
if (predicate.operator() != null) {
|
||||
queryBuilder.operator(predicate.operator().toEs());
|
||||
}
|
||||
options.forEach((k, v) -> {
|
||||
if (BUILDER_APPLIERS.containsKey(k)) {
|
||||
BUILDER_APPLIERS.get(k).accept(queryBuilder, v);
|
||||
} else {
|
||||
throw new IllegalArgumentException("illegal multi_match option [" + k + "]");
|
||||
}
|
||||
});
|
||||
return queryBuilder;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.sql.querydsl.query;
|
||||
|
||||
import org.elasticsearch.ElasticsearchParseException;
|
||||
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.sql.expression.predicate.fulltext.MultiMatchQueryPredicate;
|
||||
import org.elasticsearch.xpack.sql.tree.Location;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
public class MultiMatchQueryTests extends ESTestCase {
|
||||
|
||||
public void testQueryBuilding() {
|
||||
MultiMatchQueryBuilder qb = getBuilder("lenient=true");
|
||||
assertThat(qb.lenient(), equalTo(true));
|
||||
|
||||
qb = getBuilder("use_dis_max=true;type=best_fields");
|
||||
assertThat(qb.useDisMax(), equalTo(true));
|
||||
assertThat(qb.getType(), equalTo(MultiMatchQueryBuilder.Type.BEST_FIELDS));
|
||||
|
||||
Exception e = expectThrows(IllegalArgumentException.class, () -> getBuilder("pizza=yummy"));
|
||||
assertThat(e.getMessage(), equalTo("illegal multi_match option [pizza]"));
|
||||
|
||||
e = expectThrows(ElasticsearchParseException.class, () -> getBuilder("type=aoeu"));
|
||||
assertThat(e.getMessage(), equalTo("failed to parse [multi_match] query type [aoeu]. unknown type."));
|
||||
}
|
||||
|
||||
private static MultiMatchQueryBuilder getBuilder(String options) {
|
||||
final Location location = new Location(1, 1);
|
||||
final MultiMatchQueryPredicate mmqp = new MultiMatchQueryPredicate(location, "foo,bar", "eggplant", options);
|
||||
final Map<String, Float> fields = new HashMap<>();
|
||||
fields.put("foo", 1.0f);
|
||||
fields.put("bar", 1.0f);
|
||||
final MultiMatchQuery mmq = new MultiMatchQuery(location, "eggplant", fields, mmqp);
|
||||
return (MultiMatchQueryBuilder) mmq.asBuilder();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue