From 2038429f63cd31721c0522d2d49eab66303c68fb Mon Sep 17 00:00:00 2001 From: Areek Zillur Date: Wed, 10 Feb 2016 16:21:24 -0500 Subject: initial refactoring of completion suggester --- .../common/io/stream/StreamInput.java | 10 +- .../common/io/stream/StreamOutput.java | 10 +- .../completion/CompletionSuggestParser.java | 27 +- .../completion/CompletionSuggestionBuilder.java | 310 ++++++--------------- .../completion/CompletionSuggestionContext.java | 34 +-- .../search/suggest/completion/FuzzyOptions.java | 277 ++++++++++++++++++ .../search/suggest/completion/RegexOptions.java | 153 ++++++++++ .../completion/context/CategoryContextMapping.java | 8 +- .../completion/context/CategoryQueryContext.java | 132 ++++++--- .../completion/context/ContextMappings.java | 7 +- .../completion/context/GeoContextMapping.java | 9 +- .../completion/context/GeoQueryContext.java | 161 ++++++++--- .../suggest/completion/context/QueryContext.java | 34 +++ .../suggest/AbstractSuggestionBuilderTestCase.java | 29 +- .../search/suggest/CompletionSuggestSearchIT.java | 35 ++- .../search/suggest/SuggestBuilderTests.java | 2 +- .../completion/CategoryQueryContextTests.java | 95 +++++++ .../CompletionSuggesterBuilderTests.java | 135 +++++++++ .../suggest/completion/FuzzyOptionsTests.java | 131 +++++++++ .../suggest/completion/GeoQueryContextTests.java | 136 +++++++++ .../suggest/completion/QueryContextTestCase.java | 60 ++++ .../suggest/completion/RegexOptionsTests.java | 71 +++++ .../suggest/completion/WritableTestCase.java | 4 +- .../phrase/DirectCandidateGeneratorTests.java | 6 - 24 files changed, 1468 insertions(+), 408 deletions(-) create mode 100644 core/src/main/java/org/elasticsearch/search/suggest/completion/FuzzyOptions.java create mode 100644 core/src/main/java/org/elasticsearch/search/suggest/completion/RegexOptions.java create mode 100644 core/src/main/java/org/elasticsearch/search/suggest/completion/context/QueryContext.java create mode 100644 core/src/test/java/org/elasticsearch/search/suggest/completion/CategoryQueryContextTests.java create mode 100644 core/src/test/java/org/elasticsearch/search/suggest/completion/CompletionSuggesterBuilderTests.java create mode 100644 core/src/test/java/org/elasticsearch/search/suggest/completion/FuzzyOptionsTests.java create mode 100644 core/src/test/java/org/elasticsearch/search/suggest/completion/GeoQueryContextTests.java create mode 100644 core/src/test/java/org/elasticsearch/search/suggest/completion/QueryContextTestCase.java create mode 100644 core/src/test/java/org/elasticsearch/search/suggest/completion/RegexOptionsTests.java (limited to 'core/src') 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 8c2dc444d2..10f7a5dba8 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 @@ -39,6 +39,7 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder; import org.elasticsearch.search.rescore.RescoreBuilder; import org.elasticsearch.search.suggest.SuggestionBuilder; +import org.elasticsearch.search.suggest.completion.context.QueryContext; import org.elasticsearch.search.suggest.phrase.SmoothingModel; import org.elasticsearch.tasks.Task; import org.joda.time.DateTime; @@ -693,6 +694,13 @@ public abstract class StreamInput extends InputStream { return readNamedWriteable(SuggestionBuilder.class); } + /** + * Reads a completion {@link QueryContext} from the current stream + */ + public QueryContext readCompletionSuggestionQueryContext() throws IOException { + return readNamedWriteable(QueryContext.class); + } + /** * Reads a {@link org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder} from the current stream */ @@ -706,7 +714,7 @@ public abstract class StreamInput extends InputStream { public SmoothingModel readPhraseSuggestionSmoothingModel() throws IOException { return readNamedWriteable(SmoothingModel.class); } - + /** * Reads a {@link Task.Status} from the current stream. */ diff --git a/core/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java b/core/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java index d14d6e77ff..4701948347 100644 --- a/core/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java +++ b/core/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java @@ -38,6 +38,7 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder; import org.elasticsearch.search.rescore.RescoreBuilder; import org.elasticsearch.search.suggest.SuggestionBuilder; +import org.elasticsearch.search.suggest.completion.context.QueryContext; import org.elasticsearch.search.suggest.phrase.SmoothingModel; import org.elasticsearch.tasks.Task; import org.joda.time.ReadableInstant; @@ -678,7 +679,7 @@ public abstract class StreamOutput extends OutputStream { public void writePhraseSuggestionSmoothingModel(SmoothingModel smoothinModel) throws IOException { writeNamedWriteable(smoothinModel); } - + /** * Writes a {@link Task.Status} to the current stream. */ @@ -717,4 +718,11 @@ public abstract class StreamOutput extends OutputStream { public void writeSuggestion(SuggestionBuilder suggestion) throws IOException { writeNamedWriteable(suggestion); } + + /** + * Writes a completion {@link QueryContext} to the current stream + */ + public void writeCompletionSuggestionQueryContext(QueryContext queryContext) throws IOException { + writeNamedWriteable(queryContext); + } } diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestParser.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestParser.java index 9d29525115..04f63042d4 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestParser.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestParser.java @@ -34,8 +34,6 @@ import org.elasticsearch.index.query.RegexpFlag; import org.elasticsearch.search.suggest.SuggestContextParser; import org.elasticsearch.search.suggest.SuggestUtils.Fields; import org.elasticsearch.search.suggest.SuggestionSearchContext; -import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder.FuzzyOptionsBuilder; -import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder.RegexOptionsBuilder; import org.elasticsearch.search.suggest.completion.context.ContextMapping; import org.elasticsearch.search.suggest.completion.context.ContextMappings; @@ -76,14 +74,14 @@ import java.util.Map; public class CompletionSuggestParser implements SuggestContextParser { private static ObjectParser TLP_PARSER = new ObjectParser<>(CompletionSuggestionBuilder.SUGGESTION_NAME, null); - private static ObjectParser REGEXP_PARSER = new ObjectParser<>(RegexOptionsBuilder.REGEX_OPTIONS.getPreferredName(), CompletionSuggestionBuilder.RegexOptionsBuilder::new); - private static ObjectParser FUZZY_PARSER = new ObjectParser<>(FuzzyOptionsBuilder.FUZZY_OPTIONS.getPreferredName(), CompletionSuggestionBuilder.FuzzyOptionsBuilder::new); + private static ObjectParser REGEXP_PARSER = new ObjectParser<>(RegexOptions.REGEX_OPTIONS.getPreferredName(), RegexOptions.Builder::new); + private static ObjectParser FUZZY_PARSER = new ObjectParser<>(FuzzyOptions.FUZZY_OPTIONS.getPreferredName(), FuzzyOptions.Builder::new); static { - FUZZY_PARSER.declareInt(CompletionSuggestionBuilder.FuzzyOptionsBuilder::setFuzzyMinLength, FuzzyOptionsBuilder.MIN_LENGTH_FIELD); - FUZZY_PARSER.declareInt(CompletionSuggestionBuilder.FuzzyOptionsBuilder::setMaxDeterminizedStates, FuzzyOptionsBuilder.MAX_DETERMINIZED_STATES_FIELD); - FUZZY_PARSER.declareBoolean(CompletionSuggestionBuilder.FuzzyOptionsBuilder::setUnicodeAware, FuzzyOptionsBuilder.UNICODE_AWARE_FIELD); - FUZZY_PARSER.declareInt(CompletionSuggestionBuilder.FuzzyOptionsBuilder::setFuzzyPrefixLength, FuzzyOptionsBuilder.PREFIX_LENGTH_FIELD); - FUZZY_PARSER.declareBoolean(CompletionSuggestionBuilder.FuzzyOptionsBuilder::setTranspositions, FuzzyOptionsBuilder.TRANSPOSITION_FIELD); + FUZZY_PARSER.declareInt(FuzzyOptions.Builder::setFuzzyMinLength, FuzzyOptions.MIN_LENGTH_FIELD); + FUZZY_PARSER.declareInt(FuzzyOptions.Builder::setMaxDeterminizedStates, FuzzyOptions.MAX_DETERMINIZED_STATES_FIELD); + FUZZY_PARSER.declareBoolean(FuzzyOptions.Builder::setUnicodeAware, FuzzyOptions.UNICODE_AWARE_FIELD); + FUZZY_PARSER.declareInt(FuzzyOptions.Builder::setFuzzyPrefixLength, FuzzyOptions.PREFIX_LENGTH_FIELD); + FUZZY_PARSER.declareBoolean(FuzzyOptions.Builder::setTranspositions, FuzzyOptions.TRANSPOSITION_FIELD); FUZZY_PARSER.declareValue((a, b) -> { try { a.setFuzziness(Fuzziness.parse(b).asDistance()); @@ -91,12 +89,12 @@ public class CompletionSuggestParser implements SuggestContextParser { throw new ElasticsearchException(e); } }, Fuzziness.FIELD); - REGEXP_PARSER.declareInt(CompletionSuggestionBuilder.RegexOptionsBuilder::setMaxDeterminizedStates, RegexOptionsBuilder.MAX_DETERMINIZED_STATES); - REGEXP_PARSER.declareStringOrNull(CompletionSuggestionBuilder.RegexOptionsBuilder::setFlags, RegexOptionsBuilder.FLAGS_VALUE); + REGEXP_PARSER.declareInt(RegexOptions.Builder::setMaxDeterminizedStates, RegexOptions.MAX_DETERMINIZED_STATES); + REGEXP_PARSER.declareStringOrNull(RegexOptions.Builder::setFlags, RegexOptions.FLAGS_VALUE); TLP_PARSER.declareStringArray(CompletionSuggestionContext::setPayloadFields, CompletionSuggestionBuilder.PAYLOAD_FIELD); - TLP_PARSER.declareObjectOrDefault(CompletionSuggestionContext::setFuzzyOptionsBuilder, FUZZY_PARSER, CompletionSuggestionBuilder.FuzzyOptionsBuilder::new, FuzzyOptionsBuilder.FUZZY_OPTIONS); - TLP_PARSER.declareObject(CompletionSuggestionContext::setRegexOptionsBuilder, REGEXP_PARSER, RegexOptionsBuilder.REGEX_OPTIONS); + TLP_PARSER.declareObjectOrDefault(CompletionSuggestionContext::setFuzzyOptionsBuilder, FUZZY_PARSER, FuzzyOptions.Builder::new, FuzzyOptions.FUZZY_OPTIONS); + TLP_PARSER.declareObject(CompletionSuggestionContext::setRegexOptionsBuilder, REGEXP_PARSER, RegexOptions.REGEX_OPTIONS); TLP_PARSER.declareString(SuggestionSearchContext.SuggestionContext::setField, Fields.FIELD); TLP_PARSER.declareField((p, v, c) -> { String analyzerName = p.text(); @@ -172,7 +170,4 @@ public class CompletionSuggestParser implements SuggestContextParser { throw new IllegalArgumentException("Field [" + suggestion.getField() + "] is not a completion suggest field"); } } - - - } 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 0bd37be128..fa8561998e 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 @@ -18,9 +18,6 @@ */ package org.elasticsearch.search.suggest.completion; -import org.apache.lucene.search.suggest.document.FuzzyCompletionQuery; -import org.apache.lucene.util.automaton.Operations; -import org.apache.lucene.util.automaton.RegExp; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -29,11 +26,11 @@ import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.QueryParseContext; import org.elasticsearch.index.query.QueryShardContext; -import org.elasticsearch.index.query.RegexpFlag; import org.elasticsearch.search.suggest.SuggestionBuilder; import org.elasticsearch.search.suggest.SuggestionSearchContext.SuggestionContext; import org.elasticsearch.search.suggest.completion.context.CategoryQueryContext; import org.elasticsearch.search.suggest.completion.context.GeoQueryContext; +import org.elasticsearch.search.suggest.completion.context.QueryContext; import java.io.IOException; import java.util.ArrayList; @@ -42,6 +39,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; /** @@ -57,207 +55,15 @@ public class CompletionSuggestionBuilder extends SuggestionBuilder> queryContexts = new HashMap<>(); + private FuzzyOptions fuzzyOptions; + private RegexOptions regexOptions; + private final Map> queryContexts = new HashMap<>(); private final Set payloadFields = new HashSet<>(); public CompletionSuggestionBuilder(String name) { super(name); } - /** - * Options for fuzzy queries - */ - public static class FuzzyOptionsBuilder implements ToXContent { - static final ParseField FUZZY_OPTIONS = new ParseField("fuzzy"); - static final ParseField TRANSPOSITION_FIELD = new ParseField("transpositions"); - static final ParseField MIN_LENGTH_FIELD = new ParseField("min_length"); - static final ParseField PREFIX_LENGTH_FIELD = new ParseField("prefix_length"); - static final ParseField UNICODE_AWARE_FIELD = new ParseField("unicode_aware"); - static final ParseField MAX_DETERMINIZED_STATES_FIELD = new ParseField("max_determinized_states"); - - private int editDistance = FuzzyCompletionQuery.DEFAULT_MAX_EDITS; - private boolean transpositions = FuzzyCompletionQuery.DEFAULT_TRANSPOSITIONS; - private int fuzzyMinLength = FuzzyCompletionQuery.DEFAULT_MIN_FUZZY_LENGTH; - private int fuzzyPrefixLength = FuzzyCompletionQuery.DEFAULT_NON_FUZZY_PREFIX; - private boolean unicodeAware = FuzzyCompletionQuery.DEFAULT_UNICODE_AWARE; - private int maxDeterminizedStates = Operations.DEFAULT_MAX_DETERMINIZED_STATES; - - public FuzzyOptionsBuilder() { - } - - /** - * Sets the level of fuzziness used to create suggestions using a {@link Fuzziness} instance. - * The default value is {@link Fuzziness#ONE} which allows for an "edit distance" of one. - */ - public FuzzyOptionsBuilder setFuzziness(int editDistance) { - this.editDistance = editDistance; - return this; - } - - /** - * Sets the level of fuzziness used to create suggestions using a {@link Fuzziness} instance. - * The default value is {@link Fuzziness#ONE} which allows for an "edit distance" of one. - */ - public FuzzyOptionsBuilder setFuzziness(Fuzziness fuzziness) { - this.editDistance = fuzziness.asDistance(); - return this; - } - - /** - * Sets if transpositions (swapping one character for another) counts as one character - * change or two. - * Defaults to true, meaning it uses the fuzzier option of counting transpositions as - * a single change. - */ - public FuzzyOptionsBuilder setTranspositions(boolean transpositions) { - this.transpositions = transpositions; - return this; - } - - /** - * Sets the minimum length of input string before fuzzy suggestions are returned, defaulting - * to 3. - */ - public FuzzyOptionsBuilder setFuzzyMinLength(int fuzzyMinLength) { - this.fuzzyMinLength = fuzzyMinLength; - return this; - } - - /** - * Sets the minimum length of the input, which is not checked for fuzzy alternatives, defaults to 1 - */ - public FuzzyOptionsBuilder setFuzzyPrefixLength(int fuzzyPrefixLength) { - this.fuzzyPrefixLength = fuzzyPrefixLength; - return this; - } - - /** - * Sets the maximum automaton states allowed for the fuzzy expansion - */ - public FuzzyOptionsBuilder setMaxDeterminizedStates(int maxDeterminizedStates) { - this.maxDeterminizedStates = maxDeterminizedStates; - return this; - } - - /** - * Set to true if all measurements (like edit distance, transpositions and lengths) are in unicode - * code points (actual letters) instead of bytes. Default is false. - */ - public FuzzyOptionsBuilder setUnicodeAware(boolean unicodeAware) { - this.unicodeAware = unicodeAware; - return this; - } - - /** - * Returns the maximum number of edits - */ - int getEditDistance() { - return editDistance; - } - - /** - * Returns if transpositions option is set - * - * if transpositions is set, then swapping one character for another counts as one edit instead of two. - */ - boolean isTranspositions() { - return transpositions; - } - - - /** - * Returns the length of input prefix after which edits are applied - */ - int getFuzzyMinLength() { - return fuzzyMinLength; - } - - /** - * Returns the minimum length of the input prefix required to apply any edits - */ - int getFuzzyPrefixLength() { - return fuzzyPrefixLength; - } - - /** - * Returns if all measurements (like edit distance, transpositions and lengths) are in unicode code - * points (actual letters) instead of bytes. - */ - boolean isUnicodeAware() { - return unicodeAware; - } - - /** - * Returns the maximum automaton states allowed for fuzzy expansion - */ - int getMaxDeterminizedStates() { - return maxDeterminizedStates; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(FUZZY_OPTIONS.getPreferredName()); - builder.field(Fuzziness.FIELD.getPreferredName(), editDistance); - builder.field(TRANSPOSITION_FIELD.getPreferredName(), transpositions); - builder.field(MIN_LENGTH_FIELD.getPreferredName(), fuzzyMinLength); - builder.field(PREFIX_LENGTH_FIELD.getPreferredName(), fuzzyPrefixLength); - builder.field(UNICODE_AWARE_FIELD.getPreferredName(), unicodeAware); - builder.field(MAX_DETERMINIZED_STATES_FIELD.getPreferredName(), maxDeterminizedStates); - builder.endObject(); - return builder; - } - } - - /** - * Options for regular expression queries - */ - public static class RegexOptionsBuilder implements ToXContent { - static final ParseField REGEX_OPTIONS = new ParseField("regex"); - static final ParseField FLAGS_VALUE = new ParseField("flags", "flags_value"); - static final ParseField MAX_DETERMINIZED_STATES = new ParseField("max_determinized_states"); - private int flagsValue = RegExp.ALL; - private int maxDeterminizedStates = Operations.DEFAULT_MAX_DETERMINIZED_STATES; - - public RegexOptionsBuilder() { - } - - /** - * Sets the regular expression syntax flags - * see {@link RegexpFlag} - */ - public RegexOptionsBuilder setFlags(String flags) { - this.flagsValue = RegexpFlag.resolveValue(flags); - return this; - } - - /** - * Sets the maximum automaton states allowed for the regular expression expansion - */ - public RegexOptionsBuilder setMaxDeterminizedStates(int maxDeterminizedStates) { - this.maxDeterminizedStates = maxDeterminizedStates; - return this; - } - - int getFlagsValue() { - return flagsValue; - } - - int getMaxDeterminizedStates() { - return maxDeterminizedStates; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(REGEX_OPTIONS.getPreferredName()); - builder.field(FLAGS_VALUE.getPreferredName(), flagsValue); - builder.field(MAX_DETERMINIZED_STATES.getPreferredName(), maxDeterminizedStates); - builder.endObject(); - return builder; - } - } - /** * Sets the prefix to provide completions for. * The prefix gets analyzed by the suggest analyzer. @@ -273,17 +79,17 @@ public class CompletionSuggestionBuilder extends SuggestionBuilder fields) { + this.payloadFields.addAll(fields); return this; } @@ -333,8 +139,8 @@ public class CompletionSuggestionBuilder extends SuggestionBuilder contexts = this.queryContexts.get(name); + private CompletionSuggestionBuilder contexts(String name, QueryContext... queryContexts) { + List contexts = this.queryContexts.get(name); if (contexts == null) { contexts = new ArrayList<>(2); this.queryContexts.put(name, contexts); @@ -345,22 +151,22 @@ public class CompletionSuggestionBuilder extends SuggestionBuilder> entry : this.queryContexts.entrySet()) { + for (Map.Entry> entry : this.queryContexts.entrySet()) { builder.startArray(entry.getKey()); for (ToXContent queryContext : entry.getValue()) { queryContext.toXContent(builder, params); @@ -374,8 +180,8 @@ public class CompletionSuggestionBuilder extends SuggestionBuilder> namedQueryContexts : queryContexts.entrySet()) { + out.writeString(namedQueryContexts.getKey()); + List queryContexts = namedQueryContexts.getValue(); + out.writeVInt(queryContexts.size()); + for (QueryContext queryContext : queryContexts) { + out.writeCompletionSuggestionQueryContext(queryContext); + } + } + } } @Override public CompletionSuggestionBuilder doReadFrom(StreamInput in, String name) throws IOException { - // NORELEASE - throw new UnsupportedOperationException(); + CompletionSuggestionBuilder completionSuggestionBuilder = new CompletionSuggestionBuilder(name); + if (in.readBoolean()) { + int numPayloadField = in.readVInt(); + for (int i = 0; i < numPayloadField; i++) { + completionSuggestionBuilder.payloadFields.add(in.readString()); + } + } + if (in.readBoolean()) { + completionSuggestionBuilder.fuzzyOptions = FuzzyOptions.readFuzzyOptions(in); + } + if (in.readBoolean()) { + completionSuggestionBuilder.regexOptions = RegexOptions.readRegexOptions(in); + } + if (in.readBoolean()) { + int numNamedQueryContexts = in.readVInt(); + for (int i = 0; i < numNamedQueryContexts; i++) { + String queryContextName = in.readString(); + int numQueryContexts = in.readVInt(); + List queryContexts = new ArrayList<>(numQueryContexts); + for (int j = 0; j < numQueryContexts; j++) { + queryContexts.add(in.readCompletionSuggestionQueryContext()); + } + completionSuggestionBuilder.queryContexts.put(queryContextName, queryContexts); + } + } + return completionSuggestionBuilder; } @Override protected boolean doEquals(CompletionSuggestionBuilder other) { - // NORELEASE - return false; + return Objects.equals(payloadFields, other.payloadFields) && + Objects.equals(fuzzyOptions, other.fuzzyOptions) && + Objects.equals(regexOptions, other.regexOptions) && + Objects.equals(queryContexts, other.queryContexts); } @Override protected int doHashCode() { - // NORELEASE - return 0; + return Objects.hash(payloadFields, fuzzyOptions, regexOptions, queryContexts); } } diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestionContext.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestionContext.java index f6d6de88f4..b20b9a5aee 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestionContext.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestionContext.java @@ -42,8 +42,8 @@ public class CompletionSuggestionContext extends SuggestionSearchContext.Suggest } private CompletionFieldMapper.CompletionFieldType fieldType; - private CompletionSuggestionBuilder.FuzzyOptionsBuilder fuzzyOptionsBuilder; - private CompletionSuggestionBuilder.RegexOptionsBuilder regexOptionsBuilder; + private FuzzyOptions fuzzyOptions; + private RegexOptions regexOptions; private Map> queryContexts = Collections.emptyMap(); private Set payloadFields = Collections.emptySet(); @@ -55,12 +55,12 @@ public class CompletionSuggestionContext extends SuggestionSearchContext.Suggest this.fieldType = fieldType; } - void setRegexOptionsBuilder(CompletionSuggestionBuilder.RegexOptionsBuilder regexOptionsBuilder) { - this.regexOptionsBuilder = regexOptionsBuilder; + void setRegexOptionsBuilder(RegexOptions.Builder regexOptionsBuilder) { + this.regexOptions = regexOptionsBuilder.build(); } - void setFuzzyOptionsBuilder(CompletionSuggestionBuilder.FuzzyOptionsBuilder fuzzyOptionsBuilder) { - this.fuzzyOptionsBuilder = fuzzyOptionsBuilder; + void setFuzzyOptionsBuilder(FuzzyOptions.Builder fuzzyOptionsBuilder) { + this.fuzzyOptions = fuzzyOptionsBuilder.build(); } void setQueryContexts(Map> queryContexts) { @@ -72,7 +72,7 @@ public class CompletionSuggestionContext extends SuggestionSearchContext.Suggest } void setPayloadFields(List fields) { - setPayloadFields(new HashSet(fields)); + setPayloadFields(new HashSet<>(fields)); } Set getPayloadFields() { @@ -83,24 +83,24 @@ public class CompletionSuggestionContext extends SuggestionSearchContext.Suggest CompletionFieldMapper.CompletionFieldType fieldType = getFieldType(); final CompletionQuery query; if (getPrefix() != null) { - if (fuzzyOptionsBuilder != null) { + if (fuzzyOptions != null) { query = fieldType.fuzzyQuery(getPrefix().utf8ToString(), - Fuzziness.fromEdits(fuzzyOptionsBuilder.getEditDistance()), - fuzzyOptionsBuilder.getFuzzyPrefixLength(), fuzzyOptionsBuilder.getFuzzyMinLength(), - fuzzyOptionsBuilder.getMaxDeterminizedStates(), fuzzyOptionsBuilder.isTranspositions(), - fuzzyOptionsBuilder.isUnicodeAware()); + Fuzziness.fromEdits(fuzzyOptions.getEditDistance()), + fuzzyOptions.getFuzzyPrefixLength(), fuzzyOptions.getFuzzyMinLength(), + fuzzyOptions.getMaxDeterminizedStates(), fuzzyOptions.isTranspositions(), + fuzzyOptions.isUnicodeAware()); } else { query = fieldType.prefixQuery(getPrefix()); } } else if (getRegex() != null) { - if (fuzzyOptionsBuilder != null) { + if (fuzzyOptions != null) { throw new IllegalArgumentException("can not use 'fuzzy' options with 'regex"); } - if (regexOptionsBuilder == null) { - regexOptionsBuilder = new CompletionSuggestionBuilder.RegexOptionsBuilder(); + if (regexOptions == null) { + regexOptions = RegexOptions.builder().build(); } - query = fieldType.regexpQuery(getRegex(), regexOptionsBuilder.getFlagsValue(), - regexOptionsBuilder.getMaxDeterminizedStates()); + query = fieldType.regexpQuery(getRegex(), regexOptions.getFlagsValue(), + regexOptions.getMaxDeterminizedStates()); } else { throw new IllegalArgumentException("'prefix' or 'regex' must be defined"); } diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/FuzzyOptions.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/FuzzyOptions.java new file mode 100644 index 0000000000..317ac049d6 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/FuzzyOptions.java @@ -0,0 +1,277 @@ +/* + * 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.search.suggest.completion; + +import org.apache.lucene.search.suggest.document.FuzzyCompletionQuery; +import org.apache.lucene.util.automaton.Operations; +import org.elasticsearch.common.ParseField; +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.unit.Fuzziness; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +/** + * Fuzzy options for completion suggester + */ +public class FuzzyOptions implements ToXContent, Writeable { + static final ParseField FUZZY_OPTIONS = new ParseField("fuzzy"); + static final ParseField TRANSPOSITION_FIELD = new ParseField("transpositions"); + static final ParseField MIN_LENGTH_FIELD = new ParseField("min_length"); + static final ParseField PREFIX_LENGTH_FIELD = new ParseField("prefix_length"); + static final ParseField UNICODE_AWARE_FIELD = new ParseField("unicode_aware"); + static final ParseField MAX_DETERMINIZED_STATES_FIELD = new ParseField("max_determinized_states"); + + private int editDistance; + private boolean transpositions; + private int fuzzyMinLength; + private int fuzzyPrefixLength; + private boolean unicodeAware; + private int maxDeterminizedStates; + + private FuzzyOptions(int editDistance, boolean transpositions, int fuzzyMinLength, int fuzzyPrefixLength, + boolean unicodeAware, int maxDeterminizedStates) { + this.editDistance = editDistance; + this.transpositions = transpositions; + this.fuzzyMinLength = fuzzyMinLength; + this.fuzzyPrefixLength = fuzzyPrefixLength; + this.unicodeAware = unicodeAware; + this.maxDeterminizedStates = maxDeterminizedStates; + } + + private FuzzyOptions() { + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the maximum number of edits + */ + public int getEditDistance() { + return editDistance; + } + + /** + * Returns if transpositions option is set + * + * if transpositions is set, then swapping one character for another counts as one edit instead of two. + */ + public boolean isTranspositions() { + return transpositions; + } + + /** + * Returns the length of input prefix after which edits are applied + */ + public int getFuzzyMinLength() { + return fuzzyMinLength; + } + + /** + * Returns the minimum length of the input prefix required to apply any edits + */ + public int getFuzzyPrefixLength() { + return fuzzyPrefixLength; + } + + /** + * Returns if all measurements (like edit distance, transpositions and lengths) are in unicode code + * points (actual letters) instead of bytes. + */ + public boolean isUnicodeAware() { + return unicodeAware; + } + + /** + * Returns the maximum automaton states allowed for fuzzy expansion + */ + public int getMaxDeterminizedStates() { + return maxDeterminizedStates; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FuzzyOptions that = (FuzzyOptions) o; + + if (editDistance != that.editDistance) return false; + if (transpositions != that.transpositions) return false; + if (fuzzyMinLength != that.fuzzyMinLength) return false; + if (fuzzyPrefixLength != that.fuzzyPrefixLength) return false; + if (unicodeAware != that.unicodeAware) return false; + return maxDeterminizedStates == that.maxDeterminizedStates; + + } + + @Override + public int hashCode() { + int result = editDistance; + result = 31 * result + (transpositions ? 1 : 0); + result = 31 * result + fuzzyMinLength; + result = 31 * result + fuzzyPrefixLength; + result = 31 * result + (unicodeAware ? 1 : 0); + result = 31 * result + maxDeterminizedStates; + return result; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(FUZZY_OPTIONS.getPreferredName()); + builder.field(Fuzziness.FIELD.getPreferredName(), editDistance); + builder.field(TRANSPOSITION_FIELD.getPreferredName(), transpositions); + builder.field(MIN_LENGTH_FIELD.getPreferredName(), fuzzyMinLength); + builder.field(PREFIX_LENGTH_FIELD.getPreferredName(), fuzzyPrefixLength); + builder.field(UNICODE_AWARE_FIELD.getPreferredName(), unicodeAware); + builder.field(MAX_DETERMINIZED_STATES_FIELD.getPreferredName(), maxDeterminizedStates); + builder.endObject(); + return builder; + } + + public static FuzzyOptions readFuzzyOptions(StreamInput in) throws IOException { + FuzzyOptions fuzzyOptions = new FuzzyOptions(); + fuzzyOptions.readFrom(in); + return fuzzyOptions; + } + + @Override + public FuzzyOptions readFrom(StreamInput in) throws IOException { + this.transpositions = in.readBoolean(); + this.unicodeAware = in.readBoolean(); + this.editDistance = in.readVInt(); + this.fuzzyMinLength = in.readVInt(); + this.fuzzyPrefixLength = in.readVInt(); + this.maxDeterminizedStates = in.readVInt(); + return this; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeBoolean(transpositions); + out.writeBoolean(unicodeAware); + out.writeVInt(editDistance); + out.writeVInt(fuzzyMinLength); + out.writeVInt(fuzzyPrefixLength); + out.writeVInt(maxDeterminizedStates); + } + + /** + * Options for fuzzy queries + */ + public static class Builder { + + private int editDistance = FuzzyCompletionQuery.DEFAULT_MAX_EDITS; + private boolean transpositions = FuzzyCompletionQuery.DEFAULT_TRANSPOSITIONS; + private int fuzzyMinLength = FuzzyCompletionQuery.DEFAULT_MIN_FUZZY_LENGTH; + private int fuzzyPrefixLength = FuzzyCompletionQuery.DEFAULT_NON_FUZZY_PREFIX; + private boolean unicodeAware = FuzzyCompletionQuery.DEFAULT_UNICODE_AWARE; + private int maxDeterminizedStates = Operations.DEFAULT_MAX_DETERMINIZED_STATES; + + public Builder() { + } + + /** + * Sets the level of fuzziness used to create suggestions using a {@link Fuzziness} instance. + * The default value is {@link Fuzziness#ONE} which allows for an "edit distance" of one. + */ + public Builder setFuzziness(int editDistance) { + if (editDistance < 0 || editDistance > 2) { + throw new IllegalArgumentException("fuzziness must be between 0 and 2"); + } + this.editDistance = editDistance; + return this; + } + + /** + * Sets the level of fuzziness used to create suggestions using a {@link Fuzziness} instance. + * The default value is {@link Fuzziness#ONE} which allows for an "edit distance" of one. + */ + public Builder setFuzziness(Fuzziness fuzziness) { + Objects.requireNonNull(fuzziness, "fuzziness must not be null"); + return setFuzziness(fuzziness.asDistance()); + } + + /** + * Sets if transpositions (swapping one character for another) counts as one character + * change or two. + * Defaults to true, meaning it uses the fuzzier option of counting transpositions as + * a single change. + */ + public Builder setTranspositions(boolean transpositions) { + this.transpositions = transpositions; + return this; + } + + /** + * Sets the minimum length of input string before fuzzy suggestions are returned, defaulting + * to 3. + */ + public Builder setFuzzyMinLength(int fuzzyMinLength) { + if (fuzzyMinLength < 0) { + throw new IllegalArgumentException("fuzzyMinLength must not be negative"); + } + this.fuzzyMinLength = fuzzyMinLength; + return this; + } + + /** + * Sets the minimum length of the input, which is not checked for fuzzy alternatives, defaults to 1 + */ + public Builder setFuzzyPrefixLength(int fuzzyPrefixLength) { + if (fuzzyPrefixLength < 0) { + throw new IllegalArgumentException("fuzzyPrefixLength must not be negative"); + } + this.fuzzyPrefixLength = fuzzyPrefixLength; + return this; + } + + /** + * Sets the maximum automaton states allowed for the fuzzy expansion + */ + public Builder setMaxDeterminizedStates(int maxDeterminizedStates) { + if (maxDeterminizedStates < 0) { + throw new IllegalArgumentException("maxDeterminizedStates must not be negative"); + } + this.maxDeterminizedStates = maxDeterminizedStates; + return this; + } + + /** + * Set to true if all measurements (like edit distance, transpositions and lengths) are in unicode + * code points (actual letters) instead of bytes. Default is false. + */ + public Builder setUnicodeAware(boolean unicodeAware) { + this.unicodeAware = unicodeAware; + return this; + } + + public FuzzyOptions build() { + return new FuzzyOptions(editDistance, transpositions, fuzzyMinLength, fuzzyPrefixLength, + unicodeAware, maxDeterminizedStates); + } + } +} diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/RegexOptions.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/RegexOptions.java new file mode 100644 index 0000000000..fc183cdb1c --- /dev/null +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/RegexOptions.java @@ -0,0 +1,153 @@ +/* + * 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.search.suggest.completion; + +import org.apache.lucene.util.automaton.Operations; +import org.apache.lucene.util.automaton.RegExp; +import org.elasticsearch.common.ParseField; +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.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.RegexpFlag; + +import java.io.IOException; + +/** + * Regular expression options for completion suggester + */ +public class RegexOptions implements ToXContent, Writeable { + static final String NAME = "regex"; + static final ParseField REGEX_OPTIONS = new ParseField(NAME); + static final ParseField FLAGS_VALUE = new ParseField("flags", "flags_value"); + static final ParseField MAX_DETERMINIZED_STATES = new ParseField("max_determinized_states"); + private int flagsValue; + private int maxDeterminizedStates; + + private RegexOptions() { + } + + private RegexOptions(int flagsValue, int maxDeterminizedStates) { + this.flagsValue = flagsValue; + this.maxDeterminizedStates = maxDeterminizedStates; + } + + /** + * Returns internal regular expression syntax flag value + * see {@link RegexpFlag#value()} + */ + public int getFlagsValue() { + return flagsValue; + } + + /** + * Returns the maximum automaton states allowed for fuzzy expansion + */ + public int getMaxDeterminizedStates() { + return maxDeterminizedStates; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RegexOptions that = (RegexOptions) o; + + if (flagsValue != that.flagsValue) return false; + return maxDeterminizedStates == that.maxDeterminizedStates; + + } + + @Override + public int hashCode() { + int result = flagsValue; + result = 31 * result + maxDeterminizedStates; + return result; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(REGEX_OPTIONS.getPreferredName()); + builder.field(FLAGS_VALUE.getPreferredName(), flagsValue); + builder.field(MAX_DETERMINIZED_STATES.getPreferredName(), maxDeterminizedStates); + builder.endObject(); + return builder; + } + + public static RegexOptions readRegexOptions(StreamInput in) throws IOException { + RegexOptions regexOptions = new RegexOptions(); + regexOptions.readFrom(in); + return regexOptions; + } + + @Override + public RegexOptions readFrom(StreamInput in) throws IOException { + this.flagsValue = in.readVInt(); + this.maxDeterminizedStates = in.readVInt(); + return this; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVInt(flagsValue); + out.writeVInt(maxDeterminizedStates); + } + + /** + * Options for regular expression queries + */ + public static class Builder { + private int flagsValue = RegExp.ALL; + private int maxDeterminizedStates = Operations.DEFAULT_MAX_DETERMINIZED_STATES; + + public Builder() { + } + + /** + * Sets the regular expression syntax flags + * see {@link RegexpFlag} + */ + public Builder setFlags(String flags) { + this.flagsValue = RegexpFlag.resolveValue(flags); + return this; + } + + /** + * Sets the maximum automaton states allowed for the regular expression expansion + */ + public Builder setMaxDeterminizedStates(int maxDeterminizedStates) { + if (maxDeterminizedStates < 0) { + throw new IllegalArgumentException("maxDeterminizedStates must not be negative"); + } + this.maxDeterminizedStates = maxDeterminizedStates; + return this; + } + + public RegexOptions build() { + return new RegexOptions(flagsValue, maxDeterminizedStates); + } + } +} diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryContextMapping.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryContextMapping.java index dffbb1aa80..10ac3935cc 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryContextMapping.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryContextMapping.java @@ -158,12 +158,12 @@ public class CategoryContextMapping extends ContextMapping { List queryContexts = new ArrayList<>(); Token token = parser.nextToken(); if (token == Token.START_OBJECT || token == Token.VALUE_STRING) { - CategoryQueryContext parse = CategoryQueryContext.parse(parser); - queryContexts.add(new QueryContext(parse.getCategory().toString(), parse.getBoost(), parse.isPrefix())); + CategoryQueryContext parse = CategoryQueryContext.PROTOTYPE.fromXContext(parser); + queryContexts.add(new QueryContext(parse.getCategory(), parse.getBoost(), parse.isPrefix())); } else if (token == Token.START_ARRAY) { while (parser.nextToken() != Token.END_ARRAY) { - CategoryQueryContext parse = CategoryQueryContext.parse(parser); - queryContexts.add(new QueryContext(parse.getCategory().toString(), parse.getBoost(), parse.isPrefix())); + CategoryQueryContext parse = CategoryQueryContext.PROTOTYPE.fromXContext(parser); + queryContexts.add(new QueryContext(parse.getCategory(), parse.getBoost(), parse.isPrefix())); } } return queryContexts; diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryQueryContext.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryQueryContext.java index c493126577..8db9afe5ae 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryQueryContext.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/CategoryQueryContext.java @@ -21,12 +21,15 @@ package org.elasticsearch.search.suggest.completion.context; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ObjectParser; -import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; +import java.util.Collections; +import java.util.Objects; import static org.elasticsearch.search.suggest.completion.context.CategoryContextMapping.CONTEXT_BOOST; import static org.elasticsearch.search.suggest.completion.context.CategoryContextMapping.CONTEXT_PREFIX; @@ -35,12 +38,15 @@ import static org.elasticsearch.search.suggest.completion.context.CategoryContex /** * Defines the query context for {@link CategoryContextMapping} */ -public final class CategoryQueryContext implements ToXContent { - private final CharSequence category; +public final class CategoryQueryContext implements QueryContext { + public static final String NAME = "category"; + public static final CategoryQueryContext PROTOTYPE = new CategoryQueryContext("", 1, false); + + private final String category; private final boolean isPrefix; private final int boost; - private CategoryQueryContext(CharSequence category, int boost, boolean isPrefix) { + private CategoryQueryContext(String category, int boost, boolean isPrefix) { this.category = category; this.boost = boost; this.isPrefix = isPrefix; @@ -49,7 +55,7 @@ public final class CategoryQueryContext implements ToXContent { /** * Returns the category of the context */ - public CharSequence getCategory() { + public String getCategory() { return category; } @@ -71,8 +77,81 @@ public final class CategoryQueryContext implements ToXContent { return new Builder(); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CategoryQueryContext that = (CategoryQueryContext) o; + + if (isPrefix != that.isPrefix) return false; + if (boost != that.boost) return false; + return category != null ? category.equals(that.category) : that.category == null; + + } + + @Override + public int hashCode() { + int result = category != null ? category.hashCode() : 0; + result = 31 * result + (isPrefix ? 1 : 0); + result = 31 * result + boost; + return result; + } + + @Override + public String getWriteableName() { + return NAME; + } + + private static ObjectParser CATEGORY_PARSER = new ObjectParser<>(NAME, null); + static { + CATEGORY_PARSER.declareString(Builder::setCategory, new ParseField(CONTEXT_VALUE)); + CATEGORY_PARSER.declareInt(Builder::setBoost, new ParseField(CONTEXT_BOOST)); + CATEGORY_PARSER.declareBoolean(Builder::setPrefix, new ParseField(CONTEXT_PREFIX)); + } + + @Override + public CategoryQueryContext fromXContext(XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + Builder builder = builder(); + if (token == XContentParser.Token.START_OBJECT) { + CATEGORY_PARSER.parse(parser, builder); + } else if (token == XContentParser.Token.VALUE_STRING) { + builder.setCategory(parser.text()); + } else { + throw new ElasticsearchParseException("category context must be an object or string"); + } + return builder.build(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(CONTEXT_VALUE, category); + builder.field(CONTEXT_BOOST, boost); + builder.field(CONTEXT_PREFIX, isPrefix); + builder.endObject(); + return builder; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeBoolean(isPrefix); + out.writeVInt(boost); + out.writeString(category); + } + + @Override + public QueryContext readFrom(StreamInput in) throws IOException { + Builder builder = new Builder(); + builder.isPrefix = in.readBoolean(); + builder.boost = in.readVInt(); + builder.category = in.readString(); + return builder.build(); + } + public static class Builder { - private CharSequence category; + private String category; private boolean isPrefix = false; private int boost = 1; @@ -80,11 +159,12 @@ public final class CategoryQueryContext implements ToXContent { } /** - * Sets the category of the context. + * Sets the category of the category. * This is a required field */ - public Builder setCategory(CharSequence context) { - this.category = context; + public Builder setCategory(String category) { + Objects.requireNonNull(category, "category must not be null"); + this.category = category; return this; } @@ -102,42 +182,16 @@ public final class CategoryQueryContext implements ToXContent { * Defaults to 1. */ public Builder setBoost(int boost) { + if (boost <= 0) { + throw new IllegalArgumentException("boost must be greater than 0"); + } this.boost = boost; return this; } public CategoryQueryContext build() { + Objects.requireNonNull(category, "category must not be null"); return new CategoryQueryContext(category, boost, isPrefix); } } - - private static ObjectParser CATEGORY_PARSER = new ObjectParser<>("category", null); - static { - CATEGORY_PARSER.declareString(Builder::setCategory, new ParseField("context")); - CATEGORY_PARSER.declareInt(Builder::setBoost, new ParseField("boost")); - CATEGORY_PARSER.declareBoolean(Builder::setPrefix, new ParseField("prefix")); - } - - public static CategoryQueryContext parse(XContentParser parser) throws IOException { - XContentParser.Token token = parser.currentToken(); - Builder builder = builder(); - if (token == XContentParser.Token.START_OBJECT) { - CATEGORY_PARSER.parse(parser, builder); - } else if (token == XContentParser.Token.VALUE_STRING) { - builder.setCategory(parser.text()); - } else { - throw new ElasticsearchParseException("category context must be an object or string"); - } - return builder.build(); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field(CONTEXT_VALUE, category); - builder.field(CONTEXT_BOOST, boost); - builder.field(CONTEXT_PREFIX, isPrefix); - builder.endObject(); - return builder; - } } diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMappings.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMappings.java index 9d4bed4f66..ccd4b2d584 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMappings.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMappings.java @@ -43,7 +43,6 @@ import java.util.Set; import static org.elasticsearch.search.suggest.completion.context.ContextMapping.FIELD_NAME; import static org.elasticsearch.search.suggest.completion.context.ContextMapping.FIELD_TYPE; -import static org.elasticsearch.search.suggest.completion.context.ContextMapping.QueryContext; import static org.elasticsearch.search.suggest.completion.context.ContextMapping.Type; /** @@ -153,7 +152,7 @@ public class ContextMappings implements ToXContent { * @param queryContexts a map of context mapping name and collected query contexts * @return a context-enabled query */ - public ContextQuery toContextQuery(CompletionQuery query, Map> queryContexts) { + public ContextQuery toContextQuery(CompletionQuery query, Map> queryContexts) { ContextQuery typedContextQuery = new ContextQuery(query); if (queryContexts.isEmpty() == false) { CharsRefBuilder scratch = new CharsRefBuilder(); @@ -162,9 +161,9 @@ public class ContextMappings implements ToXContent { scratch.setCharAt(0, (char) typeId); scratch.setLength(1); ContextMapping mapping = contextMappings.get(typeId); - List queryContext = queryContexts.get(mapping.name()); + List queryContext = queryContexts.get(mapping.name()); if (queryContext != null) { - for (QueryContext context : queryContext) { + for (ContextMapping.QueryContext context : queryContext) { scratch.append(context.context); typedContextQuery.addContext(scratch.toCharsRef(), context.boost, !context.isPrefix); scratch.setLength(1); diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoContextMapping.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoContextMapping.java index f2f3d10215..2c90429302 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoContextMapping.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoContextMapping.java @@ -247,18 +247,15 @@ public class GeoContextMapping extends ContextMapping { List queryContexts = new ArrayList<>(); Token token = parser.nextToken(); if (token == Token.START_OBJECT || token == Token.VALUE_STRING) { - queryContexts.add(GeoQueryContext.parse(parser)); + queryContexts.add(GeoQueryContext.PROTOTYPE.fromXContext(parser)); } else if (token == Token.START_ARRAY) { while (parser.nextToken() != Token.END_ARRAY) { - queryContexts.add(GeoQueryContext.parse(parser)); + queryContexts.add(GeoQueryContext.PROTOTYPE.fromXContext(parser)); } } List queryContextList = new ArrayList<>(); for (GeoQueryContext queryContext : queryContexts) { - int minPrecision = this.precision; - if (queryContext.getPrecision() != -1) { - minPrecision = Math.min(minPrecision, queryContext.getPrecision()); - } + int minPrecision = Math.min(this.precision, queryContext.getPrecision()); GeoPoint point = queryContext.getGeoPoint(); final Collection locations = new HashSet<>(); String geoHash = GeoHashUtils.stringEncode(point.getLon(), point.getLat(), minPrecision); diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoQueryContext.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoQueryContext.java index da9191bf2d..5b406abc1d 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoQueryContext.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoQueryContext.java @@ -23,14 +23,17 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoUtils; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ObjectParser; -import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import static org.elasticsearch.search.suggest.completion.context.GeoContextMapping.CONTEXT_BOOST; import static org.elasticsearch.search.suggest.completion.context.GeoContextMapping.CONTEXT_NEIGHBOURS; @@ -40,7 +43,10 @@ import static org.elasticsearch.search.suggest.completion.context.GeoContextMapp /** * Defines the query context for {@link GeoContextMapping} */ -public final class GeoQueryContext implements ToXContent { +public final class GeoQueryContext implements QueryContext { + public static final String NAME = "geo"; + public static final GeoQueryContext PROTOTYPE = new GeoQueryContext(null, 1, 12, Collections.emptyList()); + private final GeoPoint geoPoint; private final int boost; private final int precision; @@ -81,14 +87,109 @@ public final class GeoQueryContext implements ToXContent { return neighbours; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GeoQueryContext that = (GeoQueryContext) o; + + if (boost != that.boost) return false; + if (precision != that.precision) return false; + if (geoPoint != null ? !geoPoint.equals(that.geoPoint) : that.geoPoint != null) return false; + return neighbours != null ? neighbours.equals(that.neighbours) : that.neighbours == null; + + } + + @Override + public int hashCode() { + int result = geoPoint != null ? geoPoint.hashCode() : 0; + result = 31 * result + boost; + result = 31 * result + precision; + result = 31 * result + (neighbours != null ? neighbours.hashCode() : 0); + return result; + } + public static Builder builder() { return new Builder(); } + @Override + public String getWriteableName() { + return NAME; + } + + private static ObjectParser GEO_CONTEXT_PARSER = new ObjectParser<>(NAME, null); + static { + GEO_CONTEXT_PARSER.declareField((parser, geoQueryContext, geoContextMapping) -> geoQueryContext.setGeoPoint(GeoUtils.parseGeoPoint(parser)), new ParseField(CONTEXT_VALUE), ObjectParser.ValueType.OBJECT); + GEO_CONTEXT_PARSER.declareInt(GeoQueryContext.Builder::setBoost, new ParseField(CONTEXT_BOOST)); + // TODO : add string support for precision for GeoUtils.geoHashLevelsForPrecision() + GEO_CONTEXT_PARSER.declareInt(GeoQueryContext.Builder::setPrecision, new ParseField(CONTEXT_PRECISION)); + // TODO : add string array support for precision for GeoUtils.geoHashLevelsForPrecision() + GEO_CONTEXT_PARSER.declareIntArray(GeoQueryContext.Builder::setNeighbours, new ParseField(CONTEXT_NEIGHBOURS)); + GEO_CONTEXT_PARSER.declareDouble(GeoQueryContext.Builder::setLat, new ParseField("lat")); + GEO_CONTEXT_PARSER.declareDouble(GeoQueryContext.Builder::setLon, new ParseField("lon")); + } + + @Override + public GeoQueryContext fromXContext(XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + GeoQueryContext.Builder builder = new Builder(); + if (token == XContentParser.Token.START_OBJECT) { + GEO_CONTEXT_PARSER.parse(parser, builder); + } else if (token == XContentParser.Token.VALUE_STRING) { + builder.setGeoPoint(GeoPoint.fromGeohash(parser.text())); + } else { + throw new ElasticsearchParseException("geo context must be an object or string"); + } + return builder.build(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.startObject(CONTEXT_VALUE); + builder.field("lat", geoPoint.getLat()); + builder.field("lon", geoPoint.getLon()); + builder.endObject(); + builder.field(CONTEXT_BOOST, boost); + builder.field(CONTEXT_NEIGHBOURS, neighbours); + builder.field(CONTEXT_PRECISION, precision); + builder.endObject(); + return builder; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeGeoPoint(geoPoint); + out.writeVInt(boost); + out.writeInt(precision); + out.writeVInt(neighbours.size()); + for (Integer neighbour : neighbours) { + out.writeVInt(neighbour); + } + } + + @Override + public QueryContext readFrom(StreamInput in) throws IOException { + Builder builder = new Builder(); + builder.geoPoint = in.readGeoPoint(); + builder.boost = in.readVInt(); + builder.precision = in.readInt(); + int nNeighbour = in.readVInt(); + if (nNeighbour != 0) { + builder.neighbours = new ArrayList<>(nNeighbour); + for (int i = 0; i < nNeighbour; i++) { + builder.neighbours.add(in.readVInt()); + } + } + return builder.build(); + } + public static class Builder { private GeoPoint geoPoint; private int boost = 1; - private int precision = -1; + private int precision = 12; private List neighbours = Collections.emptyList(); public Builder() { @@ -99,6 +200,9 @@ public final class GeoQueryContext implements ToXContent { * Defaults to 1 */ public Builder setBoost(int boost) { + if (boost <= 0) { + throw new IllegalArgumentException("boost must be greater than 0"); + } this.boost = boost; return this; } @@ -108,6 +212,9 @@ public final class GeoQueryContext implements ToXContent { * Defaults to using index-time precision level */ public Builder setPrecision(int precision) { + if (precision < 1 || precision > 12) { + throw new IllegalArgumentException("precision must be between 1 and 12"); + } this.precision = precision; return this; } @@ -117,6 +224,11 @@ public final class GeoQueryContext implements ToXContent { * Defaults to only considering neighbours at the index-time precision level */ public Builder setNeighbours(List neighbours) { + for (int neighbour : neighbours) { + if (neighbour < 1 || neighbour > 12) { + throw new IllegalArgumentException("neighbour value must be between 1 and 12"); + } + } this.neighbours = neighbours; return this; } @@ -126,6 +238,7 @@ public final class GeoQueryContext implements ToXContent { * This is a required field */ public Builder setGeoPoint(GeoPoint geoPoint) { + Objects.requireNonNull(geoPoint, "geoPoint must not be null"); this.geoPoint = geoPoint; return this; } @@ -144,50 +257,10 @@ public final class GeoQueryContext implements ToXContent { if (geoPoint == null) { if (Double.isNaN(lat) == false && Double.isNaN(lon) == false) { geoPoint = new GeoPoint(lat, lon); - } else { - throw new IllegalArgumentException("no geohash or geo point provided"); } } + Objects.requireNonNull(geoPoint, "geoPoint must not be null"); return new GeoQueryContext(geoPoint, boost, precision, neighbours); } } - - private static ObjectParser GEO_CONTEXT_PARSER = new ObjectParser<>("geo", null); - static { - GEO_CONTEXT_PARSER.declareField((parser, geoQueryContext, geoContextMapping) -> geoQueryContext.setGeoPoint(GeoUtils.parseGeoPoint(parser)), new ParseField("context"), ObjectParser.ValueType.OBJECT); - GEO_CONTEXT_PARSER.declareInt(GeoQueryContext.Builder::setBoost, new ParseField("boost")); - // TODO : add string support for precision for GeoUtils.geoHashLevelsForPrecision() - GEO_CONTEXT_PARSER.declareInt(GeoQueryContext.Builder::setPrecision, new ParseField("precision")); - // TODO : add string array support for precision for GeoUtils.geoHashLevelsForPrecision() - GEO_CONTEXT_PARSER.declareIntArray(GeoQueryContext.Builder::setNeighbours, new ParseField("neighbours")); - GEO_CONTEXT_PARSER.declareDouble(GeoQueryContext.Builder::setLat, new ParseField("lat")); - GEO_CONTEXT_PARSER.declareDouble(GeoQueryContext.Builder::setLon, new ParseField("lon")); - } - - public static GeoQueryContext parse(XContentParser parser) throws IOException { - XContentParser.Token token = parser.currentToken(); - GeoQueryContext.Builder builder = new Builder(); - if (token == XContentParser.Token.START_OBJECT) { - GEO_CONTEXT_PARSER.parse(parser, builder); - } else if (token == XContentParser.Token.VALUE_STRING) { - builder.setGeoPoint(GeoPoint.fromGeohash(parser.text())); - } else { - throw new ElasticsearchParseException("geo context must be an object or string"); - } - return builder.build(); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.startObject(CONTEXT_VALUE); - builder.field("lat", geoPoint.getLat()); - builder.field("lon", geoPoint.getLon()); - builder.endObject(); - builder.field(CONTEXT_BOOST, boost); - builder.field(CONTEXT_NEIGHBOURS, neighbours); - builder.field(CONTEXT_PRECISION, precision); - builder.endObject(); - return builder; - } } diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/context/QueryContext.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/QueryContext.java new file mode 100644 index 0000000000..ccfd4a8d3d --- /dev/null +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/context/QueryContext.java @@ -0,0 +1,34 @@ +/* + * 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.search.suggest.completion.context; + +import org.elasticsearch.common.io.stream.NamedWriteable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; + +/** + * Interface for serializing/de-serializing completion query context + */ +public interface QueryContext extends ToXContent, NamedWriteable { + + QueryContext fromXContext(XContentParser parser) throws IOException; +} 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 71431e70f3..a7354f36cb 100644 --- a/core/src/test/java/org/elasticsearch/search/suggest/AbstractSuggestionBuilderTestCase.java +++ b/core/src/test/java/org/elasticsearch/search/suggest/AbstractSuggestionBuilderTestCase.java @@ -69,9 +69,6 @@ import java.nio.file.Path; import java.util.Collections; import java.util.Iterator; import java.util.Map; -import java.util.Map.Entry; -import java.util.function.Consumer; -import java.util.function.Supplier; import static org.elasticsearch.common.settings.Settings.settingsBuilder; import static org.hamcrest.Matchers.equalTo; @@ -276,9 +273,9 @@ public abstract class AbstractSuggestionBuilderTestCase> iterator = buildSuggestSearchContext.suggestions().entrySet().iterator(); - for (Entry entry : parsedSuggestionSearchContext.suggestions().entrySet()) { - Entry other = iterator.next(); + Iterator> iterator = buildSuggestSearchContext.suggestions().entrySet().iterator(); + for (Map.Entry entry : parsedSuggestionSearchContext.suggestions().entrySet()) { + Map.Entry other = iterator.next(); assertEquals(entry.getKey(), other.getKey()); SuggestionContext oldSchoolContext = entry.getValue(); @@ -353,29 +350,9 @@ public abstract class AbstractSuggestionBuilderTestCase void maybeSet(Consumer consumer, T value) { - if (randomBoolean()) { - consumer.accept(value); - } - } - - /** - * helper to get a random value in a certain range that's different from the - * input - */ - protected static T randomValueOtherThan(T input, Supplier randomSupplier) { - T randomValue = null; - do { - randomValue = randomSupplier.get(); - } while (randomValue.equals(input)); - return randomValue; - } - } diff --git a/core/src/test/java/org/elasticsearch/search/suggest/CompletionSuggestSearchIT.java b/core/src/test/java/org/elasticsearch/search/suggest/CompletionSuggestSearchIT.java index 896d20895e..d12b70c59a 100644 --- a/core/src/test/java/org/elasticsearch/search/suggest/CompletionSuggestSearchIT.java +++ b/core/src/test/java/org/elasticsearch/search/suggest/CompletionSuggestSearchIT.java @@ -47,7 +47,7 @@ import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.search.suggest.completion.CompletionStats; import org.elasticsearch.search.suggest.completion.CompletionSuggestion; import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder; -import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder.FuzzyOptionsBuilder; +import org.elasticsearch.search.suggest.completion.FuzzyOptions; import org.elasticsearch.search.suggest.completion.context.CategoryContextMapping; import org.elasticsearch.search.suggest.completion.context.ContextMapping; import org.elasticsearch.search.suggest.completion.context.GeoContextMapping; @@ -56,6 +56,7 @@ import org.elasticsearch.test.ESIntegTestCase; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; @@ -197,7 +198,8 @@ public class CompletionSuggestSearchIT extends ESIntegTestCase { } indexRandom(true, indexRequestBuilders); - CompletionSuggestionBuilder prefix = SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("sugg").size(numDocs).payload("count"); + CompletionSuggestionBuilder prefix = SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("sugg"). + size(numDocs).payload(Collections.singletonList("count")); SuggestResponse suggestResponse = client().prepareSuggest(INDEX).addSuggestion(prefix).execute().actionGet(); assertNoFailures(suggestResponse); CompletionSuggestion completionSuggestion = suggestResponse.getSuggest().getSuggestion("foo"); @@ -243,7 +245,8 @@ public class CompletionSuggestSearchIT extends ESIntegTestCase { client().prepareIndex(INDEX, TYPE, "2").setSource(FIELD, "suggestion") ); indexRandom(true, indexRequestBuilders); - CompletionSuggestionBuilder prefix = SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("sugg").payload("test_field"); + CompletionSuggestionBuilder prefix = SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("sugg") + .payload(Collections.singletonList("test_field")); SuggestResponse suggestResponse = client().prepareSuggest(INDEX).addSuggestion(prefix).execute().actionGet(); assertNoFailures(suggestResponse); CompletionSuggestion completionSuggestion = suggestResponse.getSuggest().getSuggestion("foo"); @@ -280,7 +283,8 @@ public class CompletionSuggestSearchIT extends ESIntegTestCase { indexRequestBuilders.add(client().prepareIndex(INDEX, TYPE, "2").setSource(source)); indexRandom(true, indexRequestBuilders); - CompletionSuggestionBuilder prefix = SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("sugg").payload("title", "count"); + CompletionSuggestionBuilder prefix = SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("sugg") + .payload(Arrays.asList("title", "count")); SuggestResponse suggestResponse = client().prepareSuggest(INDEX).addSuggestion(prefix).execute().actionGet(); assertNoFailures(suggestResponse); CompletionSuggestion completionSuggestion = suggestResponse.getSuggest().getSuggestion("foo"); @@ -325,12 +329,13 @@ public class CompletionSuggestSearchIT extends ESIntegTestCase { int suggestionSize = randomIntBetween(1, numDocs); int numRequestedPayloadFields = randomIntBetween(2, numPayloadFields); - String[] payloadFields = new String[numRequestedPayloadFields]; + List payloadFields = new ArrayList<>(numRequestedPayloadFields); for (int i = 0; i < numRequestedPayloadFields; i++) { - payloadFields[i] = "test_field" + i; + payloadFields.add("test_field" + i); } - CompletionSuggestionBuilder prefix = SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("sugg").size(suggestionSize).payload(payloadFields); + CompletionSuggestionBuilder prefix = SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("sugg") + .size(suggestionSize).payload(payloadFields); SuggestResponse suggestResponse = client().prepareSuggest(INDEX).addSuggestion(prefix).execute().actionGet(); assertNoFailures(suggestResponse); CompletionSuggestion completionSuggestion = suggestResponse.getSuggest().getSuggestion("foo"); @@ -702,7 +707,7 @@ public class CompletionSuggestSearchIT extends ESIntegTestCase { refresh(); SuggestResponse suggestResponse = client().prepareSuggest(INDEX).addSuggestion( - SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("Nriv", new FuzzyOptionsBuilder().setTranspositions(false)).size(10) + SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("Nriv", FuzzyOptions.builder().setTranspositions(false).build()).size(10) ).execute().actionGet(); assertSuggestions(suggestResponse, false, "foo"); @@ -724,12 +729,12 @@ public class CompletionSuggestSearchIT extends ESIntegTestCase { refresh(); SuggestResponse suggestResponse = client().prepareSuggest(INDEX).addSuggestion( - SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("Nriva", new FuzzyOptionsBuilder().setFuzzyMinLength(6)).size(10) + SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("Nriva", FuzzyOptions.builder().setFuzzyMinLength(6).build()).size(10) ).execute().actionGet(); assertSuggestions(suggestResponse, false, "foo"); suggestResponse = client().prepareSuggest(INDEX).addSuggestion( - SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("Nrivan", new FuzzyOptionsBuilder().setFuzzyMinLength(6)).size(10) + SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("Nrivan", FuzzyOptions.builder().setFuzzyMinLength(6).build()).size(10) ).execute().actionGet(); assertSuggestions(suggestResponse, false, "foo", "Nirvana"); } @@ -746,12 +751,12 @@ public class CompletionSuggestSearchIT extends ESIntegTestCase { refresh(); SuggestResponse suggestResponse = client().prepareSuggest(INDEX).addSuggestion( - SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("Nirw", new FuzzyOptionsBuilder().setFuzzyPrefixLength(4)).size(10) + SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("Nirw", FuzzyOptions.builder().setFuzzyPrefixLength(4).build()).size(10) ).execute().actionGet(); assertSuggestions(suggestResponse, false, "foo"); suggestResponse = client().prepareSuggest(INDEX).addSuggestion( - SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("Nirvo", new FuzzyOptionsBuilder().setFuzzyPrefixLength(4)).size(10) + SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("Nirvo", FuzzyOptions.builder().setFuzzyPrefixLength(4).build()).size(10) ).execute().actionGet(); assertSuggestions(suggestResponse, false, "foo", "Nirvana"); } @@ -769,18 +774,18 @@ public class CompletionSuggestSearchIT extends ESIntegTestCase { // suggestion with a character, which needs unicode awareness org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder completionSuggestionBuilder = - SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("öööи", new FuzzyOptionsBuilder().setUnicodeAware(true)).size(10); + SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("öööи", FuzzyOptions.builder().setUnicodeAware(true).build()).size(10); SuggestResponse suggestResponse = client().prepareSuggest(INDEX).addSuggestion(completionSuggestionBuilder).execute().actionGet(); assertSuggestions(suggestResponse, false, "foo", "ööööö"); // removing unicode awareness leads to no result - completionSuggestionBuilder = SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("öööи", new FuzzyOptionsBuilder().setUnicodeAware(false)).size(10); + completionSuggestionBuilder = SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("öööи", FuzzyOptions.builder().setUnicodeAware(false).build()).size(10); suggestResponse = client().prepareSuggest(INDEX).addSuggestion(completionSuggestionBuilder).execute().actionGet(); assertSuggestions(suggestResponse, false, "foo"); // increasing edit distance instead of unicode awareness works again, as this is only a single character - completionSuggestionBuilder = SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("öööи", new FuzzyOptionsBuilder().setUnicodeAware(false).setFuzziness(Fuzziness.TWO)).size(10); + completionSuggestionBuilder = SuggestBuilders.completionSuggestion("foo").field(FIELD).prefix("öööи", FuzzyOptions.builder().setUnicodeAware(false).setFuzziness(Fuzziness.TWO).build()).size(10); suggestResponse = client().prepareSuggest(INDEX).addSuggestion(completionSuggestionBuilder).execute().actionGet(); assertSuggestions(suggestResponse, false, "foo", "ööööö"); } diff --git a/core/src/test/java/org/elasticsearch/search/suggest/SuggestBuilderTests.java b/core/src/test/java/org/elasticsearch/search/suggest/SuggestBuilderTests.java index f4551b3de9..8e324330fe 100644 --- a/core/src/test/java/org/elasticsearch/search/suggest/SuggestBuilderTests.java +++ b/core/src/test/java/org/elasticsearch/search/suggest/SuggestBuilderTests.java @@ -47,7 +47,7 @@ public class SuggestBuilderTests extends WritableTestCase { @Override - protected NamedWriteableRegistry provideNamedWritbaleRegistry() { + protected NamedWriteableRegistry provideNamedWritableRegistry() { NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry(); namedWriteableRegistry.registerPrototype(SuggestionBuilder.class, TermSuggestionBuilder.PROTOTYPE); namedWriteableRegistry.registerPrototype(SuggestionBuilder.class, PhraseSuggestionBuilder.PROTOTYPE); diff --git a/core/src/test/java/org/elasticsearch/search/suggest/completion/CategoryQueryContextTests.java b/core/src/test/java/org/elasticsearch/search/suggest/completion/CategoryQueryContextTests.java new file mode 100644 index 0000000000..b4d80ebba9 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/suggest/completion/CategoryQueryContextTests.java @@ -0,0 +1,95 @@ +/* + * 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.search.suggest.completion; + +import org.elasticsearch.search.suggest.completion.context.CategoryQueryContext; + +import java.io.IOException; + +public class CategoryQueryContextTests extends QueryContextTestCase { + + public static CategoryQueryContext randomCategoryQueryContext() { + final CategoryQueryContext.Builder builder = CategoryQueryContext.builder(); + builder.setCategory(randomAsciiOfLength(10)); + maybeSet(builder::setBoost, randomIntBetween(1, 10)); + maybeSet(builder::setPrefix, randomBoolean()); + return builder.build(); + } + + @Override + protected CategoryQueryContext createTestModel() { + return randomCategoryQueryContext(); + } + + @Override + protected CategoryQueryContext createMutation(CategoryQueryContext original) throws IOException { + final CategoryQueryContext.Builder builder = CategoryQueryContext.builder(); + builder.setCategory(original.getCategory()).setBoost(original.getBoost()).setPrefix(original.isPrefix()); + switch (randomIntBetween(0, 2)) { + case 0: + builder.setCategory(randomValueOtherThan(original.getCategory(), () -> randomAsciiOfLength(10))); + break; + case 1: + builder.setBoost(randomValueOtherThan(original.getBoost(), () -> randomIntBetween(1, 5))); + break; + case 2: + builder.setPrefix(!original.isPrefix()); + break; + + } + return builder.build(); + } + + @Override + protected CategoryQueryContext prototype() { + return CategoryQueryContext.PROTOTYPE; + } + + public void testNullCategoryIsIllegal() { + final CategoryQueryContext categoryQueryContext = randomCategoryQueryContext(); + final CategoryQueryContext.Builder builder = CategoryQueryContext.builder() + .setBoost(categoryQueryContext.getBoost()) + .setPrefix(categoryQueryContext.isPrefix()); + try { + builder.build(); + fail("null category is illegal"); + } catch (NullPointerException e) { + assertEquals(e.getMessage(), "category must not be null"); + } + } + + public void testIllegalArguments() { + final CategoryQueryContext.Builder builder = CategoryQueryContext.builder(); + + try { + builder.setCategory(null); + fail("category must not be null"); + } catch (NullPointerException e) { + assertEquals(e.getMessage(), "category must not be null"); + } + + try { + builder.setBoost(-randomIntBetween(1, Integer.MAX_VALUE)); + fail("boost must be positive"); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "boost must be greater than 0"); + } + } +} diff --git a/core/src/test/java/org/elasticsearch/search/suggest/completion/CompletionSuggesterBuilderTests.java b/core/src/test/java/org/elasticsearch/search/suggest/completion/CompletionSuggesterBuilderTests.java new file mode 100644 index 0000000000..9fc5988c07 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/suggest/completion/CompletionSuggesterBuilderTests.java @@ -0,0 +1,135 @@ +/* + * 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.search.suggest.completion; + +import org.elasticsearch.common.unit.Fuzziness; +import org.elasticsearch.search.suggest.AbstractSuggestionBuilderTestCase; +import org.elasticsearch.search.suggest.SuggestionSearchContext; +import org.elasticsearch.search.suggest.SuggestionSearchContext.SuggestionContext; +import org.elasticsearch.search.suggest.completion.context.CategoryQueryContext; +import org.elasticsearch.search.suggest.completion.context.GeoQueryContext; +import org.elasticsearch.search.suggest.completion.context.QueryContext; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class CompletionSuggesterBuilderTests extends AbstractSuggestionBuilderTestCase { + + @BeforeClass + public static void initQueryContexts() { + namedWriteableRegistry.registerPrototype(QueryContext.class, CategoryQueryContext.PROTOTYPE); + namedWriteableRegistry.registerPrototype(QueryContext.class, GeoQueryContext.PROTOTYPE); + } + + @Override + protected CompletionSuggestionBuilder randomSuggestionBuilder() { + CompletionSuggestionBuilder testBuilder = new CompletionSuggestionBuilder(randomAsciiOfLength(10)); + switch (randomIntBetween(0, 3)) { + case 0: + testBuilder.prefix(randomAsciiOfLength(10)); + break; + case 1: + testBuilder.prefix(randomAsciiOfLength(10), FuzzyOptionsTests.randomFuzzyOptions()); + break; + case 2: + testBuilder.prefix(randomAsciiOfLength(10), randomFrom(Fuzziness.ZERO, Fuzziness.ONE, Fuzziness.TWO)); + break; + case 3: + testBuilder.regex(randomAsciiOfLength(10), RegexOptionsTests.randomRegexOptions()); + break; + } + List payloads = new ArrayList<>(); + Collections.addAll(payloads, generateRandomStringArray(5, 10, false, false)); + maybeSet(testBuilder::payload, payloads); + if (randomBoolean()) { + int numContext = randomIntBetween(1, 5); + CategoryQueryContext[] contexts = new CategoryQueryContext[numContext]; + for (int i = 0; i < numContext; i++) { + contexts[i] = CategoryQueryContextTests.randomCategoryQueryContext(); + } + testBuilder.categoryContexts(randomAsciiOfLength(10), contexts); + } + if (randomBoolean()) { + int numContext = randomIntBetween(1, 5); + GeoQueryContext[] contexts = new GeoQueryContext[numContext]; + for (int i = 0; i < numContext; i++) { + contexts[i] = GeoQueryContextTests.randomGeoQueryContext(); + } + testBuilder.geoContexts(randomAsciiOfLength(10), contexts); + } + return testBuilder; + } + + @Override + protected void assertSuggestionContext(SuggestionContext oldSuggestion, SuggestionContext newSuggestion) { + + } + + @Override + public void testBuild() throws IOException { + // skip for now + } + + @Override + public void testFromXContent() throws IOException { + // skip for now + } + + protected void mutateSpecificParameters(CompletionSuggestionBuilder builder) throws IOException { + switch (randomIntBetween(0, 5)) { + case 0: + List payloads = new ArrayList<>(); + Collections.addAll(payloads, generateRandomStringArray(5, 10, false, false)); + builder.payload(payloads); + break; + case 1: + int numCategoryContext = randomIntBetween(1, 5); + CategoryQueryContext[] categoryContexts = new CategoryQueryContext[numCategoryContext]; + for (int i = 0; i < numCategoryContext; i++) { + categoryContexts[i] = CategoryQueryContextTests.randomCategoryQueryContext(); + } + builder.categoryContexts(randomAsciiOfLength(10), categoryContexts); + break; + case 2: + int numGeoContext = randomIntBetween(1, 5); + GeoQueryContext[] geoContexts = new GeoQueryContext[numGeoContext]; + for (int i = 0; i < numGeoContext; i++) { + geoContexts[i] = GeoQueryContextTests.randomGeoQueryContext(); + } + builder.geoContexts(randomAsciiOfLength(10), geoContexts); + break; + case 3: + builder.prefix(randomAsciiOfLength(10), FuzzyOptionsTests.randomFuzzyOptions()); + break; + case 4: + builder.prefix(randomAsciiOfLength(10), randomFrom(Fuzziness.ZERO, Fuzziness.ONE, Fuzziness.TWO)); + break; + case 5: + builder.regex(randomAsciiOfLength(10), RegexOptionsTests.randomRegexOptions()); + break; + default: + throw new IllegalStateException("should not through"); + } + } +} diff --git a/core/src/test/java/org/elasticsearch/search/suggest/completion/FuzzyOptionsTests.java b/core/src/test/java/org/elasticsearch/search/suggest/completion/FuzzyOptionsTests.java new file mode 100644 index 0000000000..848a9088bc --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/suggest/completion/FuzzyOptionsTests.java @@ -0,0 +1,131 @@ +/* + * 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.search.suggest.completion; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.unit.Fuzziness; + +import java.io.IOException; + +public class FuzzyOptionsTests extends WritableTestCase { + + public static FuzzyOptions randomFuzzyOptions() { + final FuzzyOptions.Builder builder = FuzzyOptions.builder(); + if (randomBoolean()) { + maybeSet(builder::setFuzziness, randomFrom(Fuzziness.ZERO, Fuzziness.ONE, Fuzziness.TWO)); + } else { + maybeSet(builder::setFuzziness, randomFrom(0, 1, 2)); + } + maybeSet(builder::setFuzzyMinLength, randomIntBetween(0, 10)); + maybeSet(builder::setFuzzyPrefixLength, randomIntBetween(0, 10)); + maybeSet(builder::setMaxDeterminizedStates, randomIntBetween(1, 1000)); + maybeSet(builder::setTranspositions, randomBoolean()); + maybeSet(builder::setUnicodeAware, randomBoolean()); + return builder.build(); + } + + @Override + protected FuzzyOptions createTestModel() { + return randomFuzzyOptions(); + } + + @Override + protected FuzzyOptions createMutation(FuzzyOptions original) throws IOException { + final FuzzyOptions.Builder builder = FuzzyOptions.builder(); + builder.setFuzziness(original.getEditDistance()) + .setFuzzyPrefixLength(original.getFuzzyPrefixLength()) + .setFuzzyMinLength(original.getFuzzyMinLength()) + .setMaxDeterminizedStates(original.getMaxDeterminizedStates()) + .setTranspositions(original.isTranspositions()) + .setUnicodeAware(original.isUnicodeAware()); + switch (randomIntBetween(0, 5)) { + case 0: + builder.setFuzziness(randomValueOtherThan(original.getEditDistance(), () -> randomFrom(0, 1, 2))); + break; + case 1: + builder.setFuzzyPrefixLength(randomValueOtherThan(original.getFuzzyPrefixLength(), () -> + randomIntBetween(1, 3))); + break; + case 2: + builder.setFuzzyMinLength(randomValueOtherThan(original.getFuzzyMinLength(), () -> + randomIntBetween(1, 3))); + break; + case 3: + builder.setMaxDeterminizedStates(randomValueOtherThan(original.getMaxDeterminizedStates(), () -> + randomIntBetween(1, 10))); + break; + case 4: + builder.setTranspositions(!original.isTranspositions()); + break; + case 5: + builder.setUnicodeAware(!original.isUnicodeAware()); + break; + } + return builder.build(); + } + + @Override + protected FuzzyOptions readFrom(StreamInput in) throws IOException { + return FuzzyOptions.readFuzzyOptions(in); + } + + public void testIllegalArguments() { + final FuzzyOptions.Builder builder = FuzzyOptions.builder(); + try { + builder.setFuzziness(-randomIntBetween(1, Integer.MAX_VALUE)); + fail("fuzziness must be > 0"); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "fuzziness must be between 0 and 2"); + } + try { + builder.setFuzziness(randomIntBetween(3, Integer.MAX_VALUE)); + fail("fuzziness must be < 2"); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "fuzziness must be between 0 and 2"); + } + try { + builder.setFuzziness(null); + fail("fuzziness must not be null"); + } catch (NullPointerException e) { + assertEquals(e.getMessage(), "fuzziness must not be null"); + } + + try { + builder.setFuzzyMinLength(-randomIntBetween(1, Integer.MAX_VALUE)); + fail("fuzzyMinLength must be >= 0"); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "fuzzyMinLength must not be negative"); + } + + try { + builder.setFuzzyPrefixLength(-randomIntBetween(1, Integer.MAX_VALUE)); + fail("fuzzyPrefixLength must be >= 0"); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "fuzzyPrefixLength must not be negative"); + } + + try { + builder.setMaxDeterminizedStates(-randomIntBetween(1, Integer.MAX_VALUE)); + fail("max determinized state must be >= 0"); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "maxDeterminizedStates must not be negative"); + } + } +} diff --git a/core/src/test/java/org/elasticsearch/search/suggest/completion/GeoQueryContextTests.java b/core/src/test/java/org/elasticsearch/search/suggest/completion/GeoQueryContextTests.java new file mode 100644 index 0000000000..d26be5036e --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/suggest/completion/GeoQueryContextTests.java @@ -0,0 +1,136 @@ +/* + * 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.search.suggest.completion; + +import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.search.suggest.completion.context.GeoQueryContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; + +public class GeoQueryContextTests extends QueryContextTestCase { + + public static GeoQueryContext randomGeoQueryContext() { + final GeoQueryContext.Builder builder = GeoQueryContext.builder(); + builder.setGeoPoint(new GeoPoint(randomDouble(), randomDouble())); + maybeSet(builder::setBoost, randomIntBetween(1, 10)); + maybeSet(builder::setPrecision, randomIntBetween(1, 12)); + List neighbours = new ArrayList<>(); + for (int i = 0; i < randomIntBetween(1, 12); i++) { + neighbours.add(randomIntBetween(1, 12)); + } + maybeSet(builder::setNeighbours, neighbours); + return builder.build(); + } + + @Override + protected GeoQueryContext createTestModel() { + return randomGeoQueryContext(); + } + + @Override + protected GeoQueryContext createMutation(GeoQueryContext original) throws IOException { + final GeoQueryContext.Builder builder = GeoQueryContext.builder(); + builder.setGeoPoint(original.getGeoPoint()).setBoost(original.getBoost()) + .setNeighbours(original.getNeighbours()).setPrecision(original.getPrecision()); + switch (randomIntBetween(0, 3)) { + case 0: + builder.setGeoPoint(randomValueOtherThan(original.getGeoPoint() ,() -> + new GeoPoint(randomDouble(), randomDouble()))); + break; + case 1: + builder.setBoost(randomValueOtherThan(original.getBoost() ,() -> randomIntBetween(1, 5))); + break; + case 2: + builder.setPrecision(randomValueOtherThan(original.getPrecision() ,() -> randomIntBetween(1, 12))); + break; + case 3: + builder.setNeighbours(randomValueOtherThan(original.getNeighbours(), () -> { + List newNeighbours = new ArrayList<>(); + for (int i = 0; i < randomIntBetween(1, 12); i++) { + newNeighbours.add(randomIntBetween(1, 12)); + } + return newNeighbours; + })); + break; + } + return builder.build(); + } + + @Override + protected GeoQueryContext prototype() { + return GeoQueryContext.PROTOTYPE; + } + + public void testNullGeoPointIsIllegal() { + final GeoQueryContext geoQueryContext = randomGeoQueryContext(); + final GeoQueryContext.Builder builder = GeoQueryContext.builder() + .setNeighbours(geoQueryContext.getNeighbours()) + .setPrecision(geoQueryContext.getPrecision()) + .setBoost(geoQueryContext.getBoost()); + try { + builder.build(); + fail("null geo point is illegal"); + } catch (NullPointerException e) { + assertThat(e.getMessage(), equalTo("geoPoint must not be null")); + } + } + + public void testIllegalArguments() { + final GeoQueryContext.Builder builder = GeoQueryContext.builder(); + + try { + builder.setGeoPoint(null); + fail("geoPoint must not be null"); + } catch (NullPointerException e) { + assertEquals(e.getMessage(), "geoPoint must not be null"); + } + try { + builder.setBoost(-randomIntBetween(1, Integer.MAX_VALUE)); + fail("boost must be positive"); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "boost must be greater than 0"); + } + int precision = 0; + try { + do { + precision = randomInt(); + } while (precision >= 1 && precision <= 12); + builder.setPrecision(precision); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "precision must be between 1 and 12"); + } + try { + List neighbours = new ArrayList<>(); + neighbours.add(precision); + for (int i = 1; i < randomIntBetween(1, 11); i++) { + neighbours.add(i); + } + Collections.shuffle(neighbours, random()); + builder.setNeighbours(neighbours); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "neighbour value must be between 1 and 12"); + } + } +} diff --git a/core/src/test/java/org/elasticsearch/search/suggest/completion/QueryContextTestCase.java b/core/src/test/java/org/elasticsearch/search/suggest/completion/QueryContextTestCase.java new file mode 100644 index 0000000000..b4a6a5b1da --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/suggest/completion/QueryContextTestCase.java @@ -0,0 +1,60 @@ +/* + * 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.search.suggest.completion; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.suggest.completion.context.QueryContext; + +import java.io.IOException; + + +public abstract class QueryContextTestCase extends WritableTestCase { + + private static final int NUMBER_OF_RUNS = 20; + + /** + * query context prototype to read serialized format + */ + protected abstract QC prototype(); + + @Override + protected QC readFrom(StreamInput in) throws IOException { + return (QC) prototype().readFrom(in); + } + + public void testToXContext() throws IOException { + for (int i = 0; i < NUMBER_OF_RUNS; i++) { + QueryContext toXContent = createTestModel(); + XContentBuilder builder = XContentFactory.jsonBuilder(); + toXContent.toXContent(builder, ToXContent.EMPTY_PARAMS); + BytesReference bytesReference = builder.bytes(); + XContentParser parser = XContentFactory.xContent(bytesReference).createParser(bytesReference); + parser.nextToken(); + QueryContext fromXContext = prototype().fromXContext(parser); + assertEquals(toXContent, fromXContext); + assertEquals(toXContent.hashCode(), fromXContext.hashCode()); + } + } +} diff --git a/core/src/test/java/org/elasticsearch/search/suggest/completion/RegexOptionsTests.java b/core/src/test/java/org/elasticsearch/search/suggest/completion/RegexOptionsTests.java new file mode 100644 index 0000000000..082e2bc268 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/suggest/completion/RegexOptionsTests.java @@ -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.search.suggest.completion; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.index.query.RegexpFlag; + +import java.io.IOException; + +public class RegexOptionsTests extends WritableTestCase { + + public static RegexOptions randomRegexOptions() { + final RegexOptions.Builder builder = RegexOptions.builder(); + maybeSet(builder::setMaxDeterminizedStates, randomIntBetween(1, 1000)); + StringBuilder sb = new StringBuilder(); + for (RegexpFlag regexpFlag : RegexpFlag.values()) { + if (randomBoolean()) { + if (sb.length() != 0) { + sb.append("|"); + } + sb.append(regexpFlag.name()); + } + } + maybeSet(builder::setFlags, sb.toString()); + return builder.build(); + } + + @Override + protected RegexOptions createTestModel() { + return randomRegexOptions(); + } + + @Override + protected RegexOptions createMutation(RegexOptions original) throws IOException { + final RegexOptions.Builder builder = RegexOptions.builder(); + builder.setMaxDeterminizedStates(randomValueOtherThan(original.getMaxDeterminizedStates(), () -> randomIntBetween(1, 10))); + return builder.build(); + } + + @Override + protected RegexOptions readFrom(StreamInput in) throws IOException { + return RegexOptions.readRegexOptions(in); + } + + public void testIllegalArgument() { + final RegexOptions.Builder builder = RegexOptions.builder(); + try { + builder.setMaxDeterminizedStates(-randomIntBetween(1, Integer.MAX_VALUE)); + fail("max determinized state must be positive"); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "maxDeterminizedStates must not be negative"); + } + } +} diff --git a/core/src/test/java/org/elasticsearch/search/suggest/completion/WritableTestCase.java b/core/src/test/java/org/elasticsearch/search/suggest/completion/WritableTestCase.java index 47b3373342..68cc30f8de 100644 --- a/core/src/test/java/org/elasticsearch/search/suggest/completion/WritableTestCase.java +++ b/core/src/test/java/org/elasticsearch/search/suggest/completion/WritableTestCase.java @@ -103,13 +103,13 @@ public abstract class WritableTestCase extends ESTestCase { private M copyModel(M original) throws IOException { try (BytesStreamOutput output = new BytesStreamOutput()) { original.writeTo(output); - try (StreamInput in = new NamedWriteableAwareStreamInput(StreamInput.wrap(output.bytes()), provideNamedWritbaleRegistry())) { + try (StreamInput in = new NamedWriteableAwareStreamInput(StreamInput.wrap(output.bytes()), provideNamedWritableRegistry())) { return readFrom(in); } } } - protected NamedWriteableRegistry provideNamedWritbaleRegistry() { + protected NamedWriteableRegistry provideNamedWritableRegistry() { return new NamedWriteableRegistry(); } } diff --git a/core/src/test/java/org/elasticsearch/search/suggest/phrase/DirectCandidateGeneratorTests.java b/core/src/test/java/org/elasticsearch/search/suggest/phrase/DirectCandidateGeneratorTests.java index 9bf8447f8d..28c0647881 100644 --- a/core/src/test/java/org/elasticsearch/search/suggest/phrase/DirectCandidateGeneratorTests.java +++ b/core/src/test/java/org/elasticsearch/search/suggest/phrase/DirectCandidateGeneratorTests.java @@ -296,12 +296,6 @@ public class DirectCandidateGeneratorTests extends ESTestCase{ return generator; } - private static void maybeSet(Consumer consumer, T value) { - if (randomBoolean()) { - consumer.accept(value); - } - } - private static DirectCandidateGeneratorBuilder serializedCopy(DirectCandidateGeneratorBuilder original) throws IOException { try (BytesStreamOutput output = new BytesStreamOutput()) { original.writeTo(output); -- cgit v1.2.3