Skip to content

Support for DynamicTemplates. #2969

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -252,4 +252,11 @@
* @since 5.4
*/
String mappedTypeName() default "";

/**
* Maps your data beyond the dynamic field mapping rules.
*
* @since 5.4
*/
boolean dynamicTemplate() default false;
}
Original file line number Diff line number Diff line change
@@ -80,7 +80,6 @@ public IndicesTemplate(ElasticsearchIndicesClient client, ClusterTemplate cluste
this.elasticsearchConverter = elasticsearchConverter;
this.boundClass = boundClass;
this.boundIndex = null;

}

public IndicesTemplate(ElasticsearchIndicesClient client, ClusterTemplate clusterTemplate,
@@ -95,7 +94,6 @@ public IndicesTemplate(ElasticsearchIndicesClient client, ClusterTemplate cluste
this.elasticsearchConverter = elasticsearchConverter;
this.boundClass = null;
this.boundIndex = boundIndex;

}

protected Class<?> checkForBoundClass() {
@@ -145,6 +143,8 @@ protected boolean doCreate(IndexCoordinates indexCoordinates, Map<String, Object

CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexSettings);
CreateIndexResponse createIndexResponse = execute(client -> client.create(createIndexRequest));
// refresh cached mappings
refreshMapping();
return Boolean.TRUE.equals(createIndexResponse.acknowledged());
}

@@ -241,6 +241,28 @@ public Map<String, Object> getMapping() {
return responseConverter.indicesGetMapping(getMappingResponse, indexCoordinates);
}

/**
* Refreshes the mapping for the current entity.
* <p>
* This method is responsible for retrieving and updating the metadata related to the current entity.
*/
private void refreshMapping() {
if (boundClass == null) {
return;
}

ElasticsearchPersistentEntity<?> entity = this.elasticsearchConverter.getMappingContext()
.getPersistentEntity(boundClass);
if (entity == null) {
return;
}

Object dynamicTemplates = getMapping().get("dynamic_templates");
if (dynamicTemplates instanceof List<?> value) {
entity.buildDynamicTemplates(value);
}
}

@Override
public Settings createSettings() {
return createSettings(checkForBoundClass());
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@
import reactor.core.publisher.Mono;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -142,7 +143,12 @@ private Mono<Boolean> doCreate(IndexCoordinates indexCoordinates, Map<String, Ob

CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexSettings);
Mono<CreateIndexResponse> createIndexResponse = Mono.from(execute(client -> client.create(createIndexRequest)));
return createIndexResponse.map(CreateIndexResponse::acknowledged);
return createIndexResponse
.doOnNext((result) -> {
// refresh cached mappings
refreshMapping();
})
.map(CreateIndexResponse::acknowledged);
}

@Override
@@ -218,6 +224,30 @@ public Mono<Document> getMapping() {
return getMappingResponse.map(response -> responseConverter.indicesGetMapping(response, indexCoordinates));
}

/**
* Refreshes the mapping for the current entity.
* <p>
* This method is responsible for retrieving and updating the metadata related to the current entity.
*/
private void refreshMapping() {
if (boundClass == null) {
return;
}

ElasticsearchPersistentEntity<?> entity = this.elasticsearchConverter.getMappingContext()
.getPersistentEntity(boundClass);
if (entity == null) {
return;
}

getMapping().subscribe((mappings) -> {
Object dynamicTemplates = mappings.get("dynamic_templates");
if (dynamicTemplates instanceof List<?> value) {
entity.buildDynamicTemplates(value);
}
});
}

@Override
public Mono<Settings> createSettings() {
return createSettings(checkForBoundClass());
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
*/
package org.springframework.data.elasticsearch.core.convert;

import static org.springframework.util.PatternMatchUtils.simpleMatch;

import java.time.temporal.TemporalAccessor;
import java.util.*;
import java.util.Map.Entry;
@@ -42,6 +44,7 @@
import org.springframework.data.elasticsearch.annotations.ScriptedField;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.mapping.DynamicTemplate;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.mapping.PropertyValueConverter;
@@ -388,6 +391,10 @@ private <R> R readEntity(ElasticsearchPersistentEntity<?> entity, Map<String, Ob
targetEntity.getPropertyAccessor(result).setProperty(property, seqNoPrimaryTerm);
}
}

if (targetEntity.hasDynamicTemplates()) {
populateFieldsUsingDynamicTemplates(targetEntity, result, document);
}
}

