Merge pull request #16505 from cbuescher/suggestBuilder-refactoring

Add fromXContent method to SuggestBuilder
This commit is contained in:
Christoph Büscher 2016-02-09 17:57:20 +01:00
commit 16d01088de
6 changed files with 293 additions and 5 deletions

View File

@ -20,6 +20,7 @@
package org.elasticsearch.search;
import com.carrotsearch.hppc.ObjectFloatHashMap;
import org.apache.lucene.search.FieldDoc;
import org.apache.lucene.search.TopDocs;
import org.elasticsearch.ExceptionsHelper;

View File

@ -20,10 +20,15 @@ package org.elasticsearch.search.suggest;
import org.elasticsearch.action.support.ToXContentToBytes;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParseFieldMatcher;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.query.QueryParseContext;
import java.io.IOException;
import java.util.ArrayList;
@ -39,6 +44,9 @@ import java.util.Objects;
*/
public class SuggestBuilder extends ToXContentToBytes implements Writeable<SuggestBuilder> {
public static final SuggestBuilder PROTOTYPE = new SuggestBuilder();
protected static final ParseField GLOBAL_TEXT_FIELD = new ParseField("text");
private String globalText;
private final List<SuggestionBuilder<?>> suggestions = new ArrayList<>();
@ -58,7 +66,14 @@ public class SuggestBuilder extends ToXContentToBytes implements Writeable<Sugge
}
/**
* Adds an {@link org.elasticsearch.search.suggest.term.TermSuggestionBuilder} instance under a user defined name.
* Gets the global suggest text
*/
public String getText() {
return null;
}
/**
* Adds an {@link org.elasticsearch.search.suggest.SuggestionBuilder} instance under a user defined name.
* The order in which the <code>Suggestions</code> are added, is the same as in the response.
*/
public SuggestBuilder addSuggestion(SuggestionBuilder<?> suggestion) {
@ -66,6 +81,13 @@ public class SuggestBuilder extends ToXContentToBytes implements Writeable<Sugge
return this;
}
/**
* Get the <code>Suggestions</code> that were added to the globat {@link SuggestBuilder}
*/
public List<SuggestionBuilder<?>> getSuggestions() {
return suggestions;
}
/**
* Returns all suggestions with the defined names.
*/
@ -86,6 +108,35 @@ public class SuggestBuilder extends ToXContentToBytes implements Writeable<Sugge
return builder;
}
public static SuggestBuilder fromXContent(QueryParseContext parseContext, Suggesters suggesters) throws IOException {
XContentParser parser = parseContext.parser();
ParseFieldMatcher parseFieldMatcher = parseContext.parseFieldMatcher();
SuggestBuilder suggestBuilder = new SuggestBuilder();
String fieldName = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
fieldName = parser.currentName();
} else if (token.isValue()) {
if (parseFieldMatcher.match(fieldName, GLOBAL_TEXT_FIELD)) {
suggestBuilder.setText(parser.text());
} else {
throw new IllegalArgumentException("[suggest] does not support [" + fieldName + "]");
}
} else if (token == XContentParser.Token.START_OBJECT) {
String suggestionName = fieldName;
if (suggestionName == null) {
throw new IllegalArgumentException("Suggestion must have name");
}
suggestBuilder.addSuggestion(SuggestionBuilder.fromXContent(parseContext, suggestionName, suggesters));
} else {
throw new ParsingException(parser.getTokenLocation(), "unexpected token [" + token + "] after [" + fieldName + "]");
}
}
return suggestBuilder;
}
@Override
public SuggestBuilder readFrom(StreamInput in) throws IOException {
final SuggestBuilder builder = new SuggestBuilder();
@ -125,5 +176,4 @@ public class SuggestBuilder extends ToXContentToBytes implements Writeable<Sugge
public int hashCode() {
return Objects.hash(globalText, suggestions);
}
}

View File

