diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index 816631ff94fbb..268603d3ce7af 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -163,8 +163,11 @@ import org.elasticsearch.search.aggregations.pipeline.derivative.ParsedDerivative; import org.elasticsearch.search.suggest.Suggest; import org.elasticsearch.search.suggest.completion.CompletionSuggestion; +import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder; import org.elasticsearch.search.suggest.phrase.PhraseSuggestion; +import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder; import org.elasticsearch.search.suggest.term.TermSuggestion; +import org.elasticsearch.search.suggest.term.TermSuggestionBuilder; import java.io.Closeable; import java.io.IOException; @@ -1141,11 +1144,11 @@ static List getDefaultNamedXContents() { List entries = map.entrySet().stream() .map(entry -> new NamedXContentRegistry.Entry(Aggregation.class, new ParseField(entry.getKey()), entry.getValue())) .collect(Collectors.toList()); - entries.add(new NamedXContentRegistry.Entry(Suggest.Suggestion.class, new ParseField(TermSuggestion.NAME), + entries.add(new NamedXContentRegistry.Entry(Suggest.Suggestion.class, new ParseField(TermSuggestionBuilder.SUGGESTION_NAME), (parser, context) -> TermSuggestion.fromXContent(parser, (String)context))); - entries.add(new NamedXContentRegistry.Entry(Suggest.Suggestion.class, new ParseField(PhraseSuggestion.NAME), + entries.add(new NamedXContentRegistry.Entry(Suggest.Suggestion.class, new ParseField(PhraseSuggestionBuilder.SUGGESTION_NAME), (parser, context) -> PhraseSuggestion.fromXContent(parser, (String)context))); - entries.add(new NamedXContentRegistry.Entry(Suggest.Suggestion.class, new ParseField(CompletionSuggestion.NAME), + entries.add(new NamedXContentRegistry.Entry(Suggest.Suggestion.class, new ParseField(CompletionSuggestionBuilder.SUGGESTION_NAME), (parser, context) -> CompletionSuggestion.fromXContent(parser, (String)context))); return entries; } diff --git a/docs/reference/release-notes/7.0.0-alpha1.asciidoc b/docs/reference/release-notes/7.0.0-alpha1.asciidoc index cf2e1e30be050..c3a03d77f8118 100644 --- a/docs/reference/release-notes/7.0.0-alpha1.asciidoc +++ b/docs/reference/release-notes/7.0.0-alpha1.asciidoc @@ -21,4 +21,10 @@ Aggregations:: * The Percentiles and PercentileRanks aggregations now return `null` in the REST response, instead of `NaN`. This makes it consistent with the rest of the aggregations. Note: this only applies to the REST response, the java objects continue to return `NaN` (also - consistent with other aggregations) \ No newline at end of file + consistent with other aggregations) + +Suggesters:: +* Plugins that register suggesters can now define their own types of suggestions and must + explicitly indicate the type of suggestion that they produce. Existing plugins will + require changes to their plugin registration. See the `custom-suggester` example + plugin {pull}30284[#30284] \ No newline at end of file diff --git a/plugins/examples/custom-suggester/build.gradle b/plugins/examples/custom-suggester/build.gradle new file mode 100644 index 0000000000000..b36d5cd218d27 --- /dev/null +++ b/plugins/examples/custom-suggester/build.gradle @@ -0,0 +1,33 @@ +/* + * 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. + */ + +apply plugin: 'elasticsearch.esplugin' + +esplugin { + name 'custom-suggester' + description 'An example plugin showing how to write and register a custom suggester' + classname 'org.elasticsearch.example.customsuggester.CustomSuggesterPlugin' +} + +integTestCluster { + numNodes = 2 +} + +// this plugin has no unit tests, only rest tests +tasks.test.enabled = false \ No newline at end of file diff --git a/plugins/examples/custom-suggester/src/main/java/org/elasticsearch/example/customsuggester/CustomSuggester.java b/plugins/examples/custom-suggester/src/main/java/org/elasticsearch/example/customsuggester/CustomSuggester.java new file mode 100644 index 0000000000000..b6a5b5e8f842f --- /dev/null +++ b/plugins/examples/custom-suggester/src/main/java/org/elasticsearch/example/customsuggester/CustomSuggester.java @@ -0,0 +1,62 @@ +/* + * 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.example.customsuggester; + +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.util.CharsRefBuilder; +import org.elasticsearch.common.text.Text; +import org.elasticsearch.search.suggest.Suggest; +import org.elasticsearch.search.suggest.Suggester; + +import java.util.Locale; + +public class CustomSuggester extends Suggester { + + // This is a pretty dumb implementation which returns the original text + fieldName + custom config option + 12 or 123 + @Override + public Suggest.Suggestion> innerExecute( + String name, + CustomSuggestionContext suggestion, + IndexSearcher searcher, + CharsRefBuilder spare) { + + // Get the suggestion context + String text = suggestion.getText().utf8ToString(); + + // create two suggestions with 12 and 123 appended + CustomSuggestion response = new CustomSuggestion(name, suggestion.getSize(), "suggestion-dummy-value"); + + CustomSuggestion.Entry entry = new CustomSuggestion.Entry(new Text(text), 0, text.length(), "entry-dummy-value"); + + String firstOption = + String.format(Locale.ROOT, "%s-%s-%s-%s", text, suggestion.getField(), suggestion.options.get("suffix"), "12"); + CustomSuggestion.Entry.Option option12 = new CustomSuggestion.Entry.Option(new Text(firstOption), 0.9f, "option-dummy-value-1"); + entry.addOption(option12); + + String secondOption = + String.format(Locale.ROOT, "%s-%s-%s-%s", text, suggestion.getField(), suggestion.options.get("suffix"), "123"); + CustomSuggestion.Entry.Option option123 = new CustomSuggestion.Entry.Option(new Text(secondOption), 0.8f, "option-dummy-value-2"); + entry.addOption(option123); + + response.addTerm(entry); + + return response; + } +} diff --git a/plugins/examples/custom-suggester/src/main/java/org/elasticsearch/example/customsuggester/CustomSuggesterPlugin.java b/plugins/examples/custom-suggester/src/main/java/org/elasticsearch/example/customsuggester/CustomSuggesterPlugin.java new file mode 100644 index 0000000000000..91ffa672e5351 --- /dev/null +++ b/plugins/examples/custom-suggester/src/main/java/org/elasticsearch/example/customsuggester/CustomSuggesterPlugin.java @@ -0,0 +1,40 @@ +/* + * 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.example.customsuggester; + +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.SearchPlugin; + +import java.util.Collections; +import java.util.List; + +public class CustomSuggesterPlugin extends Plugin implements SearchPlugin { + @Override + public List> getSuggesters() { + return Collections.singletonList( + new SearchPlugin.SuggesterSpec<>( + CustomSuggestionBuilder.SUGGESTION_NAME, + CustomSuggestionBuilder::new, + CustomSuggestionBuilder::fromXContent, + CustomSuggestion::new + ) + ); + } +} diff --git a/plugins/examples/custom-suggester/src/main/java/org/elasticsearch/example/customsuggester/CustomSuggestion.java b/plugins/examples/custom-suggester/src/main/java/org/elasticsearch/example/customsuggester/CustomSuggestion.java new file mode 100644 index 0000000000000..f7ec27b7af002 --- /dev/null +++ b/plugins/examples/custom-suggester/src/main/java/org/elasticsearch/example/customsuggester/CustomSuggestion.java @@ -0,0 +1,227 @@ +/* + * 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.example.customsuggester; + +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.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.suggest.Suggest; + +import java.io.IOException; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +public class CustomSuggestion extends Suggest.Suggestion { + + public static final int TYPE = 999; + + public static final ParseField DUMMY = new ParseField("dummy"); + + private String dummy; + + public CustomSuggestion(String name, int size, String dummy) { + super(name, size); + this.dummy = dummy; + } + + public CustomSuggestion(StreamInput in) throws IOException { + super(in); + dummy = in.readString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(dummy); + } + + @Override + public String getWriteableName() { + return CustomSuggestionBuilder.SUGGESTION_NAME; + } + + @Override + public int getWriteableType() { + return TYPE; + } + + /** + * A meaningless value used to test that plugin suggesters can add fields to their Suggestion types + * + * This can't be serialized to xcontent because Suggestions appear in xcontent as an array of entries, so there is no place + * to add a custom field. But we can still use a custom field internally and use it to define a Suggestion's behavior + */ + public String getDummy() { + return dummy; + } + + @Override + protected Entry newEntry() { + return new Entry(); + } + + @Override + protected Entry newEntry(StreamInput in) throws IOException { + return new Entry(in); + } + + public static CustomSuggestion fromXContent(XContentParser parser, String name) throws IOException { + CustomSuggestion suggestion = new CustomSuggestion(name, -1, null); + parseEntries(parser, suggestion, Entry::fromXContent); + return suggestion; + } + + public static class Entry extends Suggest.Suggestion.Entry { + + private static final ObjectParser PARSER = new ObjectParser<>("CustomSuggestionEntryParser", true, Entry::new); + + static { + declareCommonFields(PARSER); + PARSER.declareString((entry, dummy) -> entry.dummy = dummy, DUMMY); + PARSER.declareObjectArray(Entry::addOptions, (p, c) -> Option.fromXContent(p), new ParseField(OPTIONS)); + } + + private String dummy; + + public Entry() {} + + public Entry(Text text, int offset, int length, String dummy) { + super(text, offset, length); + this.dummy = dummy; + } + + public Entry(StreamInput in) throws IOException { + super(in); + dummy = in.readString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(dummy); + } + + @Override + protected Option newOption() { + return new Option(); + } + + @Override + protected Option newOption(StreamInput in) throws IOException { + return new Option(in); + } + + /* + * the value of dummy will always be the same, so this just tests that we can merge entries with custom fields + */ + @Override + protected void merge(Suggest.Suggestion.Entry