/*
* 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.context;
import org.apache.lucene.index.IndexableField;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.Version;
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 org.elasticsearch.index.query.QueryParseContext;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A {@link ContextMapping} that uses a simple string as a criteria
* The suggestions are boosted and/or filtered by their associated
* category (string) value.
* {@link CategoryQueryContext} defines options for constructing
* a unit of query context for this context type
*/
public class CategoryContextMapping extends ContextMapping<CategoryQueryContext> {
private static final String FIELD_FIELDNAME = "path";
static final String CONTEXT_VALUE = "context";
static final String CONTEXT_BOOST = "boost";
static final String CONTEXT_PREFIX = "prefix";
private final String fieldName;
/**
* Create a new {@link CategoryContextMapping} with field
* <code>fieldName</code>
*/
private CategoryContextMapping(String name, String fieldName) {
super(Type.CATEGORY, name);
this.fieldName = fieldName;
}
/**
* Name of the field to get contexts from at index-time
*/
public String getFieldName() {
return fieldName;
}
/**
* Loads a <code>name</code>d {@link CategoryContextMapping} instance
* from a map.
* see {@link ContextMappings#load(Object, Version)}
*
* Acceptable map param: <code>path</code>
*/
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);
if (fieldName != null) {
mapping.field(fieldName.toString());
config.remove(FIELD_FIELDNAME);
}
return mapping.build();
}
@Override
protected XContentBuilder toInnerXContent(XContentBuilder builder, Params params) throws IOException {
if (fieldName != null) {
builder.field(FIELD_FIELDNAME, fieldName);
}
return builder;
}
/**
* Parse a set of {@link CharSequence} contexts at index-time.
* Acceptable formats:
*
* <ul>
* <li>Array: <pre>[<i><string></i>, ..]</pre></li>
* <li>String: <pre>"string"</pre></li>
* </ul>
*/
@Override
public Set<CharSequence> parseContext(ParseContext parseContext, XContentParser parser)
throws IOException, ElasticsearchParseException {
final Set<CharSequence> contexts = new HashSet<>();
Token token = parser.currentToken();
if (token == Token.VALUE_STRING || token == Token.VALUE_NUMBER || token == Token.VALUE_BOOLEAN) {
contexts.add(parser.text());
} else if (token == Token.START_ARRAY) {
while ((token = parser.nextToken()) != Token.END_ARRAY) {
if (token == Token.VALUE_STRING || token == Token.VALUE_NUMBER || token == Token.VALUE_BOOLEAN) {
contexts.add(parser.text());
} else {
throw new ElasticsearchParseException(
"context array must have string, number or boolean values, but was [" + token + "]");
}
}
} else {
throw new ElasticsearchParseException(
"contexts must be a string, number or boolean or a list of string, number or boolean, but was [" + token + "]");
}
return contexts;
}
@Override
public Set<CharSequence> parseContext(Document document) {
Set<CharSequence> values = null;
if (fieldName != null) {
IndexableField[] fields = document.getFields(fieldName);
values = new HashSet<>(fields.length);
for (IndexableField field : fields) {
values.add(field.stringValue());
}
}
return (values == null) ? Collections.<CharSequence>emptySet() : values;
}
@Override
protected CategoryQueryContext fromXContent(QueryParseContext context) throws IOException {
return CategoryQueryContext.fromXContent(context);
}
/**
* Parse a list of {@link CategoryQueryContext}
* using <code>parser</code>. A QueryContexts accepts one of the following forms:
*
* <ul>
* <li>Object: CategoryQueryContext</li>
* <li>String: CategoryQueryContext value with prefix=false and boost=1</li>
* <li>Array: <pre>[CategoryQueryContext, ..]</pre></li>
* </ul>
*
* A CategoryQueryContext has one of the following forms:
* <ul>
* <li>Object: <pre>{"context": <i><string></i>, "boost": <i><int></i>, "prefix": <i><boolean></i>}</pre></li>
* <li>String: <pre>"string"</pre></li>
* </ul>
*/
@Override
public List<InternalQueryContext> toInternalQueryContexts(List<CategoryQueryContext> queryContexts) {
List<InternalQueryContext> internalInternalQueryContexts = new ArrayList<>(queryContexts.size());
internalInternalQueryContexts.addAll(
queryContexts.stream()
.map(queryContext -> new InternalQueryContext(queryContext.getCategory(), queryContext.getBoost(), queryContext.isPrefix()))
.collect(Collectors.toList()));
return internalInternalQueryContexts;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
CategoryContextMapping mapping = (CategoryContextMapping) o;
return !(fieldName != null ? !fieldName.equals(mapping.fieldName) : mapping.fieldName != null);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), fieldName);
}
/**
* Builder for {@link CategoryContextMapping}
*/
public static class Builder extends ContextBuilder<CategoryContextMapping> {
private String fieldName;
/**
* Create a builder for
* a named {@link CategoryContextMapping}
* @param name name of the mapping
*/
public Builder(String name) {
super(name);
}
/**
* Set the name of the field to use
*/
public Builder field(String fieldName) {
this.fieldName = fieldName;
return this;
}
@Override
public CategoryContextMapping build() {
return new CategoryContextMapping(name, fieldName);
}
}
}