@ -414,7 +414,7 @@ public final class PhraseSuggestionBuilder extends SuggestionBuilder<PhraseSugge
* Default discount parameter for {@link StupidBackoff} smoothing
*/
public static final double DEFAULT_BACKOFF_DISCOUNT = 0.4;
static final StupidBackoff PROTOTYPE = new StupidBackoff(DEFAULT_BACKOFF_DISCOUNT);
public static final StupidBackoff PROTOTYPE = new StupidBackoff(DEFAULT_BACKOFF_DISCOUNT);
private double discount = DEFAULT_BACKOFF_DISCOUNT;
private static final String NAME = "stupid_backoff";
private static final ParseField DISCOUNT_FIELD = new ParseField("discount");
@ -511,7 +511,7 @@ public final class PhraseSuggestionBuilder extends SuggestionBuilder<PhraseSugge
* Default alpha parameter for laplace smoothing
*/
public static final double DEFAULT_LAPLACE_ALPHA = 0.5;
static final Laplace PROTOTYPE = new Laplace(DEFAULT_LAPLACE_ALPHA);
public static final Laplace PROTOTYPE = new Laplace(DEFAULT_LAPLACE_ALPHA);
/**
* Creates a Laplace smoothing model.
@ -656,7 +656,7 @@ public final class PhraseSuggestionBuilder extends SuggestionBuilder<PhraseSugge
*/
public static final class LinearInterpolation extends SmoothingModel {
private static final String NAME = "linear";
static final LinearInterpolation PROTOTYPE = new LinearInterpolation(0.8, 0.1, 0.1);
public static final LinearInterpolation PROTOTYPE = new LinearInterpolation(0.8, 0.1, 0.1);
private final double trigramLambda;
private final double bigramLambda;
private final double unigramLambda;

View File

@ -0,0 +1,118 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.suggest;
import org.elasticsearch.common.ParseFieldMatcher;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryParseContext;
import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder;
import org.elasticsearch.search.suggest.completion.WritableTestCase;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder.Laplace;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder.LinearInterpolation;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder.SmoothingModel;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder.StupidBackoff;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilderTests;
import org.elasticsearch.search.suggest.term.TermSuggestionBuilder;
import java.io.IOException;
import java.util.Collections;
public class SuggestBuilderTests extends WritableTestCase<SuggestBuilder> {
@Override
protected NamedWriteableRegistry provideNamedWritbaleRegistry() {
NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry();
namedWriteableRegistry.registerPrototype(SuggestionBuilder.class, TermSuggestionBuilder.PROTOTYPE);
namedWriteableRegistry.registerPrototype(SuggestionBuilder.class, PhraseSuggestionBuilder.PROTOTYPE);
namedWriteableRegistry.registerPrototype(SuggestionBuilder.class, CompletionSuggestionBuilder.PROTOTYPE);
namedWriteableRegistry.registerPrototype(SmoothingModel.class, Laplace.PROTOTYPE);
namedWriteableRegistry.registerPrototype(SmoothingModel.class, LinearInterpolation.PROTOTYPE);
namedWriteableRegistry.registerPrototype(SmoothingModel.class, StupidBackoff.PROTOTYPE);
return namedWriteableRegistry;
}
/**
* creates random suggestion builder, renders it to xContent and back to new instance that should be equal to original
*/
public void testFromXContent() throws IOException {
Suggesters suggesters = new Suggesters(Collections.emptyMap(), null, null);
QueryParseContext context = new QueryParseContext(null);
context.parseFieldMatcher(new ParseFieldMatcher(Settings.EMPTY));
for (int runs = 0; runs < NUMBER_OF_RUNS; runs++) {
SuggestBuilder suggestBuilder = createTestModel();
XContentBuilder xContentBuilder = XContentFactory.contentBuilder(randomFrom(XContentType.values()));
if (randomBoolean()) {
xContentBuilder.prettyPrint();
}
suggestBuilder.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS);
XContentParser parser = XContentHelper.createParser(xContentBuilder.bytes());
context.reset(parser);
parser.nextToken();
SuggestBuilder secondSuggestBuilder = SuggestBuilder.fromXContent(context, suggesters);
assertNotSame(suggestBuilder, secondSuggestBuilder);
assertEquals(suggestBuilder, secondSuggestBuilder);
assertEquals(suggestBuilder.hashCode(), secondSuggestBuilder.hashCode());
}
}
@Override
protected SuggestBuilder createTestModel() {
SuggestBuilder suggestBuilder = new SuggestBuilder();
if (randomBoolean()) {
suggestBuilder.setText(randomAsciiOfLengthBetween(5, 50));
}
int numberOfSuggestions = randomIntBetween(0, 5);
for (int i = 0; i < numberOfSuggestions; i++) {
suggestBuilder.addSuggestion(PhraseSuggestionBuilderTests.randomPhraseSuggestionBuilder());
}
return suggestBuilder;
}
@Override
protected SuggestBuilder createMutation(SuggestBuilder original) throws IOException {
SuggestBuilder mutation = new SuggestBuilder().setText(original.getText());
for (SuggestionBuilder<?> suggestionBuilder : original.getSuggestions()) {
mutation.addSuggestion(suggestionBuilder);
}
if (randomBoolean()) {
mutation.setText(randomAsciiOfLengthBetween(5, 60));
} else {
mutation.addSuggestion(PhraseSuggestionBuilderTests.randomPhraseSuggestionBuilder());
}
return mutation;
}
@Override
protected SuggestBuilder readFrom(StreamInput in) throws IOException {
return SuggestBuilder.PROTOTYPE.readFrom(in);
}
}

