-
Notifications
You must be signed in to change notification settings - Fork 25.2k
Add "did you mean" to ObjectParser #50938
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
11ccd11
bf8ef05
5be1e7a
bb36d91
e97b199
0a63a71
cc67258
477e9ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* | ||
* Licensed to Elasticsearch under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
package org.elasticsearch.common.xcontent; | ||
|
||
import java.util.ServiceLoader; | ||
|
||
/** | ||
* Extension point to customize the error message for unknown fields. We expect | ||
* Elasticsearch to plug a fancy implementation that uses Lucene's spelling | ||
* correction infrastructure to suggest corrections. | ||
*/ | ||
public interface ErrorOnUnknown { | ||
/** | ||
* The implementation of this interface that was loaded from SPI. | ||
*/ | ||
ErrorOnUnknown IMPLEMENTATION = findImplementation(); | ||
|
||
/** | ||
* Build the error message to use when {@link ObjectParser} encounters an unknown field. | ||
* @param parserName the name of the thing we're parsing | ||
* @param unknownField the field that we couldn't recognize | ||
* @param candidates the possible fields | ||
*/ | ||
String errorMessage(String parserName, String unknownField, Iterable<String> candidates); | ||
|
||
/** | ||
* Priority that this error message handler should be used. | ||
*/ | ||
int priority(); | ||
|
||
private static ErrorOnUnknown findImplementation() { | ||
ErrorOnUnknown best = new ErrorOnUnknown() { | ||
@Override | ||
public String errorMessage(String parserName, String unknownField, Iterable<String> candidates) { | ||
return "[" + parserName + "] unknown field [" + unknownField + "]"; | ||
} | ||
|
||
@Override | ||
public int priority() { | ||
return Integer.MIN_VALUE; | ||
} | ||
}; | ||
for (ErrorOnUnknown c : ServiceLoader.load(ErrorOnUnknown.class)) { | ||
if (best.priority() < c.priority()) { | ||
best = c; | ||
} | ||
} | ||
return best; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -206,7 +206,7 @@ public void setTest(int test) { | |
{ | ||
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"not_supported_field\" : \"foo\"}"); | ||
XContentParseException ex = expectThrows(XContentParseException.class, () -> objectParser.parse(parser, s, null)); | ||
assertEquals(ex.getMessage(), "[1:2] [the_parser] unknown field [not_supported_field], parser not found"); | ||
assertEquals(ex.getMessage(), "[1:2] [the_parser] unknown field [not_supported_field]"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could preserve this bit of the message, but I don't think it was really helping anything. |
||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
--- | ||
'Misspelled fields get "did you mean"': | ||
- skip: | ||
version: " - 7.99.99" | ||
reason: Implemented in 8.0 | ||
- do: | ||
catch: /\[UpdateRequest\] unknown field \[dac\] did you mean \[doc\]\?/ | ||
update: | ||
index: test | ||
id: 1 | ||
body: | ||
dac: { foo: baz } | ||
upsert: { foo: bar } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/* | ||
* Licensed to Elasticsearch under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
package org.elasticsearch.common.xcontent; | ||
|
||
import org.apache.lucene.search.spell.LevenshteinDistance; | ||
import org.apache.lucene.util.CollectionUtil; | ||
import org.elasticsearch.common.collect.Tuple; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Locale; | ||
|
||
import static java.util.stream.Collectors.toList; | ||
|
||
public class SuggestingErrorOnUnknown implements ErrorOnUnknown { | ||
@Override | ||
public String errorMessage(String parserName, String unknownField, Iterable<String> candidates) { | ||
String message = String.format(Locale.ROOT, "[%s] unknown field [%s]", parserName, unknownField); | ||
// TODO it'd be nice to combine this with BaseRestHandler's implementation. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like a problem for a follow up PR. I don't think it'd be hard, but a little fiddly. |
||
LevenshteinDistance ld = new LevenshteinDistance(); | ||
final List<Tuple<Float, String>> scored = new ArrayList<>(); | ||
for (String candidate : candidates) { | ||
float distance = ld.getDistance(unknownField, candidate); | ||
if (distance > 0.5f) { | ||
scored.add(new Tuple<>(distance, candidate)); | ||
} | ||
} | ||
if (scored.isEmpty()) { | ||
return message; | ||
} | ||
CollectionUtil.timSort(scored, (a, b) -> { | ||
// sort by distance in reverse order, then parameter name for equal distances | ||
int compare = a.v1().compareTo(b.v1()); | ||
if (compare != 0) { | ||
return -compare; | ||
} | ||
return a.v2().compareTo(b.v2()); | ||
}); | ||
List<String> keys = scored.stream().map(Tuple::v2).collect(toList()); | ||
StringBuilder builder = new StringBuilder(message).append(" did you mean "); | ||
if (keys.size() == 1) { | ||
builder.append("[").append(keys.get(0)).append("]"); | ||
} else { | ||
builder.append("any of ").append(keys.toString()); | ||
} | ||
builder.append("?"); | ||
return builder.toString(); | ||
} | ||
|
||
@Override | ||
public int priority() { | ||
return 0; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
org.elasticsearch.common.xcontent.SuggestingErrorOnUnknown |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/* | ||
* Licensed to Elasticsearch under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
package org.elasticsearch.common.xcontent; | ||
|
||
import org.elasticsearch.test.ESTestCase; | ||
|
||
import java.util.Arrays; | ||
|
||
import static org.hamcrest.Matchers.equalTo; | ||
|
||
public class SuggestingErrorOnUnknownTests extends ESTestCase { | ||
private String errorMessage(String unknownField, String... candidates) { | ||
return new SuggestingErrorOnUnknown().errorMessage("test", unknownField, Arrays.asList(candidates)); | ||
} | ||
|
||
public void testNoCandidates() { | ||
assertThat(errorMessage("foo"), equalTo("[test] unknown field [foo]")); | ||
} | ||
public void testBadCandidates() { | ||
assertThat(errorMessage("foo", "bar", "baz"), equalTo("[test] unknown field [foo]")); | ||
} | ||
public void testOneCandidate() { | ||
assertThat(errorMessage("foo", "bar", "fop"), equalTo("[test] unknown field [foo] did you mean [fop]?")); | ||
} | ||
public void testManyCandidate() { | ||
assertThat(errorMessage("foo", "bar", "fop", "fou", "baz"), | ||
equalTo("[test] unknown field [foo] did you mean any of [fop, fou]?")); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think passing
ObjectParser
here is ok because the interface is entirely private already. I could certainly be convinced otherwise though.