Browse Source

Make Fuzziness reject illegal values earlier (#33511)

The current java implementation of Fuzziness leaves a lot of room for
initializing it with illegal values that either cause errors later when the
queries reach the shards where they are executed or values that are silently
ignored in favour of defaults. We should instead tighten the java implementation
of the class so that we only accept supported values. Currently those are
numeric values representing the edit distances 0, 1 and 2, optionally also as
float or string, and the "AUTO" fuzziness, which can come in a cusomizable
variant that allows specifying two value that define the positions in a term
where the AUTO option increases the allowed edit distance.

This change removes several redundant ways of object construction and adds input
validation to the remaining ones. Java users should either use one of the
predefined constants or use the static factory methods `fromEdits(int)` or
`fromString(String)` to create instances of the class, while other ctors are
hidden. This allows for instance control, e.g. returning one of the constants
when creating instances from an integer value.

Previously the class would accept any positive integer value and any float
value, while in effect the maximum allowed edit distance was capped at 2 in
practice. These values while throw an error now, as will any other String value
other than "AUTO" that where previously accepted but led to numeric exceptions
when the query was executed.
Christoph Büscher 6 years ago
parent
commit
1597f69e44

+ 2 - 0
docs/reference/migration/migrate_8_0.asciidoc

@@ -16,6 +16,7 @@ coming[8.0.0]
 * <<breaking_80_mappings_changes>>
 * <<breaking_80_snapshots_changes>>
 * <<breaking_80_security_changes>>
+* <<breaking_80_java_changes>>
 
 //NOTE: The notable-breaking-changes tagged regions are re-used in the
 //Installation and Upgrade Guide
@@ -43,3 +44,4 @@ include::migrate_8_0/discovery.asciidoc[]
 include::migrate_8_0/mappings.asciidoc[]
 include::migrate_8_0/snapshots.asciidoc[]
 include::migrate_8_0/security.asciidoc[]
+include::migrate_8_0/java.asciidoc[]

+ 20 - 0
docs/reference/migration/migrate_8_0/java.asciidoc

@@ -0,0 +1,20 @@
+[float]
+[[breaking_80_java_changes]]
+=== Java API changes
+
+[float]
+==== Changes to Fuzziness
+
+To create `Fuzziness` instances, use the `fromString` and `fromEdits` method
+instead of the `build` method that used to accept both Strings and numeric
+values. Several fuzziness setters on query builders (e.g.
+MatchQueryBuilder#fuzziness) now accept only a `Fuzziness`instance instead of
+an Object. You should preferably use the available constants (e.g.
+Fuzziness.ONE, Fuzziness.AUTO) or build your own instance using the above
+mentioned factory methods.
+
+Fuzziness used to be lenient when it comes to parsing arbitrary numeric values
+while silently truncating them to one of the three allowed edit distances 0, 1
+or 2. This leniency is now removed and the class will throw errors when trying
+to construct an instance with another value (e.g. floats like 1.3 used to get
+accepted but truncated to 1). You should use one of the allowed values.

+ 74 - 77
server/src/main/java/org/elasticsearch/common/unit/Fuzziness.java

@@ -20,6 +20,7 @@ package org.elasticsearch.common.unit;
 
 import org.elasticsearch.ElasticsearchParseException;
 import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
@@ -38,41 +39,73 @@ import java.util.Objects;
  */
 public final class Fuzziness implements ToXContentFragment, Writeable {
 
-    public static final String X_FIELD_NAME = "fuzziness";
-    public static final Fuzziness ZERO = new Fuzziness(0);
-    public static final Fuzziness ONE = new Fuzziness(1);
-    public static final Fuzziness TWO = new Fuzziness(2);
-    public static final Fuzziness AUTO = new Fuzziness("AUTO");
+    static final String X_FIELD_NAME = "fuzziness";
     public static final ParseField FIELD = new ParseField(X_FIELD_NAME);
-    private static final int DEFAULT_LOW_DISTANCE = 3;
-    private static final int DEFAULT_HIGH_DISTANCE = 6;
+
+    public static final Fuzziness ZERO = new Fuzziness("0");
+    public static final Fuzziness ONE = new Fuzziness("1");
+    public static final Fuzziness TWO = new Fuzziness("2");
+    public static final Fuzziness AUTO = new Fuzziness("AUTO");
+
+    static final int DEFAULT_LOW_DISTANCE = 3;
+    static final int DEFAULT_HIGH_DISTANCE = 6;
 
     private final String fuzziness;
     private int lowDistance = DEFAULT_LOW_DISTANCE;
     private int highDistance = DEFAULT_HIGH_DISTANCE;
 
-    private Fuzziness(int fuzziness) {
-        if (fuzziness != 0 && fuzziness != 1 && fuzziness != 2) {
-            throw new IllegalArgumentException("Valid edit distances are [0, 1, 2] but was [" + fuzziness + "]");
-        }
-        this.fuzziness = Integer.toString(fuzziness);
+    private Fuzziness(String fuzziness) {
+        this.fuzziness = fuzziness;
     }
 
-    private Fuzziness(String fuzziness) {
-        if (fuzziness == null || fuzziness.isEmpty()) {
-            throw new IllegalArgumentException("fuzziness can't be null!");
+    /**
+     * Creates a {@link Fuzziness} instance from an edit distance. The value must be one of {@code [0, 1, 2]}
+     * Note: Using this method only makes sense if the field you are applying Fuzziness to is some sort of string.
+     * @throws IllegalArgumentException if the edit distance is not in [0, 1, 2]
+     */
+    public static Fuzziness fromEdits(int edits) {
+        switch (edits) {
+        case 0: return Fuzziness.ZERO;
+        case 1: return Fuzziness.ONE;
+        case 2: return Fuzziness.TWO;
+        default:
+            throw new IllegalArgumentException("Valid edit distances are [0, 1, 2] but was [" + edits + "]");
         }
-        this.fuzziness = fuzziness.toUpperCase(Locale.ROOT);
     }
 
-    private Fuzziness(String fuzziness, int lowDistance, int highDistance) {
-        this(fuzziness);
-        if (lowDistance < 0 || highDistance < 0 || lowDistance > highDistance) {
-            throw new IllegalArgumentException("fuzziness wrongly configured, must be: lowDistance > 0, highDistance" +
-                " > 0 and lowDistance <= highDistance ");
+    /**
+     * Creates a {@link Fuzziness} instance from a String representation. This can
+     * either be an edit distance where the value must be one of
+     * {@code ["0", "1", "2"]} or "AUTO" for a fuzziness that depends on the term
+     * length. Using the "AUTO" fuzziness, you can optionally supply low and high
+     * distance arguments in the format {@code "AUTO:[low],[high]"}. See the query
+     * DSL documentation for more information about how these values affect the
+     * fuzziness value.
+     * Note: Using this method only makes sense if the field you
+     * are applying Fuzziness to is some sort of string.
+     */
+    public static Fuzziness fromString(String fuzzinessString) {
+        if (Strings.isEmpty(fuzzinessString)) {
+            throw new IllegalArgumentException("fuzziness cannot be null or empty.");
+        }
+        String upperCase = fuzzinessString.toUpperCase(Locale.ROOT);
+        // check if it is one of the "AUTO" variants
+        if (upperCase.equals("AUTO")) {
+            return Fuzziness.AUTO;
+        } else if (upperCase.startsWith("AUTO:")) {
+            return parseCustomAuto(upperCase);
+        } else {
+            // should be a float or int representing a valid edit distance, otherwise throw error
+            try {
+                float parsedFloat = Float.parseFloat(upperCase);
+                if (parsedFloat % 1 > 0) {
+                    throw new IllegalArgumentException("fuzziness needs to be one of 0.0, 1.0 or 2.0 but was " + parsedFloat);
+                }
+                return fromEdits((int) parsedFloat);
+            } catch (NumberFormatException e) {
+                throw new IllegalArgumentException("fuzziness cannot be [" + fuzzinessString + "].", e);
+            }
         }
-        this.lowDistance = lowDistance;
-        this.highDistance = highDistance;
     }
 
     /**
@@ -101,39 +134,23 @@ public final class Fuzziness implements ToXContentFragment, Writeable {
         }
     }
 
-    /**
-     * Creates a {@link Fuzziness} instance from an edit distance. The value must be one of {@code [0, 1, 2]}
-     *
-     * Note: Using this method only makes sense if the field you are applying Fuzziness to is some sort of string.
-     */
-    public static Fuzziness fromEdits(int edits) {
-        return new Fuzziness(edits);
-    }
-
-    public static Fuzziness build(Object fuzziness) {
-        if (fuzziness instanceof Fuzziness) {
-            return (Fuzziness) fuzziness;
-        }
-        String string = fuzziness.toString();
-        if (AUTO.asString().equalsIgnoreCase(string)) {
-            return AUTO;
-        } else if (string.toUpperCase(Locale.ROOT).startsWith(AUTO.asString() + ":")) {
-            return parseCustomAuto(string);
-        }
-        return new Fuzziness(string);
-    }
-
-    private static Fuzziness parseCustomAuto( final String string) {
-        assert string.toUpperCase(Locale.ROOT).startsWith(AUTO.asString() + ":");
-        String[] fuzzinessLimit = string.substring(AUTO.asString().length() + 1).split(",");
+    private static Fuzziness parseCustomAuto(final String fuzzinessString) {
+        assert fuzzinessString.toUpperCase(Locale.ROOT).startsWith(AUTO.asString() + ":");
+        String[] fuzzinessLimit = fuzzinessString.substring(AUTO.asString().length() + 1).split(",");
         if (fuzzinessLimit.length == 2) {
             try {
                 int lowerLimit = Integer.parseInt(fuzzinessLimit[0]);
                 int highLimit = Integer.parseInt(fuzzinessLimit[1]);
-                return new Fuzziness("AUTO", lowerLimit, highLimit);
+                if (lowerLimit < 0 || highLimit < 0 || lowerLimit > highLimit) {
+                    throw new ElasticsearchParseException("fuzziness wrongly configured [{}]. Must be 0 < lower value <= higher value.",
+                            fuzzinessString);
+                }
+                Fuzziness fuzziness = new Fuzziness("AUTO");
+                fuzziness.lowDistance = lowerLimit;
+                fuzziness.highDistance = highLimit;
+                return fuzziness;
             } catch (NumberFormatException e) {
-                throw new ElasticsearchParseException("failed to parse [{}] as a \"auto:int,int\"", e,
-                    string);
+                throw new ElasticsearchParseException("failed to parse [{}] as a \"auto:int,int\"", e, fuzzinessString);
             }
         } else {
             throw new ElasticsearchParseException("failed to find low and high distance values");
@@ -144,29 +161,9 @@ public final class Fuzziness implements ToXContentFragment, Writeable {
         XContentParser.Token token = parser.currentToken();
         switch (token) {
             case VALUE_STRING:
+                return fromString(parser.text());
             case VALUE_NUMBER:
-                final String fuzziness = parser.text();
-                if (AUTO.asString().equalsIgnoreCase(fuzziness)) {
-                    return AUTO;
-                } else if (fuzziness.toUpperCase(Locale.ROOT).startsWith(AUTO.asString() + ":")) {
-                    return parseCustomAuto(fuzziness);
-                }
-                try {
-                    final int minimumSimilarity = Integer.parseInt(fuzziness);
-                    switch (minimumSimilarity) {
-                        case 0:
-                            return ZERO;
-                        case 1:
-                            return ONE;
-                        case 2:
-                            return TWO;
-                        default:
-                            return build(fuzziness);
-                    }
-                } catch (NumberFormatException ex) {
-                    return build(fuzziness);
-                }
-
+                return fromEdits(parser.intValue());
             default:
                 throw new IllegalArgumentException("Can't parse fuzziness on token: [" + token + "]");
         }
@@ -200,7 +197,7 @@ public final class Fuzziness implements ToXContentFragment, Writeable {
         if (this.equals(AUTO) || isAutoWithCustomValues()) {
             return 1f;
         }
-        return Float.parseFloat(fuzziness.toString());
+        return Float.parseFloat(fuzziness);
     }
 
     private int termLen(String text) {
@@ -209,13 +206,13 @@ public final class Fuzziness implements ToXContentFragment, Writeable {
 
     public String asString() {
         if (isAutoWithCustomValues()) {
-            return fuzziness.toString() + ":" + lowDistance + "," + highDistance;
+            return fuzziness + ":" + lowDistance + "," + highDistance;
         }
-        return fuzziness.toString();
+        return fuzziness;
     }
 
     private boolean isAutoWithCustomValues() {
-        return fuzziness.startsWith("AUTO") && (lowDistance != DEFAULT_LOW_DISTANCE ||
+        return fuzziness.equals("AUTO") && (lowDistance != DEFAULT_LOW_DISTANCE ||
             highDistance != DEFAULT_HIGH_DISTANCE);
     }
 

+ 2 - 2
server/src/main/java/org/elasticsearch/index/query/MatchBoolPrefixQueryBuilder.java

@@ -161,8 +161,8 @@ public class MatchBoolPrefixQueryBuilder extends AbstractQueryBuilder<MatchBoolP
     }
 
     /** Sets the fuzziness used when evaluated to a fuzzy query type. Defaults to "AUTO". */
-    public MatchBoolPrefixQueryBuilder fuzziness(Object fuzziness) {
-        this.fuzziness = Fuzziness.build(fuzziness);
+    public MatchBoolPrefixQueryBuilder fuzziness(Fuzziness fuzziness) {
+        this.fuzziness = fuzziness;
         return this;
     }
 

+ 2 - 2
server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java

@@ -183,8 +183,8 @@ public class MatchQueryBuilder extends AbstractQueryBuilder<MatchQueryBuilder> {
     }
 
     /** Sets the fuzziness used when evaluated to a fuzzy query type. Defaults to "AUTO". */
-    public MatchQueryBuilder fuzziness(Object fuzziness) {
-        this.fuzziness = Fuzziness.build(fuzziness);
+    public MatchQueryBuilder fuzziness(Fuzziness fuzziness) {
+        this.fuzziness = Objects.requireNonNull(fuzziness);
         return this;
     }
 

+ 5 - 5
server/src/main/java/org/elasticsearch/index/query/MultiMatchQueryBuilder.java

@@ -376,10 +376,8 @@ public class MultiMatchQueryBuilder extends AbstractQueryBuilder<MultiMatchQuery
     /**
      * Sets the fuzziness used when evaluated to a fuzzy query type. Defaults to "AUTO".
      */
-    public MultiMatchQueryBuilder fuzziness(Object fuzziness) {
-        if (fuzziness != null) {
-            this.fuzziness = Fuzziness.build(fuzziness);
-        }
+    public MultiMatchQueryBuilder fuzziness(Fuzziness fuzziness) {
+        this.fuzziness = Objects.requireNonNull(fuzziness);
         return this;
     }
 
@@ -707,7 +705,6 @@ public class MultiMatchQueryBuilder extends AbstractQueryBuilder<MultiMatchQuery
                 .type(type)
                 .analyzer(analyzer)
                 .cutoffFrequency(cutoffFrequency)
-                .fuzziness(fuzziness)
                 .fuzzyRewrite(fuzzyRewrite)
                 .maxExpansions(maxExpansions)
                 .minimumShouldMatch(minimumShouldMatch)
@@ -723,6 +720,9 @@ public class MultiMatchQueryBuilder extends AbstractQueryBuilder<MultiMatchQuery
         if (lenient != null) {
             builder.lenient(lenient);
         }
+        if (fuzziness != null) {
+            builder.fuzziness(fuzziness);
+        }
         return builder;
     }
 

+ 5 - 5
server/src/main/java/org/elasticsearch/index/search/QueryStringQueryParser.java

@@ -423,7 +423,7 @@ public class QueryStringQueryParser extends XQueryParser {
         if (fuzzySlop.image.length() == 1) {
             return getFuzzyQuery(field, termImage, fuzziness.asDistance(termImage));
         }
-        return getFuzzyQuery(field, termImage, Fuzziness.build(fuzzySlop.image.substring(1)).asFloat());
+        return getFuzzyQuery(field, termImage, Fuzziness.fromString(fuzzySlop.image.substring(1)).asFloat());
     }
 
     @Override
@@ -434,7 +434,7 @@ public class QueryStringQueryParser extends XQueryParser {
         }
         List<Query> queries = new ArrayList<>();
         for (Map.Entry<String, Float> entry : fields.entrySet()) {
-            Query q = getFuzzyQuerySingle(entry.getKey(), termStr, minSimilarity);
+            Query q = getFuzzyQuerySingle(entry.getKey(), termStr, (int) minSimilarity);
             assert q != null;
             queries.add(applyBoost(q, entry.getValue()));
         }
@@ -447,7 +447,7 @@ public class QueryStringQueryParser extends XQueryParser {
         }
     }
 
-    private Query getFuzzyQuerySingle(String field, String termStr, float minSimilarity) throws ParseException {
+    private Query getFuzzyQuerySingle(String field, String termStr, int minSimilarity) throws ParseException {
         currentFieldType = context.fieldMapper(field);
         if (currentFieldType == null) {
             return newUnmappedFieldQuery(field);
@@ -455,7 +455,7 @@ public class QueryStringQueryParser extends XQueryParser {
         try {
             Analyzer normalizer = forceAnalyzer == null ? queryBuilder.context.getSearchAnalyzer(currentFieldType) : forceAnalyzer;
             BytesRef term = termStr == null ? null : normalizer.normalize(field, termStr);
-            return currentFieldType.fuzzyQuery(term, Fuzziness.fromEdits((int) minSimilarity),
+            return currentFieldType.fuzzyQuery(term, Fuzziness.fromEdits(minSimilarity),
                 getFuzzyPrefixLength(), fuzzyMaxExpansions, fuzzyTranspositions);
         } catch (RuntimeException e) {
             if (lenient) {
@@ -467,7 +467,7 @@ public class QueryStringQueryParser extends XQueryParser {
 
     @Override
     protected Query newFuzzyQuery(Term term, float minimumSimilarity, int prefixLength) {
-        int numEdits = Fuzziness.build(minimumSimilarity).asDistance(term.text());
+        int numEdits = Fuzziness.fromEdits((int) minimumSimilarity).asDistance(term.text());
         FuzzyQuery query = new FuzzyQuery(term, numEdits, prefixLength,
             fuzzyMaxExpansions, fuzzyTranspositions);
         QueryParsers.setRewriteMethod(query, fuzzyRewriteMethod);

+ 134 - 63
server/src/test/java/org/elasticsearch/common/unit/FuzzinessTests.java

@@ -18,6 +18,7 @@
  */
 package org.elasticsearch.common.unit;
 
+import org.elasticsearch.ElasticsearchParseException;
 import org.elasticsearch.common.io.stream.BytesStreamOutput;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -32,37 +33,131 @@ import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.sameInstance;
 
 public class FuzzinessTests extends ESTestCase {
-    public void testNumerics() {
-        String[] options = new String[]{"1.0", "1", "1.000000"};
-        assertThat(Fuzziness.build(randomFrom(options)).asFloat(), equalTo(1f));
+
+    public void testFromString() {
+        assertSame(Fuzziness.AUTO, Fuzziness.fromString("AUTO"));
+        assertSame(Fuzziness.AUTO, Fuzziness.fromString("auto"));
+        assertSame(Fuzziness.ZERO, Fuzziness.fromString("0"));
+        assertSame(Fuzziness.ZERO, Fuzziness.fromString("0.0"));
+        assertSame(Fuzziness.ONE, Fuzziness.fromString("1"));
+        assertSame(Fuzziness.ONE, Fuzziness.fromString("1.0"));
+        assertSame(Fuzziness.TWO, Fuzziness.fromString("2"));
+        assertSame(Fuzziness.TWO, Fuzziness.fromString("2.0"));
+
+        // cases that should throw exceptions
+        IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> Fuzziness.fromString(null));
+        assertEquals("fuzziness cannot be null or empty.", ex.getMessage());
+        ex = expectThrows(IllegalArgumentException.class, () -> Fuzziness.fromString(""));
+        assertEquals("fuzziness cannot be null or empty.", ex.getMessage());
+        ex = expectThrows(IllegalArgumentException.class, () -> Fuzziness.fromString("foo"));
+        assertEquals("fuzziness cannot be [foo].", ex.getMessage());
+        ex = expectThrows(IllegalArgumentException.class, () -> Fuzziness.fromString("1.2"));
+        assertEquals("fuzziness needs to be one of 0.0, 1.0 or 2.0 but was 1.2", ex.getMessage());
+    }
+
+    public void testNumericConstants() {
+        assertSame(Fuzziness.ZERO, Fuzziness.fromEdits(0));
+        assertSame(Fuzziness.ZERO, Fuzziness.fromString("0"));
+        assertSame(Fuzziness.ZERO, Fuzziness.fromString("0.0"));
+        assertThat(Fuzziness.ZERO.asString(), equalTo("0"));
+        assertThat(Fuzziness.ZERO.asDistance(), equalTo(0));
+        assertThat(Fuzziness.ZERO.asDistance(randomAlphaOfLengthBetween(0, randomIntBetween(1, 500))), equalTo(0));
+        assertThat(Fuzziness.ZERO.asFloat(), equalTo(0.0f));
+
+        assertSame(Fuzziness.ONE, Fuzziness.fromEdits(1));
+        assertSame(Fuzziness.ONE, Fuzziness.fromString("1"));
+        assertSame(Fuzziness.ONE, Fuzziness.fromString("1.0"));
+        assertThat(Fuzziness.ONE.asString(), equalTo("1"));
+        assertThat(Fuzziness.ONE.asDistance(), equalTo(1));
+        assertThat(Fuzziness.ONE.asDistance(randomAlphaOfLengthBetween(0, randomIntBetween(1, 500))), equalTo(1));
+        assertThat(Fuzziness.ONE.asFloat(), equalTo(1.0f));
+
+        assertSame(Fuzziness.TWO, Fuzziness.fromEdits(2));
+        assertSame(Fuzziness.TWO, Fuzziness.fromString("2"));
+        assertSame(Fuzziness.TWO, Fuzziness.fromString("2.0"));
+        assertThat(Fuzziness.TWO.asString(), equalTo("2"));
+        assertThat(Fuzziness.TWO.asDistance(), equalTo(2));
+        assertThat(Fuzziness.TWO.asDistance(randomAlphaOfLengthBetween(0, randomIntBetween(1, 500))), equalTo(2));
+        assertThat(Fuzziness.TWO.asFloat(), equalTo(2.0f));
+    }
+
+    public void testAutoFuzziness() {
+        assertSame(Fuzziness.AUTO, Fuzziness.fromString("auto"));
+        assertSame(Fuzziness.AUTO, Fuzziness.fromString("AUTO"));
+        assertThat(Fuzziness.AUTO.asString(), equalTo("AUTO"));
+        assertThat(Fuzziness.AUTO.asDistance(), equalTo(1));
+        assertThat(Fuzziness.AUTO.asDistance(randomAlphaOfLengthBetween(0, 2)), equalTo(0));
+        assertThat(Fuzziness.AUTO.asDistance(randomAlphaOfLengthBetween(3, 5)), equalTo(1));
+        assertThat(Fuzziness.AUTO.asDistance(randomAlphaOfLengthBetween(6, 100)), equalTo(2));
+        assertThat(Fuzziness.AUTO.asFloat(), equalTo(1.0f));
+    }
+
+    public void testCustomAutoFuzziness() {
+        int lowDistance = randomIntBetween(1, 10);
+        int highDistance = randomIntBetween(lowDistance, 20);
+        String auto = randomFrom("auto", "AUTO");
+        Fuzziness fuzziness = Fuzziness.fromString(auto + ":" + lowDistance + "," + highDistance);
+        assertNotSame(Fuzziness.AUTO, fuzziness);
+        if (lowDistance != Fuzziness.DEFAULT_LOW_DISTANCE || highDistance != Fuzziness.DEFAULT_HIGH_DISTANCE) {
+            assertThat(fuzziness.asString(), equalTo("AUTO:" + lowDistance + "," + highDistance));
+        }
+        if (lowDistance > 5) {
+            assertThat(fuzziness.asDistance(), equalTo(0));
+        } else if (highDistance > 5) {
+            assertThat(fuzziness.asDistance(), equalTo(1));
+        } else {
+            assertThat(fuzziness.asDistance(), equalTo(2));
+        }
+        assertThat(fuzziness.asDistance(randomAlphaOfLengthBetween(0, lowDistance - 1)), equalTo(0));
+        if (lowDistance != highDistance) {
+            assertThat(fuzziness.asDistance(randomAlphaOfLengthBetween(lowDistance, highDistance - 1)), equalTo(1));
+        }
+        assertThat(fuzziness.asDistance(randomAlphaOfLengthBetween(highDistance, 100)), equalTo(2));
+        assertThat(fuzziness.asFloat(), equalTo(1.0f));
+    }
+
+    public void testFromEditsIllegalArgs() {
+        int illegalValue = randomValueOtherThanMany(i -> i >= 0 && i <= 2, () -> randomInt());
+        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> Fuzziness.fromEdits(illegalValue));
+        assertThat(e.getMessage(), equalTo("Valid edit distances are [0, 1, 2] but was [" + illegalValue + "]"));
+    }
+
+    public void testFromStringIllegalArgs() {
+        Exception e = expectThrows(IllegalArgumentException.class, () -> Fuzziness.fromString(null));
+        assertThat(e.getMessage(), equalTo("fuzziness cannot be null or empty."));
+
+        e = expectThrows(IllegalArgumentException.class, () -> Fuzziness.fromString(""));
+        assertThat(e.getMessage(), equalTo("fuzziness cannot be null or empty."));
+
+        e = expectThrows(IllegalArgumentException.class, () -> Fuzziness.fromString("illegal"));
+        assertThat(e.getMessage(), equalTo("fuzziness cannot be [illegal]."));
+
+        e = expectThrows(ElasticsearchParseException.class, () -> Fuzziness.fromString("AUTO:badFormat"));
+        assertThat(e.getMessage(), equalTo("failed to find low and high distance values"));
     }
 
     public void testParseFromXContent() throws IOException {
         final int iters = randomIntBetween(10, 50);
         for (int i = 0; i < iters; i++) {
             {
-                float floatValue = randomFloat();
+                float floatValue = randomFrom(0.0f, 1.0f, 2.0f);
                 XContentBuilder json = jsonBuilder().startObject()
-                        .field(Fuzziness.X_FIELD_NAME, floatValue)
+                        .field(Fuzziness.X_FIELD_NAME, randomBoolean() ? String.valueOf(floatValue) : floatValue)
                         .endObject();
                 try (XContentParser parser = createParser(json)) {
                     assertThat(parser.nextToken(), equalTo(XContentParser.Token.START_OBJECT));
                     assertThat(parser.nextToken(), equalTo(XContentParser.Token.FIELD_NAME));
-                    assertThat(parser.nextToken(), equalTo(XContentParser.Token.VALUE_NUMBER));
+                    assertThat(parser.nextToken(),
+                            anyOf(equalTo(XContentParser.Token.VALUE_NUMBER), equalTo(XContentParser.Token.VALUE_STRING)));
                     Fuzziness fuzziness = Fuzziness.parse(parser);
                     assertThat(fuzziness.asFloat(), equalTo(floatValue));
                     assertThat(parser.nextToken(), equalTo(XContentParser.Token.END_OBJECT));
                 }
             }
             {
-                Integer intValue = frequently() ? randomIntBetween(0, 2) : randomIntBetween(0, 100);
-                Float floatRep = randomFloat();
-                Number value = intValue;
-                if (randomBoolean()) {
-                    value = Float.valueOf(floatRep += intValue);
-                }
+                int intValue = randomIntBetween(0, 2);
                 XContentBuilder json = jsonBuilder().startObject()
-                        .field(Fuzziness.X_FIELD_NAME, randomBoolean() ? value.toString() : value)
+                        .field(Fuzziness.X_FIELD_NAME, randomBoolean() ? String.valueOf(intValue) : intValue)
                         .endObject();
                 try (XContentParser parser = createParser(json)) {
                     assertThat(parser.nextToken(), equalTo(XContentParser.Token.START_OBJECT));
@@ -70,24 +165,19 @@ public class FuzzinessTests extends ESTestCase {
                     assertThat(parser.nextToken(), anyOf(equalTo(XContentParser.Token.VALUE_NUMBER),
                         equalTo(XContentParser.Token.VALUE_STRING)));
                     Fuzziness fuzziness = Fuzziness.parse(parser);
-                    if (value.intValue() >= 1) {
-                        assertThat(fuzziness.asDistance(), equalTo(Math.min(2, value.intValue())));
-                    }
                     assertThat(parser.nextToken(), equalTo(XContentParser.Token.END_OBJECT));
-                    if (intValue.equals(value)) {
-                        switch (intValue) {
-                            case 1:
-                                assertThat(fuzziness, sameInstance(Fuzziness.ONE));
-                                break;
-                            case 2:
-                                assertThat(fuzziness, sameInstance(Fuzziness.TWO));
-                                break;
-                            case 0:
-                                assertThat(fuzziness, sameInstance(Fuzziness.ZERO));
-                                break;
-                            default:
-                                break;
-                        }
+                    switch (intValue) {
+                    case 1:
+                        assertThat(fuzziness, sameInstance(Fuzziness.ONE));
+                        break;
+                    case 2:
+                        assertThat(fuzziness, sameInstance(Fuzziness.TWO));
+                        break;
+                    case 0:
+                        assertThat(fuzziness, sameInstance(Fuzziness.ZERO));
+                        break;
+                    default:
+                        break;
                     }
                 }
             }
@@ -104,7 +194,7 @@ public class FuzzinessTests extends ESTestCase {
                         int lowDistance = randomIntBetween(1, 3);
                         int highDistance = randomIntBetween(4, 10);
                         auto.append(":").append(lowDistance).append(",").append(highDistance);
-                        expectedFuzziness = Fuzziness.build(auto.toString());
+                        expectedFuzziness = Fuzziness.fromString(auto.toString());
                     }
                     json = expectedFuzziness.toXContent(jsonBuilder().startObject(), null).endObject();
                 }
@@ -122,20 +212,6 @@ public class FuzzinessTests extends ESTestCase {
                 }
             }
         }
-
-    }
-
-    public void testAuto() {
-        assertThat(Fuzziness.AUTO.asFloat(), equalTo(1f));
-    }
-
-    public void testAsDistance() {
-        final int iters = randomIntBetween(10, 50);
-        for (int i = 0; i < iters; i++) {
-            Integer integer = Integer.valueOf(randomIntBetween(0, 10));
-            String value = "" + (randomBoolean() ? integer.intValue() : integer.floatValue());
-            assertThat(Fuzziness.build(value).asDistance(), equalTo(Math.min(2, integer.intValue())));
-        }
     }
 
     public void testSerialization() throws IOException {
@@ -146,21 +222,16 @@ public class FuzzinessTests extends ESTestCase {
         fuzziness = Fuzziness.fromEdits(randomIntBetween(0, 2));
         deserializedFuzziness = doSerializeRoundtrip(fuzziness);
         assertEquals(fuzziness, deserializedFuzziness);
-    }
 
-    public void testSerializationDefaultAuto() throws IOException {
-        Fuzziness fuzziness = Fuzziness.AUTO;
-        Fuzziness deserializedFuzziness = doSerializeRoundtrip(fuzziness);
-        assertEquals(fuzziness, deserializedFuzziness);
-        assertEquals(fuzziness.asFloat(), deserializedFuzziness.asFloat(), 0f);
-    }
+        // custom AUTO
+        int lowDistance = randomIntBetween(1, 10);
+        int highDistance = randomIntBetween(lowDistance, 20);
+        fuzziness = Fuzziness.fromString("AUTO:" + lowDistance + "," + highDistance);
 
-    public void testSerializationCustomAuto() throws IOException {
-        Fuzziness original = Fuzziness.build("AUTO:4,7");
-        Fuzziness deserializedFuzziness = doSerializeRoundtrip(original);
-        assertNotSame(original, deserializedFuzziness);
-        assertEquals(original, deserializedFuzziness);
-        assertEquals(original.asString(), deserializedFuzziness.asString());
+        deserializedFuzziness = doSerializeRoundtrip(fuzziness);
+        assertNotSame(fuzziness, deserializedFuzziness);
+        assertEquals(fuzziness, deserializedFuzziness);
+        assertEquals(fuzziness.asString(), deserializedFuzziness.asString());
     }
 
     private static Fuzziness doSerializeRoundtrip(Fuzziness in) throws IOException {
@@ -171,21 +242,21 @@ public class FuzzinessTests extends ESTestCase {
     }
 
     public void testAsDistanceString() {
-        Fuzziness fuzziness = Fuzziness.build("0");
+        Fuzziness fuzziness = Fuzziness.fromEdits(0);
         assertEquals(0, fuzziness.asDistance(randomAlphaOfLengthBetween(0, 10)));
-        fuzziness = Fuzziness.build("1");
+        fuzziness = Fuzziness.fromEdits(1);
         assertEquals(1, fuzziness.asDistance(randomAlphaOfLengthBetween(0, 10)));
-        fuzziness = Fuzziness.build("2");
+        fuzziness = Fuzziness.fromEdits(2);
         assertEquals(2, fuzziness.asDistance(randomAlphaOfLengthBetween(0, 10)));
 
-        fuzziness = Fuzziness.build("AUTO");
+        fuzziness = Fuzziness.fromString("AUTO");
         assertEquals(0, fuzziness.asDistance(""));
         assertEquals(0, fuzziness.asDistance("ab"));
         assertEquals(1, fuzziness.asDistance("abc"));
         assertEquals(1, fuzziness.asDistance("abcde"));
         assertEquals(2, fuzziness.asDistance("abcdef"));
 
-        fuzziness = Fuzziness.build("AUTO:5,7");
+        fuzziness = Fuzziness.fromString("AUTO:5,7");
         assertEquals(0, fuzziness.asDistance(""));
         assertEquals(0, fuzziness.asDistance("abcd"));
         assertEquals(1, fuzziness.asDistance("abcde"));

+ 2 - 12
server/src/test/java/org/elasticsearch/index/query/FuzzyQueryBuilderTests.java

@@ -33,7 +33,6 @@ import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 
-import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.instanceOf;
 
@@ -95,15 +94,6 @@ public class FuzzyQueryBuilderTests extends AbstractQueryTestCase<FuzzyQueryBuil
         assertEquals("query value cannot be null", e.getMessage());
     }
 
-    public void testUnsupportedFuzzinessForStringType() throws IOException {
-        QueryShardContext context = createShardContext();
-        context.setAllowUnmappedFields(true);
-        FuzzyQueryBuilder fuzzyQueryBuilder = new FuzzyQueryBuilder(STRING_FIELD_NAME, "text");
-        fuzzyQueryBuilder.fuzziness(Fuzziness.build(randomFrom("a string which is not auto", "3h", "200s")));
-        NumberFormatException e = expectThrows(NumberFormatException.class, () -> fuzzyQueryBuilder.toQuery(context));
-        assertThat(e.getMessage(), containsString("For input string"));
-    }
-
     public void testToQueryWithStringField() throws IOException {
         String query = "{\n" +
                 "    \"fuzzy\":{\n" +
@@ -175,7 +165,7 @@ public class FuzzyQueryBuilderTests extends AbstractQueryTestCase<FuzzyQueryBuil
             "    }\n" +
             "}";
         String msg2 = "fuzziness wrongly configured";
-        IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class,
+        ElasticsearchParseException e2 = expectThrows(ElasticsearchParseException.class,
             () -> parseQuery(queryHavingNegativeFuzzinessLowLimit).toQuery(createShardContext()));
         assertTrue(e2.getMessage() + " didn't contain: " + msg2 + " but: " + e.getMessage(), e.getMessage().contains
             (msg));
@@ -215,7 +205,7 @@ public class FuzzyQueryBuilderTests extends AbstractQueryTestCase<FuzzyQueryBuil
                 "    \"fuzzy\":{\n" +
                 "        \"" + INT_FIELD_NAME + "\":{\n" +
                 "            \"value\":12,\n" +
-                "            \"fuzziness\":5\n" +
+                "            \"fuzziness\":2\n" +
                 "        }\n" +
                 "    }\n" +
                 "}\n";

+ 1 - 1
server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java

@@ -808,7 +808,7 @@ public class QueryStringQueryBuilderTests extends AbstractQueryTestCase<QueryStr
     }
 
     public void testFuzzyNumeric() throws Exception {
-        QueryStringQueryBuilder query = queryStringQuery("12~0.2").defaultField(INT_FIELD_NAME);
+        QueryStringQueryBuilder query = queryStringQuery("12~1.0").defaultField(INT_FIELD_NAME);
         QueryShardContext context = createShardContext();
         IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
                 () -> query.toQuery(context));

+ 2 - 1
server/src/test/java/org/elasticsearch/search/query/MultiMatchQueryIT.java

@@ -24,6 +24,7 @@ import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
 import org.elasticsearch.action.index.IndexRequestBuilder;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.unit.Fuzziness;
 import org.elasticsearch.common.util.set.Sets;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
@@ -703,7 +704,7 @@ public class MultiMatchQueryIT extends ESIntegTestCase {
         SearchResponse searchResponse = client().prepareSearch(idx)
                 .setExplain(true)
                 .setQuery(multiMatchQuery("foo").field("title", 100).field("body")
-                        .fuzziness(0)
+                        .fuzziness(Fuzziness.ZERO)
                         ).get();
         SearchHit[] hits = searchResponse.getHits().getHits();
         assertNotEquals("both documents should be on different shards", hits[0].getShard().getShardId(), hits[1].getShard().getShardId());

+ 7 - 5
server/src/test/java/org/elasticsearch/search/query/SearchQueryIT.java

@@ -30,6 +30,7 @@ import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.document.DocumentField;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.time.DateFormatter;
+import org.elasticsearch.common.unit.Fuzziness;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
 import org.elasticsearch.common.xcontent.XContentType;
@@ -670,21 +671,22 @@ public class SearchQueryIT extends ESIntegTestCase {
         indexRandom(true, client().prepareIndex("test", "_doc", "1").setSource("text", "Unit"),
                 client().prepareIndex("test", "_doc", "2").setSource("text", "Unity"));
 
-        SearchResponse searchResponse = client().prepareSearch().setQuery(matchQuery("text", "uniy").fuzziness("0")).get();
+        SearchResponse searchResponse = client().prepareSearch().setQuery(matchQuery("text", "uniy").fuzziness(Fuzziness.fromEdits(0)))
+                .get();
         assertHitCount(searchResponse, 0L);
 
-        searchResponse = client().prepareSearch().setQuery(matchQuery("text", "uniy").fuzziness("1")).get();
+        searchResponse = client().prepareSearch().setQuery(matchQuery("text", "uniy").fuzziness(Fuzziness.fromEdits(1))).get();
         assertHitCount(searchResponse, 2L);
         assertSearchHits(searchResponse, "1", "2");
 
-        searchResponse = client().prepareSearch().setQuery(matchQuery("text", "uniy").fuzziness("AUTO")).get();
+        searchResponse = client().prepareSearch().setQuery(matchQuery("text", "uniy").fuzziness(Fuzziness.fromString("AUTO"))).get();
         assertHitCount(searchResponse, 2L);
         assertSearchHits(searchResponse, "1", "2");
 
-        searchResponse = client().prepareSearch().setQuery(matchQuery("text", "uniy").fuzziness("AUTO:5,7")).get();
+        searchResponse = client().prepareSearch().setQuery(matchQuery("text", "uniy").fuzziness(Fuzziness.fromString("AUTO:5,7"))).get();
         assertHitCount(searchResponse, 0L);
 
-        searchResponse = client().prepareSearch().setQuery(matchQuery("text", "unify").fuzziness("AUTO:5,7")).get();
+        searchResponse = client().prepareSearch().setQuery(matchQuery("text", "unify").fuzziness(Fuzziness.fromString("AUTO:5,7"))).get();
         assertHitCount(searchResponse, 1L);
         assertSearchHits(searchResponse, "2");
     }

+ 2 - 4
test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java

@@ -20,6 +20,7 @@
 package org.elasticsearch.test;
 
 import com.fasterxml.jackson.core.io.JsonStringEncoder;
+
 import org.apache.lucene.search.BoostQuery;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.TermQuery;
@@ -710,13 +711,10 @@ public abstract class AbstractQueryTestCase<QB extends AbstractQueryBuilder<QB>>
     protected static Fuzziness randomFuzziness(String fieldName) {
         switch (fieldName) {
             case INT_FIELD_NAME:
-                return Fuzziness.build(randomIntBetween(3, 100));
             case DOUBLE_FIELD_NAME:
-                return Fuzziness.build(1 + randomFloat() * 10);
             case DATE_FIELD_NAME:
-                return Fuzziness.build(randomTimeValue());
             case DATE_NANOS_FIELD_NAME:
-                return Fuzziness.build(randomTimeValue());
+                return Fuzziness.fromEdits(randomIntBetween(0, 2));
             default:
                 if (randomBoolean()) {
                     return Fuzziness.fromEdits(randomIntBetween(0, 2));

+ 3 - 3
x-pack/plugin/sql/qa/src/main/resources/fulltext.csv-spec

@@ -31,7 +31,7 @@ SELECT emp_no, first_name, gender, last_name FROM test_emp WHERE QUERY('Man*', '
 ;
 
 matchWithFuzziness
-SELECT first_name, SCORE() FROM test_emp WHERE MATCH(first_name, 'geo', 'fuzziness=6');
+SELECT first_name, SCORE() FROM test_emp WHERE MATCH(first_name, 'geo', 'fuzziness=2');
 
   first_name:s  |   SCORE():f
 ----------------+---------------
@@ -57,7 +57,7 @@ Shir            |McClurg          |8.210788
 ;
 
 multiMatchWithFuzziness
-SELECT first_name, last_name, SCORE() FROM test_emp WHERE MATCH('first_name^3,last_name^5', 'geo hir', 'fuzziness=5;operator=or') ORDER BY first_name;
+SELECT first_name, last_name, SCORE() FROM test_emp WHERE MATCH('first_name^3,last_name^5', 'geo hir', 'fuzziness=2;operator=or') ORDER BY first_name;
 
   first_name:s  |   last_name:s   |    SCORE():f
 ----------------+-----------------+---------------
@@ -68,7 +68,7 @@ Uri             |Lenart           |4.105394
 ;
 
 queryWithFuzziness
-SELECT first_name, SCORE() FROM test_emp WHERE QUERY('geo~', 'fuzziness=5;default_field=first_name');
+SELECT first_name, SCORE() FROM test_emp WHERE QUERY('geo~', 'fuzziness=2;default_field=first_name');
 
   first_name:s  |    SCORE():f
 ----------------+---------------

+ 1 - 1
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/query/MatchQuery.java

@@ -32,7 +32,7 @@ public class MatchQuery extends LeafQuery {
         appliers.put("analyzer", (qb, s) -> qb.analyzer(s));
         appliers.put("auto_generate_synonyms_phrase_query", (qb, s) -> qb.autoGenerateSynonymsPhraseQuery(Booleans.parseBoolean(s)));
         appliers.put("cutoff_frequency", (qb, s) -> qb.cutoffFrequency(Float.valueOf(s)));
-        appliers.put("fuzziness", (qb, s) -> qb.fuzziness(Fuzziness.build(s)));
+        appliers.put("fuzziness", (qb, s) -> qb.fuzziness(Fuzziness.fromString(s)));
         appliers.put("fuzzy_transpositions", (qb, s) -> qb.fuzzyTranspositions(Booleans.parseBoolean(s)));
         appliers.put("fuzzy_rewrite", (qb, s) -> qb.fuzzyRewrite(s));
         appliers.put("lenient", (qb, s) -> qb.lenient(Booleans.parseBoolean(s)));

+ 1 - 1
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/query/MultiMatchQuery.java

@@ -33,7 +33,7 @@ public class MultiMatchQuery extends LeafQuery {
         appliers.put("analyzer", (qb, s) -> qb.analyzer(s));
         appliers.put("auto_generate_synonyms_phrase_query", (qb, s) -> qb.autoGenerateSynonymsPhraseQuery(Booleans.parseBoolean(s)));
         appliers.put("cutoff_frequency", (qb, s) -> qb.cutoffFrequency(Float.valueOf(s)));
-        appliers.put("fuzziness", (qb, s) -> qb.fuzziness(Fuzziness.build(s)));
+        appliers.put("fuzziness", (qb, s) -> qb.fuzziness(Fuzziness.fromString(s)));
         appliers.put("fuzzy_rewrite", (qb, s) -> qb.fuzzyRewrite(s));
         appliers.put("fuzzy_transpositions", (qb, s) -> qb.fuzzyTranspositions(Booleans.parseBoolean(s)));
         appliers.put("lenient", (qb, s) -> qb.lenient(Booleans.parseBoolean(s)));

+ 1 - 1
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/query/QueryStringQuery.java

@@ -37,7 +37,7 @@ public class QueryStringQuery extends LeafQuery {
         appliers.put("default_operator", (qb, s) -> qb.defaultOperator(Operator.fromString(s)));
         appliers.put("enable_position_increments", (qb, s) -> qb.enablePositionIncrements(Booleans.parseBoolean(s)));
         appliers.put("escape", (qb, s) -> qb.escape(Booleans.parseBoolean(s)));
-        appliers.put("fuzziness", (qb, s) -> qb.fuzziness(Fuzziness.build(s)));
+        appliers.put("fuzziness", (qb, s) -> qb.fuzziness(Fuzziness.fromString(s)));
         appliers.put("fuzzy_max_expansions", (qb, s) -> qb.fuzzyMaxExpansions(Integer.valueOf(s)));
         appliers.put("fuzzy_prefix_length", (qb, s) -> qb.fuzzyPrefixLength(Integer.valueOf(s)));
         appliers.put("fuzzy_rewrite", (qb, s) -> qb.fuzzyRewrite(s));