/*
* 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.phrase;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionContext.DirectCandidateGenerator;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashCode;
public class DirectCandidateGeneratorTests extends ESTestCase {
private static final int NUMBER_OF_RUNS = 20;
/**
* Test serialization and deserialization of the generator
*/
public void testSerialization() throws IOException {
for (int runs = 0; runs < NUMBER_OF_RUNS; runs++) {
DirectCandidateGeneratorBuilder original = randomCandidateGenerator();
DirectCandidateGeneratorBuilder deserialized = copy(original);
assertEquals(deserialized, original);
assertEquals(deserialized.hashCode(), original.hashCode());
assertNotSame(deserialized, original);
}
}
/**
* Test equality and hashCode properties
*/
public void testEqualsAndHashcode() throws IOException {
for (int runs = 0; runs < NUMBER_OF_RUNS; runs++) {
final DirectCandidateGeneratorBuilder original = randomCandidateGenerator();
checkEqualsAndHashCode(original, DirectCandidateGeneratorTests::copy, DirectCandidateGeneratorTests::mutate);
}
}
private static DirectCandidateGeneratorBuilder mutate(DirectCandidateGeneratorBuilder original) throws IOException {
DirectCandidateGeneratorBuilder mutation = copy(original);
List<Supplier<DirectCandidateGeneratorBuilder>> mutators = new ArrayList<>();
mutators.add(() -> new DirectCandidateGeneratorBuilder(original.field() + "_other"));
mutators.add(() -> mutation.accuracy(original.accuracy() == null ? 0.1f : original.accuracy() + 0.1f));
mutators.add(() -> {
Integer maxEdits = original.maxEdits() == null ? 1 : original.maxEdits();
if (maxEdits == 1) {
maxEdits = 2;
} else {
maxEdits = 1;
}
return mutation.maxEdits(maxEdits);
});
mutators.add(() -> mutation.maxInspections(original.maxInspections() == null ? 1 : original.maxInspections() + 1));
mutators.add(() -> mutation.minWordLength(original.minWordLength() == null ? 1 : original.minWordLength() + 1));
mutators.add(() -> mutation.prefixLength(original.prefixLength() == null ? 1 : original.prefixLength() + 1));
mutators.add(() -> mutation.size(original.size() == null ? 1 : original.size() + 1));
mutators.add(() -> mutation.maxTermFreq(original.maxTermFreq() == null ? 0.1f : original.maxTermFreq() + 0.1f));
mutators.add(() -> mutation.minDocFreq(original.minDocFreq() == null ? 0.1f : original.minDocFreq() + 0.1f));
mutators.add(() -> mutation.postFilter(original.postFilter() == null ? "postFilter" : original.postFilter() + "_other"));
mutators.add(() -> mutation.preFilter(original.preFilter() == null ? "preFilter" : original.preFilter() + "_other"));
mutators.add(() -> mutation.sort(original.sort() == null ? "score" : original.sort() + "_other"));
mutators.add(
() -> mutation.stringDistance(original.stringDistance() == null ? "levenstein" : original.stringDistance() + "_other"));
mutators.add(() -> mutation.suggestMode(original.suggestMode() == null ? "missing" : original.suggestMode() + "_other"));
return randomFrom(mutators).get();
}
/**
* creates random candidate generator, renders it to xContent and back to new instance that should be equal to original
*/
public void testFromXContent() throws IOException {
for (int runs = 0; runs < NUMBER_OF_RUNS; runs++) {
DirectCandidateGeneratorBuilder generator = randomCandidateGenerator();
XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values()));
if (randomBoolean()) {
builder.prettyPrint();
}
generator.toXContent(builder, ToXContent.EMPTY_PARAMS);
XContentParser parser = createParser(shuffleXContent(builder));
parser.nextToken();
DirectCandidateGeneratorBuilder secondGenerator = DirectCandidateGeneratorBuilder.PARSER.apply(parser, null);
assertNotSame(generator, secondGenerator);
assertEquals(generator, secondGenerator);
assertEquals(generator.hashCode(), secondGenerator.hashCode());
}
}
public static void assertEqualGenerators(DirectCandidateGenerator first, DirectCandidateGenerator second) {
assertEquals(first.field(), second.field());
assertEquals(first.accuracy(), second.accuracy(), Float.MIN_VALUE);
assertEquals(first.maxTermFreq(), second.maxTermFreq(), Float.MIN_VALUE);
assertEquals(first.maxEdits(), second.maxEdits());
assertEquals(first.maxInspections(), second.maxInspections());
assertEquals(first.minDocFreq(), second.minDocFreq(), Float.MIN_VALUE);
assertEquals(first.minWordLength(), second.minWordLength());
assertEquals(first.postFilter(), second.postFilter());
assertEquals(first.prefixLength(), second.prefixLength());
assertEquals(first.preFilter(), second.preFilter());
assertEquals(first.sort(), second.sort());
assertEquals(first.size(), second.size());
// some instances of StringDistance don't support equals, just checking the class here
assertEquals(first.stringDistance().getClass(), second.stringDistance().getClass());
assertEquals(first.suggestMode(), second.suggestMode());
}
/**
* test that bad xContent throws exception
*/
public void testIllegalXContent() throws IOException {
// test missing fieldname
String directGenerator = "{ }";
assertIllegalXContent(directGenerator, IllegalArgumentException.class,
"Required [field]");
// test two fieldnames
if (XContent.isStrictDuplicateDetectionEnabled()) {
logger.info("Skipping test as it uses a custom duplicate check that is obsolete when strict duplicate checks are enabled.");
} else {
directGenerator = "{ \"field\" : \"f1\", \"field\" : \"f2\" }";
assertIllegalXContent(directGenerator, ParsingException.class,
"[direct_generator] failed to parse field [field]");
}
// test unknown field
directGenerator = "{ \"unknown_param\" : \"f1\" }";
assertIllegalXContent(directGenerator, IllegalArgumentException.class,
"[direct_generator] unknown field [unknown_param], parser not found");
// test bad value for field (e.g. size expects an int)
directGenerator = "{ \"size\" : \"xxl\" }";
assertIllegalXContent(directGenerator, ParsingException.class,
"[direct_generator] failed to parse field [size]");
// test unexpected token
directGenerator = "{ \"size\" : [ \"xxl\" ] }";
assertIllegalXContent(directGenerator, IllegalArgumentException.class,
"[direct_generator] size doesn't support values of type: START_ARRAY");
}
private void assertIllegalXContent(String directGenerator, Class<? extends Exception> exceptionClass, String exceptionMsg)
throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, directGenerator);
Exception e = expectThrows(exceptionClass, () -> DirectCandidateGeneratorBuilder.PARSER.apply(parser, null));
assertEquals(exceptionMsg, e.getMessage());
}
/**
* create random {@link DirectCandidateGeneratorBuilder}
*/
public static DirectCandidateGeneratorBuilder randomCandidateGenerator() {
DirectCandidateGeneratorBuilder generator = new DirectCandidateGeneratorBuilder(randomAlphaOfLength(10));
maybeSet(generator::accuracy, randomFloat());
maybeSet(generator::maxEdits, randomIntBetween(1, 2));
maybeSet(generator::maxInspections, randomIntBetween(1, 20));
maybeSet(generator::maxTermFreq, randomFloat());
maybeSet(generator::minDocFreq, randomFloat());
maybeSet(generator::minWordLength, randomIntBetween(1, 20));
maybeSet(generator::prefixLength, randomIntBetween(1, 20));
maybeSet(generator::preFilter, randomAlphaOfLengthBetween(1, 20));
maybeSet(generator::postFilter, randomAlphaOfLengthBetween(1, 20));
maybeSet(generator::size, randomIntBetween(1, 20));
maybeSet(generator::sort, randomFrom("score", "frequency"));
maybeSet(generator::stringDistance, randomFrom("internal", "damerau_levenshtein", "levenstein", "jarowinkler", "ngram"));
maybeSet(generator::suggestMode, randomFrom("missing", "popular", "always"));
return generator;
}
private static DirectCandidateGeneratorBuilder copy(DirectCandidateGeneratorBuilder original) throws IOException {
return copyWriteable(original, new NamedWriteableRegistry(Collections.emptyList()), DirectCandidateGeneratorBuilder::new);
}
}