
org.elasticsearch.search.suggest.completion.context.ContextMappings Maven / Gradle / Ivy
/*
* 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.common.util.Maps;
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.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 = Maps.newMapWithExpectedSize(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 = Maps.newMapWithExpectedSize(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
© 2015 - 2025 Weber Informatics LLC | Privacy Policy