-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: constant keyword field (#12285)
Constant keyword fields behave similarly to regular keyword fields, except that they are defined only in the index mapping, and all documents in the index appear to have the same value for the constant keyword field. --------- Signed-off-by: Mohammad Hasnain <[email protected]>
- Loading branch information
1 parent
62776d1
commit 1ec49bd
Showing
7 changed files
with
409 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
191 changes: 191 additions & 0 deletions
191
server/src/main/java/org/opensearch/index/mapper/ConstantKeywordFieldMapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
package org.opensearch.index.mapper; | ||
|
||
import org.apache.lucene.search.MatchAllDocsQuery; | ||
import org.apache.lucene.search.Query; | ||
import org.opensearch.OpenSearchParseException; | ||
import org.opensearch.common.annotation.PublicApi; | ||
import org.opensearch.common.regex.Regex; | ||
import org.opensearch.index.fielddata.IndexFieldData; | ||
import org.opensearch.index.fielddata.plain.ConstantIndexFieldData; | ||
import org.opensearch.index.query.QueryShardContext; | ||
import org.opensearch.search.aggregations.support.CoreValuesSourceType; | ||
import org.opensearch.search.lookup.SearchLookup; | ||
|
||
import java.io.IOException; | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.function.Supplier; | ||
|
||
/** | ||
* Index specific field mapper | ||
* | ||
* @opensearch.api | ||
*/ | ||
@PublicApi(since = "2.14.0") | ||
public class ConstantKeywordFieldMapper extends ParametrizedFieldMapper { | ||
|
||
public static final String CONTENT_TYPE = "constant_keyword"; | ||
|
||
private static final String valuePropertyName = "value"; | ||
|
||
/** | ||
* A {@link Mapper.TypeParser} for the constant keyword field. | ||
* | ||
* @opensearch.internal | ||
*/ | ||
public static class TypeParser implements Mapper.TypeParser { | ||
@Override | ||
public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException { | ||
if (!node.containsKey(valuePropertyName)) { | ||
throw new OpenSearchParseException("Field [" + name + "] is missing required parameter [value]"); | ||
} | ||
Object value = node.remove(valuePropertyName); | ||
if (!(value instanceof String)) { | ||
throw new OpenSearchParseException("Field [" + name + "] is expected to be a string value"); | ||
} | ||
return new Builder(name, (String) value); | ||
} | ||
} | ||
|
||
private static ConstantKeywordFieldMapper toType(FieldMapper in) { | ||
return (ConstantKeywordFieldMapper) in; | ||
} | ||
|
||
/** | ||
* Builder for the binary field mapper | ||
* | ||
* @opensearch.internal | ||
*/ | ||
public static class Builder extends ParametrizedFieldMapper.Builder { | ||
|
||
private final Parameter<String> value; | ||
|
||
public Builder(String name, String value) { | ||
super(name); | ||
this.value = Parameter.stringParam(valuePropertyName, false, m -> toType(m).value, value); | ||
} | ||
|
||
@Override | ||
public List<Parameter<?>> getParameters() { | ||
return Arrays.asList(value); | ||
} | ||
|
||
@Override | ||
public ConstantKeywordFieldMapper build(BuilderContext context) { | ||
return new ConstantKeywordFieldMapper( | ||
name, | ||
new ConstantKeywordFieldMapper.ConstantKeywordFieldType(buildFullName(context), value.getValue()), | ||
multiFieldsBuilder.build(this, context), | ||
copyTo.build(), | ||
this | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Field type for Index field mapper | ||
* | ||
* @opensearch.internal | ||
*/ | ||
@PublicApi(since = "2.14.0") | ||
protected static final class ConstantKeywordFieldType extends ConstantFieldType { | ||
|
||
protected final String value; | ||
|
||
public ConstantKeywordFieldType(String name, String value) { | ||
super(name, Collections.emptyMap()); | ||
this.value = value; | ||
} | ||
|
||
@Override | ||
public String typeName() { | ||
return CONTENT_TYPE; | ||
} | ||
|
||
@Override | ||
protected boolean matches(String pattern, boolean caseInsensitive, QueryShardContext context) { | ||
return Regex.simpleMatch(pattern, value, caseInsensitive); | ||
} | ||
|
||
@Override | ||
public Query existsQuery(QueryShardContext context) { | ||
return new MatchAllDocsQuery(); | ||
} | ||
|
||
@Override | ||
public IndexFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, Supplier<SearchLookup> searchLookup) { | ||
return new ConstantIndexFieldData.Builder(fullyQualifiedIndexName, name(), CoreValuesSourceType.BYTES); | ||
} | ||
|
||
@Override | ||
public ValueFetcher valueFetcher(QueryShardContext context, SearchLookup searchLookup, String format) { | ||
if (format != null) { | ||
throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() + "] doesn't " + "support formats."); | ||
} | ||
|
||
return new SourceValueFetcher(name(), context) { | ||
@Override | ||
protected Object parseSourceValue(Object value) { | ||
String keywordValue = value.toString(); | ||
return Collections.singletonList(keywordValue); | ||
} | ||
}; | ||
} | ||
} | ||
|
||
private final String value; | ||
|
||
protected ConstantKeywordFieldMapper( | ||
String simpleName, | ||
MappedFieldType mappedFieldType, | ||
MultiFields multiFields, | ||
CopyTo copyTo, | ||
ConstantKeywordFieldMapper.Builder builder | ||
) { | ||
super(simpleName, mappedFieldType, multiFields, copyTo); | ||
this.value = builder.value.getValue(); | ||
} | ||
|
||
public ParametrizedFieldMapper.Builder getMergeBuilder() { | ||
return new ConstantKeywordFieldMapper.Builder(simpleName(), this.value).init(this); | ||
} | ||
|
||
@Override | ||
protected void parseCreateField(ParseContext context) throws IOException { | ||
|
||
final String value; | ||
if (context.externalValueSet()) { | ||
value = context.externalValue().toString(); | ||
} else { | ||
value = context.parser().textOrNull(); | ||
} | ||
if (value == null) { | ||
throw new IllegalArgumentException("constant keyword field [" + name() + "] must have a value"); | ||
} | ||
|
||
if (!value.equals(fieldType().value)) { | ||
throw new IllegalArgumentException("constant keyword field [" + name() + "] must have a value of [" + this.value + "]"); | ||
} | ||
|
||
} | ||
|
||
@Override | ||
public ConstantKeywordFieldMapper.ConstantKeywordFieldType fieldType() { | ||
return (ConstantKeywordFieldMapper.ConstantKeywordFieldType) super.fieldType(); | ||
} | ||
|
||
@Override | ||
protected String contentType() { | ||
return CONTENT_TYPE; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
114 changes: 114 additions & 0 deletions
114
server/src/test/java/org/opensearch/index/mapper/ConstantKeywordFieldMapperTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
package org.opensearch.index.mapper; | ||
|
||
import org.apache.lucene.index.IndexableField; | ||
import org.opensearch.OpenSearchParseException; | ||
import org.opensearch.common.CheckedConsumer; | ||
import org.opensearch.common.compress.CompressedXContent; | ||
import org.opensearch.common.xcontent.XContentFactory; | ||
import org.opensearch.common.xcontent.json.JsonXContent; | ||
import org.opensearch.core.common.bytes.BytesReference; | ||
import org.opensearch.core.xcontent.MediaTypeRegistry; | ||
import org.opensearch.core.xcontent.XContentBuilder; | ||
import org.opensearch.index.IndexService; | ||
import org.opensearch.plugins.Plugin; | ||
import org.opensearch.test.InternalSettingsPlugin; | ||
import org.opensearch.test.OpenSearchSingleNodeTestCase; | ||
import org.junit.Before; | ||
|
||
import java.io.IOException; | ||
import java.util.Collection; | ||
|
||
import static org.hamcrest.Matchers.containsString; | ||
|
||
public class ConstantKeywordFieldMapperTests extends OpenSearchSingleNodeTestCase { | ||
|
||
private IndexService indexService; | ||
private DocumentMapperParser parser; | ||
|
||
@Override | ||
protected Collection<Class<? extends Plugin>> getPlugins() { | ||
return pluginList(InternalSettingsPlugin.class); | ||
} | ||
|
||
@Before | ||
public void setup() { | ||
indexService = createIndex("test"); | ||
parser = indexService.mapperService().documentMapperParser(); | ||
} | ||
|
||
public void testDefaultDisabledIndexMapper() throws Exception { | ||
|
||
XContentBuilder mapping = XContentFactory.jsonBuilder() | ||
.startObject() | ||
.startObject("type") | ||
.startObject("properties") | ||
.startObject("field") | ||
.field("type", "constant_keyword") | ||
.field("value", "default_value") | ||
.endObject() | ||
.startObject("field2") | ||
.field("type", "keyword") | ||
.endObject(); | ||
mapping = mapping.endObject().endObject().endObject(); | ||
DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping.toString())); | ||
|
||
MapperParsingException e = expectThrows(MapperParsingException.class, () -> mapper.parse(source(b -> { | ||
b.field("field", "sdf"); | ||
b.field("field2", "szdfvsddf"); | ||
}))); | ||
assertThat( | ||
e.getMessage(), | ||
containsString( | ||
"failed to parse field [field] of type [constant_keyword] in document with id '1'. Preview of field's value: 'sdf'" | ||
) | ||
); | ||
|
||
final ParsedDocument doc = mapper.parse(source(b -> { | ||
b.field("field", "default_value"); | ||
b.field("field2", "field_2_value"); | ||
})); | ||
|
||
final IndexableField field = doc.rootDoc().getField("field"); | ||
|
||
// constantKeywordField should not be stored | ||
assertNull(field); | ||
} | ||
|
||
public void testMissingDefaultIndexMapper() throws Exception { | ||
|
||
final XContentBuilder mapping = XContentFactory.jsonBuilder() | ||
.startObject() | ||
.startObject("type") | ||
.startObject("properties") | ||
.startObject("field") | ||
.field("type", "constant_keyword") | ||
.endObject() | ||
.startObject("field2") | ||
.field("type", "keyword") | ||
.endObject() | ||
.endObject() | ||
.endObject() | ||
.endObject(); | ||
|
||
OpenSearchParseException e = expectThrows( | ||
OpenSearchParseException.class, | ||
() -> parser.parse("type", new CompressedXContent(mapping.toString())) | ||
); | ||
assertThat(e.getMessage(), containsString("Field [field] is missing required parameter [value]")); | ||
} | ||
|
||
private final SourceToParse source(CheckedConsumer<XContentBuilder, IOException> build) throws IOException { | ||
XContentBuilder builder = JsonXContent.contentBuilder().startObject(); | ||
build.accept(builder); | ||
builder.endObject(); | ||
return new SourceToParse("test", "1", BytesReference.bytes(builder), MediaTypeRegistry.JSON); | ||
} | ||
} |
Oops, something went wrong.