Skip to content

Commit 2ef020e

Browse files
hasnain2808msfroh
authored andcommitted
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 <hasnain2808@gmail.com> (cherry picked from commit 1ec49bd)
1 parent f69da6c commit 2ef020e

File tree

7 files changed

+409
-1
lines changed

7 files changed

+409
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
## [Unreleased 2.x]
77
### Added
88
- Add explicit dependency to validatePom and generatePom tasks ([#12909](https://github.com/opensearch-project/OpenSearch/pull/12909))
9+
- Constant Keyword Field ([#12285](https://github.com/opensearch-project/OpenSearch/pull/12285))
910
- [Concurrent Segment Search] Perform buildAggregation concurrently and support Composite Aggregations ([#12697](https://github.com/opensearch-project/OpenSearch/pull/12697))
1011
- Convert ingest processor supports ip type ([#12818](https://github.com/opensearch-project/OpenSearch/pull/12818))
1112
- Allow setting KEYSTORE_PASSWORD through env variable ([#12865](https://github.com/opensearch-project/OpenSearch/pull/12865))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.index.mapper;
10+
11+
import org.apache.lucene.search.MatchAllDocsQuery;
12+
import org.apache.lucene.search.Query;
13+
import org.opensearch.OpenSearchParseException;
14+
import org.opensearch.common.annotation.PublicApi;
15+
import org.opensearch.common.regex.Regex;
16+
import org.opensearch.index.fielddata.IndexFieldData;
17+
import org.opensearch.index.fielddata.plain.ConstantIndexFieldData;
18+
import org.opensearch.index.query.QueryShardContext;
19+
import org.opensearch.search.aggregations.support.CoreValuesSourceType;
20+
import org.opensearch.search.lookup.SearchLookup;
21+
22+
import java.io.IOException;
23+
import java.util.Arrays;
24+
import java.util.Collections;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.function.Supplier;
28+
29+
/**
30+
* Index specific field mapper
31+
*
32+
* @opensearch.api
33+
*/
34+
@PublicApi(since = "2.14.0")
35+
public class ConstantKeywordFieldMapper extends ParametrizedFieldMapper {
36+
37+
public static final String CONTENT_TYPE = "constant_keyword";
38+
39+
private static final String valuePropertyName = "value";
40+
41+
/**
42+
* A {@link Mapper.TypeParser} for the constant keyword field.
43+
*
44+
* @opensearch.internal
45+
*/
46+
public static class TypeParser implements Mapper.TypeParser {
47+
@Override
48+
public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException {
49+
if (!node.containsKey(valuePropertyName)) {
50+
throw new OpenSearchParseException("Field [" + name + "] is missing required parameter [value]");
51+
}
52+
Object value = node.remove(valuePropertyName);
53+
if (!(value instanceof String)) {
54+
throw new OpenSearchParseException("Field [" + name + "] is expected to be a string value");
55+
}
56+
return new Builder(name, (String) value);
57+
}
58+
}
59+
60+
private static ConstantKeywordFieldMapper toType(FieldMapper in) {
61+
return (ConstantKeywordFieldMapper) in;
62+
}
63+
64+
/**
65+
* Builder for the binary field mapper
66+
*
67+
* @opensearch.internal
68+
*/
69+
public static class Builder extends ParametrizedFieldMapper.Builder {
70+
71+
private final Parameter<String> value;
72+
73+
public Builder(String name, String value) {
74+
super(name);
75+
this.value = Parameter.stringParam(valuePropertyName, false, m -> toType(m).value, value);
76+
}
77+
78+
@Override
79+
public List<Parameter<?>> getParameters() {
80+
return Arrays.asList(value);
81+
}
82+
83+
@Override
84+
public ConstantKeywordFieldMapper build(BuilderContext context) {
85+
return new ConstantKeywordFieldMapper(
86+
name,
87+
new ConstantKeywordFieldMapper.ConstantKeywordFieldType(buildFullName(context), value.getValue()),
88+
multiFieldsBuilder.build(this, context),
89+
copyTo.build(),
90+
this
91+
);
92+
}
93+
}
94+
95+
/**
96+
* Field type for Index field mapper
97+
*
98+
* @opensearch.internal
99+
*/
100+
@PublicApi(since = "2.14.0")
101+
protected static final class ConstantKeywordFieldType extends ConstantFieldType {
102+
103+
protected final String value;
104+
105+
public ConstantKeywordFieldType(String name, String value) {
106+
super(name, Collections.emptyMap());
107+
this.value = value;
108+
}
109+
110+
@Override
111+
public String typeName() {
112+
return CONTENT_TYPE;
113+
}
114+
115+
@Override
116+
protected boolean matches(String pattern, boolean caseInsensitive, QueryShardContext context) {
117+
return Regex.simpleMatch(pattern, value, caseInsensitive);
118+
}
119+
120+
@Override
121+
public Query existsQuery(QueryShardContext context) {
122+
return new MatchAllDocsQuery();
123+
}
124+
125+
@Override
126+
public IndexFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, Supplier<SearchLookup> searchLookup) {
127+
return new ConstantIndexFieldData.Builder(fullyQualifiedIndexName, name(), CoreValuesSourceType.BYTES);
128+
}
129+
130+
@Override
131+
public ValueFetcher valueFetcher(QueryShardContext context, SearchLookup searchLookup, String format) {
132+
if (format != null) {
133+
throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() + "] doesn't " + "support formats.");
134+
}
135+
136+
return new SourceValueFetcher(name(), context) {
137+
@Override
138+
protected Object parseSourceValue(Object value) {
139+
String keywordValue = value.toString();
140+
return Collections.singletonList(keywordValue);
141+
}
142+
};
143+
}
144+
}
145+
146+
private final String value;
147+
148+
protected ConstantKeywordFieldMapper(
149+
String simpleName,
150+
MappedFieldType mappedFieldType,
151+
MultiFields multiFields,
152+
CopyTo copyTo,
153+
ConstantKeywordFieldMapper.Builder builder
154+
) {
155+
super(simpleName, mappedFieldType, multiFields, copyTo);
156+
this.value = builder.value.getValue();
157+
}
158+
159+
public ParametrizedFieldMapper.Builder getMergeBuilder() {
160+
return new ConstantKeywordFieldMapper.Builder(simpleName(), this.value).init(this);
161+
}
162+
163+
@Override
164+
protected void parseCreateField(ParseContext context) throws IOException {
165+
166+
final String value;
167+
if (context.externalValueSet()) {
168+
value = context.externalValue().toString();
169+
} else {
170+
value = context.parser().textOrNull();
171+
}
172+
if (value == null) {
173+
throw new IllegalArgumentException("constant keyword field [" + name() + "] must have a value");
174+
}
175+
176+
if (!value.equals(fieldType().value)) {
177+
throw new IllegalArgumentException("constant keyword field [" + name() + "] must have a value of [" + this.value + "]");
178+
}
179+
180+
}
181+
182+
@Override
183+
public ConstantKeywordFieldMapper.ConstantKeywordFieldType fieldType() {
184+
return (ConstantKeywordFieldMapper.ConstantKeywordFieldType) super.fieldType();
185+
}
186+
187+
@Override
188+
protected String contentType() {
189+
return CONTENT_TYPE;
190+
}
191+
}

server/src/main/java/org/opensearch/indices/IndicesModule.java

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.opensearch.index.mapper.BinaryFieldMapper;
4747
import org.opensearch.index.mapper.BooleanFieldMapper;
4848
import org.opensearch.index.mapper.CompletionFieldMapper;
49+
import org.opensearch.index.mapper.ConstantKeywordFieldMapper;
4950
import org.opensearch.index.mapper.DataStreamFieldMapper;
5051
import org.opensearch.index.mapper.DateFieldMapper;
5152
import org.opensearch.index.mapper.DocCountFieldMapper;
@@ -168,6 +169,7 @@ public static Map<String, Mapper.TypeParser> getMappers(List<MapperPlugin> mappe
168169
mappers.put(FieldAliasMapper.CONTENT_TYPE, new FieldAliasMapper.TypeParser());
169170
mappers.put(GeoPointFieldMapper.CONTENT_TYPE, new GeoPointFieldMapper.TypeParser());
170171
mappers.put(FlatObjectFieldMapper.CONTENT_TYPE, FlatObjectFieldMapper.PARSER);
172+
mappers.put(ConstantKeywordFieldMapper.CONTENT_TYPE, new ConstantKeywordFieldMapper.TypeParser());
171173

172174
for (MapperPlugin mapperPlugin : mapperPlugins) {
173175
for (Map.Entry<String, Mapper.TypeParser> entry : mapperPlugin.getMappers().entrySet()) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.index.mapper;
10+
11+
import org.apache.lucene.index.IndexableField;
12+
import org.opensearch.OpenSearchParseException;
13+
import org.opensearch.common.CheckedConsumer;
14+
import org.opensearch.common.compress.CompressedXContent;
15+
import org.opensearch.common.xcontent.XContentFactory;
16+
import org.opensearch.common.xcontent.json.JsonXContent;
17+
import org.opensearch.core.common.bytes.BytesReference;
18+
import org.opensearch.core.xcontent.MediaTypeRegistry;
19+
import org.opensearch.core.xcontent.XContentBuilder;
20+
import org.opensearch.index.IndexService;
21+
import org.opensearch.plugins.Plugin;
22+
import org.opensearch.test.InternalSettingsPlugin;
23+
import org.opensearch.test.OpenSearchSingleNodeTestCase;
24+
import org.junit.Before;
25+
26+
import java.io.IOException;
27+
import java.util.Collection;
28+
29+
import static org.hamcrest.Matchers.containsString;
30+
31+
public class ConstantKeywordFieldMapperTests extends OpenSearchSingleNodeTestCase {
32+
33+
private IndexService indexService;
34+
private DocumentMapperParser parser;
35+
36+
@Override
37+
protected Collection<Class<? extends Plugin>> getPlugins() {
38+
return pluginList(InternalSettingsPlugin.class);
39+
}
40+
41+
@Before
42+
public void setup() {
43+
indexService = createIndex("test");
44+
parser = indexService.mapperService().documentMapperParser();
45+
}
46+
47+
public void testDefaultDisabledIndexMapper() throws Exception {
48+
49+
XContentBuilder mapping = XContentFactory.jsonBuilder()
50+
.startObject()
51+
.startObject("type")
52+
.startObject("properties")
53+
.startObject("field")
54+
.field("type", "constant_keyword")
55+
.field("value", "default_value")
56+
.endObject()
57+
.startObject("field2")
58+
.field("type", "keyword")
59+
.endObject();
60+
mapping = mapping.endObject().endObject().endObject();
61+
DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping.toString()));
62+
63+
MapperParsingException e = expectThrows(MapperParsingException.class, () -> mapper.parse(source(b -> {
64+
b.field("field", "sdf");
65+
b.field("field2", "szdfvsddf");
66+
})));
67+
assertThat(
68+
e.getMessage(),
69+
containsString(
70+
"failed to parse field [field] of type [constant_keyword] in document with id '1'. Preview of field's value: 'sdf'"
71+
)
72+
);
73+
74+
final ParsedDocument doc = mapper.parse(source(b -> {
75+
b.field("field", "default_value");
76+
b.field("field2", "field_2_value");
77+
}));
78+
79+
final IndexableField field = doc.rootDoc().getField("field");
80+
81+
// constantKeywordField should not be stored
82+
assertNull(field);
83+
}
84+
85+
public void testMissingDefaultIndexMapper() throws Exception {
86+
87+
final XContentBuilder mapping = XContentFactory.jsonBuilder()
88+
.startObject()
89+
.startObject("type")
90+
.startObject("properties")
91+
.startObject("field")
92+
.field("type", "constant_keyword")
93+
.endObject()
94+
.startObject("field2")
95+
.field("type", "keyword")
96+
.endObject()
97+
.endObject()
98+
.endObject()
99+
.endObject();
100+
101+
OpenSearchParseException e = expectThrows(
102+
OpenSearchParseException.class,
103+
() -> parser.parse("type", new CompressedXContent(mapping.toString()))
104+
);
105+
assertThat(e.getMessage(), containsString("Field [field] is missing required parameter [value]"));
106+
}
107+
108+
private final SourceToParse source(CheckedConsumer<XContentBuilder, IOException> build) throws IOException {
109+
XContentBuilder builder = JsonXContent.contentBuilder().startObject();
110+
build.accept(builder);
111+
builder.endObject();
112+
return new SourceToParse("test", "1", BytesReference.bytes(builder), MediaTypeRegistry.JSON);
113+
}
114+
}

0 commit comments

Comments
 (0)