Can use @ScriptedFields annotated property as ctor parameter in records and Kotlin data classes.

Original Pull Request #1802 
Closes #1488
This commit is contained in:
Peter-Josef Meisch 2021-05-07 08:14:13 +02:00 committed by GitHub
parent 159687e241
commit be93ebd6a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 163 additions and 44 deletions

View File

@ -23,7 +23,6 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -63,36 +62,38 @@ import com.fasterxml.jackson.core.JsonGenerator;
* @author Matt Gilene
* @since 4.0
*/
public class DocumentAdapters {
public final class DocumentAdapters {
private DocumentAdapters() {}
/**
* Create a {@link Document} from {@link GetResponse}.
* <p>
* Returns a {@link Document} using the source if available.
* Returns a {@link Document} using the getResponse if available.
*
* @param source the source {@link GetResponse}.
* @return the adapted {@link Document}, null if source.isExists() returns false.
* @param getResponse the getResponse {@link GetResponse}.
* @return the adapted {@link Document}, null if getResponse.isExists() returns false.
*/
@Nullable
public static Document from(GetResponse source) {
public static Document from(GetResponse getResponse) {
Assert.notNull(source, "GetResponse must not be null");
Assert.notNull(getResponse, "GetResponse must not be null");
if (!source.isExists()) {
if (!getResponse.isExists()) {
return null;
}
if (source.isSourceEmpty()) {
return fromDocumentFields(source, source.getIndex(), source.getId(), source.getVersion(), source.getSeqNo(),
source.getPrimaryTerm());
if (getResponse.isSourceEmpty()) {
return fromDocumentFields(getResponse, getResponse.getIndex(), getResponse.getId(), getResponse.getVersion(),
getResponse.getSeqNo(), getResponse.getPrimaryTerm());
}
Document document = Document.from(source.getSourceAsMap());
document.setIndex(source.getIndex());
document.setId(source.getId());
document.setVersion(source.getVersion());
document.setSeqNo(source.getSeqNo());
document.setPrimaryTerm(source.getPrimaryTerm());
Document document = Document.from(getResponse.getSourceAsMap());
document.setIndex(getResponse.getIndex());
document.setId(getResponse.getId());
document.setVersion(getResponse.getVersion());
document.setSeqNo(getResponse.getSeqNo());
document.setPrimaryTerm(getResponse.getPrimaryTerm());
return document;
}
@ -188,9 +189,10 @@ public class DocumentAdapters {
if (sourceRef == null || sourceRef.length() == 0) {
return new SearchDocumentAdapter(
source.getScore(), source.getSortValues(), source.getFields(), highlightFields, fromDocumentFields(source,
source.getIndex(), source.getId(), source.getVersion(), source.getSeqNo(), source.getPrimaryTerm()),
innerHits, nestedMetaData, explanation, matchedQueries);
fromDocumentFields(source, source.getIndex(), source.getId(), source.getVersion(), source.getSeqNo(),
source.getPrimaryTerm()),
source.getScore(), source.getSortValues(), source.getFields(), highlightFields, innerHits, nestedMetaData,
explanation, matchedQueries);
}
Document document = Document.from(source.getSourceAsMap());
@ -203,8 +205,8 @@ public class DocumentAdapters {
document.setSeqNo(source.getSeqNo());
document.setPrimaryTerm(source.getPrimaryTerm());
return new SearchDocumentAdapter(source.getScore(), source.getSortValues(), source.getFields(), highlightFields,
document, innerHits, nestedMetaData, explanation, matchedQueries);
return new SearchDocumentAdapter(document, source.getScore(), source.getSortValues(), source.getFields(),
highlightFields, innerHits, nestedMetaData, explanation, matchedQueries);
}
@Nullable
@ -243,6 +245,10 @@ public class DocumentAdapters {
*
* @param documentFields the {@link DocumentField}s backing the {@link Document}.
* @param index the index where the Document was found
* @param id the document id
* @param version the document version
* @param seqNo the seqNo if the document
* @param primaryTerm the primaryTerm of the document
* @return the adapted {@link Document}.
*/
public static Document fromDocumentFields(Iterable<DocumentField> documentFields, String index, String id,
@ -261,10 +267,13 @@ public class DocumentAdapters {
return new DocumentFieldAdapter(fields, index, id, version, seqNo, primaryTerm);
}
// TODO: Performance regarding keys/values/entry-set
/**
* Adapter for a collection of {@link DocumentField}s.
*/
static class DocumentFieldAdapter implements Document {
private final Collection<DocumentField> documentFields;
private final Map<String, DocumentField> documentFieldMap;
private final String index;
private final String id;
private final long version;
@ -274,6 +283,8 @@ public class DocumentAdapters {
DocumentFieldAdapter(Collection<DocumentField> documentFields, String index, String id, long version, long seqNo,
long primaryTerm) {
this.documentFields = documentFields;
this.documentFieldMap = new LinkedHashMap<>(documentFields.size());
documentFields.forEach(documentField -> documentFieldMap.put(documentField.getName(), documentField));
this.index = index;
this.id = id;
this.version = version;
@ -353,14 +364,7 @@ public class DocumentAdapters {
@Override
public boolean containsKey(Object key) {
for (DocumentField documentField : documentFields) {
if (documentField.getName().equals(key)) {
return true;
}
}
return false;
return documentFieldMap.containsKey(key);
}
@Override
@ -380,11 +384,9 @@ public class DocumentAdapters {
@Override
@Nullable
public Object get(Object key) {
return documentFields.stream() //
.filter(documentField -> documentField.getName().equals(key)) //
.map(DocumentField::getValue).findFirst() //
.orElse(null); //
DocumentField documentField = documentFieldMap.get(key);
return documentField != null ? documentField.getValue() : null;
}
@Override
@ -409,17 +411,18 @@ public class DocumentAdapters {
@Override
public Set<String> keySet() {
return documentFields.stream().map(DocumentField::getName).collect(Collectors.toCollection(LinkedHashSet::new));
return documentFieldMap.keySet();
}
@Override
public Collection<Object> values() {
return documentFields.stream().map(DocumentFieldAdapter::getValue).collect(Collectors.toList());
return documentFieldMap.values().stream().map(DocumentFieldAdapter::getValue).collect(Collectors.toList());
}
@Override
public Set<Entry<String, Object>> entrySet() {
return documentFields.stream().collect(Collectors.toMap(DocumentField::getName, DocumentFieldAdapter::getValue))
return documentFieldMap.entrySet().stream()
.collect(Collectors.toMap(Entry::getKey, entry -> DocumentFieldAdapter.getValue(entry.getValue())))
.entrySet();
}
@ -458,7 +461,6 @@ public class DocumentAdapters {
}
}
@Override
public String toString() {
return getClass().getSimpleName() + '@' + this.id + '#' + this.version + ' ' + toJson();
@ -494,14 +496,14 @@ public class DocumentAdapters {
@Nullable private final Explanation explanation;
@Nullable private final List<String> matchedQueries;
SearchDocumentAdapter(float score, Object[] sortValues, Map<String, DocumentField> fields,
Map<String, List<String>> highlightFields, Document delegate, Map<String, SearchDocumentResponse> innerHits,
SearchDocumentAdapter(Document delegate, float score, Object[] sortValues, Map<String, DocumentField> fields,
Map<String, List<String>> highlightFields, Map<String, SearchDocumentResponse> innerHits,
@Nullable NestedMetaData nestedMetaData, @Nullable Explanation explanation,
@Nullable List<String> matchedQueries) {
this.delegate = delegate;
this.score = score;
this.sortValues = sortValues;
this.delegate = delegate;
fields.forEach((name, documentField) -> this.fields.put(name, documentField.getValues()));
this.highlightFields.putAll(highlightFields);
this.innerHits.putAll(innerHits);
@ -646,7 +648,13 @@ public class DocumentAdapters {
@Override
public Object get(Object key) {
return delegate.get(key);
if (delegate.containsKey(key)) {
return delegate.get(key);
}
// fallback to fields
return fields.get(key);
}
@Override

View File

@ -0,0 +1,28 @@
/*
* Copyright 2019-2021 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.mapping;
import org.springframework.data.mapping.model.CamelCaseSplittingFieldNamingStrategy;
/**
* @author Peter-Josef Meisch
* @since 4.3
*/
public class KebabCaseFieldNamingStrategy extends CamelCaseSplittingFieldNamingStrategy {
public KebabCaseFieldNamingStrategy() {
super("-");
}
}

View File

@ -19,6 +19,7 @@ import static java.util.Collections.*;
import static org.assertj.core.api.Assertions.*;
import static org.elasticsearch.index.query.QueryBuilders.*;
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
import static org.springframework.data.elasticsearch.annotations.FieldType.Integer;
import static org.springframework.data.elasticsearch.core.document.Document.*;
import static org.springframework.data.elasticsearch.utils.IdGenerator.*;
import static org.springframework.data.elasticsearch.utils.IndexBuilder.*;
@ -3585,6 +3586,31 @@ public abstract class ElasticsearchTemplateTests {
assertThat(retrieved).isEqualTo(saved);
}
@Test // #1488
@DisplayName("should set scripted fields on immutable objects")
void shouldSetScriptedFieldsOnImmutableObjects() {
ImmutableWithScriptedEntity entity = new ImmutableWithScriptedEntity("42", 42, null);
operations.save(entity);
Map<String, Object> params = new HashMap<>();
params.put("factor", 2);
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery())
.withSourceFilter(new FetchSourceFilter(new String[] { "*" }, new String[] {}))
.withScriptField(
new ScriptField("scriptedRate", new Script(ScriptType.INLINE, "expression", "doc['rate'] * factor", params)))
.build();
SearchHits<ImmutableWithScriptedEntity> searchHits = operations.search(searchQuery,
ImmutableWithScriptedEntity.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1);
ImmutableWithScriptedEntity foundEntity = searchHits.getSearchHit(0).getContent();
assertThat(foundEntity.getId()).isEqualTo("42");
assertThat(foundEntity.getRate()).isEqualTo(42);
assertThat(foundEntity.getScriptedRate()).isEqualTo(84.0);
}
// region entities
@Document(indexName = INDEX_NAME_SAMPLE_ENTITY)
@Setting(shards = 1, replicas = 0, refreshInterval = "-1")
@ -4366,7 +4392,7 @@ public abstract class ElasticsearchTemplateTests {
@Document(indexName = "immutable-class")
private static final class ImmutableEntity {
@Id private final String id;
@Id @Nullable private final String id;
@Field(type = FieldType.Text) private final String text;
@Nullable private final SeqNoPrimaryTerm seqNoPrimaryTerm;
@ -4376,6 +4402,7 @@ public abstract class ElasticsearchTemplateTests {
this.seqNoPrimaryTerm = seqNoPrimaryTerm;
}
@Nullable
public String getId() {
return id;
}
@ -4419,5 +4446,61 @@ public abstract class ElasticsearchTemplateTests {
+ seqNoPrimaryTerm + '}';
}
}
@Document(indexName = "immutable-scripted")
public static final class ImmutableWithScriptedEntity {
@Id private final String id;
@Field(type = Integer) @Nullable private final int rate;
@Nullable @ScriptedField private final Double scriptedRate;
public ImmutableWithScriptedEntity(String id, int rate, @Nullable java.lang.Double scriptedRate) {
this.id = id;
this.rate = rate;
this.scriptedRate = scriptedRate;
}
public String getId() {
return id;
}
public int getRate() {
return rate;
}
@Nullable
public Double getScriptedRate() {
return scriptedRate;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ImmutableWithScriptedEntity that = (ImmutableWithScriptedEntity) o;
if (rate != that.rate)
return false;
if (!id.equals(that.id))
return false;
return scriptedRate != null ? scriptedRate.equals(that.scriptedRate) : that.scriptedRate == null;
}
@Override
public int hashCode() {
int result = id.hashCode();
result = 31 * result + rate;
result = 31 * result + (scriptedRate != null ? scriptedRate.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "ImmutableWithScriptedEntity{" + "id='" + id + '\'' + ", rate=" + rate + ", scriptedRate=" + scriptedRate
+ '}';
}
}
// endregion
}