diff --git a/docs/changelog/138548.yaml b/docs/changelog/138548.yaml new file mode 100644 index 0000000000000..f7787059385b4 --- /dev/null +++ b/docs/changelog/138548.yaml @@ -0,0 +1,5 @@ +pr: 138548 +summary: Store high-cardinality keyword fields in binary doc values +area: Mapping +type: feature +issues: [] diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java index 67527a4d27436..c98e70131b554 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java @@ -32,6 +32,7 @@ import org.elasticsearch.index.mapper.TestDocumentParserContext; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.TermQueryBuilder; +import org.elasticsearch.script.field.BinaryDocValuesField; import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; import org.elasticsearch.test.ESTestCase; @@ -88,7 +89,7 @@ public void testStoringQueryBuilders() throws IOException { when(searchExecutionContext.getWriteableRegistry()).thenReturn(writableRegistry()); when(searchExecutionContext.getParserConfig()).thenReturn(parserConfig()); when(searchExecutionContext.getForField(fieldMapper.fieldType(), fielddataOperation)).thenReturn( - new BytesBinaryIndexFieldData(fieldMapper.fullPath(), CoreValuesSourceType.KEYWORD) + new BytesBinaryIndexFieldData(fieldMapper.fullPath(), CoreValuesSourceType.KEYWORD, BinaryDocValuesField::new) ); when(searchExecutionContext.getFieldType(Mockito.anyString())).thenAnswer(invocation -> { final String fieldName = (String) invocation.getArguments()[0]; diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/395_binary_doc_values_search.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/395_binary_doc_values_search.yml new file mode 100644 index 0000000000000..8f98e460d583f --- /dev/null +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/395_binary_doc_values_search.yml @@ -0,0 +1,115 @@ +--- +setup: + - requires: + cluster_features: ["mapper.keyword.store_high_cardinality_in_binary_doc_values"] + reason: "testing binary doc values search" + + - do: + indices.create: + index: test + body: + mappings: + dynamic: false + properties: + keyword: + type: keyword + index: false + doc_values: + cardinality: high + + - do: + index: + index: test + id: "1" + body: + keyword: "key1" + + - do: + index: + index: test + id: "2" + body: + keyword: "key2" + - do: + indices.refresh: {} + +--- +"Test match query on keyword field where only binary doc values are enabled": + + - do: + search: + index: test + body: { query: { match: { keyword: { query: "key1" } } } } + - length: { hits.hits: 1 } + +--- +"Test terms query on keyword field where only binary doc values are enabled": + + - do: + search: + index: test + body: { query: { terms: { keyword: [ "key1", "key2" ] } } } + - length: { hits.hits: 2 } + +--- +"Test range query on keyword field where only binary doc values are enabled": + + - do: + search: + index: test + body: { query: { range: { keyword: { gte: "key1" } } } } + - length: { hits.hits: 2 } + +--- +"Test fuzzy query on keyword field where only binary doc values are enabled": + + - do: + search: + index: test + body: { query: { fuzzy: { keyword: { value: "kay1", fuzziness: 1 } } } } + - length: { hits.hits: 1 } + +--- +"Test prefix query on keyword field where only binary doc values are enabled": + + - do: + search: + index: test + body: { query: { prefix: { keyword: { value: "key" } } } } + - length: { hits.hits: 2 } + +--- +"Test case insensitive term query on keyword field where only binary doc values are enabled": + + - do: + search: + index: test + body: { query: { term: { keyword: { value: "KeY1", case_insensitive: true } } } } + - length: { hits.hits: 1 } + +--- +"Test wildcard query on keyword field where only binary doc values are enabled": + + - do: + search: + index: test + body: { query: { wildcard: { keyword: { value: "k*1" } } } } + - length: { hits.hits: 1 } + +--- +"Test case insensitive wildcard query on keyword field where only binary doc values are enabled": + + - do: + search: + index: test + body: { query: { wildcard: { keyword: { value: "K*1", case_insensitive: true } } } } + - length: { hits.hits: 1 } + +--- +"Test regexp query on keyword field where only binary doc values are enabled": + + - do: + search: + index: test + body: { query: { regexp: { keyword: { value: "k.*1" } } } } + - length: { hits.hits: 1 } diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/MultiValuedSortedBinaryDocValues.java b/server/src/main/java/org/elasticsearch/index/fielddata/MultiValuedSortedBinaryDocValues.java new file mode 100644 index 0000000000000..2b6293b584bb5 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/fielddata/MultiValuedSortedBinaryDocValues.java @@ -0,0 +1,59 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.fielddata; + +import org.apache.lucene.index.BinaryDocValues; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.io.stream.ByteArrayStreamInput; + +import java.io.IOException; + +/** + * Wrapper around {@link BinaryDocValues} to decode the typical multivalued encoding used by + * {@link org.elasticsearch.index.mapper.BinaryFieldMapper.CustomBinaryDocValuesField}. + */ +public class MultiValuedSortedBinaryDocValues extends SortedBinaryDocValues { + + BinaryDocValues values; + int count; + final ByteArrayStreamInput in = new ByteArrayStreamInput(); + final BytesRef scratch = new BytesRef(); + + public MultiValuedSortedBinaryDocValues(BinaryDocValues values) { + this.values = values; + } + + @Override + public boolean advanceExact(int doc) throws IOException { + if (values.advanceExact(doc)) { + final BytesRef bytes = values.binaryValue(); + assert bytes.length > 0; + in.reset(bytes.bytes, bytes.offset, bytes.length); + count = in.readVInt(); + scratch.bytes = bytes.bytes; + return true; + } else { + return false; + } + } + + @Override + public int docValueCount() { + return count; + } + + @Override + public BytesRef nextValue() throws IOException { + scratch.length = in.readVInt(); + scratch.offset = in.getPosition(); + in.setPosition(scratch.offset + scratch.length); + return scratch; + } +} diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/plain/AbstractBinaryDVLeafFieldData.java b/server/src/main/java/org/elasticsearch/index/fielddata/plain/AbstractBinaryDVLeafFieldData.java deleted file mode 100644 index 63fa0e21c6cb6..0000000000000 --- a/server/src/main/java/org/elasticsearch/index/fielddata/plain/AbstractBinaryDVLeafFieldData.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.index.fielddata.plain; - -import org.apache.lucene.index.BinaryDocValues; -import org.apache.lucene.util.BytesRef; -import org.elasticsearch.common.io.stream.ByteArrayStreamInput; -import org.elasticsearch.index.fielddata.LeafFieldData; -import org.elasticsearch.index.fielddata.SortedBinaryDocValues; - -import java.io.IOException; - -abstract class AbstractBinaryDVLeafFieldData implements LeafFieldData { - private final BinaryDocValues values; - - AbstractBinaryDVLeafFieldData(BinaryDocValues values) { - super(); - this.values = values; - } - - @Override - public long ramBytesUsed() { - return 0; // not exposed by Lucene - } - - @Override - public SortedBinaryDocValues getBytesValues() { - return new SortedBinaryDocValues() { - - int count; - final ByteArrayStreamInput in = new ByteArrayStreamInput(); - final BytesRef scratch = new BytesRef(); - - @Override - public boolean advanceExact(int doc) throws IOException { - if (values.advanceExact(doc)) { - final BytesRef bytes = values.binaryValue(); - assert bytes.length > 0; - in.reset(bytes.bytes, bytes.offset, bytes.length); - count = in.readVInt(); - scratch.bytes = bytes.bytes; - return true; - } else { - return false; - } - } - - @Override - public int docValueCount() { - return count; - } - - @Override - public BytesRef nextValue() throws IOException { - scratch.length = in.readVInt(); - scratch.offset = in.getPosition(); - in.setPosition(scratch.offset + scratch.length); - return scratch; - } - - }; - } - -} diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/plain/BytesBinaryDVLeafFieldData.java b/server/src/main/java/org/elasticsearch/index/fielddata/plain/BytesBinaryDVLeafFieldData.java deleted file mode 100644 index 9bb6062fb08c9..0000000000000 --- a/server/src/main/java/org/elasticsearch/index/fielddata/plain/BytesBinaryDVLeafFieldData.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.index.fielddata.plain; - -import org.apache.lucene.index.BinaryDocValues; -import org.elasticsearch.script.field.BinaryDocValuesField; -import org.elasticsearch.script.field.DocValuesScriptFieldFactory; - -final class BytesBinaryDVLeafFieldData extends AbstractBinaryDVLeafFieldData { - BytesBinaryDVLeafFieldData(BinaryDocValues values) { - super(values); - } - - @Override - public DocValuesScriptFieldFactory getScriptFieldFactory(String name) { - return new BinaryDocValuesField(getBytesValues(), name); - } -} diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/plain/BytesBinaryIndexFieldData.java b/server/src/main/java/org/elasticsearch/index/fielddata/plain/BytesBinaryIndexFieldData.java index f5d4bf837f062..043857ae0a0fc 100644 --- a/server/src/main/java/org/elasticsearch/index/fielddata/plain/BytesBinaryIndexFieldData.java +++ b/server/src/main/java/org/elasticsearch/index/fielddata/plain/BytesBinaryIndexFieldData.java @@ -17,7 +17,9 @@ import org.elasticsearch.index.fielddata.IndexFieldData; import org.elasticsearch.index.fielddata.IndexFieldData.XFieldComparatorSource.Nested; import org.elasticsearch.index.fielddata.IndexFieldDataCache; +import org.elasticsearch.index.fielddata.SortedBinaryDocValues; import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.script.field.ToScriptFieldFactory; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.MultiValueMode; import org.elasticsearch.search.aggregations.support.ValuesSourceType; @@ -26,14 +28,20 @@ import java.io.IOException; -public class BytesBinaryIndexFieldData implements IndexFieldData { +public class BytesBinaryIndexFieldData implements IndexFieldData { protected final String fieldName; protected final ValuesSourceType valuesSourceType; + protected final ToScriptFieldFactory toScriptFieldFactory; - public BytesBinaryIndexFieldData(String fieldName, ValuesSourceType valuesSourceType) { + public BytesBinaryIndexFieldData( + String fieldName, + ValuesSourceType valuesSourceType, + ToScriptFieldFactory toScriptFieldFactory + ) { this.fieldName = fieldName; this.valuesSourceType = valuesSourceType; + this.toScriptFieldFactory = toScriptFieldFactory; } @Override @@ -66,32 +74,34 @@ public BucketedSort newBucketedSort( } @Override - public BytesBinaryDVLeafFieldData load(LeafReaderContext context) { + public MultiValuedBinaryDVLeafFieldData load(LeafReaderContext context) { try { - return new BytesBinaryDVLeafFieldData(DocValues.getBinary(context.reader(), fieldName)); + return new MultiValuedBinaryDVLeafFieldData(DocValues.getBinary(context.reader(), fieldName), toScriptFieldFactory); } catch (IOException e) { throw new IllegalStateException("Cannot load doc values", e); } } @Override - public BytesBinaryDVLeafFieldData loadDirect(LeafReaderContext context) { + public MultiValuedBinaryDVLeafFieldData loadDirect(LeafReaderContext context) { return load(context); } public static class Builder implements IndexFieldData.Builder { private final String name; + private final ToScriptFieldFactory toScriptFieldFactory; private final ValuesSourceType valuesSourceType; - public Builder(String name, ValuesSourceType valuesSourceType) { + public Builder(String name, ValuesSourceType valuesSourceType, ToScriptFieldFactory toScriptFieldFactory) { this.name = name; this.valuesSourceType = valuesSourceType; + this.toScriptFieldFactory = toScriptFieldFactory; } @Override public IndexFieldData build(IndexFieldDataCache cache, CircuitBreakerService breakerService) { // Ignore breaker - return new BytesBinaryIndexFieldData(name, valuesSourceType); + return new BytesBinaryIndexFieldData(name, valuesSourceType, toScriptFieldFactory); } } } diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/plain/StringBinaryDVLeafFieldData.java b/server/src/main/java/org/elasticsearch/index/fielddata/plain/MultiValuedBinaryDVLeafFieldData.java similarity index 57% rename from server/src/main/java/org/elasticsearch/index/fielddata/plain/StringBinaryDVLeafFieldData.java rename to server/src/main/java/org/elasticsearch/index/fielddata/plain/MultiValuedBinaryDVLeafFieldData.java index 032ea2a022b25..eb73e423c1c42 100644 --- a/server/src/main/java/org/elasticsearch/index/fielddata/plain/StringBinaryDVLeafFieldData.java +++ b/server/src/main/java/org/elasticsearch/index/fielddata/plain/MultiValuedBinaryDVLeafFieldData.java @@ -10,22 +10,35 @@ package org.elasticsearch.index.fielddata.plain; import org.apache.lucene.index.BinaryDocValues; +import org.elasticsearch.index.fielddata.LeafFieldData; +import org.elasticsearch.index.fielddata.MultiValuedSortedBinaryDocValues; import org.elasticsearch.index.fielddata.SortedBinaryDocValues; import org.elasticsearch.script.field.DocValuesScriptFieldFactory; import org.elasticsearch.script.field.ToScriptFieldFactory; -final class StringBinaryDVLeafFieldData extends AbstractBinaryDVLeafFieldData { +public final class MultiValuedBinaryDVLeafFieldData implements LeafFieldData { + private final BinaryDocValues values; + private final ToScriptFieldFactory toScriptFieldFactory; - protected final ToScriptFieldFactory toScriptFieldFactory; + MultiValuedBinaryDVLeafFieldData(BinaryDocValues values, ToScriptFieldFactory toScriptFieldFactory) { + super(); + this.values = values; + this.toScriptFieldFactory = toScriptFieldFactory; + } - StringBinaryDVLeafFieldData(BinaryDocValues values, ToScriptFieldFactory toScriptFieldFactory) { - super(values); + @Override + public long ramBytesUsed() { + return 0; // not exposed by Lucene + } - this.toScriptFieldFactory = toScriptFieldFactory; + @Override + public SortedBinaryDocValues getBytesValues() { + return new MultiValuedSortedBinaryDocValues(values); } @Override public DocValuesScriptFieldFactory getScriptFieldFactory(String name) { return toScriptFieldFactory.getScriptFieldFactory(getBytesValues(), name); } + } diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/plain/StringBinaryIndexFieldData.java b/server/src/main/java/org/elasticsearch/index/fielddata/plain/StringBinaryIndexFieldData.java index 7c8b058cb01f1..012697e750227 100644 --- a/server/src/main/java/org/elasticsearch/index/fielddata/plain/StringBinaryIndexFieldData.java +++ b/server/src/main/java/org/elasticsearch/index/fielddata/plain/StringBinaryIndexFieldData.java @@ -26,7 +26,7 @@ import java.io.IOException; -public class StringBinaryIndexFieldData implements IndexFieldData { +public class StringBinaryIndexFieldData implements IndexFieldData { protected final String fieldName; protected final ValuesSourceType valuesSourceType; @@ -59,9 +59,9 @@ public SortField sortField(Object missingValue, MultiValueMode sortMode, Nested } @Override - public StringBinaryDVLeafFieldData load(LeafReaderContext context) { + public MultiValuedBinaryDVLeafFieldData load(LeafReaderContext context) { try { - return new StringBinaryDVLeafFieldData(DocValues.getBinary(context.reader(), fieldName), toScriptFieldFactory); + return new MultiValuedBinaryDVLeafFieldData(DocValues.getBinary(context.reader(), fieldName), toScriptFieldFactory); } catch (IOException e) { throw new IllegalStateException("Cannot load doc values", e); } @@ -82,7 +82,7 @@ public BucketedSort newBucketedSort( } @Override - public StringBinaryDVLeafFieldData loadDirect(LeafReaderContext context) { + public MultiValuedBinaryDVLeafFieldData loadDirect(LeafReaderContext context) { return load(context); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/BinaryDocValuesSyntheticFieldLoaderLayer.java b/server/src/main/java/org/elasticsearch/index/mapper/BinaryDocValuesSyntheticFieldLoaderLayer.java index 1f0c0be1f9555..ac0f102094146 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/BinaryDocValuesSyntheticFieldLoaderLayer.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/BinaryDocValuesSyntheticFieldLoaderLayer.java @@ -9,76 +9,64 @@ package org.elasticsearch.index.mapper; -import org.apache.lucene.index.BinaryDocValues; import org.apache.lucene.index.LeafReader; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.common.io.stream.ByteArrayStreamInput; +import org.elasticsearch.index.fielddata.MultiValuedSortedBinaryDocValues; +import org.elasticsearch.index.fielddata.SortedBinaryDocValues; import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; public final class BinaryDocValuesSyntheticFieldLoaderLayer implements CompositeSyntheticFieldLoader.DocValuesLayer { - private final String fieldName; + private final String name; + private SortedBinaryDocValues bytesValues; + private boolean hasValue; - // the binary doc values for a document are all encoded in a single binary array, which this stream knows how to read - // the doc values in the array take the form of [doc value count][length of value 1][value 1][length of value 2][value 2]... - private final ByteArrayStreamInput stream; - private int valueCount; + public BinaryDocValuesSyntheticFieldLoaderLayer(String name) { + this.name = name; + } - public BinaryDocValuesSyntheticFieldLoaderLayer(String fieldName) { - this.fieldName = fieldName; - this.stream = new ByteArrayStreamInput(); + @Override + public long valueCount() { + return bytesValues == null ? 0 : bytesValues.docValueCount(); } @Override public DocValuesLoader docValuesLoader(LeafReader leafReader, int[] docIdsInLeaf) throws IOException { - BinaryDocValues docValues = leafReader.getBinaryDocValues(fieldName); - - // there are no values associated with this field + var docValues = leafReader.getBinaryDocValues(name); if (docValues == null) { - valueCount = 0; + bytesValues = null; return null; } - return docId -> { - // there are no more documents to process - if (docValues.advanceExact(docId) == false) { - valueCount = 0; - return false; - } + bytesValues = new MultiValuedSortedBinaryDocValues(docValues); - // otherwise, extract the doc values into a stream to later read from - BytesRef docValuesBytes = docValues.binaryValue(); - stream.reset(docValuesBytes.bytes, docValuesBytes.offset, docValuesBytes.length); - valueCount = stream.readVInt(); - - return hasValue(); + return docId -> { + hasValue = bytesValues.advanceExact(docId); + return hasValue; }; } - @Override - public void write(XContentBuilder b) throws IOException { - for (int i = 0; i < valueCount; i++) { - // this function already knows how to decode the underlying bytes array, so no need to explicitly call VInt() - BytesRef valueBytes = stream.readBytesRef(); - b.value(valueBytes.utf8ToString()); - } - } - @Override public boolean hasValue() { - return valueCount > 0; + return hasValue; } @Override - public long valueCount() { - return valueCount; + public void write(XContentBuilder b) throws IOException { + if (hasValue == false) { + return; + } + + for (int i = 0; i < bytesValues.docValueCount(); ++i) { + BytesRef value = bytesValues.nextValue(); + b.utf8Value(value.bytes, value.offset, value.length); + } } @Override public String fieldName() { - return fieldName; + return name; } - } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/BinaryFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/BinaryFieldMapper.java index 01daf1bf6d392..6db6a5b00755c 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/BinaryFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/BinaryFieldMapper.java @@ -22,6 +22,7 @@ import org.elasticsearch.index.fielddata.IndexFieldData; import org.elasticsearch.index.fielddata.plain.BytesBinaryIndexFieldData; import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.script.field.BinaryDocValuesField; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; import org.elasticsearch.xcontent.XContentBuilder; @@ -127,7 +128,7 @@ public BytesReference valueForDisplay(Object value) { @Override public IndexFieldData.Builder fielddataBuilder(FieldDataContext fieldDataContext) { failIfNoDocValues(); - return new BytesBinaryIndexFieldData.Builder(name(), CoreValuesSourceType.KEYWORD); + return new BytesBinaryIndexFieldData.Builder(name(), CoreValuesSourceType.KEYWORD, BinaryDocValuesField::new); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java index 14c1232696976..b235f837b6970 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java @@ -617,7 +617,7 @@ public Builder add(FieldMapper.Builder builder) { if (builder instanceof KeywordFieldMapper.Builder kwd) { if ((kwd.hasNormalizer() == false || kwd.isNormalizerSkipStoreOriginalValue()) - && (kwd.hasDocValues() || kwd.isStored())) { + && (kwd.docValuesParameters().enabled || kwd.isStored())) { hasSyntheticSourceCompatibleKeywordField = true; } } @@ -778,7 +778,7 @@ public interface SerializerCheck { * A configurable parameter for a field mapper * @param the type of the value the parameter holds */ - public static final class Parameter implements Supplier { + public static class Parameter implements Supplier { public final String name; private List deprecatedNames = List.of(); @@ -1405,6 +1405,84 @@ public static Parameter onScriptErrorParam( } } + public static final class DocValuesParameter extends Parameter { + public static final String PARAMETER_NAME = "doc_values"; + + public record Values(boolean enabled, Cardinality cardinality) { + public enum Cardinality { + LOW, + HIGH; + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } + } + + public static Values DISABLED = new Values(false, Cardinality.LOW); + } + + public final Parameter cardinalityParameter; + + public DocValuesParameter(Values defaultValue, Function initializer) { + super(PARAMETER_NAME, false, () -> defaultValue, null, initializer, null, Values::toString); + + cardinalityParameter = Parameter.enumParam( + "cardinality", + false, + m -> initializer.apply(m).cardinality, + defaultValue.cardinality, + Values.Cardinality.class + ); + } + + @Override + public void parse(String field, MappingParserContext context, Object value) { + if (value instanceof Boolean valueBool) { + if (valueBool) { + setValue(getDefaultValue()); + } else { + setValue(Values.DISABLED); + } + } else if (value instanceof String) { + if (value.equals("true")) { + setValue(getDefaultValue()); + } else if (value.equals("false")) { + setValue(Values.DISABLED); + } else { + throw new IllegalArgumentException("Illegal value [" + value + "] for parameter [" + name + "]"); + } + } else if (value instanceof Map) { + @SuppressWarnings("unchecked") + Map valueMap = (Map) value; + cardinalityParameter.parse(field, context, valueMap.get(cardinalityParameter.name)); + + setValue(new Values(true, cardinalityParameter.getValue())); + } else { + throw new IllegalArgumentException("Illegal value [" + value + "] for parameter [" + name + "]"); + } + } + + @Override + public void setValue(Values value) { + super.setValue(value); + cardinalityParameter.setValue(value.cardinality); + } + + protected void toXContent(XContentBuilder builder, boolean includeDefaults) throws IOException { + Values value = getValue(); + if (includeDefaults || isConfigured()) { + if (value.enabled == false) { + builder.field(name, false); + } else { + builder.startObject(name); + builder.field(cardinalityParameter.name, value.cardinality); + builder.endObject(); + } + } + } + } + public static final class Conflicts { private final String mapperName; @@ -1499,11 +1577,7 @@ protected final void validate() { @Override public abstract FieldMapper build(MapperBuilderContext context); - protected void addScriptValidation( - Parameter