diff options
5 files changed, 238 insertions, 20 deletions
diff --git a/core/src/main/java/org/elasticsearch/search/suggest/Suggest.java b/core/src/main/java/org/elasticsearch/search/suggest/Suggest.java index f1875564f8..36a780fec3 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/Suggest.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/Suggest.java @@ -25,6 +25,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Streamable; import org.elasticsearch.common.text.Text; import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -371,37 +372,38 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex /** * Represents a part from the suggest text with suggested options. */ - public static class Entry<O extends Entry.Option> implements Iterable<O>, Streamable, ToXContent { + public static class Entry<O extends Entry.Option> implements Iterable<O>, Streamable, ToXContentObject { - static class Fields { - - static final String TEXT = "text"; - static final String OFFSET = "offset"; - static final String LENGTH = "length"; - static final String OPTIONS = "options"; - - } + private static final String TEXT = "text"; + private static final String OFFSET = "offset"; + private static final String LENGTH = "length"; + protected static final String OPTIONS = "options"; protected Text text; protected int offset; protected int length; - protected List<O> options; + protected List<O> options = new ArrayList<>(5); public Entry(Text text, int offset, int length) { this.text = text; this.offset = offset; this.length = length; - this.options = new ArrayList<>(5); } - public Entry() { + protected Entry() { } public void addOption(O option) { options.add(option); } + protected void addOptions(List<O> options) { + for (O option : options) { + addOption(option); + } + } + protected void sort(Comparator<O> comparator) { CollectionUtil.timSort(options, comparator); } @@ -539,10 +541,10 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(Fields.TEXT, text); - builder.field(Fields.OFFSET, offset); - builder.field(Fields.LENGTH, length); - builder.startArray(Fields.OPTIONS); + builder.field(TEXT, text); + builder.field(OFFSET, offset); + builder.field(LENGTH, length); + builder.startArray(OPTIONS); for (Option option : options) { option.toXContent(builder, params); } @@ -551,6 +553,23 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex return builder; } + private static ObjectParser<Entry<Option>, Void> PARSER = new ObjectParser<>("SuggestionEntryParser", true, Entry::new); + + static { + declareCommonFields(PARSER); + PARSER.declareObjectArray(Entry::addOptions, (p,c) -> Option.fromXContent(p), new ParseField(OPTIONS)); + } + + protected static void declareCommonFields(ObjectParser<? extends Entry<? extends Option>, Void> parser) { + parser.declareString((entry, text) -> entry.text = new Text(text), new ParseField(TEXT)); + parser.declareInt((entry, offset) -> entry.offset = offset, new ParseField(OFFSET)); + parser.declareInt((entry, length) -> entry.length = length, new ParseField(LENGTH)); + } + + public static Entry<? extends Option> fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + /** * Contains the suggested text with its document frequency and score. */ diff --git a/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestion.java b/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestion.java index 33ff15fbbb..51b44a300d 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestion.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/completion/CompletionSuggestion.java @@ -194,8 +194,7 @@ public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSug super(text, offset, length); } - protected Entry() { - super(); + Entry() { } @Override @@ -203,6 +202,18 @@ public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSug return new Option(); } + private static ObjectParser<Entry, Void> PARSER = new ObjectParser<>("CompletionSuggestionEntryParser", true, + Entry::new); + + static { + declareCommonFields(PARSER); + PARSER.declareObjectArray(Entry::addOptions, (p,c) -> Option.fromXContent(p), new ParseField(OPTIONS)); + } + + public static Entry fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + public static class Option extends Suggest.Suggestion.Entry.Option { private Map<String, Set<CharSequence>> contexts = Collections.emptyMap(); private ScoreDoc doc; diff --git a/core/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestion.java b/core/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestion.java index e673ccb128..3ea0d61d72 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestion.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestion.java @@ -19,9 +19,12 @@ package org.elasticsearch.search.suggest.phrase; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.text.Text; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.suggest.Suggest; import org.elasticsearch.search.suggest.Suggest.Suggestion; @@ -69,7 +72,7 @@ public class PhraseSuggestion extends Suggest.Suggestion<PhraseSuggestion.Entry> this.cutoffScore = cutoffScore; } - public Entry() { + Entry() { } /** @@ -100,6 +103,17 @@ public class PhraseSuggestion extends Suggest.Suggestion<PhraseSuggestion.Entry> } } + private static ObjectParser<Entry, Void> PARSER = new ObjectParser<>("PhraseSuggestionEntryParser", true, Entry::new); + + static { + declareCommonFields(PARSER); + PARSER.declareObjectArray(Entry::addOptions, (p,c) -> Option.fromXContent(p), new ParseField(OPTIONS)); + } + + public static Entry fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); diff --git a/core/src/main/java/org/elasticsearch/search/suggest/term/TermSuggestion.java b/core/src/main/java/org/elasticsearch/search/suggest/term/TermSuggestion.java index 68aed8b80a..5f6cd310ad 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/term/TermSuggestion.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/term/TermSuggestion.java @@ -24,6 +24,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.text.Text; import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.suggest.SortBy; @@ -142,7 +143,7 @@ public class TermSuggestion extends Suggestion<TermSuggestion.Entry> { public static class Entry extends org.elasticsearch.search.suggest.Suggest.Suggestion.Entry<TermSuggestion.Entry.Option> { - Entry(Text text, int offset, int length) { + public Entry(Text text, int offset, int length) { super(text, offset, length); } @@ -154,6 +155,17 @@ public class TermSuggestion extends Suggestion<TermSuggestion.Entry> { return new Option(); } + private static ObjectParser<Entry, Void> PARSER = new ObjectParser<>("TermSuggestionEntryParser", true, Entry::new); + + static { + declareCommonFields(PARSER); + PARSER.declareObjectArray(Entry::addOptions, (p,c) -> Option.fromXContent(p), new ParseField(OPTIONS)); + } + + public static Entry fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + /** * Contains the suggested text with its document frequency and score. */ diff --git a/core/src/test/java/org/elasticsearch/search/suggest/SuggestionEntryTests.java b/core/src/test/java/org/elasticsearch/search/suggest/SuggestionEntryTests.java new file mode 100644 index 0000000000..f1bf602ae6 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/suggest/SuggestionEntryTests.java @@ -0,0 +1,162 @@ +/* + * 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; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.text.Text; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.search.suggest.Suggest.Suggestion; +import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry; +import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry.Option; +import org.elasticsearch.search.suggest.completion.CompletionSuggestion; +import org.elasticsearch.search.suggest.phrase.PhraseSuggestion; +import org.elasticsearch.search.suggest.term.TermSuggestion; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import static org.elasticsearch.common.xcontent.XContentHelper.toXContent; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; + +public class SuggestionEntryTests extends ESTestCase { + + @SuppressWarnings("rawtypes") + private static final Map<Class<? extends Entry>, Function<XContentParser, ? extends Entry>> ENTRY_PARSERS = new HashMap<>(); + static { + ENTRY_PARSERS.put(Suggestion.Entry.class, Suggestion.Entry::fromXContent); + ENTRY_PARSERS.put(TermSuggestion.Entry.class, TermSuggestion.Entry::fromXContent); + ENTRY_PARSERS.put(PhraseSuggestion.Entry.class, PhraseSuggestion.Entry::fromXContent); + ENTRY_PARSERS.put(CompletionSuggestion.Entry.class, CompletionSuggestion.Entry::fromXContent); + } + + /** + * Create a randomized Suggestion.Entry + */ + @SuppressWarnings("unchecked") + public static <O extends Option> Entry<O> createTestItem(Class<? extends Entry> entryType) { + Text entryText = new Text(randomAsciiOfLengthBetween(5, 15)); + int offset = randomInt(); + int length = randomInt(); + @SuppressWarnings("rawtypes") + Entry entry = null; + Supplier<Option> supplier = null; + if (entryType == Suggestion.Entry.class) { + entry = new Suggestion.Entry<>(entryText, offset, length); + supplier = SuggestionOptionTests::createTestItem; + } else if (entryType == TermSuggestion.Entry.class) { + entry = new TermSuggestion.Entry(entryText, offset, length); + supplier = TermSuggestionOptionTests::createTestItem; + } else if (entryType == PhraseSuggestion.Entry.class) { + entry = new PhraseSuggestion.Entry(entryText, offset, length, randomDouble()); + supplier = SuggestionOptionTests::createTestItem; + } else if (entryType == CompletionSuggestion.Entry.class) { + entry = new CompletionSuggestion.Entry(entryText, offset, length); + supplier = CompletionSuggestionOptionTests::createTestItem; + } + int numOptions = randomIntBetween(0, 5); + for (int i = 0; i < numOptions; i++) { + entry.addOption(supplier.get()); + } + return entry; + } + + @SuppressWarnings("unchecked") + public void testFromXContent() throws IOException { + for (Class<? extends Entry> entryType : ENTRY_PARSERS.keySet()) { + Entry<Option> entry = createTestItem(entryType); + XContentType xContentType = randomFrom(XContentType.values()); + boolean humanReadable = randomBoolean(); + BytesReference originalBytes = toXContent(entry, xContentType, humanReadable); + Entry<Option> parsed; + try (XContentParser parser = createParser(xContentType.xContent(), originalBytes)) { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + parsed = ENTRY_PARSERS.get(entry.getClass()).apply(parser); + assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); + assertNull(parser.nextToken()); + } + assertEquals(entry.getClass(), parsed.getClass()); + assertEquals(entry.getText(), parsed.getText()); + assertEquals(entry.getLength(), parsed.getLength()); + assertEquals(entry.getOffset(), parsed.getOffset()); + assertEquals(entry.getOptions().size(), parsed.getOptions().size()); + for (int i = 0; i < entry.getOptions().size(); i++) { + assertEquals(entry.getOptions().get(i).getClass(), parsed.getOptions().get(i).getClass()); + } + assertToXContentEquivalent(originalBytes, toXContent(parsed, xContentType, humanReadable), xContentType); + } + } + + public void testToXContent() throws IOException { + Option option = new Option(new Text("someText"), new Text("somethingHighlighted"), 1.3f, true); + Entry<Option> entry = new Entry<>(new Text("entryText"), 42, 313); + entry.addOption(option); + BytesReference xContent = toXContent(entry, XContentType.JSON, randomBoolean()); + assertEquals( + "{\"text\":\"entryText\"," + + "\"offset\":42," + + "\"length\":313," + + "\"options\":[" + + "{\"text\":\"someText\"," + + "\"highlighted\":\"somethingHighlighted\"," + + "\"score\":1.3," + + "\"collate_match\":true}" + + "]}", xContent.utf8ToString()); + + org.elasticsearch.search.suggest.term.TermSuggestion.Entry.Option termOption = + new org.elasticsearch.search.suggest.term.TermSuggestion.Entry.Option(new Text("termSuggestOption"), 42, 3.13f); + entry = new Entry<>(new Text("entryText"), 42, 313); + entry.addOption(termOption); + xContent = toXContent(entry, XContentType.JSON, randomBoolean()); + assertEquals( + "{\"text\":\"entryText\"," + + "\"offset\":42," + + "\"length\":313," + + "\"options\":[" + + "{\"text\":\"termSuggestOption\"," + + "\"score\":3.13," + + "\"freq\":42}" + + "]}", xContent.utf8ToString()); + + org.elasticsearch.search.suggest.completion.CompletionSuggestion.Entry.Option completionOption = + new org.elasticsearch.search.suggest.completion.CompletionSuggestion.Entry.Option(-1, new Text("completionOption"), + 3.13f, Collections.singletonMap("key", Collections.singleton("value"))); + entry = new Entry<>(new Text("entryText"), 42, 313); + entry.addOption(completionOption); + xContent = toXContent(entry, XContentType.JSON, randomBoolean()); + assertEquals( + "{\"text\":\"entryText\"," + + "\"offset\":42," + + "\"length\":313," + + "\"options\":[" + + "{\"text\":\"completionOption\"," + + "\"score\":3.13," + + "\"contexts\":{\"key\":[\"value\"]}" + + "}" + + "]}", xContent.utf8ToString()); + } + +} |