diff --git a/core/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java b/core/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java index 25faf568561..eae35d0fb23 100644 --- a/core/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java +++ b/core/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java @@ -70,17 +70,9 @@ import static org.elasticsearch.ElasticsearchException.readStackTrace; public abstract class StreamInput extends InputStream { - private final NamedWriteableRegistry namedWriteableRegistry; - private Version version = Version.CURRENT; - protected StreamInput() { - this.namedWriteableRegistry = new NamedWriteableRegistry(); - } - - protected StreamInput(NamedWriteableRegistry namedWriteableRegistry) { - this.namedWriteableRegistry = namedWriteableRegistry; - } + protected StreamInput() { } public Version getVersion() { return this.version; diff --git a/core/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java b/core/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java index e58caea5320..3e91a1c1280 100644 --- a/core/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java +++ b/core/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java @@ -55,6 +55,7 @@ import static org.elasticsearch.common.unit.TimeValue.parseTimeValue; import static org.elasticsearch.rest.RestRequest.Method.GET; import static org.elasticsearch.rest.RestRequest.Method.POST; import static org.elasticsearch.search.suggest.SuggestBuilders.termSuggestion; +import static org.elasticsearch.search.suggest.term.TermSuggestionBuilder.SuggestMode; /** * @@ -262,7 +263,9 @@ public class RestSearchAction extends BaseRestHandler { int suggestSize = request.paramAsInt("suggest_size", 5); String suggestMode = request.param("suggest_mode"); searchSourceBuilder.suggest(new SuggestBuilder().addSuggestion( - termSuggestion(suggestField).field(suggestField).text(suggestText).size(suggestSize).suggestMode(suggestMode))); + termSuggestion(suggestField).field(suggestField) + .text(suggestText).size(suggestSize) + .suggestMode(SuggestMode.fromString(suggestMode)))); modified = true; } return modified; diff --git a/core/src/main/java/org/elasticsearch/search/SearchModule.java b/core/src/main/java/org/elasticsearch/search/SearchModule.java index 739b97034bf..1f8e926bf81 100644 --- a/core/src/main/java/org/elasticsearch/search/SearchModule.java +++ b/core/src/main/java/org/elasticsearch/search/SearchModule.java @@ -223,6 +223,10 @@ import org.elasticsearch.search.rescore.QueryRescorerBuilder; import org.elasticsearch.search.rescore.RescoreBuilder; import org.elasticsearch.search.suggest.Suggester; import org.elasticsearch.search.suggest.Suggesters; +import org.elasticsearch.search.suggest.SuggestionBuilder; +import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder; +import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder; +import org.elasticsearch.search.suggest.term.TermSuggestionBuilder; import java.util.ArrayList; import java.util.HashMap; @@ -365,6 +369,9 @@ public class SearchModule extends AbstractModule { protected void configureSuggesters() { suggesters.bind(binder()); + namedWriteableRegistry.registerPrototype(SuggestionBuilder.class, TermSuggestionBuilder.PROTOTYPE); + namedWriteableRegistry.registerPrototype(SuggestionBuilder.class, PhraseSuggestionBuilder.PROTOTYPE); + namedWriteableRegistry.registerPrototype(SuggestionBuilder.class, CompletionSuggestionBuilder.PROTOTYPE); } protected void configureHighlighters() { diff --git a/core/src/main/java/org/elasticsearch/search/suggest/DirectSpellcheckerSettings.java b/core/src/main/java/org/elasticsearch/search/suggest/DirectSpellcheckerSettings.java index 2b4687c8497..a173781de8b 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/DirectSpellcheckerSettings.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/DirectSpellcheckerSettings.java @@ -25,16 +25,30 @@ import org.apache.lucene.util.automaton.LevenshteinAutomata; public class DirectSpellcheckerSettings { - private SuggestMode suggestMode = SuggestMode.SUGGEST_WHEN_NOT_IN_INDEX; - private float accuracy = 0.5f; - private Suggest.Suggestion.Sort sort = Suggest.Suggestion.Sort.SCORE; - private StringDistance stringDistance = DirectSpellChecker.INTERNAL_LEVENSHTEIN; - private int maxEdits = LevenshteinAutomata.MAXIMUM_SUPPORTED_DISTANCE; - private int maxInspections = 5; - private float maxTermFreq = 0.01f; - private int prefixLength = 1; - private int minWordLength = 4; - private float minDocFreq = 0f; + // NB: If this changes, make sure to change the default in TermBuilderSuggester + public static SuggestMode DEFAULT_SUGGEST_MODE = SuggestMode.SUGGEST_WHEN_NOT_IN_INDEX; + public static float DEFAULT_ACCURACY = 0.5f; + // NB: If this changes, make sure to change the default in TermBuilderSuggester + public static Suggest.Suggestion.Sort DEFAULT_SORT = Suggest.Suggestion.Sort.SCORE; + // NB: If this changes, make sure to change the default in TermBuilderSuggester + public static StringDistance DEFAULT_STRING_DISTANCE = DirectSpellChecker.INTERNAL_LEVENSHTEIN; + public static int DEFAULT_MAX_EDITS = LevenshteinAutomata.MAXIMUM_SUPPORTED_DISTANCE; + public static int DEFAULT_MAX_INSPECTIONS = 5; + public static float DEFAULT_MAX_TERM_FREQ = 0.01f; + public static int DEFAULT_PREFIX_LENGTH = 1; + public static int DEFAULT_MIN_WORD_LENGTH = 4; + public static float DEFAULT_MIN_DOC_FREQ = 0f; + + private SuggestMode suggestMode = DEFAULT_SUGGEST_MODE; + private float accuracy = DEFAULT_ACCURACY; + private Suggest.Suggestion.Sort sort = DEFAULT_SORT; + private StringDistance stringDistance = DEFAULT_STRING_DISTANCE; + private int maxEdits = DEFAULT_MAX_EDITS; + private int maxInspections = DEFAULT_MAX_INSPECTIONS; + private float maxTermFreq = DEFAULT_MAX_TERM_FREQ; + private int prefixLength = DEFAULT_PREFIX_LENGTH; + private int minWordLength = DEFAULT_MIN_WORD_LENGTH; + private float minDocFreq = DEFAULT_MIN_DOC_FREQ; public SuggestMode suggestMode() { return suggestMode; @@ -116,4 +130,4 @@ public class DirectSpellcheckerSettings { this.minDocFreq = minDocFreq; } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/elasticsearch/search/suggest/SuggestBuilder.java b/core/src/main/java/org/elasticsearch/search/suggest/SuggestBuilder.java index 92661b21f18..8037646f152 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/SuggestBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/SuggestBuilder.java @@ -19,32 +19,30 @@ package org.elasticsearch.search.suggest; import org.elasticsearch.action.support.ToXContentToBytes; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * Defines how to perform suggesting. This builders allows a number of global options to be specified and - * an arbitrary number of {@link org.elasticsearch.search.suggest.term.TermSuggestionBuilder} instances. + * an arbitrary number of {@link SuggestionBuilder} instances. *

- * Suggesting works by suggesting terms that appear in the suggest text that are similar compared to the terms in - * provided text. These spelling suggestions are based on several options described in this class. + * Suggesting works by suggesting terms/phrases that appear in the suggest text that are similar compared + * to the terms in provided text. These suggestions are based on several options described in this class. */ -public class SuggestBuilder extends ToXContentToBytes { +public class SuggestBuilder extends ToXContentToBytes implements Writeable { - private final String name; private String globalText; - private final List> suggestions = new ArrayList<>(); public SuggestBuilder() { - this.name = null; - } - - public SuggestBuilder(String name) { - this.name = name; } /** @@ -54,7 +52,7 @@ public class SuggestBuilder extends ToXContentToBytes { * The suggest text gets analyzed by the suggest analyzer or the suggest field search analyzer. * For each analyzed token, suggested terms are suggested if possible. */ - public SuggestBuilder setText(String globalText) { + public SuggestBuilder setText(@Nullable String globalText) { this.globalText = globalText; return this; } @@ -77,12 +75,7 @@ public class SuggestBuilder extends ToXContentToBytes { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - if(name == null) { - builder.startObject(); - } else { - builder.startObject(name); - } - + builder.startObject(); if (globalText != null) { builder.field("text", globalText); } @@ -92,4 +85,45 @@ public class SuggestBuilder extends ToXContentToBytes { builder.endObject(); return builder; } + + @Override + public SuggestBuilder readFrom(StreamInput in) throws IOException { + final SuggestBuilder builder = new SuggestBuilder(); + builder.globalText = in.readOptionalString(); + final int size = in.readVInt(); + for (int i = 0; i < size; i++) { + builder.suggestions.add(in.readSuggestion()); + } + return builder; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeOptionalString(globalText); + final int size = suggestions.size(); + out.writeVInt(size); + for (int i = 0; i < size; i++) { + out.writeSuggestion(suggestions.get(i)); + } + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + @SuppressWarnings("unchecked") + SuggestBuilder o = (SuggestBuilder)other; + return Objects.equals(globalText, o.globalText) && + Objects.equals(suggestions, o.suggestions); + } + + @Override + public int hashCode() { + return Objects.hash(globalText, suggestions); + } + } diff --git a/core/src/main/java/org/elasticsearch/search/suggest/SuggestUtils.java b/core/src/main/java/org/elasticsearch/search/suggest/SuggestUtils.java index 2509f792ecc..b9f2e29321f 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/SuggestUtils.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/SuggestUtils.java @@ -226,22 +226,22 @@ public final class SuggestUtils { } else if (parseFieldMatcher.match(fieldName, Fields.SORT)) { suggestion.sort(SuggestUtils.resolveSort(parser.text())); } else if (parseFieldMatcher.match(fieldName, Fields.STRING_DISTANCE)) { - suggestion.stringDistance(SuggestUtils.resolveDistance(parser.text())); + suggestion.stringDistance(SuggestUtils.resolveDistance(parser.text())); } else if (parseFieldMatcher.match(fieldName, Fields.MAX_EDITS)) { - suggestion.maxEdits(parser.intValue()); + suggestion.maxEdits(parser.intValue()); if (suggestion.maxEdits() < 1 || suggestion.maxEdits() > LevenshteinAutomata.MAXIMUM_SUPPORTED_DISTANCE) { throw new IllegalArgumentException("Illegal max_edits value " + suggestion.maxEdits()); } } else if (parseFieldMatcher.match(fieldName, Fields.MAX_INSPECTIONS)) { - suggestion.maxInspections(parser.intValue()); + suggestion.maxInspections(parser.intValue()); } else if (parseFieldMatcher.match(fieldName, Fields.MAX_TERM_FREQ)) { - suggestion.maxTermFreq(parser.floatValue()); + suggestion.maxTermFreq(parser.floatValue()); } else if (parseFieldMatcher.match(fieldName, Fields.PREFIX_LENGTH)) { - suggestion.prefixLength(parser.intValue()); + suggestion.prefixLength(parser.intValue()); } else if (parseFieldMatcher.match(fieldName, Fields.MIN_WORD_LENGTH)) { - suggestion.minQueryLength(parser.intValue()); + suggestion.minQueryLength(parser.intValue()); } else if (parseFieldMatcher.match(fieldName, Fields.MIN_DOC_FREQ)) { - suggestion.minDocFreq(parser.floatValue()); + suggestion.minDocFreq(parser.floatValue()); } else { return false; } diff --git a/core/src/main/java/org/elasticsearch/search/suggest/SuggestionBuilder.java b/core/src/main/java/org/elasticsearch/search/suggest/SuggestionBuilder.java index 7705f2201d1..59304fdd578 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/SuggestionBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/SuggestionBuilder.java @@ -53,6 +53,7 @@ public abstract class SuggestionBuilder> extends protected static final ParseField SHARDSIZE_FIELD = new ParseField("shard_size"); public SuggestionBuilder(String name) { + Objects.requireNonNull(name, "Suggester 'name' cannot be null"); this.name = name; } @@ -296,4 +297,5 @@ public abstract class SuggestionBuilder> extends * HashCode for the subclass of {@link SuggestionBuilder} to implement. */ protected abstract int doHashCode(); -} \ No newline at end of file + +} diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestionBuilder.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestionBuilder.java index 1b515e75409..afa0760e704 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestionBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestionBuilder.java @@ -49,9 +49,11 @@ import java.util.Set; */ public class CompletionSuggestionBuilder extends SuggestionBuilder { + public static final CompletionSuggestionBuilder PROTOTYPE = new CompletionSuggestionBuilder("_na_"); // name doesn't matter final static String SUGGESTION_NAME = "completion"; static final ParseField PAYLOAD_FIELD = new ParseField("payload"); static final ParseField CONTEXTS_FIELD = new ParseField("contexts", "context"); + private FuzzyOptionsBuilder fuzzyOptionsBuilder; private RegexOptionsBuilder regexOptionsBuilder; private final Map> queryContexts = new HashMap<>(); diff --git a/core/src/main/java/org/elasticsearch/search/suggest/term/TermSuggestionBuilder.java b/core/src/main/java/org/elasticsearch/search/suggest/term/TermSuggestionBuilder.java index e2a14c1a2b2..bd318e1a013 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/term/TermSuggestionBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/term/TermSuggestionBuilder.java @@ -16,13 +16,26 @@ * specific language governing permissions and limitations * under the License. */ + package org.elasticsearch.search.suggest.term; + import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.search.suggest.SuggestionBuilder; import java.io.IOException; +import java.util.Locale; +import java.util.Objects; + +import static org.elasticsearch.search.suggest.DirectSpellcheckerSettings.DEFAULT_ACCURACY; +import static org.elasticsearch.search.suggest.DirectSpellcheckerSettings.DEFAULT_MAX_EDITS; +import static org.elasticsearch.search.suggest.DirectSpellcheckerSettings.DEFAULT_MAX_INSPECTIONS; +import static org.elasticsearch.search.suggest.DirectSpellcheckerSettings.DEFAULT_MAX_TERM_FREQ; +import static org.elasticsearch.search.suggest.DirectSpellcheckerSettings.DEFAULT_MIN_DOC_FREQ; +import static org.elasticsearch.search.suggest.DirectSpellcheckerSettings.DEFAULT_MIN_WORD_LENGTH; +import static org.elasticsearch.search.suggest.DirectSpellcheckerSettings.DEFAULT_PREFIX_LENGTH; /** * Defines the actual suggest command. Each command uses the global options @@ -31,18 +44,19 @@ import java.io.IOException; */ public class TermSuggestionBuilder extends SuggestionBuilder { + public static final TermSuggestionBuilder PROTOTYPE = new TermSuggestionBuilder("_na_"); // name doesn't matter static final String SUGGESTION_NAME = "term"; - private String suggestMode; - private Float accuracy; - private String sort; - private String stringDistance; - private Integer maxEdits; - private Integer maxInspections; - private Float maxTermFreq; - private Integer prefixLength; - private Integer minWordLength; - private Float minDocFreq; + private SuggestMode suggestMode = SuggestMode.MISSING; + private Float accuracy = DEFAULT_ACCURACY; + private SortBy sort = SortBy.SCORE; + private StringDistanceImpl stringDistance = StringDistanceImpl.INTERNAL; + private Integer maxEdits = DEFAULT_MAX_EDITS; + private Integer maxInspections = DEFAULT_MAX_INSPECTIONS; + private Float maxTermFreq = DEFAULT_MAX_TERM_FREQ; + private Integer prefixLength = DEFAULT_PREFIX_LENGTH; + private Integer minWordLength = DEFAULT_MIN_WORD_LENGTH; + private Float minDocFreq = DEFAULT_MIN_DOC_FREQ; /** * @param name @@ -65,11 +79,19 @@ public class TermSuggestionBuilder extends SuggestionBuilder */ - public TermSuggestionBuilder suggestMode(String suggestMode) { + public TermSuggestionBuilder suggestMode(SuggestMode suggestMode) { + Objects.requireNonNull(suggestMode, "suggestMode must not be null"); this.suggestMode = suggestMode; return this; } + /** + * Get the suggest mode setting. + */ + public SuggestMode suggestMode() { + return suggestMode; + } + /** * s how similar the suggested terms at least need to be compared to the * original suggest text tokens. A value between 0 and 1 can be specified. @@ -78,11 +100,21 @@ public class TermSuggestionBuilder extends SuggestionBuilder * Default is 0.5 */ - public TermSuggestionBuilder setAccuracy(float accuracy) { + public TermSuggestionBuilder accuracy(float accuracy) { + if (accuracy < 0.0f || accuracy > 1.0f) { + throw new IllegalArgumentException("accuracy must be between 0 and 1"); + } this.accuracy = accuracy; return this; } + /** + * Get the accuracy setting. + */ + public Float accuracy() { + return accuracy; + } + /** * Sets how to sort the suggest terms per suggest text token. Two possible * values: @@ -90,19 +122,27 @@ public class TermSuggestionBuilder extends SuggestionBuilderscore - Sort should first be based on score, then * document frequency and then the term itself. *

  • frequency - Sort should first be based on document - * frequency, then scotr and then the term itself. + * frequency, then score and then the term itself. * *

    * What the score is depends on the suggester being used. */ - public TermSuggestionBuilder sort(String sort) { + public TermSuggestionBuilder sort(SortBy sort) { + Objects.requireNonNull(sort, "sort must not be null"); this.sort = sort; return this; } + /** + * Get the sort setting. + */ + public SortBy sort() { + return sort; + } + /** * Sets what string distance implementation to use for comparing how similar - * suggested terms are. Four possible values can be specified: + * suggested terms are. Five possible values can be specified: *

      *
    1. internal - This is the default and is based on * damerau_levenshtein, but highly optimized for comparing @@ -117,32 +157,60 @@ public class TermSuggestionBuilder extends SuggestionBuilder */ - public TermSuggestionBuilder stringDistance(String stringDistance) { + public TermSuggestionBuilder stringDistance(StringDistanceImpl stringDistance) { + Objects.requireNonNull(stringDistance, "stringDistance must not be null"); this.stringDistance = stringDistance; return this; } + /** + * Get the string distance implementation setting. + */ + public StringDistanceImpl stringDistance() { + return stringDistance; + } + /** * Sets the maximum edit distance candidate suggestions can have in order to * be considered as a suggestion. Can only be a value between 1 and 2. Any * other value result in an bad request error being thrown. Defaults to * 2. */ - public TermSuggestionBuilder maxEdits(Integer maxEdits) { + public TermSuggestionBuilder maxEdits(int maxEdits) { + if (maxEdits < 1 || maxEdits > 2) { + throw new IllegalArgumentException("maxEdits must be between 1 and 2"); + } this.maxEdits = maxEdits; return this; } + /** + * Get the maximum edit distance setting. + */ + public Integer maxEdits() { + return maxEdits; + } + /** * A factor that is used to multiply with the size in order to inspect more * candidate suggestions. Can improve accuracy at the cost of performance. * Defaults to 5. */ - public TermSuggestionBuilder maxInspections(Integer maxInspections) { + public TermSuggestionBuilder maxInspections(int maxInspections) { + if (maxInspections < 0) { + throw new IllegalArgumentException("maxInspections must be positive"); + } this.maxInspections = maxInspections; return this; } + /** + * Get the factor for inspecting more candidate suggestions setting. + */ + public Integer maxInspections() { + return maxInspections; + } + /** * Sets a maximum threshold in number of documents a suggest text token can * exist in order to be corrected. Can be a relative percentage number (e.g @@ -155,10 +223,23 @@ public class TermSuggestionBuilder extends SuggestionBuilder 1.0f && maxTermFreq != Math.floor(maxTermFreq)) { + throw new IllegalArgumentException("if maxTermFreq is greater than 1, it must not be a fraction"); + } this.maxTermFreq = maxTermFreq; return this; } + /** + * Get the maximum term frequency threshold setting. + */ + public Float maxTermFreq() { + return maxTermFreq; + } + /** * Sets the number of minimal prefix characters that must match in order be * a candidate suggestion. Defaults to 1. Increasing this number improves @@ -166,19 +247,39 @@ public class TermSuggestionBuilder extends SuggestionBuilder4. */ public TermSuggestionBuilder minWordLength(int minWordLength) { + if (minWordLength < 1) { + throw new IllegalArgumentException("minWordLength must be greater or equal to 1"); + } this.minWordLength = minWordLength; return this; } + /** + * Get the minimum length of a text term to be corrected setting. + */ + public Integer minWordLength() { + return minWordLength; + } + /** * Sets a minimal threshold in number of documents a suggested term should * appear in. This can be specified as an absolute number or as a relative @@ -187,10 +288,24 @@ public class TermSuggestionBuilder extends SuggestionBuilder 1.0f && minDocFreq != Math.floor(minDocFreq)) { + throw new IllegalArgumentException("if minDocFreq is greater than 1, it must not be a fraction"); + } this.minDocFreq = minDocFreq; return this; } + /** + * Get the minimal threshold for the frequency of a term appearing in the + * document set setting. + */ + public Float minDocFreq() { + return minDocFreq; + } + @Override public XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException { if (suggestMode != null) { @@ -233,25 +348,149 @@ public class TermSuggestionBuilder extends SuggestionBuilder { + /** Only suggest terms in the suggest text that aren't in the index. This is the default. */ + MISSING, + /** Only suggest terms that occur in more docs then the original suggest text term. */ + POPULAR, + /** Suggest any matching suggest terms based on tokens in the suggest text. */ + ALWAYS; + + protected static SuggestMode PROTOTYPE = MISSING; + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeVInt(ordinal()); + } + + @Override + public SuggestMode readFrom(final StreamInput in) throws IOException { + int ordinal = in.readVInt(); + if (ordinal < 0 || ordinal >= values().length) { + throw new IOException("Unknown SuggestMode ordinal [" + ordinal + "]"); + } + return values()[ordinal]; + } + + public static SuggestMode fromString(final String str) { + Objects.requireNonNull(str, "Input string is null"); + return valueOf(str.toUpperCase(Locale.ROOT)); + } + } + + /** An enum representing the valid sorting options */ + public enum SortBy implements Writeable { + /** Sort should first be based on score, then document frequency and then the term itself. */ + SCORE, + /** Sort should first be based on document frequency, then score and then the term itself. */ + FREQUENCY; + + protected static SortBy PROTOTYPE = SCORE; + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeVInt(ordinal()); + } + + @Override + public SortBy readFrom(final StreamInput in) throws IOException { + int ordinal = in.readVInt(); + if (ordinal < 0 || ordinal >= values().length) { + throw new IOException("Unknown SortBy ordinal [" + ordinal + "]"); + } + return values()[ordinal]; + } + + public static SortBy fromString(final String str) { + Objects.requireNonNull(str, "Input string is null"); + return valueOf(str.toUpperCase(Locale.ROOT)); + } + } + + /** An enum representing the valid string edit distance algorithms for determining suggestions. */ + public enum StringDistanceImpl implements Writeable { + /** This is the default and is based on damerau_levenshtein, but highly optimized + * for comparing string distance for terms inside the index. */ + INTERNAL, + /** String distance algorithm based on Damerau-Levenshtein algorithm. */ + DAMERAU_LEVENSHTEIN, + /** String distance algorithm based on Levenstein edit distance algorithm. */ + LEVENSTEIN, + /** String distance algorithm based on Jaro-Winkler algorithm. */ + JAROWINKLER, + /** String distance algorithm based on character n-grams. */ + NGRAM; + + protected static StringDistanceImpl PROTOTYPE = INTERNAL; + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeVInt(ordinal()); + } + + @Override + public StringDistanceImpl readFrom(final StreamInput in) throws IOException { + int ordinal = in.readVInt(); + if (ordinal < 0 || ordinal >= values().length) { + throw new IOException("Unknown StringDistanceImpl ordinal [" + ordinal + "]"); + } + return values()[ordinal]; + } + + public static StringDistanceImpl fromString(final String str) { + Objects.requireNonNull(str, "Input string is null"); + return valueOf(str.toUpperCase(Locale.ROOT)); + } + } + } diff --git a/core/src/test/java/org/elasticsearch/common/io/stream/AbstractWriteableEnumTestCase.java b/core/src/test/java/org/elasticsearch/common/io/stream/AbstractWriteableEnumTestCase.java new file mode 100644 index 00000000000..b8be6fb1493 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/common/io/stream/AbstractWriteableEnumTestCase.java @@ -0,0 +1,74 @@ +/* + * 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.io.stream; + +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; + +import static org.hamcrest.Matchers.equalTo; + +/** + * Abstract class offering base functionality for testing @{link Writeable} enums. + */ +public abstract class AbstractWriteableEnumTestCase extends ESTestCase { + + /** + * Test that the ordinals for the enum are consistent (i.e. the order hasn't changed) + * because writing an enum to a stream often uses the ordinal value. + */ + public abstract void testValidOrdinals(); + + /** + * Test that the conversion from a string to enum is correct. + */ + public abstract void testFromString(); + + /** + * Test that the correct enum value is produced from the serialized value in the {@link StreamInput}. + */ + public abstract void testReadFrom() throws IOException; + + /** + * Test that the correct serialized value is produced from the {@link StreamOutput}. + */ + public abstract void testWriteTo() throws IOException; + + // a convenience method for testing the write of a writeable enum + protected static void assertWriteToStream(final Writeable writeableEnum, final int ordinal) throws IOException { + try (BytesStreamOutput out = new BytesStreamOutput()) { + writeableEnum.writeTo(out); + try (StreamInput in = StreamInput.wrap(out.bytes())) { + assertThat(in.readVInt(), equalTo(ordinal)); + } + } + } + + // a convenience method for testing the read of a writeable enum + protected static > void assertReadFromStream(final int ordinal, final Writeable expected) throws IOException { + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.writeVInt(ordinal); + try (StreamInput in = StreamInput.wrap(out.bytes())) { + assertThat(expected.readFrom(in), equalTo(expected)); + } + } + } + +} diff --git a/core/src/test/java/org/elasticsearch/search/suggest/AbstractSuggestionBuilderTestCase.java b/core/src/test/java/org/elasticsearch/search/suggest/AbstractSuggestionBuilderTestCase.java index 61f678b5a08..77aada31a46 100644 --- a/core/src/test/java/org/elasticsearch/search/suggest/AbstractSuggestionBuilderTestCase.java +++ b/core/src/test/java/org/elasticsearch/search/suggest/AbstractSuggestionBuilderTestCase.java @@ -23,7 +23,9 @@ import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder; import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder; +import org.elasticsearch.search.suggest.term.TermSuggestionBuilder; import org.elasticsearch.test.ESTestCase; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -46,7 +48,9 @@ public abstract class AbstractSuggestionBuilderTestCase { + + @Override + protected TermSuggestionBuilder randomSuggestionBuilder() { + TermSuggestionBuilder testBuilder = new TermSuggestionBuilder(randomAsciiOfLength(10)); + maybeSet(testBuilder::suggestMode, randomSuggestMode()); + maybeSet(testBuilder::accuracy, randomFloat()); + maybeSet(testBuilder::sort, randomSort()); + maybeSet(testBuilder::stringDistance, randomStringDistance()); + maybeSet(testBuilder::maxEdits, randomIntBetween(1, 2)); + maybeSet(testBuilder::maxInspections, randomInt(Integer.MAX_VALUE)); + maybeSet(testBuilder::maxTermFreq, randomFloat()); + maybeSet(testBuilder::prefixLength, randomInt(Integer.MAX_VALUE)); + maybeSet(testBuilder::minWordLength, randomInt(Integer.MAX_VALUE)); + maybeSet(testBuilder::minDocFreq, randomFloat()); + return testBuilder; + } + + private SuggestMode randomSuggestMode() { + final int randomVal = randomIntBetween(0, 2); + switch (randomVal) { + case 0: return SuggestMode.MISSING; + case 1: return SuggestMode.POPULAR; + case 2: return SuggestMode.ALWAYS; + default: throw new IllegalArgumentException("No suggest mode with an ordinal of " + randomVal); + } + } + + private SortBy randomSort() { + int randomVal = randomIntBetween(0, 1); + switch (randomVal) { + case 0: return SortBy.SCORE; + case 1: return SortBy.FREQUENCY; + default: throw new IllegalArgumentException("No sort mode with an ordinal of " + randomVal); + } + } + + private StringDistanceImpl randomStringDistance() { + int randomVal = randomIntBetween(0, 4); + switch (randomVal) { + case 0: return StringDistanceImpl.INTERNAL; + case 1: return StringDistanceImpl.DAMERAU_LEVENSHTEIN; + case 2: return StringDistanceImpl.LEVENSTEIN; + case 3: return StringDistanceImpl.JAROWINKLER; + case 4: return StringDistanceImpl.NGRAM; + default: throw new IllegalArgumentException("No string distance algorithm with an ordinal of " + randomVal); + } + } + + @Override + protected void mutateSpecificParameters(TermSuggestionBuilder builder) throws IOException { + switch (randomIntBetween(0, 9)) { + case 0: + builder.suggestMode(randomValueOtherThan(builder.suggestMode(), () -> randomSuggestMode())); + break; + case 1: + builder.accuracy(randomValueOtherThan(builder.accuracy(), () -> randomFloat())); + break; + case 2: + builder.sort(randomValueOtherThan(builder.sort(), () -> randomSort())); + break; + case 3: + builder.stringDistance(randomValueOtherThan(builder.stringDistance(), () -> randomStringDistance())); + break; + case 4: + builder.maxEdits(randomValueOtherThan(builder.maxEdits(), () -> randomIntBetween(1, 2))); + break; + case 5: + builder.maxInspections(randomValueOtherThan(builder.maxInspections(), () -> randomInt(Integer.MAX_VALUE))); + break; + case 6: + builder.maxTermFreq(randomValueOtherThan(builder.maxTermFreq(), () -> randomFloat())); + break; + case 7: + builder.prefixLength(randomValueOtherThan(builder.prefixLength(), () -> randomInt(Integer.MAX_VALUE))); + break; + case 8: + builder.minWordLength(randomValueOtherThan(builder.minWordLength(), () -> randomInt(Integer.MAX_VALUE))); + break; + case 9: + builder.minDocFreq(randomValueOtherThan(builder.minDocFreq(), () -> randomFloat())); + break; + default: + break; // do nothing + } + } + + public void testInvalidParameters() throws IOException { + TermSuggestionBuilder builder = new TermSuggestionBuilder(randomAsciiOfLength(10)); + // test invalid accuracy values + try { + builder.accuracy(-0.5f); + fail("Should not allow accuracy to be set to a negative value."); + } catch (IllegalArgumentException e) { + } + try { + builder.accuracy(1.1f); + fail("Should not allow accuracy to be greater than 1.0."); + } catch (IllegalArgumentException e) { + } + // test invalid max edit distance values + try { + builder.maxEdits(0); + fail("Should not allow maxEdits to be less than 1."); + } catch (IllegalArgumentException e) { + } + try { + builder.maxEdits(-1); + fail("Should not allow maxEdits to be a negative value."); + } catch (IllegalArgumentException e) { + } + try { + builder.maxEdits(3); + fail("Should not allow maxEdits to be greater than 2."); + } catch (IllegalArgumentException e) { + } + // test invalid max inspections values + try { + builder.maxInspections(-1); + fail("Should not allow maxInspections to be a negative value."); + } catch (IllegalArgumentException e) { + } + // test invalid max term freq values + try { + builder.maxTermFreq(-0.5f); + fail("Should not allow max term freq to be a negative value."); + } catch (IllegalArgumentException e) { + } + try { + builder.maxTermFreq(1.5f); + fail("If max term freq is greater than 1, it must be a whole number."); + } catch (IllegalArgumentException e) { + } + try { + builder.maxTermFreq(2.0f); // this should be allowed + } catch (IllegalArgumentException e) { + fail("A max term freq greater than 1 that is a whole number should be allowed."); + } + // test invalid min doc freq values + try { + builder.minDocFreq(-0.5f); + fail("Should not allow min doc freq to be a negative value."); + } catch (IllegalArgumentException e) { + } + try { + builder.minDocFreq(1.5f); + fail("If min doc freq is greater than 1, it must be a whole number."); + } catch (IllegalArgumentException e) { + } + try { + builder.minDocFreq(2.0f); // this should be allowed + } catch (IllegalArgumentException e) { + fail("A min doc freq greater than 1 that is a whole number should be allowed."); + } + // test invalid min word length values + try { + builder.minWordLength(0); + fail("A min word length < 1 should not be allowed."); + } catch (IllegalArgumentException e) { + } + try { + builder.minWordLength(-1); + fail("Should not allow min word length to be a negative value."); + } catch (IllegalArgumentException e) { + } + // test invalid prefix length values + try { + builder.prefixLength(-1); + fail("Should not allow prefix length to be a negative value."); + } catch (IllegalArgumentException e) { + } + // test invalid size values + try { + builder.size(0); + fail("Size must be a positive value."); + } catch (IllegalArgumentException e) { + } + try { + builder.size(-1); + fail("Size must be a positive value."); + } catch (IllegalArgumentException e) { + } + // null values not allowed for enums + try { + builder.sort(null); + fail("Should not allow setting a null sort value."); + } catch (NullPointerException e) { + } + try { + builder.stringDistance(null); + fail("Should not allow setting a null string distance value."); + } catch (NullPointerException e) { + } + try { + builder.suggestMode(null); + fail("Should not allow setting a null suggest mode value."); + } catch (NullPointerException e) { + } + } + + public void testDefaultValuesSet() { + TermSuggestionBuilder builder = new TermSuggestionBuilder(randomAsciiOfLength(10)); + assertThat(builder.accuracy(), notNullValue()); + assertThat(builder.maxEdits(), notNullValue()); + assertThat(builder.maxInspections(), notNullValue()); + assertThat(builder.maxTermFreq(), notNullValue()); + assertThat(builder.minDocFreq(), notNullValue()); + assertThat(builder.minWordLength(), notNullValue()); + assertThat(builder.prefixLength(), notNullValue()); + assertThat(builder.sort(), notNullValue()); + assertThat(builder.stringDistance(), notNullValue()); + assertThat(builder.suggestMode(), notNullValue()); + } + +} diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/messy/tests/SuggestSearchTests.java b/modules/lang-mustache/src/test/java/org/elasticsearch/messy/tests/SuggestSearchTests.java index 6360a444a23..d846ff47307 100644 --- a/modules/lang-mustache/src/test/java/org/elasticsearch/messy/tests/SuggestSearchTests.java +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/messy/tests/SuggestSearchTests.java @@ -27,6 +27,8 @@ import static org.elasticsearch.index.query.QueryBuilders.matchQuery; import static org.elasticsearch.search.suggest.SuggestBuilders.phraseSuggestion; import static org.elasticsearch.search.suggest.SuggestBuilders.termSuggestion; import static org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder.candidateGenerator; +import static org.elasticsearch.search.suggest.SuggestionBuilder.SortBy; +import static org.elasticsearch.search.suggest.SuggestionBuilder.SuggestMode; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSuggestion; @@ -100,7 +102,7 @@ public class SuggestSearchTests extends ESIntegTestCase { refresh(); TermSuggestionBuilder termSuggest = termSuggestion("test") - .suggestMode("always") // Always, otherwise the results can vary between requests. + .suggestMode(SuggestMode.ALWAYS) // Always, otherwise the results can vary between requests. .text("abcd") .field("text"); logger.info("--> run suggestions with one index"); @@ -114,7 +116,7 @@ public class SuggestSearchTests extends ESIntegTestCase { index("test_1", "type1", "4", "text", "ab cc"); refresh(); termSuggest = termSuggestion("test") - .suggestMode("always") // Always, otherwise the results can vary between requests. + .suggestMode(SuggestMode.ALWAYS) // Always, otherwise the results can vary between requests. .text("ab cd") .minWordLength(1) .field("text"); @@ -141,7 +143,7 @@ public class SuggestSearchTests extends ESIntegTestCase { refresh(); termSuggest = termSuggestion("test") - .suggestMode("always") // Always, otherwise the results can vary between requests. + .suggestMode(SuggestMode.ALWAYS) // Always, otherwise the results can vary between requests. .text("ab cd") .minWordLength(1) .field("text"); @@ -161,7 +163,7 @@ public class SuggestSearchTests extends ESIntegTestCase { termSuggest = termSuggestion("test") - .suggestMode("always") // Always, otherwise the results can vary between requests. + .suggestMode(SuggestMode.ALWAYS) // Always, otherwise the results can vary between requests. .text("ABCD") .minWordLength(1) .field("text"); @@ -241,7 +243,7 @@ public class SuggestSearchTests extends ESIntegTestCase { assertThat("didn't ask for suggestions but got some", search.getSuggest(), nullValue()); TermSuggestionBuilder termSuggestion = termSuggestion("test") - .suggestMode("always") // Always, otherwise the results can vary between requests. + .suggestMode(SuggestMode.ALWAYS) // Always, otherwise the results can vary between requests. .text("abcd") .field("text") .size(10); @@ -316,7 +318,7 @@ public class SuggestSearchTests extends ESIntegTestCase { assertThat("didn't ask for suggestions but got some", search.getSuggest(), nullValue()); TermSuggestionBuilder termSuggest = termSuggestion("test") - .suggestMode("always") // Always, otherwise the results can vary between requests. + .suggestMode(SuggestMode.ALWAYS) // Always, otherwise the results can vary between requests. .text("abcd") .field("text"); Suggest suggest = searchSuggest( termSuggest); @@ -336,7 +338,7 @@ public class SuggestSearchTests extends ESIntegTestCase { refresh(); TermSuggestionBuilder termSuggest = termSuggestion("test") - .suggestMode("always") // Always, otherwise the results can vary between requests. + .suggestMode(SuggestMode.ALWAYS) // Always, otherwise the results can vary between requests. .text("abcd") .field("text"); Suggest suggest = searchSuggest( termSuggest); @@ -361,13 +363,13 @@ public class SuggestSearchTests extends ESIntegTestCase { Suggest suggest = searchSuggest( termSuggestion("size1") .size(1).text("prefix_abcd").maxTermFreq(10).prefixLength(1).minDocFreq(0) - .field("field1").suggestMode("always"), + .field("field1").suggestMode(SuggestMode.ALWAYS), termSuggestion("field2") .field("field2").text("prefix_eeeh prefix_efgh") - .maxTermFreq(10).minDocFreq(0).suggestMode("always"), + .maxTermFreq(10).minDocFreq(0).suggestMode(SuggestMode.ALWAYS), termSuggestion("accuracy") - .field("field2").text("prefix_efgh").setAccuracy(1f) - .maxTermFreq(10).minDocFreq(0).suggestMode("always")); + .field("field2").text("prefix_efgh").accuracy(1f) + .maxTermFreq(10).minDocFreq(0).suggestMode(SuggestMode.ALWAYS)); assertSuggestion(suggest, 0, "size1", "prefix_aacd"); assertThat(suggest.getSuggestion("field2").getEntries().get(0).getText().string(), equalTo("prefix_eeeh")); assertSuggestion(suggest, 0, "field2", "prefix_efgh"); @@ -403,15 +405,15 @@ public class SuggestSearchTests extends ESIntegTestCase { Suggest suggest = searchSuggest( "prefix_abcd", termSuggestion("size3SortScoreFirst") - .size(3).minDocFreq(0).field("field1").suggestMode("always"), + .size(3).minDocFreq(0).field("field1").suggestMode(SuggestMode.ALWAYS), termSuggestion("size10SortScoreFirst") - .size(10).minDocFreq(0).field("field1").suggestMode("always").shardSize(50), + .size(10).minDocFreq(0).field("field1").suggestMode(SuggestMode.ALWAYS).shardSize(50), termSuggestion("size3SortScoreFirstMaxEdits1") .maxEdits(1) - .size(10).minDocFreq(0).field("field1").suggestMode("always"), + .size(10).minDocFreq(0).field("field1").suggestMode(SuggestMode.ALWAYS), termSuggestion("size10SortFrequencyFirst") - .size(10).sort("frequency").shardSize(1000) - .minDocFreq(0).field("field1").suggestMode("always")); + .size(10).sort(SortBy.FREQUENCY).shardSize(1000) + .minDocFreq(0).field("field1").suggestMode(SuggestMode.ALWAYS)); // The commented out assertions fail sometimes because suggestions are based off of shard frequencies instead of index frequencies. assertSuggestion(suggest, 0, "size3SortScoreFirst", "prefix_aacd", "prefix_abcc", "prefix_accd"); @@ -784,7 +786,7 @@ public class SuggestSearchTests extends ESIntegTestCase { Suggest suggest = searchSuggest( "foobar", termSuggestion("simple") - .size(10).minDocFreq(0).field("field1").suggestMode("always")); + .size(10).minDocFreq(0).field("field1").suggestMode(SuggestMode.ALWAYS)); ElasticsearchAssertions.assertSuggestionSize(suggest, 0, 3, "simple"); }