org.elasticsearch.search.suggest.completion.context.ContextMappings Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of elasticsearch Show documentation
Show all versions of elasticsearch Show documentation
Elasticsearch subproject :server
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.search.suggest.completion.context;
import org.apache.lucene.search.suggest.document.CompletionQuery;
import org.apache.lucene.search.suggest.document.ContextQuery;
import org.apache.lucene.search.suggest.document.ContextSuggestField;
import org.apache.lucene.util.CharsRef;
import org.apache.lucene.util.CharsRefBuilder;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.index.mapper.CompletionFieldMapper;
import org.elasticsearch.index.mapper.LuceneDocument;
import org.elasticsearch.index.mapper.MappingParser;
import org.elasticsearch.search.suggest.completion.context.ContextMapping.Type;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.XContentBuilder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static org.elasticsearch.search.suggest.completion.context.ContextMapping.FIELD_NAME;
import static org.elasticsearch.search.suggest.completion.context.ContextMapping.FIELD_TYPE;
/**
* ContextMappings indexes context-enabled suggestion fields
* and creates context queries for defined {@link ContextMapping}s
* for a {@link CompletionFieldMapper}
*/
public class ContextMappings implements ToXContent, Iterable> {
private final List> contextMappings;
private final Map> contextNameMap;
public ContextMappings(List> contextMappings) {
if (contextMappings.size() > 255) {
// we can support more, but max of 255 (1 byte) unique context types per suggest field
// seems reasonable?
throw new UnsupportedOperationException("Maximum of 10 context types are supported was: " + contextMappings.size());
}
this.contextMappings = contextMappings;
contextNameMap = new HashMap<>(contextMappings.size());
for (ContextMapping mapping : contextMappings) {
contextNameMap.put(mapping.name(), mapping);
}
}
/**
* @return number of context mappings
* held by this instance
*/
public int size() {
return contextMappings.size();
}
/**
* Returns a context mapping by its name
*/
public ContextMapping get(String name) {
ContextMapping contextMapping = contextNameMap.get(name);
if (contextMapping == null) {
List keys = new ArrayList<>(contextNameMap.keySet());
Collections.sort(keys);
throw new IllegalArgumentException("Unknown context name [" + name + "], must be one of " + keys.toString());
}
return contextMapping;
}
/**
* Adds a context-enabled field for all the defined mappings to document
* see {@link org.elasticsearch.search.suggest.completion.context.ContextMappings.TypedContextField}
*/
public void addField(LuceneDocument document, String name, String input, int weight, Map> contexts) {
document.add(new TypedContextField(name, input, weight, contexts, document));
}
@Override
public Iterator> iterator() {
return contextMappings.iterator();
}
/**
* Field prepends context values with a suggestion
* Context values are associated with a type, denoted by
* a type id, which is prepended to the context value.
*
* Every defined context mapping yields a unique type id (index of the
* corresponding context mapping in the context mappings list)
* for all its context values
*
* The type, context and suggestion values are encoded as follows:
*
* TYPE_ID | CONTEXT_VALUE | CONTEXT_SEP | SUGGESTION_VALUE
*
*
* Field can also use values of other indexed fields as contexts
* at index time
*/
private class TypedContextField extends ContextSuggestField {
private final Map> contexts;
private final LuceneDocument document;
TypedContextField(String name, String value, int weight, Map> contexts, LuceneDocument document) {
super(name, value, weight);
this.contexts = contexts;
this.document = document;
}
@Override
protected Iterable contexts() {
Set typedContexts = new HashSet<>();
final CharsRefBuilder scratch = new CharsRefBuilder();
scratch.grow(1);
for (int typeId = 0; typeId < contextMappings.size(); typeId++) {
scratch.setCharAt(0, (char) typeId);
scratch.setLength(1);
ContextMapping mapping = contextMappings.get(typeId);
Set contexts = new HashSet<>(mapping.parseContext(document));
if (this.contexts.get(mapping.name()) != null) {
contexts.addAll(this.contexts.get(mapping.name()));
}
for (String context : contexts) {
scratch.append(context);
typedContexts.add(scratch.toCharsRef());
scratch.setLength(1);
}
}
if (typedContexts.isEmpty()) {
throw new IllegalArgumentException("Contexts are mandatory in context enabled completion field [" + name + "]");
}
return new ArrayList(typedContexts);
}
}
/**
* Wraps a {@link CompletionQuery} with context queries
*
* @param query base completion query to wrap
* @param queryContexts a map of context mapping name and collected query contexts
* @return a context-enabled query
*/
public ContextQuery toContextQuery(CompletionQuery query, Map> queryContexts) {
ContextQuery typedContextQuery = new ContextQuery(query);
boolean hasContext = false;
if (queryContexts.isEmpty() == false) {
CharsRefBuilder scratch = new CharsRefBuilder();
scratch.grow(1);
for (int typeId = 0; typeId < contextMappings.size(); typeId++) {
scratch.setCharAt(0, (char) typeId);
scratch.setLength(1);
ContextMapping mapping = contextMappings.get(typeId);
List internalQueryContext = queryContexts.get(mapping.name());
if (internalQueryContext != null) {
for (ContextMapping.InternalQueryContext context : internalQueryContext) {
scratch.append(context.context);
typedContextQuery.addContext(scratch.toCharsRef(), context.boost, context.isPrefix == false);
scratch.setLength(1);
hasContext = true;
}
}
}
}
if (hasContext == false) {
throw new IllegalArgumentException("Missing mandatory contexts in context query");
}
return typedContextQuery;
}
/**
* Maps an output context list to a map of context mapping names and their values
*
* see {@link org.elasticsearch.search.suggest.completion.context.ContextMappings.TypedContextField}
* @return a map of context names and their values
*
*/
public Map> getNamedContexts(List contexts) {
Map> contextMap = new HashMap<>(contexts.size());
for (CharSequence typedContext : contexts) {
int typeId = typedContext.charAt(0);
assert typeId < contextMappings.size() : "Returned context has invalid type";
ContextMapping mapping = contextMappings.get(typeId);
Set contextEntries = contextMap.get(mapping.name());
if (contextEntries == null) {
contextEntries = new HashSet<>();
contextMap.put(mapping.name(), contextEntries);
}
contextEntries.add(typedContext.subSequence(1, typedContext.length()).toString());
}
return contextMap;
}
/**
* Loads {@link ContextMappings} from configuration
*
* Expected configuration:
* List of maps representing {@link ContextMapping}
* [{"name": .., "type": .., ..}, {..}]
*
*/
@SuppressWarnings("unchecked")
public static ContextMappings load(Object configuration) throws ElasticsearchParseException {
final List> contextMappings;
if (configuration instanceof List) {
contextMappings = new ArrayList<>();
List