org.opensearch.search.suggest.SuggestionBuilder 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;
import org.apache.lucene.analysis.Analyzer;
import org.opensearch.OpenSearchParseException;
import org.opensearch.common.annotation.PublicApi;
import org.opensearch.common.lucene.BytesRefs;
import org.opensearch.core.ParseField;
import org.opensearch.core.common.ParsingException;
import org.opensearch.core.common.io.stream.NamedWriteable;
import org.opensearch.core.common.io.stream.StreamInput;
import org.opensearch.core.common.io.stream.StreamOutput;
import org.opensearch.core.xcontent.ToXContentFragment;
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.core.xcontent.XContentParser;
import org.opensearch.index.mapper.MappedFieldType;
import org.opensearch.index.mapper.MapperService;
import org.opensearch.index.query.QueryShardContext;
import org.opensearch.search.suggest.SuggestionSearchContext.SuggestionContext;
import java.io.IOException;
import java.util.Objects;
/**
* Base class for the different suggestion implementations.
*
* @opensearch.api
*/
@PublicApi(since = "1.0.0")
public abstract class SuggestionBuilder> implements NamedWriteable, ToXContentFragment {
protected final String field;
protected String text;
protected String prefix;
protected String regex;
protected String analyzer;
protected Integer size;
protected Integer shardSize;
protected static final ParseField TEXT_FIELD = new ParseField("text");
protected static final ParseField PREFIX_FIELD = new ParseField("prefix");
protected static final ParseField REGEX_FIELD = new ParseField("regex");
protected static final ParseField FIELDNAME_FIELD = new ParseField("field");
protected static final ParseField ANALYZER_FIELD = new ParseField("analyzer");
protected static final ParseField SIZE_FIELD = new ParseField("size");
protected static final ParseField SHARDSIZE_FIELD = new ParseField("shard_size");
/**
* Creates a new suggestion.
* @param field field to execute suggestions on
*/
protected SuggestionBuilder(String field) {
Objects.requireNonNull(field, "suggestion requires a field name");
if (field.isEmpty()) {
throw new IllegalArgumentException("suggestion field name is empty");
}
this.field = field;
}
/**
* internal copy constructor that copies over all class fields from second SuggestionBuilder except field name.
*/
protected SuggestionBuilder(String field, SuggestionBuilder> in) {
this(field);
text = in.text;
prefix = in.prefix;
regex = in.regex;
analyzer = in.analyzer;
size = in.size;
shardSize = in.shardSize;
}
/**
* Read from a stream.
*/
protected SuggestionBuilder(StreamInput in) throws IOException {
field = in.readString();
text = in.readOptionalString();
prefix = in.readOptionalString();
regex = in.readOptionalString();
analyzer = in.readOptionalString();
size = in.readOptionalVInt();
shardSize = in.readOptionalVInt();
}
@Override
public final void writeTo(StreamOutput out) throws IOException {
out.writeString(field);
out.writeOptionalString(text);
out.writeOptionalString(prefix);
out.writeOptionalString(regex);
out.writeOptionalString(analyzer);
out.writeOptionalVInt(size);
out.writeOptionalVInt(shardSize);
doWriteTo(out);
}
protected abstract void doWriteTo(StreamOutput out) throws IOException;
/**
* Same as in {@link SuggestBuilder#setGlobalText(String)}, but in the suggestion scope.
*/
@SuppressWarnings("unchecked")
public T text(String text) {
this.text = text;
return (T) this;
}
/**
* get the text for this suggestion
*/
public String text() {
return this.text;
}
@SuppressWarnings("unchecked")
protected T prefix(String prefix) {
this.prefix = prefix;
return (T) this;
}
/**
* get the prefix for this suggestion
*/
public String prefix() {
return this.prefix;
}
@SuppressWarnings("unchecked")
protected T regex(String regex) {
this.regex = regex;
return (T) this;
}
/**
* get the regex for this suggestion
*/
public String regex() {
return this.regex;
}
/**
* get the {@link #field()} parameter
*/
public String field() {
return this.field;
}
/**
* Sets the analyzer to analyse to suggest text with. Defaults to the search
* analyzer of the suggest field.
*/
@SuppressWarnings("unchecked")
public T analyzer(String analyzer) {
this.analyzer = analyzer;
return (T) this;
}
/**
* get the {@link #analyzer()} parameter
*/
public String analyzer() {
return this.analyzer;
}
/**
* Sets the maximum suggestions to be returned per suggest text term.
*/
@SuppressWarnings("unchecked")
public T size(int size) {
if (size <= 0) {
throw new IllegalArgumentException("size must be positive");
}
this.size = size;
return (T) this;
}
/**
* get the {@link #size()} parameter
*/
public Integer size() {
return this.size;
}
/**
* Sets the maximum number of suggested term to be retrieved from each
* individual shard. During the reduce phase the only the top N suggestions
* are returned based on the size
option. Defaults to the
* size
option.
*
* Setting this to a value higher than the `size` can be useful in order to
* get a more accurate document frequency for suggested terms. Due to the
* fact that terms are partitioned amongst shards, the shard level document
* frequencies of suggestions may not be precise. Increasing this will make
* these document frequencies more precise.
*/
@SuppressWarnings("unchecked")
public T shardSize(Integer shardSize) {
this.shardSize = shardSize;
return (T) this;
}
/**
* get the {@link #shardSize()} parameter
*/
public Integer shardSize() {
return this.shardSize;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
if (text != null) {
builder.field(TEXT_FIELD.getPreferredName(), text);
}
if (prefix != null) {
builder.field(PREFIX_FIELD.getPreferredName(), prefix);
}
if (regex != null) {
builder.field(REGEX_FIELD.getPreferredName(), regex);
}
builder.startObject(getSuggesterName());
if (analyzer != null) {
builder.field(ANALYZER_FIELD.getPreferredName(), analyzer);
}
builder.field(FIELDNAME_FIELD.getPreferredName(), field);
if (size != null) {
builder.field(SIZE_FIELD.getPreferredName(), size);
}
if (shardSize != null) {
builder.field(SHARDSIZE_FIELD.getPreferredName(), shardSize);
}
builder = innerToXContent(builder, params);
builder.endObject();
return builder;
}
protected abstract XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException;
static SuggestionBuilder> fromXContent(XContentParser parser) throws IOException {
XContentParser.Token token;
String currentFieldName = null;
String suggestText = null;
String prefix = null;
String regex = null;
SuggestionBuilder> suggestionBuilder = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token.isValue()) {
if (TEXT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
suggestText = parser.text();
} else if (PREFIX_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
prefix = parser.text();
} else if (REGEX_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
regex = parser.text();
} else {
throw new ParsingException(parser.getTokenLocation(), "suggestion does not support [" + currentFieldName + "]");
}
} else if (token == XContentParser.Token.START_OBJECT) {
suggestionBuilder = parser.namedObject(SuggestionBuilder.class, currentFieldName, null);
}
}
if (suggestionBuilder == null) {
throw new OpenSearchParseException("missing suggestion object");
}
if (suggestText != null) {
suggestionBuilder.text(suggestText);
}
if (prefix != null) {
suggestionBuilder.prefix(prefix);
}
if (regex != null) {
suggestionBuilder.regex(regex);
}
return suggestionBuilder;
}
protected abstract SuggestionContext build(QueryShardContext context) throws IOException;
/**
* Transfers the text, prefix, regex, analyzer, field, size and shard size settings from the
* original {@link SuggestionBuilder} to the target {@link SuggestionContext}
*/
protected void populateCommonFields(MapperService mapperService, SuggestionSearchContext.SuggestionContext suggestionContext) {
Objects.requireNonNull(field, "field must not be null");
MappedFieldType fieldType = mapperService.fieldType(field);
if (fieldType == null) {
throw new IllegalArgumentException("no mapping found for field [" + field + "]");
} else if (analyzer == null) {
// no analyzer name passed in, so try the field's analyzer, or the default analyzer
if (fieldType.getTextSearchInfo().getSearchAnalyzer() == null) {
suggestionContext.setAnalyzer(mapperService.searchAnalyzer());
} else {
suggestionContext.setAnalyzer(fieldType.getTextSearchInfo().getSearchAnalyzer());
}
} else {
Analyzer luceneAnalyzer = mapperService.getNamedAnalyzer(analyzer);
if (luceneAnalyzer == null) {
throw new IllegalArgumentException("analyzer [" + analyzer + "] doesn't exists");
}
suggestionContext.setAnalyzer(luceneAnalyzer);
}
suggestionContext.setField(fieldType.name());
if (size != null) {
suggestionContext.setSize(size);
}
if (shardSize != null) {
suggestionContext.setShardSize(shardSize);
} else {
// if no shard size is set in builder, use size (or at least 5)
suggestionContext.setShardSize(Math.max(suggestionContext.getSize(), 5));
}
if (text != null) {
suggestionContext.setText(BytesRefs.toBytesRef(text));
}
if (prefix != null) {
suggestionContext.setPrefix(BytesRefs.toBytesRef(prefix));
}
if (regex != null) {
suggestionContext.setRegex(BytesRefs.toBytesRef(regex));
}
if (text != null && prefix == null) {
suggestionContext.setPrefix(BytesRefs.toBytesRef(text));
} else if (text == null && prefix != null) {
suggestionContext.setText(BytesRefs.toBytesRef(prefix));
} else if (text == null && regex != null) {
suggestionContext.setText(BytesRefs.toBytesRef(regex));
}
}
private String getSuggesterName() {
// default impl returns the same as writeable name, but we keep the distinction between the two just to make sure
return getWriteableName();
}
@Override
public final boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
@SuppressWarnings("unchecked")
T other = (T) obj;
return Objects.equals(text, other.text())
&& Objects.equals(prefix, other.prefix())
&& Objects.equals(regex, other.regex())
&& Objects.equals(field, other.field())
&& Objects.equals(analyzer, other.analyzer())
&& Objects.equals(size, other.size())
&& Objects.equals(shardSize, other.shardSize())
&& doEquals(other);
}
/**
* Indicates whether some other {@link SuggestionBuilder} of the same type is "equal to" this one.
*/
protected abstract boolean doEquals(T other);
@Override
public final int hashCode() {
return Objects.hash(text, prefix, regex, field, analyzer, size, shardSize, doHashCode());
}
/**
* HashCode for the subclass of {@link SuggestionBuilder} to implement.
*/
protected abstract int doHashCode();
}