org.opensearch.search.suggest.completion.context.ContextMappings Maven / Gradle / Ivy
Show all versions of opensearch Show documentation
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/*
* 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.
*/
/*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/
package org.opensearch.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.opensearch.OpenSearchParseException;
import org.opensearch.Version;
import org.opensearch.core.xcontent.ToXContent;
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.index.mapper.CompletionFieldMapper;
import org.opensearch.index.mapper.DocumentMapperParser;
import org.opensearch.index.mapper.ParseContext;
import org.opensearch.search.suggest.completion.context.ContextMapping.Type;
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.opensearch.search.suggest.completion.context.ContextMapping.FIELD_NAME;
import static org.opensearch.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}
*
* @opensearch.internal
*/
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.opensearch.search.suggest.completion.context.ContextMappings.TypedContextField}
*/
public void addField(ParseContext.Document 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 ParseContext.Document document;
TypedContextField(String name, String value, int weight, Map> contexts, ParseContext.Document 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);
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.opensearch.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": .., ..}, {..}]
*
*/
public static ContextMappings load(Object configuration, Version indexVersionCreated) throws OpenSearchParseException {
final List> contextMappings;
if (configuration instanceof List) {
contextMappings = new ArrayList<>();
List