serialize suggestion responses as named writeables (#30284)

Suggestion responses were previously serialized as streamables which
made writing suggesters in plugins with custom suggestion response types
impossible. This commit makes them serialized as named writeables and
provides a facility for registering a reader for suggestion responses
when registering a suggester.

This also makes Suggestion responses abstract, requiring a suggester
implementation to provide its own types. Suggesters which do not need
anything additional to what is defined in Suggest.Suggestion should
provide a minimal subclass.

The existing plugin suggester integration tests are removed and
replaced with an equivalent implementation as an example
plugin.
This commit is contained in:
Andy Bristol 2018-08-07 13:31:00 -07:00 committed by GitHub
parent 0b7fb4e7b9
commit 8bfb0f3f8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1269 additions and 562 deletions

View File

@ -163,8 +163,11 @@ import org.elasticsearch.search.aggregations.pipeline.derivative.DerivativePipel
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 @@ public class RestHighLevelClient implements Closeable {
List<NamedXContentRegistry.Entry> 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;
}

View File

@ -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)
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]

View File

@ -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

View File

@ -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<CustomSuggestionContext> {
// This is a pretty dumb implementation which returns the original text + fieldName + custom config option + 12 or 123
@Override
public Suggest.Suggestion<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>> 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;
}
}

View File

@ -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<SearchPlugin.SuggesterSpec<?>> getSuggesters() {
return Collections.singletonList(
new SearchPlugin.SuggesterSpec<>(
CustomSuggestionBuilder.SUGGESTION_NAME,
CustomSuggestionBuilder::new,
CustomSuggestionBuilder::fromXContent,
CustomSuggestion::new
)
);
}
}

View File

@ -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<CustomSuggestion.Entry> {
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<CustomSuggestion.Entry.Option> {
private static final ObjectParser<Entry, Void> 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<Option> otherEntry) {
dummy = ((Entry) otherEntry).getDummy();
}
/**
* Meaningless field used to test that plugin suggesters can add fields to their entries
*/
public String getDummy() {
return dummy;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder = super.toXContent(builder, params);
builder.field(DUMMY.getPreferredName(), getDummy());
return builder;
}
public static Entry fromXContent(XContentParser parser) {
return PARSER.apply(parser, null);
}
public static class Option extends Suggest.Suggestion.Entry.Option {
private static final ConstructingObjectParser<Option, Void> PARSER = new ConstructingObjectParser<>(
"CustomSuggestionObjectParser", true,
args -> {
Text text = new Text((String) args[0]);
float score = (float) args[1];
String dummy = (String) args[2];
return new Option(text, score, dummy);
});
static {
PARSER.declareString(constructorArg(), TEXT);
PARSER.declareFloat(constructorArg(), SCORE);
PARSER.declareString(constructorArg(), DUMMY);
}
private String dummy;
public Option() {}
public Option(Text text, float score, String dummy) {
super(text, score);
this.dummy = dummy;
}
public Option(StreamInput in) throws IOException {
super(in);
dummy = in.readString();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(dummy);
}
/**
* A meaningless value used to test that plugin suggesters can add fields to their options
*/
public String getDummy() {
return dummy;
}
/*
* the value of dummy will always be the same, so this just tests that we can merge options with custom fields
*/
@Override
protected void mergeInto(Suggest.Suggestion.Entry.Option otherOption) {
super.mergeInto(otherOption);
dummy = ((Option) otherOption).getDummy();
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder = super.toXContent(builder, params);
builder.field(DUMMY.getPreferredName(), dummy);
return builder;
}
public static Option fromXContent(XContentParser parser) {
return PARSER.apply(parser, null);
}
}
}
}

View File

@ -0,0 +1,143 @@
/*
* 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.ParsingException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.lucene.BytesRefs;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.query.QueryShardContext;
import org.elasticsearch.search.suggest.SuggestionBuilder;
import org.elasticsearch.search.suggest.SuggestionSearchContext;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class CustomSuggestionBuilder extends SuggestionBuilder<CustomSuggestionBuilder> {
public static final String SUGGESTION_NAME = "custom";
protected static final ParseField RANDOM_SUFFIX_FIELD = new ParseField("suffix");
private String randomSuffix;
public CustomSuggestionBuilder(String randomField, String randomSuffix) {
super(randomField);
this.randomSuffix = randomSuffix;
}
/**
* Read from a stream.
*/
public CustomSuggestionBuilder(StreamInput in) throws IOException {
super(in);
this.randomSuffix = in.readString();
}
@Override
public void doWriteTo(StreamOutput out) throws IOException {
out.writeString(randomSuffix);
}
@Override
protected XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException {
builder.field(RANDOM_SUFFIX_FIELD.getPreferredName(), randomSuffix);
return builder;
}
@Override
public String getWriteableName() {
return SUGGESTION_NAME;
}
@Override
protected boolean doEquals(CustomSuggestionBuilder other) {
return Objects.equals(randomSuffix, other.randomSuffix);
}
@Override
protected int doHashCode() {
return Objects.hash(randomSuffix);
}
public static CustomSuggestionBuilder fromXContent(XContentParser parser) throws IOException {
XContentParser.Token token;
String currentFieldName = null;
String fieldname = null;
String suffix = null;
String analyzer = null;
int sizeField = -1;
int shardSize = -1;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token.isValue()) {
if (SuggestionBuilder.ANALYZER_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
analyzer = parser.text();
} else if (SuggestionBuilder.FIELDNAME_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
fieldname = parser.text();
} else if (SuggestionBuilder.SIZE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
sizeField = parser.intValue();
} else if (SuggestionBuilder.SHARDSIZE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
shardSize = parser.intValue();
} else if (RANDOM_SUFFIX_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
suffix = parser.text();
}
} else {
throw new ParsingException(parser.getTokenLocation(),
"suggester[custom] doesn't support field [" + currentFieldName + "]");
}
}
// now we should have field name, check and copy fields over to the suggestion builder we return
if (fieldname == null) {
throw new ParsingException(parser.getTokenLocation(), "the required field option is missing");
}
CustomSuggestionBuilder builder = new CustomSuggestionBuilder(fieldname, suffix);
if (analyzer != null) {
builder.analyzer(analyzer);
}
if (sizeField != -1) {
builder.size(sizeField);
}
if (shardSize != -1) {
builder.shardSize(shardSize);
}
return builder;
}
@Override
public SuggestionSearchContext.SuggestionContext build(QueryShardContext context) throws IOException {
Map<String, Object> options = new HashMap<>();
options.put(FIELDNAME_FIELD.getPreferredName(), field());
options.put(RANDOM_SUFFIX_FIELD.getPreferredName(), randomSuffix);
CustomSuggestionContext customSuggestionsContext = new CustomSuggestionContext(context, options);
customSuggestionsContext.setField(field());
assert text != null;
customSuggestionsContext.setText(BytesRefs.toBytesRef(text));
return customSuggestionsContext;
}
}

View File

