/*
* Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership. Crate 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.
*
* However, if you have executed another commercial license agreement
* with Crate these terms will supersede the license and you may use the
* software solely pursuant to the terms of the relevant commercial agreement.
*/
package io.crate.lucene.match;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import io.crate.types.BooleanType;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.ParseFieldMatcher;
import org.elasticsearch.common.lucene.BytesRefs;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.support.QueryParsers;
import org.elasticsearch.index.search.MatchQuery;
import javax.annotation.Nullable;
import java.util.*;
public class OptionParser {
private static class OPTIONS {
static final String ANALYZER = "analyzer";
static final String BOOST = "boost";
static final String OPERATOR = "operator";
static final String CUTOFF_FREQUENCY = "cutoff_frequency";
static final String MINIMUM_SHOULD_MATCH = "minimum_should_match";
static final String FUZZINESS = "fuzziness";
static final String PREFIX_LENGTH = "prefix_length";
static final String MAX_EXPANSIONS = "max_expansions";
static final String REWRITE = "rewrite";
static final String SLOP = "slop";
static final String TIE_BREAKER = "tie_breaker";
static final String ZERO_TERMS_QUERY = "zero_terms_query";
static final String FUZZY_REWRITE = "fuzzy_rewrite";
static final String FUZZY_TRANSPOSITIONS = "fuzzy_transpositions";
}
private static final ImmutableSet<String> SUPPORTED_OPTIONS = ImmutableSet.<String>builder().add(
OPTIONS.ANALYZER, OPTIONS.BOOST, OPTIONS.OPERATOR, OPTIONS.CUTOFF_FREQUENCY,
OPTIONS.MINIMUM_SHOULD_MATCH, OPTIONS.FUZZINESS, OPTIONS.PREFIX_LENGTH,
OPTIONS.MAX_EXPANSIONS, OPTIONS.REWRITE, OPTIONS.SLOP, OPTIONS.TIE_BREAKER,
OPTIONS.ZERO_TERMS_QUERY, OPTIONS.FUZZY_REWRITE, OPTIONS.FUZZY_TRANSPOSITIONS
).build();
public static ParsedOptions parse(MultiMatchQueryBuilder.Type matchType,
@Nullable Map options) throws IllegalArgumentException {
if (options == null) {
options = Collections.emptyMap();
} else {
// need a copy. Otherwise manipulations on a shared option will lead to strange race conditions.
options = new HashMap(options);
}
ParsedOptions parsedOptions = new ParsedOptions(
floatValue(options, OPTIONS.BOOST, null),
analyzer(options.remove(OPTIONS.ANALYZER)),
zeroTermsQuery(options.remove(OPTIONS.ZERO_TERMS_QUERY)),
intValue(options, OPTIONS.MAX_EXPANSIONS, FuzzyQuery.defaultMaxExpansions),
fuzziness(options.remove(OPTIONS.FUZZINESS)),
intValue(options, OPTIONS.PREFIX_LENGTH, FuzzyQuery.defaultPrefixLength),
transpositions(options.remove(OPTIONS.FUZZY_TRANSPOSITIONS))
);
switch (matchType.matchQueryType()) {
case BOOLEAN:
parsedOptions.commonTermsCutoff(floatValue(options, OPTIONS.CUTOFF_FREQUENCY, null));
parsedOptions.operator(operator(options.remove(OPTIONS.OPERATOR)));
parsedOptions.minimumShouldMatch(minimumShouldMatch(options.remove(OPTIONS.MINIMUM_SHOULD_MATCH)));
break;
case PHRASE:
parsedOptions.phraseSlop(intValue(options, OPTIONS.SLOP, 0));
parsedOptions.tieBreaker(floatValue(options, OPTIONS.TIE_BREAKER, null));
break;
case PHRASE_PREFIX:
parsedOptions.phraseSlop(intValue(options, OPTIONS.SLOP, 0));
parsedOptions.tieBreaker(floatValue(options, OPTIONS.TIE_BREAKER, null));
parsedOptions.rewrite(rewrite(options.remove(OPTIONS.REWRITE)));
break;
}
if (!options.isEmpty()) {
raiseIllegalOptions(matchType, options);
}
return parsedOptions;
}
private static Boolean transpositions(@Nullable Object transpositions) {
if (transpositions == null) {
return false;
}
return BooleanType.INSTANCE.value(transpositions);
}
@Nullable
private static String minimumShouldMatch(@Nullable Object minimumShouldMatch) {
return BytesRefs.toString(minimumShouldMatch);
}
private static BooleanClause.Occur operator(@Nullable Object operator) {
if (operator == null) {
return BooleanClause.Occur.SHOULD;
}
String op = BytesRefs.toString(operator);
if ("or".equalsIgnoreCase(op)) {
return BooleanClause.Occur.SHOULD;
} else if ("and".equalsIgnoreCase(op)) {
return BooleanClause.Occur.MUST;
}
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"value for operator must be either \"or\" or \"and\" not \"%s\"", op));
}
private static Float floatValue(Map options, String optionName, Float defaultValue) {
Object o = options.remove(optionName);
if (o == null) {
return defaultValue;
} else if (o instanceof Float) {
return (Float) o;
} else if (o instanceof Number) {
return ((Number) o).floatValue();
}
throw new IllegalArgumentException(String.format(Locale.ENGLISH, "value for %s must be a number", optionName));
}
private static Integer intValue(Map options, String optionName, Integer defaultValue) {
Object o = options.remove(optionName);
if (o == null) {
return defaultValue;
} else if (o instanceof Number) {
return ((Number) o).intValue();
}
throw new IllegalArgumentException(String.format(Locale.ENGLISH, "value for %s must be a number", optionName));
}
private static org.apache.lucene.search.MultiTermQuery.RewriteMethod rewrite(@Nullable Object fuzzyRewrite) {
String rewrite = BytesRefs.toString(fuzzyRewrite);
return QueryParsers.parseRewriteMethod(ParseFieldMatcher.STRICT, rewrite);
}
@Nullable
private static Fuzziness fuzziness(@Nullable Object fuzziness) {
if (fuzziness == null) {
return null;
}
return Fuzziness.build(BytesRefs.toString(fuzziness));
}
private static MatchQuery.ZeroTermsQuery zeroTermsQuery(@Nullable Object zeroTermsQuery) {
String value = BytesRefs.toString(zeroTermsQuery);
if (value == null || "none".equalsIgnoreCase(value)) {
return MatchQuery.ZeroTermsQuery.NONE;
} else if ("all".equalsIgnoreCase(value)) {
return MatchQuery.ZeroTermsQuery.ALL;
}
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"Unsupported value for %s option. Valid are \"none\" and \"all\"", OPTIONS.ZERO_TERMS_QUERY));
}
@Nullable
private static String analyzer(@Nullable Object analyzer) {
if (analyzer == null) {
return null;
}
if (analyzer instanceof String || analyzer instanceof BytesRef) {
return BytesRefs.toString(analyzer);
}
throw new IllegalArgumentException("value for analyzer must be a string");
}
private static void raiseIllegalOptions(MultiMatchQueryBuilder.Type matchType, Map options) {
List<String> unknownOptions = new ArrayList<>();
List<String> invalidOptions = new ArrayList<>();
for (Object o : options.keySet()) {
assert o instanceof String : "option must be String";
if (!SUPPORTED_OPTIONS.contains(o)) {
unknownOptions.add((String) o);
} else {
invalidOptions.add((String) o);
}
}
if (!unknownOptions.isEmpty()) {
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"match predicate doesn't support any of the given options: %s",
Joiner.on(", ").join(unknownOptions)));
} else {
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"match predicate option(s) \"%s\" cannot be used with matchType \"%s\"",
Joiner.on(", ").join(invalidOptions),
matchType.name().toLowerCase(Locale.ENGLISH)
));
}
}
}