/* * 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.context; import com.google.common.base.Joiner; import com.google.common.collect.Iterables; import org.apache.lucene.analysis.PrefixAnalyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.index.IndexableField; import org.apache.lucene.util.automaton.Automata; import org.apache.lucene.util.automaton.Automaton; import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentParser.Token; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.ParseContext.Document; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; /** * The {@link CategoryContextMapping} is used to define a {@link ContextMapping} that * references a field within a document. The value of the field in turn will be * used to setup the suggestions made by the completion suggester. */ public class CategoryContextMapping extends ContextMapping { protected static final String TYPE = "category"; private static final String FIELD_FIELDNAME = "path"; private static final String DEFAULT_FIELDNAME = "_type"; private static final Iterable<? extends CharSequence> EMPTY_VALUES = Collections.emptyList(); private final String fieldName; private final Iterable<? extends CharSequence> defaultValues; private final FieldConfig defaultConfig; /** * Create a new {@link CategoryContextMapping} with the default field * <code>[_type]</code> */ public CategoryContextMapping(String name) { this(name, DEFAULT_FIELDNAME, EMPTY_VALUES); } /** * Create a new {@link CategoryContextMapping} with the default field * <code>[_type]</code> */ public CategoryContextMapping(String name, String fieldName) { this(name, fieldName, EMPTY_VALUES); } /** * Create a new {@link CategoryContextMapping} with the default field * <code>[_type]</code> */ public CategoryContextMapping(String name, Iterable<? extends CharSequence> defaultValues) { this(name, DEFAULT_FIELDNAME, defaultValues); } /** * Create a new {@link CategoryContextMapping} with the default field * <code>[_type]</code> */ public CategoryContextMapping(String name, String fieldName, Iterable<? extends CharSequence> defaultValues) { super(TYPE, name); this.fieldName = fieldName; this.defaultValues = defaultValues; this.defaultConfig = new FieldConfig(fieldName, defaultValues, null); } /** * Name of the field used by this {@link CategoryContextMapping} */ public String getFieldName() { return fieldName; } public Iterable<? extends CharSequence> getDefaultValues() { return defaultValues; } @Override public FieldConfig defaultConfig() { return defaultConfig; } /** * Load the specification of a {@link CategoryContextMapping} * * @param field * name of the field to use. If <code>null</code> default field * will be used * @return new {@link CategoryContextMapping} */ protected static CategoryContextMapping load(String name, Map<String, Object> config) throws ElasticsearchParseException { CategoryContextMapping.Builder mapping = new CategoryContextMapping.Builder(name); Object fieldName = config.get(FIELD_FIELDNAME); Object defaultValues = config.get(FIELD_MISSING); if (fieldName != null) { mapping.fieldName(fieldName.toString()); config.remove(FIELD_FIELDNAME); } if (defaultValues != null) { if (defaultValues instanceof Iterable) { for (Object value : (Iterable) defaultValues) { mapping.addDefaultValue(value.toString()); } } else { mapping.addDefaultValue(defaultValues.toString()); } config.remove(FIELD_MISSING); } return mapping.build(); } @Override protected XContentBuilder toInnerXContent(XContentBuilder builder, Params params) throws IOException { if (fieldName != null) { builder.field(FIELD_FIELDNAME, fieldName); } builder.startArray(FIELD_MISSING); for (CharSequence value : defaultValues) { builder.value(value); } builder.endArray(); return builder; } @Override public ContextConfig parseContext(ParseContext parseContext, XContentParser parser) throws IOException, ElasticsearchParseException { Token token = parser.currentToken(); if (token == Token.VALUE_NULL) { return new FieldConfig(fieldName, defaultValues, null); } else if (token == Token.VALUE_STRING) { return new FieldConfig(fieldName, null, Collections.singleton(parser.text())); } else if (token == Token.VALUE_NUMBER) { return new FieldConfig(fieldName, null, Collections.singleton(parser.text())); } else if (token == Token.VALUE_BOOLEAN) { return new FieldConfig(fieldName, null, Collections.singleton(parser.text())); } else if (token == Token.START_ARRAY) { ArrayList<String> values = new ArrayList<>(); while((token = parser.nextToken()) != Token.END_ARRAY) { values.add(parser.text()); } if(values.isEmpty()) { throw new ElasticsearchParseException("FieldConfig must contain a least one category"); } return new FieldConfig(fieldName, null, values); } else { throw new ElasticsearchParseException("FieldConfig must be either [null], a string or a list of strings"); } } @Override public FieldQuery parseQuery(String name, XContentParser parser) throws IOException, ElasticsearchParseException { Iterable<? extends CharSequence> values; Token token = parser.currentToken(); if (token == Token.START_ARRAY) { ArrayList<String> list = new ArrayList<>(); while ((token = parser.nextToken()) != Token.END_ARRAY) { list.add(parser.text()); } values = list; } else if (token == Token.VALUE_NULL) { values = defaultValues; } else { values = Collections.singleton(parser.text()); } return new FieldQuery(name, values); } public static FieldQuery query(String name, CharSequence... fieldvalues) { return query(name, Arrays.asList(fieldvalues)); } public static FieldQuery query(String name, Iterable<? extends CharSequence> fieldvalues) { return new FieldQuery(name, fieldvalues); } @Override public boolean equals(Object obj) { if (obj instanceof CategoryContextMapping) { CategoryContextMapping other = (CategoryContextMapping) obj; if (this.fieldName.equals(other.fieldName)) { return Iterables.elementsEqual(this.defaultValues, other.defaultValues); } } return false; } @Override public int hashCode() { int hashCode = fieldName.hashCode(); for (CharSequence seq : defaultValues) { hashCode = 31 * hashCode + seq.hashCode(); } return hashCode; } private static class FieldConfig extends ContextConfig { private final String fieldname; private final Iterable<? extends CharSequence> defaultValues; private final Iterable<? extends CharSequence> values; public FieldConfig(String fieldname, Iterable<? extends CharSequence> defaultValues, Iterable<? extends CharSequence> values) { this.fieldname = fieldname; this.defaultValues = defaultValues; this.values = values; } @Override protected TokenStream wrapTokenStream(Document doc, TokenStream stream) { if (values != null) { return new PrefixAnalyzer.PrefixTokenFilter(stream, ContextMapping.SEPARATOR, values); // if fieldname is default, BUT our default values are set, we take that one } else if ((doc.getFields(fieldname).length == 0 || fieldname.equals(DEFAULT_FIELDNAME)) && defaultValues.iterator().hasNext()) { return new PrefixAnalyzer.PrefixTokenFilter(stream, ContextMapping.SEPARATOR, defaultValues); } else { IndexableField[] fields = doc.getFields(fieldname); ArrayList<CharSequence> values = new ArrayList<>(fields.length); for (int i = 0; i < fields.length; i++) { values.add(fields[i].stringValue()); } return new PrefixAnalyzer.PrefixTokenFilter(stream, ContextMapping.SEPARATOR, values); } } @Override public String toString() { StringBuilder sb = new StringBuilder("FieldConfig(" + fieldname + " = ["); if (this.values != null && this.values.iterator().hasNext()) { sb.append("(").append(Joiner.on(", ").join(this.values.iterator())).append(")"); } if (this.defaultValues != null && this.defaultValues.iterator().hasNext()) { sb.append(" default(").append(Joiner.on(", ").join(this.defaultValues.iterator())).append(")"); } return sb.append("])").toString(); } } private static class FieldQuery extends ContextQuery { private final Iterable<? extends CharSequence> values; public FieldQuery(String name, Iterable<? extends CharSequence> values) { super(name); this.values = values; } @Override public Automaton toAutomaton() { List<Automaton> automatons = new ArrayList<>(); for (CharSequence value : values) { automatons.add(Automata.makeString(value.toString())); } return Operations.union(automatons); } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startArray(name); for (CharSequence value : values) { builder.value(value); } builder.endArray(); return builder; } } public static class Builder extends ContextBuilder<CategoryContextMapping> { private String fieldname; private List<CharSequence> defaultValues = new ArrayList<>(); public Builder(String name) { this(name, DEFAULT_FIELDNAME); } public Builder(String name, String fieldname) { super(name); this.fieldname = fieldname; } /** * Set the name of the field to use */ public Builder fieldName(String fieldname) { this.fieldname = fieldname; return this; } /** * Add value to the default values of the mapping */ public Builder addDefaultValue(CharSequence defaultValue) { this.defaultValues.add(defaultValue); return this; } /** * Add set of default values to the mapping */ public Builder addDefaultValues(CharSequence... defaultValues) { for (CharSequence defaultValue : defaultValues) { this.defaultValues.add(defaultValue); } return this; } /** * Add set of default values to the mapping */ public Builder addDefaultValues(Iterable<? extends CharSequence> defaultValues) { for (CharSequence defaultValue : defaultValues) { this.defaultValues.add(defaultValue); } return this; } @Override public CategoryContextMapping build() { return new CategoryContextMapping(name, fieldname, defaultValues); } } }