@ -0,0 +1,35 @@
/*
* 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.index.query.QueryShardContext;
import org.elasticsearch.search.suggest.SuggestionSearchContext;
import java.util.Map;
public class CustomSuggestionContext extends SuggestionSearchContext.SuggestionContext {
public Map<String, Object> options;
public CustomSuggestionContext(QueryShardContext context, Map<String, Object> options) {
super(new CustomSuggester(), context);
this.options = options;
}
}

View File

@ -0,0 +1,37 @@
/*
* 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 com.carrotsearch.randomizedtesting.annotations.Name;
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate;
import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase;
public class CustomSuggesterClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase {
public CustomSuggesterClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) {
super(testCandidate);
}
@ParametersFactory
public static Iterable<Object[]> parameters() throws Exception {
return ESClientYamlSuiteTestCase.createParameters();
}
}

View File

@ -0,0 +1,13 @@
# tests that the custom suggester plugin is installed
---
"plugin loaded":
- do:
cluster.state: {}
# Get master node id
- set: { master_node: master }
- do:
nodes.info: {}
- contains: { nodes.$master.plugins: { name: custom-suggester } }

View File

@ -0,0 +1,55 @@
# tests that the custom suggester works
# the issue that prompted serializing Suggestion as a registered named writeable was not revealed until
# a user found that it would fail when reducing suggestions in a multi node envrionment
# https://github.com/elastic/elasticsearch/issues/26585
"test custom suggester":
- do:
cluster.health:
wait_for_nodes: 2
- is_true: cluster_name
- is_false: timed_out
- gte: { number_of_nodes: 2 }
- gte: { number_of_data_nodes: 2 }
- do:
indices.create:
index: test
body:
settings:
number_of_shards: 2
number_of_replicas: 0
- do:
bulk:
index: test
type: test
refresh: true
body: |
{ "index": {} }
{ "content": "these" }
{ "index": {} }
{ "content": "aren't" }
{ "index": {} }
{ "content": "actually" }
{ "index": {} }
{ "content": "used" }
- do:
search:
size: 0
index: test
body:
suggest:
test:
text: my suggestion text
custom:
field: arbitraryField
suffix: arbitrarySuffix
- match: { suggest.test.0.dummy: entry-dummy-value }
- match: { suggest.test.0.options.0.text: my suggestion text-arbitraryField-arbitrarySuffix-12 }
- match: { suggest.test.0.options.0.dummy: option-dummy-value-1 }
- match: { suggest.test.0.options.1.text: my suggestion text-arbitraryField-arbitrarySuffix-123 }
- match: { suggest.test.0.options.1.dummy: option-dummy-value-2 }

View File

@ -48,6 +48,7 @@ import org.elasticsearch.search.fetch.FetchSubPhase;
import org.elasticsearch.search.fetch.subphase.highlight.Highlighter;
import org.elasticsearch.search.rescore.RescorerBuilder;
import org.elasticsearch.search.rescore.Rescorer;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.Suggester;
import org.elasticsearch.search.suggest.SuggestionBuilder;
@ -149,31 +150,61 @@ public interface SearchPlugin {
* Specification for a {@link Suggester}.
*/
class SuggesterSpec<T extends SuggestionBuilder<T>> extends SearchExtensionSpec<T, CheckedFunction<XContentParser, T, IOException>> {
private Writeable.Reader<? extends Suggest.Suggestion> suggestionReader;
/**
* Specification of custom {@link Suggester}.
*
* @param name holds the names by which this suggester might be parsed. The {@link ParseField#getPreferredName()} is special as it
* is the name by under which the reader is registered. So it is the name that the query should use as its
* {@link NamedWriteable#getWriteableName()} too.
* @param reader the reader registered for this suggester's builder. Typically a reference to a constructor that takes a
* is the name by under which the request builder and Suggestion response readers are registered. So it is the name that the
* query and Suggestion response should use as their {@link NamedWriteable#getWriteableName()} return values too.
* @param builderReader the reader registered for this suggester's builder. Typically a reference to a constructor that takes a
* {@link StreamInput}
* @param parser the parser the reads the query suggester from xcontent
* @param builderParser a parser that reads the suggester's builder from xcontent
* @param suggestionReader the reader registered for this suggester's Suggestion response. Typically a reference to a constructor
* that takes a {@link StreamInput}
*/
public SuggesterSpec(ParseField name, Writeable.Reader<T> reader, CheckedFunction<XContentParser, T, IOException> parser) {
super(name, reader, parser);
public SuggesterSpec(
ParseField name,
Writeable.Reader<T> builderReader,
CheckedFunction<XContentParser, T, IOException> builderParser,
Writeable.Reader<? extends Suggest.Suggestion> suggestionReader) {
super(name, builderReader, builderParser);
setSuggestionReader(suggestionReader);
}
/**
* Specification of custom {@link Suggester}.
*
* @param name the name by which this suggester might be parsed or deserialized. Make sure that the query builder returns this name
* for {@link NamedWriteable#getWriteableName()}.
* @param reader the reader registered for this suggester's builder. Typically a reference to a constructor that takes a
* @param name the name by which this suggester might be parsed or deserialized. Make sure that the query builder and Suggestion
* response reader return this name for {@link NamedWriteable#getWriteableName()}.
* @param builderReader the reader registered for this suggester's builder. Typically a reference to a constructor that takes a
* {@link StreamInput}
* @param parser the parser the reads the suggester builder from xcontent
* @param builderParser a parser that reads the suggester's builder from xcontent
* @param suggestionReader the reader registered for this suggester's Suggestion response. Typically a reference to a constructor
* that takes a {@link StreamInput}
*/
public SuggesterSpec(String name, Writeable.Reader<T> reader, CheckedFunction<XContentParser, T, IOException> parser) {
super(name, reader, parser);
public SuggesterSpec(
String name,
Writeable.Reader<T> builderReader,
CheckedFunction<XContentParser, T, IOException> builderParser,
Writeable.Reader<? extends Suggest.Suggestion> suggestionReader) {
super(name, builderReader, builderParser);
setSuggestionReader(suggestionReader);
}
private void setSuggestionReader(Writeable.Reader<? extends Suggest.Suggestion> reader) {
this.suggestionReader = reader;
}
/**
* Returns the reader used to read the {@link Suggest.Suggestion} generated by this suggester
*/
public Writeable.Reader<? extends Suggest.Suggestion> getSuggestionReader() {
return this.suggestionReader;
}
}

View File

@ -247,13 +247,17 @@ import org.elasticsearch.search.sort.GeoDistanceSortBuilder;
import org.elasticsearch.search.sort.ScoreSortBuilder;
import org.elasticsearch.search.sort.ScriptSortBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestionBuilder;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder;
import org.elasticsearch.search.suggest.phrase.Laplace;
import org.elasticsearch.search.suggest.phrase.LinearInterpolation;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestion;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder;
import org.elasticsearch.search.suggest.phrase.SmoothingModel;
import org.elasticsearch.search.suggest.phrase.StupidBackoff;
import org.elasticsearch.search.suggest.term.TermSuggestion;
import org.elasticsearch.search.suggest.term.TermSuggestionBuilder;
import java.util.ArrayList;
@ -590,9 +594,14 @@ public class SearchModule {
private void registerSuggesters(List<SearchPlugin> plugins) {
registerSmoothingModels(namedWriteables);
registerSuggester(new SuggesterSpec<>("term", TermSuggestionBuilder::new, TermSuggestionBuilder::fromXContent));
registerSuggester(new SuggesterSpec<>("phrase", PhraseSuggestionBuilder::new, PhraseSuggestionBuilder::fromXContent));
registerSuggester(new SuggesterSpec<>("completion", CompletionSuggestionBuilder::new, CompletionSuggestionBuilder::fromXContent));
registerSuggester(new SuggesterSpec<>(TermSuggestionBuilder.SUGGESTION_NAME,
TermSuggestionBuilder::new, TermSuggestionBuilder::fromXContent, TermSuggestion::new));
registerSuggester(new SuggesterSpec<>(PhraseSuggestionBuilder.SUGGESTION_NAME,
PhraseSuggestionBuilder::new, PhraseSuggestionBuilder::fromXContent, PhraseSuggestion::new));
registerSuggester(new SuggesterSpec<>(CompletionSuggestionBuilder.SUGGESTION_NAME,
CompletionSuggestionBuilder::new, CompletionSuggestionBuilder::fromXContent, CompletionSuggestion::new));
registerFromPlugin(plugins, SearchPlugin::getSuggesters, this::registerSuggester);
}
@ -602,6 +611,10 @@ public class SearchModule {
SuggestionBuilder.class, suggester.getName().getPreferredName(), suggester.getReader()));
namedXContents.add(new NamedXContentRegistry.Entry(SuggestionBuilder.class, suggester.getName(),
suggester.getParser()));
namedWriteables.add(new NamedWriteableRegistry.Entry(
Suggest.Suggestion.class, suggester.getName().getPreferredName(), suggester.getSuggestionReader()
));
}
private Map<String, Highlighter> setupHighlighters(Settings settings, List<SearchPlugin> plugins) {

View File

@ -50,7 +50,7 @@ public class InternalSearchResponse extends SearchResponseSections implements Wr
super(
SearchHits.readSearchHits(in),
in.readBoolean() ? InternalAggregations.readAggregations(in) : null,
in.readBoolean() ? Suggest.readSuggest(in) : null,
in.readBoolean() ? new Suggest(in) : null,
in.readBoolean(),
in.readOptionalBoolean(),
in.readOptionalWriteable(SearchProfileShardResults::new),
@ -62,7 +62,7 @@ public class InternalSearchResponse extends SearchResponseSections implements Wr
public void writeTo(StreamOutput out) throws IOException {
hits.writeTo(out);
out.writeOptionalStreamable((InternalAggregations)aggregations);
out.writeOptionalStreamable(suggest);
out.writeOptionalWriteable(suggest);
out.writeBoolean(timedOut);
out.writeOptionalBoolean(terminatedEarly);
out.writeOptionalWriteable(profileResults);

View File

@ -293,7 +293,7 @@ public final class QuerySearchResult extends SearchPhaseResult {
pipelineAggregators = in.readNamedWriteableList(PipelineAggregator.class).stream().map(a -> (SiblingPipelineAggregator) a)
.collect(Collectors.toList());
if (in.readBoolean()) {
suggest = Suggest.readSuggest(in);
suggest = new Suggest(in);
}
searchTimedOut = in.readBoolean();
terminatedEarly = in.readOptionalBoolean();

View File

@ -20,18 +20,18 @@ package org.elasticsearch.search.suggest;
import org.apache.lucene.util.CollectionUtil;
import org.apache.lucene.util.SetOnce;
import org.elasticsearch.Version;
import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.NamedWriteable;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Streamable;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.ToXContentFragment;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
@ -53,16 +53,15 @@ import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
/**
* Top level suggest result, containing the result for each suggestion.
*/
public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? extends Option>>>, Streamable, ToXContentFragment {
public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? extends Option>>>, Writeable, ToXContentFragment {
public static final String NAME = "suggest";
@ -92,6 +91,40 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
this.hasScoreDocs = filter(CompletionSuggestion.class).stream().anyMatch(CompletionSuggestion::hasScoreDocs);
}
public Suggest(StreamInput in) throws IOException {
// in older versions, Suggestion types were serialized as Streamable
if (in.getVersion().before(Version.V_7_0_0_alpha1)) {
final int size = in.readVInt();
suggestions = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
Suggestion<? extends Entry<? extends Option>> suggestion;
final int type = in.readVInt();
switch (type) {
case TermSuggestion.TYPE:
suggestion = new TermSuggestion(in);
break;
case CompletionSuggestion.TYPE:
suggestion = new CompletionSuggestion(in);
break;
case PhraseSuggestion.TYPE:
suggestion = new PhraseSuggestion(in);
break;
default:
throw new IllegalArgumentException("Unknown suggestion type with ordinal " + type);
}
suggestions.add(suggestion);
}
} else {
int suggestionCount = in.readVInt();
suggestions = new ArrayList<>(suggestionCount);
for (int i = 0; i < suggestionCount; i++) {
suggestions.add(in.readNamedWriteable(Suggestion.class));
}
}
hasScoreDocs = filter(CompletionSuggestion.class).stream().anyMatch(CompletionSuggestion::hasScoreDocs);
}
@Override
public Iterator<Suggestion<? extends Entry<? extends Option>>> iterator() {
return suggestions.iterator();
@ -125,42 +158,20 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
return hasScoreDocs;
}
@Override
public void readFrom(StreamInput in) throws IOException {
final int size = in.readVInt();
suggestions = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
// TODO: remove these complicated generics
Suggestion<? extends Entry<? extends Option>> suggestion;
final int type = in.readVInt();
switch (type) {
case TermSuggestion.TYPE:
suggestion = new TermSuggestion();
break;
case CompletionSuggestion.TYPE:
suggestion = new CompletionSuggestion();
break;
case 2: // CompletionSuggestion.TYPE
throw new IllegalArgumentException("Completion suggester 2.x is not supported anymore");
case PhraseSuggestion.TYPE:
suggestion = new PhraseSuggestion();
break;
default:
suggestion = new Suggestion();
break;
}
suggestion.readFrom(in);
suggestions.add(suggestion);
}
hasScoreDocs = filter(CompletionSuggestion.class).stream().anyMatch(CompletionSuggestion::hasScoreDocs);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeVInt(suggestions.size());
for (Suggestion<?> command : suggestions) {
out.writeVInt(command.getWriteableType());
command.writeTo(out);
// in older versions, Suggestion types were serialized as Streamable
if (out.getVersion().before(Version.V_7_0_0_alpha1)) {
out.writeVInt(suggestions.size());
for (Suggestion<?> command : suggestions) {
out.writeVInt(command.getWriteableType());
command.writeTo(out);
}
} else {
out.writeVInt(suggestions.size());
for (Suggestion<? extends Entry<? extends Option>> suggestion : suggestions) {
out.writeNamedWriteable(suggestion);
}
}
}
@ -195,12 +206,6 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
return new Suggest(suggestions);
}
public static Suggest readSuggest(StreamInput in) throws IOException {
Suggest result = new Suggest();
result.readFrom(in);
return result;
}
public static List<Suggestion<? extends Entry<? extends Option>>> reduce(Map<String, List<Suggest.Suggestion>> groupedSuggestions) {
List<Suggestion<? extends Entry<? extends Option>>> reduced = new ArrayList<>(groupedSuggestions.size());
for (java.util.Map.Entry<String, List<Suggestion>> unmergedResults : groupedSuggestions.entrySet()) {
@ -232,10 +237,27 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
.collect(Collectors.toList());
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
return Objects.equals(suggestions, ((Suggest) other).suggestions);
}
@Override
public int hashCode() {
return Objects.hash(suggestions);
}
/**
* The suggestion responses corresponding with the suggestions in the request.
*/
public static class Suggestion<T extends Suggestion.Entry> implements Iterable<T>, Streamable, ToXContentFragment {
public abstract static class Suggestion<T extends Suggestion.Entry> implements Iterable<T>, NamedWriteable, ToXContentFragment {
private static final String NAME = "suggestion";
@ -252,6 +274,24 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
this.size = size; // The suggested term size specified in request, only used for merging shard responses
}
public Suggestion(StreamInput in) throws IOException {
name = in.readString();
size = in.readVInt();
// this is a hack to work around slightly different serialization order of earlier versions of TermSuggestion
if (in.getVersion().before(Version.V_7_0_0_alpha1) && this instanceof TermSuggestion) {
TermSuggestion t = (TermSuggestion) this;
t.setSort(SortBy.readFromStream(in));
}
int entriesCount = in.readVInt();
entries.clear();
for (int i = 0; i < entriesCount; i++) {
T newEntry = newEntry(in);
entries.add(newEntry);
}
}
public void addTerm(T entry) {
entries.add(entry);
}
@ -259,20 +299,14 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
/**
* Returns a integer representing the type of the suggestion. This is used for
* internal serialization over the network.
*
* This class is now serialized as a NamedWriteable and this method only remains for backwards compatibility
*/
public int getWriteableType() { // TODO remove this in favor of NamedWriteable
@Deprecated
public int getWriteableType() {
return TYPE;
}
/**
* Returns a string representing the type of the suggestion. This type is added to
* the suggestion name in the XContent response, so that it can later be used by
* REST clients to determine the internal type of the suggestion.
*/
protected String getType() {
return NAME;
}
@Override
public Iterator<T> iterator() {
return entries.iterator();
@ -346,57 +380,67 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
}
}
@Override
public void readFrom(StreamInput in) throws IOException {
innerReadFrom(in);
int size = in.readVInt();
entries.clear();
for (int i = 0; i < size; i++) {
T newEntry = newEntry();
newEntry.readFrom(in);
entries.add(newEntry);
}
}
protected T newEntry() {
return (T)new Entry();
}
protected void innerReadFrom(StreamInput in) throws IOException {
name = in.readString();
size = in.readVInt();
}
protected abstract T newEntry();
protected abstract T newEntry(StreamInput in) throws IOException;
@Override
public void writeTo(StreamOutput out) throws IOException {
innerWriteTo(out);
out.writeString(name);
out.writeVInt(size);
// this is a hack to work around slightly different serialization order in older versions of TermSuggestion
if (out.getVersion().before(Version.V_7_0_0_alpha1) && this instanceof TermSuggestion) {
TermSuggestion termSuggestion = (TermSuggestion) this;
termSuggestion.getSort().writeTo(out);
}
out.writeVInt(entries.size());
for (Entry<?> entry : entries) {
entry.writeTo(out);
}
}
public void innerWriteTo(StreamOutput out) throws IOException {
out.writeString(name);
out.writeVInt(size);
}
@Override
public abstract String getWriteableName();
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
if (params.paramAsBoolean(RestSearchAction.TYPED_KEYS_PARAM, false)) {
// Concatenates the type and the name of the suggestion (ex: completion#foo)
builder.startArray(String.join(Aggregation.TYPED_KEYS_DELIMITER, getType(), getName()));
builder.startArray(String.join(Aggregation.TYPED_KEYS_DELIMITER, getWriteableName(), getName()));
} else {
builder.startArray(getName());
}
for (Entry<?> entry : entries) {
builder.startObject();
entry.toXContent(builder, params);
builder.endObject();
}
builder.endArray();
return builder;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
Suggestion otherSuggestion = (Suggestion) other;
return Objects.equals(name, otherSuggestion.name)
&& Objects.equals(size, otherSuggestion.size)
&& Objects.equals(entries, otherSuggestion.entries);
}
@Override
public int hashCode() {
return Objects.hash(name, size, entries);
}
@SuppressWarnings("unchecked")
public static Suggestion<? extends Entry<? extends Option>> fromXContent(XContentParser parser) throws IOException {
ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.currentToken(), parser::getTokenLocation);
@ -417,7 +461,7 @@ 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, ToXContentObject {
public abstract static class Entry<O extends Option> implements Iterable<O>, Writeable, ToXContentFragment {
private static final String TEXT = "text";
private static final String OFFSET = "offset";
@ -436,7 +480,18 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
this.length = length;
}
protected Entry() {
protected Entry() {}
public Entry(StreamInput in) throws IOException {
text = in.readText();
offset = in.readVInt();
length = in.readVInt();
int suggestedWords = in.readVInt();
options = new ArrayList<>(suggestedWords);
for (int j = 0; j < suggestedWords; j++) {
O newOption = newOption(in);
options.add(newOption);
}
}
public void addOption(O option) {
@ -534,44 +589,27 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Entry<?> entry = (Entry<?>) o;
if (length != entry.length) return false;
if (offset != entry.offset) return false;
if (!this.text.equals(entry.text)) return false;
return true;
return Objects.equals(length, entry.length)
&& Objects.equals(offset, entry.offset)
&& Objects.equals(text, entry.text)
&& Objects.equals(options, entry.options);
}
@Override
public int hashCode() {
int result = text.hashCode();
result = 31 * result + offset;
result = 31 * result + length;
return result;
return Objects.hash(text, offset, length, options);
}
@Override
public void readFrom(StreamInput in) throws IOException {
text = in.readText();
offset = in.readVInt();
length = in.readVInt();
int suggestedWords = in.readVInt();
options = new ArrayList<>(suggestedWords);
for (int j = 0; j < suggestedWords; j++) {
O newOption = newOption();
newOption.readFrom(in);
options.add(newOption);
}
}
@SuppressWarnings("unchecked")
protected O newOption(){
return (O) new Option();
}
protected abstract O newOption();
protected abstract O newOption(StreamInput in) throws IOException;
@Override
public void writeTo(StreamOutput out) throws IOException {
@ -586,40 +624,29 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field(TEXT, text);
builder.field(OFFSET, offset);
builder.field(LENGTH, length);
builder.startArray(OPTIONS);
for (Option option : options) {
builder.startObject();
option.toXContent(builder, params);
builder.endObject();
}
builder.endArray();
builder.endObject();
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.
*/
public static class Option implements Streamable, ToXContentObject {
public abstract static class Option implements Writeable, ToXContentFragment {
public static final ParseField TEXT = new ParseField("text");
public static final ParseField HIGHLIGHTED = new ParseField("highlighted");
@ -646,7 +673,13 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
this(text, null, score);
}
public Option() {
public Option() {}
public Option(StreamInput in) throws IOException {
text = in.readText();
score = in.readFloat();
highlighted = in.readOptionalText();
collateMatch = in.readOptionalBoolean();
}
/**
@ -683,14 +716,6 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
this.score = score;
}
@Override
public void readFrom(StreamInput in) throws IOException {
text = in.readText();
score = in.readFloat();
highlighted = in.readOptionalText();
collateMatch = in.readOptionalBoolean();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeText(text);
@ -701,45 +726,19 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
innerToXContent(builder, params);
builder.endObject();
return builder;
}
protected XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException {
builder.field(TEXT.getPreferredName(), text);
if (highlighted != null) {
builder.field(HIGHLIGHTED.getPreferredName(), highlighted);
}
builder.field(SCORE.getPreferredName(), score);
if (collateMatch != null) {
builder.field(COLLATE_MATCH.getPreferredName(), collateMatch.booleanValue());
}
return builder;
}
private static final ConstructingObjectParser<Option, Void> PARSER = new ConstructingObjectParser<>("SuggestOptionParser",
true, args -> {
Text text = new Text((String) args[0]);
float score = (Float) args[1];
String highlighted = (String) args[2];
Text highlightedText = highlighted == null ? null : new Text(highlighted);
Boolean collateMatch = (Boolean) args[3];
return new Option(text, highlightedText, score, collateMatch);
});
static {
PARSER.declareString(constructorArg(), TEXT);
PARSER.declareFloat(constructorArg(), SCORE);
PARSER.declareString(optionalConstructorArg(), HIGHLIGHTED);
PARSER.declareBoolean(optionalConstructorArg(), COLLATE_MATCH);
}
public static Option fromXContent(XContentParser parser) {
return PARSER.apply(parser, null);
}
protected void mergeInto(Option otherOption) {
score = Math.max(score, otherOption.score);
if (otherOption.collateMatch != null) {
@ -751,18 +750,25 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
}
}
/*
* We consider options equal if they have the same text, even if their other fields may differ
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Option that = (Option) o;
return text.equals(that.text);
return Objects.equals(text, that.text);
}
@Override
public int hashCode() {
return text.hashCode();
return Objects.hash(text);
}
}
}

View File

@ -66,8 +66,7 @@ import static org.elasticsearch.search.suggest.Suggest.COMPARATOR;
*/
public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSuggestion.Entry> {
public static final String NAME = "completion";
@Deprecated
public static final int TYPE = 4;
private boolean skipDuplicates;
@ -86,14 +85,18 @@ public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSug
this.skipDuplicates = skipDuplicates;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
public CompletionSuggestion(StreamInput in) throws IOException {
super(in);
if (in.getVersion().onOrAfter(Version.V_6_1_0)) {
skipDuplicates = in.readBoolean();
}
}
@Override
public String getWriteableName() {
return CompletionSuggestionBuilder.SUGGESTION_NAME;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
@ -121,6 +124,17 @@ public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSug
return getOptions().size() > 0;
}
@Override
public boolean equals(Object other) {
return super.equals(other)
&& Objects.equals(skipDuplicates, ((CompletionSuggestion) other).skipDuplicates);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), skipDuplicates);
}
public static CompletionSuggestion fromXContent(XContentParser parser, String name) throws IOException {
CompletionSuggestion suggestion = new CompletionSuggestion(name, -1, false);
parseEntries(parser, suggestion, CompletionSuggestion.Entry::fromXContent);
@ -222,13 +236,13 @@ public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSug
}
@Override
protected String getType() {
return NAME;
protected Entry newEntry() {
return new Entry();
}
@Override
protected Entry newEntry() {
return new Entry();
protected Entry newEntry(StreamInput in) throws IOException {
return new Entry(in);
}
public static final class Entry extends Suggest.Suggestion.Entry<CompletionSuggestion.Entry.Option> {
@ -237,7 +251,10 @@ public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSug
super(text, offset, length);
}
Entry() {
Entry() {}
public Entry(StreamInput in) throws IOException {
super(in);
}
@Override
@ -245,6 +262,11 @@ public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSug
return new Option();
}
@Override
protected Option newOption(StreamInput in) throws IOException {
return new Option(in);
}
private static ObjectParser<Entry, Void> PARSER = new ObjectParser<>("CompletionSuggestionEntryParser", true,
Entry::new);
@ -274,6 +296,25 @@ public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSug
super();
}
public Option(StreamInput in) throws IOException {
super(in);
this.doc = Lucene.readScoreDoc(in);
if (in.readBoolean()) {
this.hit = SearchHit.readSearchHit(in);
}
int contextSize = in.readInt();
this.contexts = new LinkedHashMap<>(contextSize);
for (int i = 0; i < contextSize; i++) {
String contextName = in.readString();
int nContexts = in.readVInt();
Set<CharSequence> contexts = new HashSet<>(nContexts);
for (int j = 0; j < nContexts; j++) {
contexts.add(in.readString());
}
this.contexts.put(contextName, contexts);
}
}
@Override
protected void mergeInto(Suggest.Suggestion.Entry.Option otherOption) {
// Completion suggestions are reduced by
@ -302,7 +343,7 @@ public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSug
}
@Override
protected XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException {
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.field(TEXT.getPreferredName(), getText());
if (hit != null) {
hit.toInnerXContent(builder, params);
@ -375,26 +416,6 @@ public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSug
return option;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
this.doc = Lucene.readScoreDoc(in);
if (in.readBoolean()) {
this.hit = SearchHit.readSearchHit(in);
}
int contextSize = in.readInt();
this.contexts = new LinkedHashMap<>(contextSize);
for (int i = 0; i < contextSize; i++) {
String contextName = in.readString();
int nContexts = in.readVInt();
Set<CharSequence> contexts = new HashSet<>(nContexts);
for (int j = 0; j < nContexts; j++) {
contexts.add(in.readString());
}
this.contexts.put(contextName, contexts);
}
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);

View File

@ -59,10 +59,12 @@ import java.util.Objects;
public class CompletionSuggestionBuilder extends SuggestionBuilder<CompletionSuggestionBuilder> {
private static final XContentType CONTEXT_BYTES_XCONTENT_TYPE = XContentType.JSON;
static final String SUGGESTION_NAME = "completion";
static final ParseField CONTEXTS_FIELD = new ParseField("contexts", "context");
static final ParseField SKIP_DUPLICATES_FIELD = new ParseField("skip_duplicates");
public static final String SUGGESTION_NAME = "completion";
/**
* {
* "field" : STRING

View File

@ -133,9 +133,9 @@ public final class PhraseSuggester extends Suggester<PhraseSuggestionContext> {
highlighted = new Text(spare.toString());
}
if (collatePrune) {
resultEntry.addOption(new Suggestion.Entry.Option(phrase, highlighted, (float) (correction.score), collateMatch));
resultEntry.addOption(new PhraseSuggestion.Entry.Option(phrase, highlighted, (float) (correction.score), collateMatch));
} else {
resultEntry.addOption(new Suggestion.Entry.Option(phrase, highlighted, (float) (correction.score)));
resultEntry.addOption(new PhraseSuggestion.Entry.Option(phrase, highlighted, (float) (correction.score)));
}
}
} else {

View File

@ -23,41 +23,54 @@ 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.XContentParser;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.Suggest.Suggestion;
import java.io.IOException;
import java.util.Objects;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
/**
* Suggestion entry returned from the {@link PhraseSuggester}.
*/
public class PhraseSuggestion extends Suggest.Suggestion<PhraseSuggestion.Entry> {
public static final String NAME = "phrase";
@Deprecated
public static final int TYPE = 3;
public PhraseSuggestion() {
}
public PhraseSuggestion() {}
public PhraseSuggestion(String name, int size) {
super(name, size);
}
public PhraseSuggestion(StreamInput in) throws IOException {
super(in);
}
@Override
public String getWriteableName() {
return PhraseSuggestionBuilder.SUGGESTION_NAME;
}
@Override
public int getWriteableType() {
return TYPE;
}
@Override
protected String getType() {
return NAME;
protected Entry newEntry() {
return new Entry();
}
@Override
protected Entry newEntry() {
return new Entry();
protected Entry newEntry(StreamInput in) throws IOException {
return new Entry(in);
}
public static PhraseSuggestion fromXContent(XContentParser parser, String name) throws IOException {
@ -75,7 +88,15 @@ public class PhraseSuggestion extends Suggest.Suggestion<PhraseSuggestion.Entry>
this.cutoffScore = cutoffScore;
}
Entry() {
public Entry(Text text, int offset, int length) {
super(text, offset, length);
}
Entry() {}
public Entry(StreamInput in) throws IOException {
super(in);
cutoffScore = in.readDouble();
}
/**
@ -118,9 +139,13 @@ public class PhraseSuggestion extends Suggest.Suggestion<PhraseSuggestion.Entry>
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
cutoffScore = in.readDouble();
protected Option newOption() {
return new Option();
}
@Override
protected Option newOption(StreamInput in) throws IOException {
return new Option(in);
}
@Override
@ -128,5 +153,56 @@ public class PhraseSuggestion extends Suggest.Suggestion<PhraseSuggestion.Entry>
super.writeTo(out);
out.writeDouble(cutoffScore);
}
@Override
public boolean equals(Object other) {
return super.equals(other)
&& Objects.equals(cutoffScore, ((Entry) other).cutoffScore);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), cutoffScore);
}
public static class Option extends Suggestion.Entry.Option {
public Option() {
super();
}
public Option(Text text, Text highlighted, float score, Boolean collateMatch) {
super(text, highlighted, score, collateMatch);
}
public Option(Text text, Text highlighted, float score) {
super(text, highlighted, score);
}
public Option(StreamInput in) throws IOException {
super(in);
}
private static final ConstructingObjectParser<Option, Void> PARSER = new ConstructingObjectParser<>("PhraseOptionParser",
true, args -> {
Text text = new Text((String) args[0]);
float score = (Float) args[1];
String highlighted = (String) args[2];
Text highlightedText = highlighted == null ? null : new Text(highlighted);
Boolean collateMatch = (Boolean) args[3];
return new Option(text, highlightedText, score, collateMatch);
});
static {
PARSER.declareString(constructorArg(), TEXT);
PARSER.declareFloat(constructorArg(), SCORE);
PARSER.declareString(optionalConstructorArg(), HIGHLIGHTED);
PARSER.declareBoolean(optionalConstructorArg(), COLLATE_MATCH);
}
public static Option fromXContent(XContentParser parser) {
return PARSER.apply(parser, null);
}
}
}
}

View File

@ -59,7 +59,7 @@ import java.util.Set;
*/
public class PhraseSuggestionBuilder extends SuggestionBuilder<PhraseSuggestionBuilder> {
private static final String SUGGESTION_NAME = "phrase";
public static final String SUGGESTION_NAME = "phrase";
protected static final ParseField MAXERRORS_FIELD = new ParseField("max_errors");
protected static final ParseField RWE_LIKELIHOOD_FIELD = new ParseField("real_word_error_likelihood");

View File

@ -19,6 +19,7 @@
package org.elasticsearch.search.suggest.term;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.Version;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
@ -28,11 +29,13 @@ import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.search.suggest.SortBy;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.Suggest.Suggestion;
import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry.Option;
import java.io.IOException;
import java.util.Comparator;
import java.util.Objects;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
@ -41,22 +44,29 @@ import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constru
*/
public class TermSuggestion extends Suggestion<TermSuggestion.Entry> {
public static final String NAME = "term";
@Deprecated
public static final int TYPE = 1;
public static final Comparator<Suggestion.Entry.Option> SCORE = new Score();
public static final Comparator<Suggestion.Entry.Option> FREQUENCY = new Frequency();
public static final int TYPE = 1;
private SortBy sort;
public TermSuggestion() {
}
public TermSuggestion() {}
public TermSuggestion(String name, int size, SortBy sort) {
super(name, size);
this.sort = sort;
}
public TermSuggestion(StreamInput in) throws IOException {
super(in);
if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
sort = SortBy.readFromStream(in);
}
}
// Same behaviour as comparators in suggest module, but for SuggestedWord
// Highest score first, then highest freq first, then lowest term first
public static class Score implements Comparator<Suggestion.Entry.Option> {
@ -103,9 +113,12 @@ public class TermSuggestion extends Suggestion<TermSuggestion.Entry> {
return TYPE;
}
@Override
protected String getType() {
return NAME;
public void setSort(SortBy sort) {
this.sort = sort;
}
public SortBy getSort() {
return sort;
}
@Override
@ -121,15 +134,17 @@ public class TermSuggestion extends Suggestion<TermSuggestion.Entry> {
}
@Override
protected void innerReadFrom(StreamInput in) throws IOException {
super.innerReadFrom(in);
sort = SortBy.readFromStream(in);
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
sort.writeTo(out);
}
}
@Override
public void innerWriteTo(StreamOutput out) throws IOException {
super.innerWriteTo(out);
sort.writeTo(out);
public String getWriteableName() {
return TermSuggestionBuilder.SUGGESTION_NAME;
}
public static TermSuggestion fromXContent(XContentParser parser, String name) throws IOException {
@ -144,16 +159,35 @@ public class TermSuggestion extends Suggestion<TermSuggestion.Entry> {
return new Entry();
}
@Override
protected Entry newEntry(StreamInput in) throws IOException {
return new Entry(in);
}
@Override
public boolean equals(Object other) {
return super.equals(other)
&& Objects.equals(sort, ((TermSuggestion) other).sort);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), sort);
}
/**
* Represents a part from the suggest text with suggested options.
*/
public static class Entry extends org.elasticsearch.search.suggest.Suggest.Suggestion.Entry<TermSuggestion.Entry.Option> {
public static class Entry extends Suggest.Suggestion.Entry<TermSuggestion.Entry.Option> {
public Entry(Text text, int offset, int length) {
super(text, offset, length);
}
Entry() {
public Entry() {}
public Entry(StreamInput in) throws IOException {
super(in);
}
@Override
@ -161,6 +195,11 @@ public class TermSuggestion extends Suggestion<TermSuggestion.Entry> {
return new Option();
}
@Override
protected Option newOption(StreamInput in) throws IOException {
return new Option(in);
}
private static ObjectParser<Entry, Void> PARSER = new ObjectParser<>("TermSuggestionEntryParser", true, Entry::new);
static {
@ -175,7 +214,7 @@ public class TermSuggestion extends Suggestion<TermSuggestion.Entry> {
/**
* Contains the suggested text with its document frequency and score.
*/
public static class Option extends org.elasticsearch.search.suggest.Suggest.Suggestion.Entry.Option {
public static class Option extends Suggest.Suggestion.Entry.Option {
public static final ParseField FREQ = new ParseField("freq");
@ -186,6 +225,11 @@ public class TermSuggestion extends Suggestion<TermSuggestion.Entry> {
this.freq = freq;
}
public Option(StreamInput in) throws IOException {
super(in);
freq = in.readVInt();
}
@Override
protected void mergeInto(Suggestion.Entry.Option otherOption) {
super.mergeInto(otherOption);
@ -207,12 +251,6 @@ public class TermSuggestion extends Suggestion<TermSuggestion.Entry> {
return freq;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
freq = in.readVInt();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
@ -220,8 +258,8 @@ public class TermSuggestion extends Suggestion<TermSuggestion.Entry> {
}
@Override
protected XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException {
builder = super.innerToXContent(builder, params);
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder = super.toXContent(builder, params);
builder.field(FREQ.getPreferredName(), freq);
return builder;
}

View File

@ -67,7 +67,7 @@ import static org.elasticsearch.search.suggest.phrase.DirectCandidateGeneratorBu
*/
public class TermSuggestionBuilder extends SuggestionBuilder<TermSuggestionBuilder> {
private static final String SUGGESTION_NAME = "term";
public static final String SUGGESTION_NAME = "term";
private SuggestMode suggestMode = SuggestMode.MISSING;
private float accuracy = DEFAULT_ACCURACY;

View File

@ -18,6 +18,8 @@
*/
package org.elasticsearch.search;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.util.CharsRefBuilder;
import org.elasticsearch.common.inject.ModuleTestCase;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
@ -64,8 +66,11 @@ import org.elasticsearch.search.internal.SearchContext;
import org.elasticsearch.search.rescore.QueryRescorerBuilder;
import org.elasticsearch.search.rescore.RescoreContext;
import org.elasticsearch.search.rescore.RescorerBuilder;
import org.elasticsearch.search.suggest.CustomSuggesterSearchIT.CustomSuggestionBuilder;
import org.elasticsearch.search.suggest.Suggest.Suggestion;
import org.elasticsearch.search.suggest.Suggester;
import org.elasticsearch.search.suggest.SuggestionBuilder;
import org.elasticsearch.search.suggest.SuggestionSearchContext;
import org.elasticsearch.search.suggest.term.TermSuggestion;
import org.elasticsearch.search.suggest.term.TermSuggestionBuilder;
import java.io.IOException;
@ -98,7 +103,8 @@ public class SearchModuleTests extends ModuleTestCase {
SearchPlugin registersDupeSuggester = new SearchPlugin() {
@Override
public List<SearchPlugin.SuggesterSpec<?>> getSuggesters() {
return singletonList(new SuggesterSpec<>("term", TermSuggestionBuilder::new, TermSuggestionBuilder::fromXContent));
return singletonList(new SuggesterSpec<>(TermSuggestionBuilder.SUGGESTION_NAME,
TermSuggestionBuilder::new, TermSuggestionBuilder::fromXContent, TermSuggestion::new));
}
};
expectThrows(IllegalArgumentException.class, registryForPlugin(registersDupeSuggester));
@ -183,9 +189,15 @@ public class SearchModuleTests extends ModuleTestCase {
SearchModule module = new SearchModule(Settings.EMPTY, false, singletonList(new SearchPlugin() {
@Override
public List<SuggesterSpec<?>> getSuggesters() {
return singletonList(new SuggesterSpec<>("custom", CustomSuggestionBuilder::new, CustomSuggestionBuilder::fromXContent));
return singletonList(
new SuggesterSpec<>(
TestSuggestionBuilder.SUGGESTION_NAME,
TestSuggestionBuilder::new,
TestSuggestionBuilder::fromXContent,
TestSuggestion::new));
}
}));
assertEquals(1, module.getNamedXContents().stream()
.filter(e -> e.categoryClass.equals(SuggestionBuilder.class) &&
e.name.match("term", LoggingDeprecationHandler.INSTANCE)).count());
@ -197,7 +209,7 @@ public class SearchModuleTests extends ModuleTestCase {
e.name.match("completion", LoggingDeprecationHandler.INSTANCE)).count());
assertEquals(1, module.getNamedXContents().stream()
.filter(e -> e.categoryClass.equals(SuggestionBuilder.class) &&
e.name.match("custom", LoggingDeprecationHandler.INSTANCE)).count());
e.name.match("test", LoggingDeprecationHandler.INSTANCE)).count());
assertEquals(1, module.getNamedWriteables().stream()
.filter(e -> e.categoryClass.equals(SuggestionBuilder.class) && e.name.equals("term")).count());
@ -206,7 +218,16 @@ public class SearchModuleTests extends ModuleTestCase {
assertEquals(1, module.getNamedWriteables().stream()
.filter(e -> e.categoryClass.equals(SuggestionBuilder.class) && e.name.equals("completion")).count());
assertEquals(1, module.getNamedWriteables().stream()
.filter(e -> e.categoryClass.equals(SuggestionBuilder.class) && e.name.equals("custom")).count());
.filter(e -> e.categoryClass.equals(SuggestionBuilder.class) && e.name.equals("test")).count());
assertEquals(1, module.getNamedWriteables().stream()
.filter(e -> e.categoryClass.equals(Suggestion.class) && e.name.equals("term")).count());
assertEquals(1, module.getNamedWriteables().stream()
.filter(e -> e.categoryClass.equals(Suggestion.class) && e.name.equals("phrase")).count());
assertEquals(1, module.getNamedWriteables().stream()
.filter(e -> e.categoryClass.equals(Suggestion.class) && e.name.equals("completion")).count());
assertEquals(1, module.getNamedWriteables().stream()
.filter(e -> e.categoryClass.equals(Suggestion.class) && e.name.equals("test")).count());
}
public void testRegisterHighlighter() {
@ -498,4 +519,77 @@ public class SearchModuleTests extends ModuleTestCase {
return null;
}
}
private static class TestSuggester extends Suggester<SuggestionSearchContext.SuggestionContext> {
@Override
protected Suggestion<? extends Suggestion.Entry<? extends Suggestion.Entry.Option>> innerExecute(
String name,
SuggestionSearchContext.SuggestionContext suggestion,
IndexSearcher searcher,
CharsRefBuilder spare) throws IOException {
return null;
}
}
private static class TestSuggestionBuilder extends SuggestionBuilder<TestSuggestionBuilder> {
public static final String SUGGESTION_NAME = "test";
TestSuggestionBuilder(StreamInput in) throws IOException {
super(in);
}
@Override
protected void doWriteTo(StreamOutput out) throws IOException {}
public static TestSuggestionBuilder fromXContent(XContentParser parser) {
return null;
}
@Override
protected XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException {
return null;
}
@Override
protected SuggestionSearchContext.SuggestionContext build(QueryShardContext context) throws IOException {
return null;
}
@Override
protected boolean doEquals(TestSuggestionBuilder other) {
return false;
}
@Override
protected int doHashCode() {
return 0;
}
@Override
public String getWriteableName() {
return "test";
}
}
private static class TestSuggestion extends Suggestion {
TestSuggestion(StreamInput in) throws IOException {
super(in);
}
@Override
protected Entry newEntry() {
return null;
}
@Override
protected Entry newEntry(StreamInput in) throws IOException {
return null;
}
@Override
public String getWriteableName() {
return "test";
}
}
}

View File

@ -1,63 +0,0 @@
/*
* 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.apache.lucene.search.IndexSearcher;
import org.apache.lucene.util.CharsRefBuilder;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.QueryShardContext;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
public class CustomSuggester extends Suggester<CustomSuggester.CustomSuggestionsContext> {
public static final CustomSuggester INSTANCE = new CustomSuggester();
// This is a pretty dumb implementation which returns the original text + fieldName + custom config option + 12 or 123
@Override
public Suggest.Suggestion<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>> innerExecute(String name, CustomSuggestionsContext suggestion, IndexSearcher searcher, CharsRefBuilder spare) throws IOException {
// Get the suggestion context
String text = suggestion.getText().utf8ToString();
// create two suggestions with 12 and 123 appended
Suggest.Suggestion<Suggest.Suggestion.Entry<Suggest.Suggestion.Entry.Option>> response = new Suggest.Suggestion<>(name, suggestion.getSize());
String firstSuggestion = String.format(Locale.ROOT, "%s-%s-%s-%s", text, suggestion.getField(), suggestion.options.get("suffix"), "12");
Suggest.Suggestion.Entry<Suggest.Suggestion.Entry.Option> resultEntry12 = new Suggest.Suggestion.Entry<>(new Text(firstSuggestion), 0, text.length() + 2);
response.addTerm(resultEntry12);
String secondSuggestion = String.format(Locale.ROOT, "%s-%s-%s-%s", text, suggestion.getField(), suggestion.options.get("suffix"), "123");
Suggest.Suggestion.Entry<Suggest.Suggestion.Entry.Option> resultEntry123 = new Suggest.Suggestion.Entry<>(new Text(secondSuggestion), 0, text.length() + 3);
response.addTerm(resultEntry123);
return response;
}
public static class CustomSuggestionsContext extends SuggestionSearchContext.SuggestionContext {
public Map<String, Object> options;
public CustomSuggestionsContext(QueryShardContext context, Map<String, Object> options) {
super(new CustomSuggester(), context);
this.options = options;
}
}
}

View File

@ -1,212 +0,0 @@
/*
* 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.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.lucene.BytesRefs;
import org.elasticsearch.common.util.CollectionUtils;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.query.QueryShardContext;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.SearchPlugin;
import org.elasticsearch.search.suggest.SuggestionSearchContext.SuggestionContext;
import org.elasticsearch.test.ESIntegTestCase;
import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
import org.elasticsearch.test.ESIntegTestCase.Scope;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import static java.util.Collections.singletonList;
import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
/**
* Integration test for registering a custom suggester.
*/
@ClusterScope(scope= Scope.SUITE, numDataNodes =1)
public class CustomSuggesterSearchIT extends ESIntegTestCase {
@Override
protected Collection<Class<? extends Plugin>> nodePlugins() {
return Arrays.asList(CustomSuggesterPlugin.class);
}
@Override
protected Collection<Class<? extends Plugin>> transportClientPlugins() {
return Arrays.asList(CustomSuggesterPlugin.class);
}
public static class CustomSuggesterPlugin extends Plugin implements SearchPlugin {
@Override
public List<SuggesterSpec<?>> getSuggesters() {
return singletonList(new SuggesterSpec<CustomSuggestionBuilder>("custom", CustomSuggestionBuilder::new,
CustomSuggestionBuilder::fromXContent));
}
}
public void testThatCustomSuggestersCanBeRegisteredAndWork() throws Exception {
createIndex("test");
client().prepareIndex("test", "test", "1").setSource(jsonBuilder()
.startObject()
.field("name", "arbitrary content")
.endObject())
.setRefreshPolicy(IMMEDIATE).get();
String randomText = randomAlphaOfLength(10);
String randomField = randomAlphaOfLength(10);
String randomSuffix = randomAlphaOfLength(10);
SuggestBuilder suggestBuilder = new SuggestBuilder();
suggestBuilder.addSuggestion("someName", new CustomSuggestionBuilder(randomField, randomSuffix).text(randomText));
SearchRequestBuilder searchRequestBuilder = client().prepareSearch("test").setTypes("test").setFrom(0).setSize(1)
.suggest(suggestBuilder);
SearchResponse searchResponse = searchRequestBuilder.execute().actionGet();
// TODO: infer type once JI-9019884 is fixed
// TODO: see also JDK-8039214
List<Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>> suggestions =
CollectionUtils.<Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>>iterableAsArrayList(
searchResponse.getSuggest().getSuggestion("someName"));
assertThat(suggestions, hasSize(2));
assertThat(suggestions.get(0).getText().string(),
is(String.format(Locale.ROOT, "%s-%s-%s-12", randomText, randomField, randomSuffix)));
assertThat(suggestions.get(1).getText().string(),
is(String.format(Locale.ROOT, "%s-%s-%s-123", randomText, randomField, randomSuffix)));
}
public static class CustomSuggestionBuilder extends SuggestionBuilder<CustomSuggestionBuilder> {
protected static final ParseField RANDOM_SUFFIX_FIELD = new ParseField("suffix");
private String randomSuffix;
public CustomSuggestionBuilder(String randomField, String randomSuffix) {
super(randomField);
this.randomSuffix = randomSuffix;
}
/**
* Read from a stream.
*/
public CustomSuggestionBuilder(StreamInput in) throws IOException {
super(in);
this.randomSuffix = in.readString();
}
@Override
public void doWriteTo(StreamOutput out) throws IOException {
out.writeString(randomSuffix);
}
@Override
protected XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException {
builder.field(RANDOM_SUFFIX_FIELD.getPreferredName(), randomSuffix);
return builder;
}
@Override
public String getWriteableName() {
return "custom";
}
@Override
protected boolean doEquals(CustomSuggestionBuilder other) {
return Objects.equals(randomSuffix, other.randomSuffix);
}
@Override
protected int doHashCode() {
return Objects.hash(randomSuffix);
}
public static CustomSuggestionBuilder fromXContent(XContentParser parser) throws IOException {
XContentParser.Token token;
String currentFieldName = null;
String fieldname = null;
String suffix = null;
String analyzer = null;
int sizeField = -1;
int shardSize = -1;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token.isValue()) {
if (SuggestionBuilder.ANALYZER_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
analyzer = parser.text();
} else if (SuggestionBuilder.FIELDNAME_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
fieldname = parser.text();
} else if (SuggestionBuilder.SIZE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
sizeField = parser.intValue();
} else if (SuggestionBuilder.SHARDSIZE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
shardSize = parser.intValue();
} else if (RANDOM_SUFFIX_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
suffix = parser.text();
}
} else {
throw new ParsingException(parser.getTokenLocation(),
"suggester[custom] doesn't support field [" + currentFieldName + "]");
}
}
// now we should have field name, check and copy fields over to the suggestion builder we return
if (fieldname == null) {
throw new ParsingException(parser.getTokenLocation(), "the required field option is missing");
}
CustomSuggestionBuilder builder = new CustomSuggestionBuilder(fieldname, suffix);
if (analyzer != null) {
builder.analyzer(analyzer);
}
if (sizeField != -1) {
builder.size(sizeField);
}
if (shardSize != -1) {
builder.shardSize(shardSize);
}
return builder;
}
@Override
public SuggestionContext build(QueryShardContext context) throws IOException {
Map<String, Object> options = new HashMap<>();
options.put(FIELDNAME_FIELD.getPreferredName(), field());
options.put(RANDOM_SUFFIX_FIELD.getPreferredName(), randomSuffix);
CustomSuggester.CustomSuggestionsContext customSuggestionsContext =
new CustomSuggester.CustomSuggestionsContext(context, options);
customSuggestionsContext.setField(field());
assert text != null;
customSuggestionsContext.setText(BytesRefs.toBytesRef(text));
return customSuggestionsContext;
}
}
}

View File

@ -19,9 +19,14 @@
package org.elasticsearch.search.suggest;
import org.elasticsearch.Version;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.ToXContent;
@ -30,6 +35,7 @@ import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.rest.action.search.RestSearchAction;
import org.elasticsearch.search.SearchModule;
import org.elasticsearch.search.suggest.Suggest.Suggestion;
import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry;
import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry.Option;
@ -37,6 +43,7 @@ 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 org.elasticsearch.test.VersionUtils;
import java.io.IOException;
import java.util.ArrayList;
@ -44,6 +51,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static java.util.Collections.emptyList;
import static org.elasticsearch.common.xcontent.XContentHelper.toXContent;
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureFieldName;
@ -114,10 +122,11 @@ public class SuggestTests extends ESTestCase {
}
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);
PhraseSuggestion.Entry.Option option = new PhraseSuggestion.Entry.Option(new Text("someText"), new Text("somethingHighlighted"),
1.3f, true);
PhraseSuggestion.Entry entry = new PhraseSuggestion.Entry(new Text("entryText"), 42, 313);
entry.addOption(option);
Suggestion<Entry<Option>> suggestion = new Suggestion<>("suggestionName", 5);
PhraseSuggestion suggestion = new PhraseSuggestion("suggestionName", 5);
suggestion.addTerm(entry);
Suggest suggest = new Suggest(Collections.singletonList(suggestion));
BytesReference xContent = toXContent(suggest, XContentType.JSON, randomBoolean());
@ -196,9 +205,9 @@ public class SuggestTests extends ESTestCase {
String secondWord = randomAlphaOfLength(10);
Text suggestionText = new Text(suggestedWord + " " + secondWord);
Text highlighted = new Text("<em>" + suggestedWord + "</em> " + secondWord);
PhraseSuggestion.Entry.Option option1 = new Option(suggestionText, highlighted, 0.7f, false);
PhraseSuggestion.Entry.Option option2 = new Option(suggestionText, highlighted, 0.8f, true);
PhraseSuggestion.Entry.Option option3 = new Option(suggestionText, highlighted, 0.6f);
PhraseSuggestion.Entry.Option option1 = new PhraseSuggestion.Entry.Option(suggestionText, highlighted, 0.7f, false);
PhraseSuggestion.Entry.Option option2 = new PhraseSuggestion.Entry.Option(suggestionText, highlighted, 0.8f, true);
PhraseSuggestion.Entry.Option option3 = new PhraseSuggestion.Entry.Option(suggestionText, highlighted, 0.6f);
assertEquals(suggestionText, option1.getText());
assertEquals(highlighted, option1.getHighlighted());
assertFalse(option1.collateMatch());
@ -214,4 +223,39 @@ public class SuggestTests extends ESTestCase {
assertTrue(option1.getScore() > 0.7f);
assertTrue(option1.collateMatch());
}
public void testSerialization() throws IOException {
final Version bwcVersion = VersionUtils.randomVersionBetween(random(),
Version.CURRENT.minimumCompatibilityVersion(), Version.CURRENT);
final Suggest suggest = createTestItem();
final Suggest bwcSuggest;
NamedWriteableRegistry registry = new NamedWriteableRegistry
(new SearchModule(Settings.EMPTY, false, emptyList()).getNamedWriteables());
try (BytesStreamOutput out = new BytesStreamOutput()) {
out.setVersion(bwcVersion);
suggest.writeTo(out);
try (NamedWriteableAwareStreamInput in = new NamedWriteableAwareStreamInput(out.bytes().streamInput(), registry)) {
in.setVersion(bwcVersion);
bwcSuggest = new Suggest(in);
}
}
assertEquals(suggest, bwcSuggest);
final Suggest backAgain;
try (BytesStreamOutput out = new BytesStreamOutput()) {
out.setVersion(Version.CURRENT);
bwcSuggest.writeTo(out);
try (NamedWriteableAwareStreamInput in = new NamedWriteableAwareStreamInput(out.bytes().streamInput(), registry)) {
in.setVersion(Version.CURRENT);
backAgain = new Suggest(in);
}
}
assertEquals(suggest, backAgain);
}
}

View File

@ -129,8 +129,9 @@ public class SuggestionEntryTests extends ESTestCase {
}
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);
PhraseSuggestion.Entry.Option option = new PhraseSuggestion.Entry.Option(new Text("someText"), new Text("somethingHighlighted"),
1.3f, true);
PhraseSuggestion.Entry entry = new PhraseSuggestion.Entry(new Text("entryText"), 42, 313);
entry.addOption(option);
BytesReference xContent = toXContent(entry, XContentType.JSON, randomBoolean());
assertEquals(
@ -146,7 +147,7 @@ public class SuggestionEntryTests extends ESTestCase {
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 = new PhraseSuggestion.Entry(new Text("entryText"), 42, 313);
entry.addOption(termOption);
xContent = toXContent(entry, XContentType.JSON, randomBoolean());
assertEquals(
@ -162,7 +163,7 @@ public class SuggestionEntryTests extends ESTestCase {
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 = new PhraseSuggestion.Entry(new Text("entryText"), 42, 313);
entry.addOption(completionOption);
xContent = toXContent(entry, XContentType.JSON, randomBoolean());
assertEquals(

View File

@ -25,6 +25,7 @@ import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry.Option;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestion;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
@ -41,7 +42,7 @@ public class SuggestionOptionTests extends ESTestCase {
float score = randomFloat();
Text highlighted = randomFrom((Text) null, new Text(randomAlphaOfLengthBetween(5, 15)));
Boolean collateMatch = randomFrom((Boolean) null, randomBoolean());
return new Option(text, highlighted, score, collateMatch);
return new PhraseSuggestion.Entry.Option(text, highlighted, score, collateMatch);
}
public void testFromXContent() throws IOException {
@ -66,7 +67,7 @@ public class SuggestionOptionTests extends ESTestCase {
Option parsed;
try (XContentParser parser = createParser(xContentType.xContent(), mutated)) {
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation);
parsed = Option.fromXContent(parser);
parsed = PhraseSuggestion.Entry.Option.fromXContent(parser);
assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken());
assertNull(parser.nextToken());
}
@ -78,7 +79,7 @@ public class SuggestionOptionTests extends ESTestCase {
}
public void testToXContent() throws IOException {
Option option = new Option(new Text("someText"), new Text("somethingHighlighted"), 1.3f, true);
Option option = new PhraseSuggestion.Entry.Option(new Text("someText"), new Text("somethingHighlighted"), 1.3f, true);
BytesReference xContent = toXContent(option, XContentType.JSON, randomBoolean());
assertEquals("{\"text\":\"someText\","
+ "\"highlighted\":\"somethingHighlighted\","

View File

@ -188,14 +188,15 @@ public class SuggestionTests extends ESTestCase {
public void testToXContent() throws IOException {
ToXContent.Params params = new ToXContent.MapParams(Collections.singletonMap(RestSearchAction.TYPED_KEYS_PARAM, "true"));
{
Option option = new Option(new Text("someText"), new Text("somethingHighlighted"), 1.3f, true);
Entry<Option> entry = new Entry<>(new Text("entryText"), 42, 313);
PhraseSuggestion.Entry.Option option = new PhraseSuggestion.Entry.Option(new Text("someText"), new Text("somethingHighlighted"),
1.3f, true);
PhraseSuggestion.Entry entry = new PhraseSuggestion.Entry(new Text("entryText"), 42, 313);
entry.addOption(option);
Suggestion<Entry<Option>> suggestion = new Suggestion<>("suggestionName", 5);
PhraseSuggestion suggestion = new PhraseSuggestion("suggestionName", 5);
suggestion.addTerm(entry);
BytesReference xContent = toXContent(suggestion, XContentType.JSON, params, randomBoolean());
assertEquals(
"{\"suggestion#suggestionName\":[{"
"{\"phrase#suggestionName\":[{"
+ "\"text\":\"entryText\","
+ "\"offset\":42,"
+ "\"length\":313,"
@ -208,7 +209,7 @@ public class SuggestionTests extends ESTestCase {
+ "}", xContent.utf8ToString());
}
{
Option option = new Option(new Text("someText"), new Text("somethingHighlighted"), 1.3f, true);
Option option = new PhraseSuggestion.Entry.Option(new Text("someText"), new Text("somethingHighlighted"), 1.3f, true);
PhraseSuggestion.Entry entry = new PhraseSuggestion.Entry(new Text("entryText"), 42, 313, 1.0);
entry.addOption(option);
PhraseSuggestion suggestion = new PhraseSuggestion("suggestionName", 5);