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 --- .../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 +++ 11 files changed, 796 insertions(+), 356 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 (limited to 'core/src/main/java/org/elasticsearch/search') 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; +} -- cgit v1.2.3