if (source instanceof SearchDocument searchDocument) {
@@ -664,6 +671,28 @@ private <T> void populateScriptFields(ElasticsearchPersistentEntity<?> entity, T
});
}

private <R> void populateFieldsUsingDynamicTemplates(ElasticsearchPersistentEntity<?> targetEntity, R result,
Document document) {
for (Entry<String, DynamicTemplate> templateEntry : targetEntity.getDynamicTemplates().entrySet()) {
ElasticsearchPersistentProperty property = targetEntity
.getPersistentPropertyWithFieldName(templateEntry.getKey());
if (property != null && property.isDynamicFieldMapping()) {
// prepare value
Map<String, Object> values = new HashMap<>();
// TODO: Path match and unmatched
document.entrySet().stream()
.filter(fieldKey -> templateEntry.getValue().getMatch().stream()
.anyMatch(regex -> simpleMatch(regex, fieldKey.getKey()))
&& templateEntry.getValue().getUnmatch().stream()
.noneMatch(regex -> simpleMatch(regex, fieldKey.getKey())))
.forEach(entry -> values.put(entry.getKey(), entry.getValue()));

// set property
targetEntity.getPropertyAccessor(result).setProperty(property, read(property.getType(), Document.from(values)));
}
}
}

/**
* Compute the type to use by checking the given entity against the store type;
*/
@@ -1035,7 +1064,14 @@ protected void writeProperty(ElasticsearchPersistentProperty property, Object va

if (valueType.isMap()) {
Map<String, Object> mapDbObj = createMap((Map<?, ?>) value, property);
sink.set(property, mapDbObj);
if (property.isDynamicFieldMapping()) {
for (Entry<String, Object> entry : mapDbObj.entrySet()) {
sink.set(entry.getKey(), entry.getValue());
}
} else {
sink.set(property, mapDbObj);
}

return;
}

@@ -1058,7 +1094,14 @@ protected void writeProperty(ElasticsearchPersistentProperty property, Object va

addCustomTypeKeyIfNecessary(value, document, TypeInformation.of(property.getRawType()));
writeInternal(value, document, entity);
sink.set(property, document);
if (property.isDynamicFieldMapping()) {
// flatten
for (Entry<String, Object> entry : document.entrySet()) {
sink.set(entry.getKey(), entry.getValue());
}
} else {
sink.set(property, document);
}
}

/**
@@ -1499,7 +1542,11 @@ public void set(ElasticsearchPersistentProperty property, @Nullable Object value
}
}

target.put(property.getFieldName(), value);
set(property.getFieldName(), value);
}

public void set(String key, @Nullable Object value) {
target.put(key, value);
}

private Map<String, Object> getAsMap(Object result) {
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package org.springframework.data.elasticsearch.core.mapping;

import java.util.ArrayList;
import java.util.List;

/**
* Immutable Value object encapsulating dynamic template(s).
* {@see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-templates.html">Elastic
* docs</a>}
*
* @author Youssef Aouichaoui
* @since 5.4
*/
public class DynamicTemplate {
/**
* Patterns to match on the field name.
*/
private final List<String> match;

/**
* Path patterns for a nested type to match the field name.
*/
private final List<String> pathMatch;

/**
* Patterns that do not match the field name.
*/
private final List<String> unmatch;

/**
* Path patterns for a nested type that do not match the field name.
*/
private final List<String> pathUnmatch;

/**
* Data types that correspond to the field.
*/
private final List<String> matchMappingType;

/**
* Data types that do not match to the field.
*/
private final List<String> unmatchMappingType;

private DynamicTemplate(Builder builder) {
this.match = builder.match;
this.pathMatch = builder.pathMatch;

this.unmatch = builder.unmatch;
this.pathUnmatch = builder.pathUnmatch;

this.matchMappingType = builder.matchMappingType;
this.unmatchMappingType = builder.unmatchMappingType;
}

public List<String> getMatch() {
return match;
}

public List<String> getPathMatch() {
return pathMatch;
}

public List<String> getUnmatch() {
return unmatch;
}

public List<String> getPathUnmatch() {
return pathUnmatch;
}

public List<String> getMatchMappingType() {
return matchMappingType;
}

public List<String> getUnmatchMappingType() {
return unmatchMappingType;
}

public boolean isRegexMatching() {
return false;
}

public static Builder builder() {
return new Builder();
}

public static class Builder {
private final List<String> match = new ArrayList<>();
private final List<String> pathMatch = new ArrayList<>();

private final List<String> unmatch = new ArrayList<>();
private final List<String> pathUnmatch = new ArrayList<>();

private final List<String> matchMappingType = new ArrayList<>();
private final List<String> unmatchMappingType = new ArrayList<>();

private Builder() {}

/**
* Patterns to match on the field name.
*/
public Builder withMatch(String... match) {
for (String value : match) {
if (value != null) {
parseValues(value, this.match);
}
}

return this;
}

/**
* Path patterns for a nested type to match the field name.
*/
public Builder withPathMatch(String... pathMatch) {
for (String value : pathMatch) {
if (value != null) {
parseValues(value, this.pathMatch);
}
}

return this;
}

/**
* Patterns that do not match the field name.
*/
public Builder withUnmatch(String... unmatch) {
for (String value : unmatch) {
if (value != null) {
parseValues(value, this.unmatch);
}
}

return this;
}

/**
* Path patterns for a nested type that do not match the field name.
*/
public Builder withPathUnmatch(String... pathUnmatch) {
for (String value : pathUnmatch) {
if (value != null) {
parseValues(value, this.pathUnmatch);
}
}

return this;
}

/**
* Data types that correspond to the field.
*/
public Builder withMatchMappingType(String... matchMappingType) {
for (String value : matchMappingType) {
if (value != null) {
parseValues(value, this.matchMappingType);
}
}

return this;
}

/**
* Data types that do not match to the field.
*/
public Builder withUnmatchMappingType(String... unmatchMappingType) {
for (String value : unmatchMappingType) {
if (value != null) {
parseValues(value, this.unmatchMappingType);
}
}

return this;
}

private void parseValues(String source, List<String> target) {
if (source.startsWith("[")) {
target.addAll(List.of(source.replace("[", "").replace("]", "").split(",", -1)));
} else {
target.add(source);
}
}

public DynamicTemplate build() {
return new DynamicTemplate(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
*/
package org.springframework.data.elasticsearch.core.mapping;

import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.data.elasticsearch.annotations.Document;
@@ -203,4 +205,25 @@ default ElasticsearchPersistentProperty getRequiredSeqNoPrimaryTermProperty() {
* @since 5.2
*/
boolean isAlwaysWriteMapping();

/**
* Retrieves the dynamic templates defined for the current document.
*
* @since 5.4
*/
Map<String, DynamicTemplate> getDynamicTemplates();

/**
* if the mapping should be written to the index on repository bootstrap even if the index already exists.
*
* @since 5.4
*/
boolean hasDynamicTemplates();

/**
* Building the dynamic templates for the current document.
*
* @since 5.4
*/
void buildDynamicTemplates(List<?> dynamicTemplates);
}
Original file line number Diff line number Diff line change
@@ -110,6 +110,13 @@ public interface ElasticsearchPersistentProperty extends PersistentProperty<Elas
*/
boolean isIndexedIndexNameProperty();

/**
* Maps your data beyond the dynamic field mapping rules.
*
* @since 5.4
*/
boolean isDynamicFieldMapping();

/**
* calls {@link #getActualType()} but returns null when an exception is thrown
*
Original file line number Diff line number Diff line change
@@ -15,7 +15,10 @@
*/
package org.springframework.data.elasticsearch.core.mapping;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -51,6 +54,8 @@
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import static java.util.Collections.unmodifiableMap;

/**
* Elasticsearch specific {@link org.springframework.data.mapping.PersistentEntity} implementation holding
*
@@ -85,6 +90,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
private @Nullable String routing;
private final ContextConfiguration contextConfiguration;
private final Set<Alias> aliases = new HashSet<>();
private final Map<String, DynamicTemplate> dynamicTemplates = new HashMap<>();

private final ConcurrentHashMap<String, Expression> indexNameExpressions = new ConcurrentHashMap<>();
private final Lazy<EvaluationContext> indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext);
@@ -627,6 +633,48 @@ public Dynamic dynamic() {
return dynamic;
}

@Override
public Map<String, DynamicTemplate> getDynamicTemplates() {
return unmodifiableMap(dynamicTemplates);
}

@Override
public boolean hasDynamicTemplates() {
return !dynamicTemplates.isEmpty();
}

@Override
public void buildDynamicTemplates(List<?> dynamicTemplates) {
for (Object dynamicTemplate : dynamicTemplates) {
if (dynamicTemplate instanceof Map<?, ?> template) {
template.forEach((name, value) -> {
if (value instanceof Map<?, ?> settings) {
this.dynamicTemplates.put((String) name,
DynamicTemplate.builder()
.withMatch(parseMapValue(settings.get("match")))
.withPathMatch(parseMapValue(settings.get("path_match")))
.withUnmatch(parseMapValue(settings.get("unmatch")))
.withPathUnmatch(parseMapValue(settings.get("path_unmatch")))
.withMatchMappingType(parseMapValue(settings.get("match_mapping_type")))
.withUnmatchMappingType(parseMapValue(settings.get("unmatch_mapping_type")))
.build());
}
});
}
}
}

/**
* Parses the provided value and converts it into an array of Strings.
*/
private String[] parseMapValue(@Nullable Object value) {
if (value instanceof List<?> values) {
return values.toArray(new String[0]);
}

return new String[0];
}

/**
* Building once the aliases for the current document.
*/
Original file line number Diff line number Diff line change
@@ -86,6 +86,7 @@ public class SimpleElasticsearchPersistentProperty extends
@Nullable private PropertyValueConverter propertyValueConverter;
private final boolean storeNullValue;
private final boolean storeEmptyValue;
private final boolean isDynamicFieldMapping;

public SimpleElasticsearchPersistentProperty(Property property,
PersistentEntity<?, ElasticsearchPersistentProperty> owner, SimpleTypeHolder simpleTypeHolder) {
@@ -114,6 +115,7 @@ public SimpleElasticsearchPersistentProperty(Property property,
: isMultiField && getRequiredAnnotation(MultiField.class).mainField().storeNullValue();
storeEmptyValue = isField ? getRequiredAnnotation(Field.class).storeEmptyValue()
: !isMultiField || getRequiredAnnotation(MultiField.class).mainField().storeEmptyValue();
isDynamicFieldMapping = isField && getRequiredAnnotation(Field.class).dynamicTemplate();
}

@Override
@@ -393,4 +395,9 @@ public boolean isCompletionProperty() {
public boolean isIndexedIndexNameProperty() {
return isAnnotationPresent(IndexedIndexName.class);
}

@Override
public boolean isDynamicFieldMapping() {
return isDynamicFieldMapping;
}
}
Original file line number Diff line number Diff line change
@@ -16,19 +16,14 @@

package org.springframework.data.elasticsearch.core.index;

import static java.util.UUID.randomUUID;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
import static org.springframework.data.elasticsearch.core.query.StringQuery.MATCH_ALL;

import java.time.Instant;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@@ -40,7 +35,10 @@
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.lang.Nullable;
@@ -280,6 +278,35 @@ void shouldWriteMappingWithFieldAliases() {
operations.indexOps(FieldAliasEntity.class).createWithMapping();
}

@Test
void shouldMapDynamicFields() {
// Given
IndexOperations documentOperations = operations.indexOps(DynamicFieldDocument.class);
documentOperations.createWithMapping();

DynamicFieldDocument document = new DynamicFieldDocument();
document.dynamicFields = Map.of("a_str", randomUUID().toString(), "b_str", randomUUID().toString());
document.value = new DynamicFieldDocument.Value(1L, new Date());
operations.save(document);

// When
SearchHits<DynamicFieldDocument> results = operations.search(new StringQuery(MATCH_ALL),
DynamicFieldDocument.class);

// Then
assertThat(results.getTotalHits()).isEqualTo(1);
assertThat(results.getSearchHits()).first()
.extracting(SearchHit::getContent)
.extracting(doc -> doc.dynamicFields)
.isEqualTo(document.dynamicFields);
assertThat(results.getSearchHits()).first()
.extracting(SearchHit::getContent)
.extracting(doc -> doc.value)
.isEqualTo(document.value);

documentOperations.delete();
}

// region Entities
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class Book {
@@ -933,5 +960,47 @@ private static class FieldAliasEntity {
@Field(type = Text) private String otherText;
}

@SuppressWarnings("unused")
@Document(indexName = "#{@indexNameProvider.indexName()}-foo")
@DynamicTemplates(mappingPath = "/mappings/test-dynamic_templates_mappings_three.json")
private static class DynamicFieldDocument {
@Nullable
@Id String id;

@Field(name = "_str", dynamicTemplate = true) private Map<String, String> dynamicFields = new HashMap<>();

@Nullable
@Field(name = "obj", dynamicTemplate = true) private Value value;

static class Value {
@Nullable
@Field(name = "value_sum", type = FieldType.Long)
private Long sum;

@Nullable
@Field(name = "value_date", type = FieldType.Long)
private Date date;

public Value() {
}

public Value(Long sum, Date date) {
this.sum = sum;
this.date = date;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Value value)) return false;
return Objects.equals(sum, value.sum) && Objects.equals(date, value.date);
}

@Override
public int hashCode() {
return Objects.hash(sum, date);
}
}
}
// endregion
}
Original file line number Diff line number Diff line change
@@ -18,12 +18,12 @@
import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*;
import static org.springframework.data.elasticsearch.core.query.StringQuery.MATCH_ALL;

import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;

import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.*;

import org.assertj.core.api.SoftAssertions;
import org.json.JSONException;
@@ -34,16 +34,20 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.DynamicTemplates;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.Setting;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.IndexOperationsAdapter;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ReactiveIndexOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.lang.Nullable;
@@ -415,6 +419,40 @@ void shouldDeleteTemplate() {
assertThat(exists).isFalse();
}

@Test
void shouldMapDynamicFields() {
// Given
IndexOperationsAdapter documentOperations = blocking(operations.indexOps(DynamicFieldDocument.class));
documentOperations.createWithMapping();

DynamicFieldDocument document = new DynamicFieldDocument();
document.dynamicFields = Map.of("a_str", UUID.randomUUID().toString(), "b_str", UUID.randomUUID().toString());
document.value = new DynamicFieldDocument.Value(1L, new Date());
operations.save(document).block();

// When
Flux<SearchHit<DynamicFieldDocument>> results = operations.search(new StringQuery(MATCH_ALL),
DynamicFieldDocument.class);

// Then
results.as(StepVerifier::create)
.expectNextMatches((hits) -> {
assertThat(hits)
.extracting(SearchHit::getContent)
.extracting(doc -> doc.dynamicFields)
.isEqualTo(document.dynamicFields);

assertThat(hits)
.extracting(SearchHit::getContent)
.extracting(doc -> doc.value)
.isEqualTo(document.value);

return true;
}).verifyComplete();

documentOperations.delete();
}

@Document(indexName = "#{@indexNameProvider.indexName()}")
@Setting(refreshInterval = "5s")
static class TemplateClass {
@@ -442,4 +480,46 @@ public void setMessage(@Nullable String message) {
}
}

@SuppressWarnings("unused")
@Document(indexName = "#{@indexNameProvider.indexName()}-foo")
@DynamicTemplates(mappingPath = "/mappings/test-dynamic_templates_mappings_three.json")
private static class DynamicFieldDocument {
@Nullable
@Id String id;

@Field(name = "_str", dynamicTemplate = true) private Map<String, String> dynamicFields = new HashMap<>();

@Nullable
@Field(name = "obj", dynamicTemplate = true) private Value value;

static class Value {
@Nullable
@Field(name = "value_sum", type = FieldType.Long)
private Long sum;

@Nullable
@Field(name = "value_date", type = FieldType.Long)
private Date date;

public Value() {
}

public Value(Long sum, Date date) {
this.sum = sum;
this.date = date;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Value value)) return false;
return Objects.equals(sum, value.sum) && Objects.equals(date, value.date);
}

@Override
public int hashCode() {
return Objects.hash(sum, date);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"dynamic_templates": [
{
"_str": {
"match": "*_str",
"mapping": {
"type": "keyword"
}
}
},
{
"obj": {
"match": "value_*",
"mapping": {
"type": "text",
"index": false
}
}
}
]
}