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:
parent
0b7fb4e7b9
commit
8bfb0f3f8d
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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]
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 } }
|
|
@ -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 }
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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\","
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue