Skip to content

Commit ca15a3f

Browse files
authored
Add "did you mean" to unknown queries (#51177) (#51254)
This replaces the message we return for unknown queries with the standard one that we use for unknown fields from `ObjectParser`. This is nice because it includes "did you mean". One day we might convert parsing queries to using object parser, but that looks complex. This change is much smaller and seems useful.
1 parent d186e47 commit ca15a3f

File tree

9 files changed

+47
-20
lines changed

9 files changed

+47
-20
lines changed

modules/lang-mustache/src/test/resources/rest-api-spec/test/lang_mustache/50_multi_search_template.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ setup:
100100
- match: { responses.1.error.root_cause.0.reason: "/Unexpected.end.of.input/" }
101101
- match: { responses.2.hits.total: 1 }
102102
- match: { responses.3.error.root_cause.0.type: parsing_exception }
103-
- match: { responses.3.error.root_cause.0.reason: "/no.\\[query\\].registered.for.\\[unknown\\]/" }
103+
- match: { responses.3.error.root_cause.0.reason: "/unknown.query.\\[unknown\\]/" }
104104

105105
---
106106
"Multi-search template with invalid request":

rest-api-spec/src/main/resources/rest-api-spec/test/indices.validate_query/10_basic.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ setup:
1212

1313
---
1414
"Validate query api":
15+
- skip:
16+
version: ' - 7.99.99'
17+
reason: message changed in 8.0.0
18+
1519
- do:
1620
indices.validate_query:
1721
q: query string
@@ -43,7 +47,17 @@ setup:
4347
invalid_query: {}
4448

4549
- is_false: valid
46-
- match: {error: 'org.elasticsearch.common.ParsingException: no [query] registered for [invalid_query]'}
50+
- match: {error: '/.+unknown\squery\s\[invalid_query\].+/' }
51+
52+
- do:
53+
indices.validate_query:
54+
explain: true
55+
body:
56+
query:
57+
boool: {}
58+
59+
- is_false: valid
60+
- match: {error: '/.+unknown\squery\s\[boool\]\sdid\syou\smean\s\[bool\]\?.+/' }
4761

4862
- do:
4963
indices.validate_query:

server/src/main/java/org/elasticsearch/common/xcontent/SuggestingErrorOnUnknown.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,19 @@
3232
public class SuggestingErrorOnUnknown implements ErrorOnUnknown {
3333
@Override
3434
public String errorMessage(String parserName, String unknownField, Iterable<String> candidates) {
35-
String message = String.format(Locale.ROOT, "[%s] unknown field [%s]", parserName, unknownField);
35+
return String.format(Locale.ROOT, "[%s] unknown field [%s]%s", parserName, unknownField, suggest(unknownField, candidates));
36+
}
37+
38+
@Override
39+
public int priority() {
40+
return 0;
41+
}
42+
43+
/**
44+
* Builds suggestions for an unknown field, returning an empty string if there
45+
* aren't any suggestions or " did you mean " and then the list of suggestions.
46+
*/
47+
public static String suggest(String unknownField, Iterable<String> candidates) {
3648
// TODO it'd be nice to combine this with BaseRestHandler's implementation.
3749
LevenshteinDistance ld = new LevenshteinDistance();
3850
final List<Tuple<Float, String>> scored = new ArrayList<>();
@@ -43,7 +55,7 @@ public String errorMessage(String parserName, String unknownField, Iterable<Stri
4355
}
4456
}
4557
if (scored.isEmpty()) {
46-
return message;
58+
return "";
4759
}
4860
CollectionUtil.timSort(scored, (a, b) -> {
4961
// sort by distance in reverse order, then parameter name for equal distances
@@ -54,7 +66,7 @@ public String errorMessage(String parserName, String unknownField, Iterable<Stri
5466
return a.v2().compareTo(b.v2());
5567
});
5668
List<String> keys = scored.stream().map(Tuple::v2).collect(toList());
57-
StringBuilder builder = new StringBuilder(message).append(" did you mean ");
69+
StringBuilder builder = new StringBuilder(" did you mean ");
5870
if (keys.size() == 1) {
5971
builder.append("[").append(keys.get(0)).append("]");
6072
} else {
@@ -63,9 +75,4 @@ public String errorMessage(String parserName, String unknownField, Iterable<Stri
6375
builder.append("?");
6476
return builder.toString();
6577
}
66-
67-
@Override
68-
public int priority() {
69-
return 0;
70-
}
7178
}

server/src/main/java/org/elasticsearch/index/query/AbstractQueryBuilder.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.elasticsearch.common.lucene.BytesRefs;
3333
import org.elasticsearch.common.xcontent.AbstractObjectParser;
3434
import org.elasticsearch.common.xcontent.NamedObjectNotFoundException;
35+
import org.elasticsearch.common.xcontent.SuggestingErrorOnUnknown;
3536
import org.elasticsearch.common.xcontent.XContentBuilder;
3637
import org.elasticsearch.common.xcontent.XContentLocation;
3738
import org.elasticsearch.common.xcontent.XContentParser;
@@ -41,6 +42,7 @@
4142
import java.util.ArrayList;
4243
import java.util.Collection;
4344
import java.util.List;
45+
import java.util.Locale;
4446
import java.util.Map;
4547
import java.util.Objects;
4648

@@ -313,10 +315,9 @@ public static QueryBuilder parseInnerQueryBuilder(XContentParser parser) throws
313315
try {
314316
result = parser.namedObject(QueryBuilder.class, queryName, null);
315317
} catch (NamedObjectNotFoundException e) {
316-
// Preserve the error message from 5.0 until we have a compellingly better message so we don't break BWC.
317-
// This intentionally doesn't include the causing exception because that'd change the "root_cause" of any unknown query errors
318-
throw new ParsingException(new XContentLocation(e.getLineNumber(), e.getColumnNumber()),
319-
"no [query] registered for [" + queryName + "]");
318+
String message = String.format(Locale.ROOT, "unknown query [%s]%s", queryName,
319+
SuggestingErrorOnUnknown.suggest(queryName, e.getCandidates()));
320+
throw new ParsingException(new XContentLocation(e.getLineNumber(), e.getColumnNumber()), message, e);
320321
}
321322
//end_object of the specific query (e.g. match, multi_match etc.) element
322323
if (parser.currentToken() != XContentParser.Token.END_OBJECT) {

server/src/test/java/org/elasticsearch/index/query/AbstractQueryBuilderTests.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,15 @@ public void testParseInnerQueryBuilderExceptions() throws IOException {
7878
assertEquals("[foo] query malformed, no start_object after query name", exception.getMessage());
7979
}
8080

81-
source = "{ \"foo\" : {} }";
81+
source = "{ \"boool\" : {} }";
8282
try (XContentParser parser = createParser(JsonXContent.jsonXContent, source)) {
8383
ParsingException exception = expectThrows(ParsingException.class, () -> parseInnerQueryBuilder(parser));
84-
assertEquals("no [query] registered for [foo]", exception.getMessage());
84+
assertEquals("unknown query [boool] did you mean [bool]?", exception.getMessage());
85+
}
86+
source = "{ \"match_\" : {} }";
87+
try (XContentParser parser = createParser(JsonXContent.jsonXContent, source)) {
88+
ParsingException exception = expectThrows(ParsingException.class, () -> parseInnerQueryBuilder(parser));
89+
assertEquals("unknown query [match_] did you mean any of [match, match_all, match_none]?", exception.getMessage());
8590
}
8691
}
8792

server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ public void testUnknownQueryName() throws IOException {
284284
String query = "{\"bool\" : {\"must\" : { \"unknown_query\" : { } } } }";
285285

286286
ParsingException ex = expectThrows(ParsingException.class, () -> parseQuery(query));
287-
assertEquals("no [query] registered for [unknown_query]", ex.getMessage());
287+
assertEquals("unknown query [unknown_query]", ex.getMessage());
288288
}
289289

290290
public void testRewrite() throws IOException {

server/src/test/java/org/elasticsearch/indices/template/SimpleIndexTemplateIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ public void testAliasInvalidFilterValidJson() throws Exception {
533533
() -> createIndex("test"));
534534
assertThat(e.getMessage(), equalTo("failed to parse filter for alias [invalid_alias]"));
535535
assertThat(e.getCause(), instanceOf(ParsingException.class));
536-
assertThat(e.getCause().getMessage(), equalTo("no [query] registered for [invalid]"));
536+
assertThat(e.getCause().getMessage(), equalTo("unknown query [invalid]"));
537537
}
538538

539539
public void testAliasInvalidFilterInvalidJson() throws Exception {

x-pack/plugin/src/test/resources/rest-api-spec/test/ml/datafeeds_crud.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ setup:
160160
---
161161
"Test put datafeed with invalid query":
162162
- do:
163-
catch: /parsing_exception/
163+
catch: /failed\sto\sparse\sfield\s\[query\]/
164164
ml.put_datafeed:
165165
datafeed_id: test-datafeed-1
166166
body: >

x-pack/plugin/src/test/resources/rest-api-spec/test/watcher/execute_watch/20_transform.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,5 +188,5 @@ setup:
188188
- match: { watch_record.state: "executed" }
189189
- match: { watch_record.status.execution_state: "executed" }
190190
- match: { watch_record.result.transform.status: "failure" }
191-
- match: { watch_record.result.transform.reason: "no [query] registered for [does_not_exist]" }
191+
- match: { watch_record.result.transform.reason: "unknown query [does_not_exist]" }
192192
- is_true: watch_record.result.transform.error

0 commit comments

Comments
 (0)