View File

@ -0,0 +1,115 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.suggest.completion;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
/**
* Base class for testing serialization and equality for
* {@link Writeable} models
*/
public abstract class WritableTestCase<M extends Writeable> extends ESTestCase {
protected static final int NUMBER_OF_RUNS = 20;
/**
* create random model that is put under test
*/
protected abstract M createTestModel();
/**
* mutate the given model so the returned model is different
*/
protected abstract M createMutation(M original) throws IOException;
/**
* model prototype to read serialized format
*/
protected abstract M readFrom(StreamInput in) throws IOException;
/**
* Test serialization and deserialization of the tested model.
*/
public void testSerialization() throws IOException {
for (int i = 0; i < NUMBER_OF_RUNS; i++) {
M testModel = createTestModel();
M deserializedModel = copyModel(testModel);
assertEquals(testModel, deserializedModel);
assertEquals(testModel.hashCode(), deserializedModel.hashCode());
assertNotSame(testModel, deserializedModel);
}
}
/**
* Test equality and hashCode properties
*/
@SuppressWarnings("unchecked")
public void testEqualsAndHashcode() throws IOException {
M firstModel = createTestModel();
String modelName = firstModel.getClass().getSimpleName();
assertFalse(modelName + " is equal to null", firstModel.equals(null));
assertFalse(modelName + " is equal to incompatible type", firstModel.equals(""));
assertTrue(modelName + " is not equal to self", firstModel.equals(firstModel));
assertThat("same "+ modelName + "'s hashcode returns different values if called multiple times", firstModel.hashCode(),
equalTo(firstModel.hashCode()));
assertThat("different " + modelName + " should not be equal", createMutation(firstModel), not(equalTo(firstModel)));
M secondModel = copyModel(firstModel);
assertTrue(modelName + " is not equal to self", secondModel.equals(secondModel));
assertTrue(modelName + " is not equal to its copy", firstModel.equals(secondModel));
assertTrue("equals is not symmetric", secondModel.equals(firstModel));
assertThat(modelName + " copy's hashcode is different from original hashcode", secondModel.hashCode(),
equalTo(firstModel.hashCode()));
M thirdModel = copyModel(secondModel);
assertTrue(modelName + " is not equal to self", thirdModel.equals(thirdModel));
assertTrue(modelName + " is not equal to its copy", secondModel.equals(thirdModel));
assertThat(modelName + " copy's hashcode is different from original hashcode", secondModel.hashCode(),
equalTo(thirdModel.hashCode()));
assertTrue("equals is not transitive", firstModel.equals(thirdModel));
assertThat(modelName + " copy's hashcode is different from original hashcode", firstModel.hashCode(),
equalTo(thirdModel.hashCode()));
assertTrue(modelName + " equals is not symmetric", thirdModel.equals(secondModel));
assertTrue(modelName + " equals is not symmetric", thirdModel.equals(firstModel));
}
private M copyModel(M original) throws IOException {
try (BytesStreamOutput output = new BytesStreamOutput()) {
original.writeTo(output);
try (StreamInput in = new NamedWriteableAwareStreamInput(StreamInput.wrap(output.bytes()), provideNamedWritbaleRegistry())) {
return readFrom(in);
}
}
}
protected NamedWriteableRegistry provideNamedWritbaleRegistry() {
return new NamedWriteableRegistry();
}
}

View File

@ -42,6 +42,10 @@ public class PhraseSuggestionBuilderTests extends AbstractSuggestionBuilderTestC
@Override
protected PhraseSuggestionBuilder randomSuggestionBuilder() {
return randomPhraseSuggestionBuilder();
}
public static PhraseSuggestionBuilder randomPhraseSuggestionBuilder() {
PhraseSuggestionBuilder testBuilder = new PhraseSuggestionBuilder(randomAsciiOfLength(10));
maybeSet(testBuilder::maxErrors, randomFloat());
maybeSet(testBuilder::separator, randomAsciiOfLengthBetween(1, 10));