From 836187c1706838fe85448ff452ff78f789cf269f Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Sat, 14 Mar 2026 08:07:01 -0400 Subject: [PATCH] Allow to customize mappings parameters. Signed-off-by: Andriy Redko --- .../core/index/MappingBuilder.java | 8 +- .../core/index/MappingParameters.java | 137 +++++++++++++++++- .../index/MappingParametersCustomizer.java | 38 +++++ .../core/index/ReactiveMappingBuilder.java | 5 + .../core/MappingContextBaseTests.java | 6 + .../core/index/MappingBuilderUnitTests.java | 32 ++++ 6 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/index/MappingParametersCustomizer.java diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java index ceb3f645b..fdd663beb 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java @@ -112,9 +112,15 @@ public class MappingBuilder { protected final ElasticsearchConverter elasticsearchConverter; private final ObjectMapper objectMapper = new ObjectMapper(); + private final MappingParametersCustomizer customizer; public MappingBuilder(ElasticsearchConverter elasticsearchConverter) { + this(elasticsearchConverter, MappingParameters::from); + } + + public MappingBuilder(ElasticsearchConverter elasticsearchConverter, MappingParametersCustomizer customizer) { this.elasticsearchConverter = elasticsearchConverter; + this.customizer = customizer; } /** @@ -589,7 +595,7 @@ public class MappingBuilder { private void addFieldMappingParameters(ObjectNode fieldNode, Annotation annotation, boolean nestedOrObjectField) throws IOException { - MappingParameters mappingParameters = MappingParameters.from(annotation); + MappingParameters mappingParameters = customizer.from(annotation); if (!nestedOrObjectField && mappingParameters.isStore()) { fieldNode.put(FIELD_PARAM_STORE, true); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java index 42d472707..2136bdf1a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java @@ -35,6 +35,8 @@ import org.springframework.util.StringUtils; * A class to hold the mapping parameters that might be set on * {@link org.springframework.data.elasticsearch.annotations.Field } or * {@link org.springframework.data.elasticsearch.annotations.InnerField} annotation. + * The class allows extensibility (non-final) to simplify mapping parameters customization, + * provided by {@link org.springframework.data.elasticsearch.core.index.MappingParametersCustomizer}. * * @author Peter-Josef Meisch * @author Aleksei Arsenev @@ -42,9 +44,10 @@ import org.springframework.util.StringUtils; * @author Morgan Lutz * @author Sascha Woo * @author Haibo Liu + * @author Andriy Redko * @since 4.0 */ -public final class MappingParameters { +public class MappingParameters { static final String FIELD_PARAM_COERCE = "coerce"; static final String FIELD_PARAM_COPY_TO = "copy_to"; @@ -137,7 +140,7 @@ public final class MappingParameters { } } - private MappingParameters(Field field) { + protected MappingParameters(Field field) { index = field.index(); store = field.store(); fielddata = field.fielddata(); @@ -184,7 +187,7 @@ public final class MappingParameters { eagerGlobalOrdinals = field.eagerGlobalOrdinals(); } - private MappingParameters(InnerField field) { + protected MappingParameters(InnerField field) { index = field.index(); store = field.store(); fielddata = field.fielddata(); @@ -417,4 +420,132 @@ public final class MappingParameters { objectNode.put(FIELD_PARAM_EAGER_GLOBAL_ORDINALS, eagerGlobalOrdinals); } } + + protected String analyzer() { + return analyzer; + } + + protected boolean coerce() { + return coerce; + } + + protected String[] copyTo() { + return copyTo; + } + + protected DateFormat[] dateFormats() { + return dateFormats; + } + + protected String[] dateFormatPatterns() { + return dateFormatPatterns; + } + + protected boolean hasDocValues() { + return docValues; + } + + protected boolean hasEagerGlobalOrdinals() { + return eagerGlobalOrdinals; + } + + protected boolean isEnabled() { + return enabled; + } + + protected boolean hasFielddata() { + return fielddata; + } + + protected Integer ignoreAbove() { + return ignoreAbove; + } + + protected boolean isIgnoreMalformed() { + return ignoreMalformed; + } + + protected boolean isIndex() { + return index; + } + + protected IndexOptions indexOptions() { + return indexOptions; + } + + protected boolean isIndexPhrases() { + return indexPhrases; + } + + protected IndexPrefixes indexPrefixes() { + return indexPrefixes; + } + + protected String normalizer() { + return normalizer; + } + + protected boolean hasNorms() { + return norms; + } + + protected Integer maxShingleSize() { + return maxShingleSize; + } + + protected String nullValue() { + return nullValue; + } + + protected NullValueType nullValueType() { + return nullValueType; + } + + protected Integer positionIncrementGap() { + return positionIncrementGap; + } + + protected boolean positiveScoreImpact() { + return positiveScoreImpact; + } + + protected Integer dims() { + return dims; + } + + protected String elementType() { + return elementType; + } + + protected KnnSimilarity knnSimilarity() { + return knnSimilarity; + } + + protected KnnIndexOptions knnIndexOptions() { + return knnIndexOptions; + } + + protected String searchAnalyzer() { + return searchAnalyzer; + } + + protected double scalingFactor() { + return scalingFactor; + } + + protected String similarity() { + return similarity; + } + + protected TermVector termVector() { + return termVector; + } + + protected FieldType type() { + return type; + } + + protected String mappedTypeName() { + return mappedTypeName; + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParametersCustomizer.java b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParametersCustomizer.java new file mode 100644 index 000000000..88ef1143b --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParametersCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019-present the original author or authors. + * + * Licensed 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 + * + * https://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. + */ +package org.springframework.data.elasticsearch.core.index; + +import java.lang.annotation.Annotation; + +import org.jspecify.annotations.NonNull; + +/** + * Allows to customize {@link org.springframework.data.elasticsearch.core.index.MappingParameters} + * that are being emitted for each supported annotation. + * + * @author Andriy Redko + * @since 6.1.0 + */ +public interface MappingParametersCustomizer { + /** + * Customize @link org.springframework.data.elasticsearch.core.index.MappingParameters} + * for each supported annotation. + * + * @param annotation supported annotation + * @return customized @link org.springframework.data.elasticsearch.core.index.MappingParameters} + */ + @NonNull MappingParameters from(@NonNull Annotation annotation); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilder.java index 937e54657..b815f0f6b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilder.java @@ -31,6 +31,7 @@ import org.springframework.data.mapping.MappingException; * Subclass of {@link MappingBuilder} with specialized methods To inhibit blocking calls * * @author Peter-Josef Meisch + * @author Andriy Redko * @since 4.3 */ public class ReactiveMappingBuilder extends MappingBuilder { @@ -38,6 +39,10 @@ public class ReactiveMappingBuilder extends MappingBuilder { public ReactiveMappingBuilder(ElasticsearchConverter elasticsearchConverter) { super(elasticsearchConverter); } + + public ReactiveMappingBuilder(ElasticsearchConverter elasticsearchConverter, MappingParametersCustomizer customizer) { + super(elasticsearchConverter, customizer); + } @Override public String buildPropertyMapping(Class clazz) throws MappingException { diff --git a/src/test/java/org/springframework/data/elasticsearch/core/MappingContextBaseTests.java b/src/test/java/org/springframework/data/elasticsearch/core/MappingContextBaseTests.java index 792aba322..f357967d1 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/MappingContextBaseTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/MappingContextBaseTests.java @@ -19,11 +19,13 @@ import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationS import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; import org.springframework.data.elasticsearch.core.index.MappingBuilder; +import org.springframework.data.elasticsearch.core.index.MappingParametersCustomizer; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.util.Lazy; /** * @author Peter-Josef Meisch + * @author Andriy Redko */ public abstract class MappingContextBaseTests { @@ -42,6 +44,10 @@ public abstract class MappingContextBaseTests { return mappingContext; } + final protected MappingBuilder getMappingBuilder(MappingParametersCustomizer customizer) { + return new MappingBuilder(elasticsearchConverter.get(), customizer); + } + final protected MappingBuilder getMappingBuilder() { return new MappingBuilder(elasticsearchConverter.get()); } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java index 1ae831a77..63a5089e0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*; import static org.skyscreamer.jsonassert.JSONAssert.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; +import java.io.IOException; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; @@ -49,6 +50,8 @@ import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; import org.springframework.data.mapping.MappingException; +import tools.jackson.databind.node.ObjectNode; + /** * @author Stuart Stevenson * @author Jakub Vavrik @@ -1339,6 +1342,35 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests { assertEquals(expected, result, true); } + @Test // #1700 + @DisplayName("should allow mapping parameters tion") + void shouldAllowMappingParametersCustomization() throws JSONException { + String expected = """ + { + "properties": { + "my_vector": { + "type": "dense_vector", + "dimensions": 16 + } + } + } + """; + + final MappingParametersCustomizer customizer = annotation -> new MappingParameters((Field) annotation) { + @Override + public void writeTypeAndParametersTo(ObjectNode objectNode) throws IOException { + if (type() == FieldType.Dense_Vector) { + objectNode.put(FIELD_PARAM_TYPE, mappedTypeName()); + objectNode.put("dimensions", dims()); + } + } + }; + + String mapping = getMappingBuilder(customizer) + .buildPropertyMapping(DenseVectorEntity.class); + + assertEquals(expected, mapping, false); + } // region entities @Document(indexName = "ignore-above-index")