From 25fecf381197a5aba78dbd111f9e8a8abe742509 Mon Sep 17 00:00:00 2001 From: Rishabh Maurya Date: Fri, 3 May 2024 18:32:32 -0700 Subject: [PATCH] Unit test and code refactor Signed-off-by: Rishabh Maurya --- .../index/mapper/DerivedFieldResolver.java | 225 ++++++------------ .../index/mapper/DerivedObjectFieldType.java | 21 +- .../index/mapper/FieldTypeInference.java | 120 ++++++++++ .../index/mapper/FieldTypeInferenceTests.java | 142 +++++++++++ .../DerivedFieldFetchAndHighlightTests.java | 20 +- 5 files changed, 370 insertions(+), 158 deletions(-) create mode 100644 server/src/main/java/org/opensearch/index/mapper/FieldTypeInference.java create mode 100644 server/src/test/java/org/opensearch/index/mapper/FieldTypeInferenceTests.java diff --git a/server/src/main/java/org/opensearch/index/mapper/DerivedFieldResolver.java b/server/src/main/java/org/opensearch/index/mapper/DerivedFieldResolver.java index 8b111c00fecba..352ee1a0a7079 100644 --- a/server/src/main/java/org/opensearch/index/mapper/DerivedFieldResolver.java +++ b/server/src/main/java/org/opensearch/index/mapper/DerivedFieldResolver.java @@ -8,50 +8,71 @@ package org.opensearch.index.mapper; -import org.apache.lucene.index.LeafReaderContext; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.common.annotation.PublicApi; import org.opensearch.common.regex.Regex; -import org.opensearch.common.xcontent.json.JsonXContent; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.index.query.QueryShardContext; import org.opensearch.script.Script; -import org.opensearch.search.lookup.SourceLookup; import java.io.IOException; -import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; - -@PublicApi(since = "2.14.0") +@PublicApi(since = "2.15.0") public class DerivedFieldResolver { private final QueryShardContext queryShardContext; private final Map derivedFieldTypeMap = new ConcurrentHashMap<>(); private final FieldTypeInference typeInference; + private static final Logger logger = LogManager.getLogger(DerivedFieldResolver.class); public DerivedFieldResolver( QueryShardContext queryShardContext, Map derivedFieldsObject, List derivedFields + ) { + this( + queryShardContext, + derivedFieldsObject, + derivedFields, + new FieldTypeInference( + queryShardContext.index().getName(), + queryShardContext.getMapperService(), + queryShardContext.getIndexReader() + ) + ); + } + + public DerivedFieldResolver( + QueryShardContext queryShardContext, + Map derivedFieldsObject, + List derivedFields, + FieldTypeInference typeInference ) { this.queryShardContext = queryShardContext; + initializeDerivedFieldTypes(derivedFieldsObject); + initializeDerivedFieldTypesFromList(derivedFields); + this.typeInference = typeInference; + } + + private void initializeDerivedFieldTypes(Map derivedFieldsObject) { if (derivedFieldsObject != null) { Map derivedFieldObject = new HashMap<>(); derivedFieldObject.put(DerivedFieldMapper.CONTENT_TYPE, derivedFieldsObject); derivedFieldTypeMap.putAll(getAllDerivedFieldTypeFromObject(derivedFieldObject)); } + } + + private void initializeDerivedFieldTypesFromList(List derivedFields) { if (derivedFields != null) { for (DerivedField derivedField : derivedFields) { derivedFieldTypeMap.put(derivedField.getName(), getDerivedFieldType(derivedField)); } } - this.typeInference = new FieldTypeInference(queryShardContext); } public Set resolvePattern(String pattern) { @@ -65,60 +86,69 @@ public Set resolvePattern(String pattern) { } public MappedFieldType resolve(String fieldName) { - if (derivedFieldTypeMap.containsKey(fieldName)) { - return derivedFieldTypeMap.get(fieldName); + MappedFieldType fieldType = derivedFieldTypeMap.get(fieldName); + if (fieldType != null) { + return fieldType; } - MappedFieldType derivedFieldType = queryShardContext.getMapperService().fieldType(fieldName); - if (derivedFieldType != null) { - return derivedFieldType; + + fieldType = queryShardContext.getMapperService().fieldType(fieldName); + if (fieldType != null) { + return fieldType; } + if (fieldName.contains(".")) { - DerivedFieldType parentDerivedField = getParentDerivedField(fieldName); - if (parentDerivedField == null) { - return null; - } - String subFieldName = fieldName.substring(fieldName.indexOf(".") + 1); - Mapper inferredFieldMapper = typeInference.infer(subFieldName, parentDerivedField.derivedField.getScript()); - Mapper.BuilderContext builderContext = new Mapper.BuilderContext( - this.queryShardContext.getMapperService().getIndexSettings().getSettings(), - new ContentPath(1) - ); - derivedFieldType = new DerivedObjectFieldType( - new DerivedField( - fieldName, - inferredFieldMapper.typeName(), - parentDerivedField.derivedField.getScript(), - parentDerivedField.derivedField.getSourceIndexedField() - ), - DerivedFieldSupportedTypes.getFieldMapperFromType( - inferredFieldMapper.typeName(), - fieldName, - builderContext, - queryShardContext.getIndexAnalyzers() - ), - DerivedFieldSupportedTypes.getIndexableFieldGeneratorType(inferredFieldMapper.typeName(), fieldName), - queryShardContext.getIndexAnalyzers() - ); - if (derivedFieldType != null) { - derivedFieldTypeMap.put(fieldName, derivedFieldType); - } - return derivedFieldType; + return resolveNestedField(fieldName); } return null; } + private MappedFieldType resolveNestedField(String fieldName) { + DerivedFieldType parentDerivedField = getParentDerivedField(fieldName); + if (parentDerivedField == null) { + return null; + } + ValueFetcher valueFetcher = getValueFetcher(fieldName, parentDerivedField.derivedField.getScript()); + Mapper inferredFieldMapper; + try { + inferredFieldMapper = typeInference.infer(valueFetcher); + } catch (IOException e) { + logger.warn(e); + return null; + } + if (inferredFieldMapper == null) { + return null; + } + return getDerivedFieldType( + new DerivedField( + fieldName, + inferredFieldMapper.typeName(), + parentDerivedField.derivedField.getScript(), + parentDerivedField.derivedField.getSourceIndexedField() + ) + ); + } + private DerivedFieldType getParentDerivedField(String fieldName) { String parentFieldName = fieldName.split("\\.")[0]; DerivedFieldType parentDerivedFieldType = (DerivedFieldType) derivedFieldTypeMap.get(parentFieldName); if (parentDerivedFieldType == null) { - parentDerivedFieldType = (DerivedFieldType) this.queryShardContext.getMapperService().fieldType(parentFieldName); + parentDerivedFieldType = (DerivedFieldType) queryShardContext.getMapperService().fieldType(parentFieldName); } return parentDerivedFieldType; } + private ValueFetcher getValueFetcher(String fieldName, Script script) { + String subFieldName = fieldName.substring(fieldName.indexOf(".") + 1); + return new DerivedObjectFieldType.DerivedObjectFieldValueFetcher( + subFieldName, + DerivedFieldType.getDerivedFieldLeafFactory(script, queryShardContext, queryShardContext.lookup()), + o -> o // raw object returned will be used to infer the type without modifying it + ); + } + private Map getAllDerivedFieldTypeFromObject(Map derivedFieldObject) { Map derivedFieldTypes = new HashMap<>(); - DocumentMapper documentMapper = this.queryShardContext.getMapperService() + DocumentMapper documentMapper = queryShardContext.getMapperService() .documentMapperParser() .parse(DerivedFieldMapper.CONTENT_TYPE, derivedFieldObject); if (documentMapper != null && documentMapper.mappers() != null) { @@ -134,110 +164,13 @@ private Map getAllDerivedFieldTypeFromObject(Map o // raw object returned will be used to infer the type without modifying it - ); - int iter = 0; - int totalDocs = queryShardContext.getIndexReader().numDocs(); - // this will lead to the probability of more than 0.95 to select on the document containing this field, - // when at least 5% of the overall documents contain the field - int limit = Math.min(totalDocs, 50); - int[] docs = getSortedRandomNum(limit, totalDocs, 10000); - int offset = 0; - int leaf = 0; - LeafReaderContext leafReaderContext = queryShardContext.getIndexReader().leaves().get(leaf); - valueFetcher.setNextReader(leafReaderContext); - SourceLookup sourceLookup = new SourceLookup(); - while (iter < limit) { - int docID = docs[iter] - offset; - if (docID >= leafReaderContext.reader().numDocs()) { - leaf++; - offset += leafReaderContext.reader().numDocs(); - docID = docs[iter] - offset; - leafReaderContext = queryShardContext.getIndexReader().leaves().get(leaf); - valueFetcher.setNextReader(leafReaderContext); - } - sourceLookup.setSegmentAndDocument(leafReaderContext, docID); - List objects = valueFetcher.fetchValues(sourceLookup); - Mapper inferredMapper = inferTypeFromObject(name, objects.get(0)); - if (inferredMapper == null) { - iter++; - continue; - } - return inferredMapper; - } - } catch (IllegalArgumentException e) { - // TODO remove illegal argument exception from DerivedFieldSupportedTypes and let consumers handle themselves. - // If inference didn't work, defaulting to keyword field type - // TODO: warning? - } catch (IOException e) { - throw new RuntimeException(e); - } - // the field isn't found in documents where it was checked - // TODO: should fallback to keyword field type> - throw new MapperException( - "Unable to infer the derived field [" - + name - + "] within object type. " - + "Ensure the field is present in majority of the documents" - ); - } - - public static int[] getSortedRandomNum(int k, int n, int attempts) { - - Set generatedNumbers = new HashSet<>(); - Random random = new Random(); - int itr = 0; - while (generatedNumbers.size() < k && itr++ < attempts) { - int randomNumber = random.nextInt(n); - generatedNumbers.add(randomNumber); - } - int[] result = new int[k]; - int i = 0; - for (int number : generatedNumbers) { - result[i++] = number; - } - Arrays.sort(result); - return result; - } - - private Mapper inferTypeFromObject(String name, Object o) throws IOException { - // TODO error handling - 1. missing value? 2. Multi-valued field? - if (o == null) { - return null; - } - DocumentMapper mapper = queryShardContext.getMapperService().documentMapper(); - SourceToParse sourceToParse = new SourceToParse( - queryShardContext.index().getName(), - "_id", - BytesReference.bytes(jsonBuilder().startObject().field(name, o).endObject()), - JsonXContent.jsonXContent.mediaType() - ); - ParsedDocument parsedDocument = mapper.parse(sourceToParse); - Mapping mapping = parsedDocument.dynamicMappingsUpdate(); - return mapping.root.getMapper(name); - } - } } diff --git a/server/src/main/java/org/opensearch/index/mapper/DerivedObjectFieldType.java b/server/src/main/java/org/opensearch/index/mapper/DerivedObjectFieldType.java index 6ab2cc95b263f..2adfe8d521db4 100644 --- a/server/src/main/java/org/opensearch/index/mapper/DerivedObjectFieldType.java +++ b/server/src/main/java/org/opensearch/index/mapper/DerivedObjectFieldType.java @@ -53,8 +53,9 @@ public DerivedFieldValueFetcher valueFetcher(QueryShardContext context, SearchLo throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() + "] doesn't support formats."); } Function valueForDisplay = DerivedFieldSupportedTypes.getValueForDisplayGenerator(getType()); + String subFieldName = name().substring(name().indexOf(".") + 1); return new DerivedObjectFieldValueFetcher( - getSubField(), + subFieldName, getDerivedFieldLeafFactory(derivedField.getScript(), context, searchLookup == null ? context.lookup() : searchLookup), valueForDisplay ); @@ -80,13 +81,23 @@ public List fetchValuesInternal(SourceLookup lookup) { List result = new ArrayList<>(); for (Object o : jsonObjects) { Map s = XContentHelper.convertToMap(JsonXContent.jsonXContent, (String) o, false); - result.add(s.get(subField)); + result.add(getNestedField(s, subField)); } return result; } - } - private String getSubField() { - return name().split("\\.")[1]; + private static Object getNestedField(Map obj, String key) { + String[] keyParts = key.split("\\."); + Map currentObj = obj; + for (int i = 0; i < keyParts.length - 1; i++) { + Object value = currentObj.get(keyParts[i]); + if (value instanceof Map) { + currentObj = (Map) value; + } else { + return null; + } + } + return currentObj.get(keyParts[keyParts.length - 1]); + } } } diff --git a/server/src/main/java/org/opensearch/index/mapper/FieldTypeInference.java b/server/src/main/java/org/opensearch/index/mapper/FieldTypeInference.java new file mode 100644 index 0000000000000..fd0dbf431b099 --- /dev/null +++ b/server/src/main/java/org/opensearch/index/mapper/FieldTypeInference.java @@ -0,0 +1,120 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.index.mapper; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.search.lookup.SourceLookup; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +public class FieldTypeInference { + private final IndexReader indexReader; + private final String indexName; + private final MapperService mapperService; + // TODO expose using a index setting? + private int sampleSize; + + // this will lead to the probability of more than 0.95 to select on the document containing this field, + // when at least 5% of the overall documents contain the field + private static final int DEFAULT_SAMPLE_SIZE = 60; + + private final int MAX_ATTEMPTS_TO_GENERATE_RANDOM_SAMPLES = 10000; + + public FieldTypeInference(String indexName, MapperService mapperService, IndexReader indexReader) { + this.indexName = indexName; + this.mapperService = mapperService; + this.indexReader = indexReader; + this.sampleSize = DEFAULT_SAMPLE_SIZE; + } + + public void setSampleSize(int sampleSize) { + this.sampleSize = sampleSize; + } + + public int getSampleSize() { + return sampleSize; + } + + public Mapper infer(ValueFetcher valueFetcher) throws IOException { + int iter = 0; + int totalDocs = indexReader.numDocs(); + int sampleSize = Math.min(totalDocs, getSampleSize()); + int[] docs = getSortedRandomNum(sampleSize, totalDocs, Math.max(getSampleSize(), MAX_ATTEMPTS_TO_GENERATE_RANDOM_SAMPLES)); + int offset = 0; + SourceLookup sourceLookup = new SourceLookup(); + for (LeafReaderContext leafReaderContext : indexReader.leaves()) { + LeafReader leafReader = leafReaderContext.reader(); + valueFetcher.setNextReader(leafReaderContext); + if (iter >= docs.length) { + break; + } + int docID = docs[iter] - offset; + while (docID < leafReader.numDocs()) { + sourceLookup.setSegmentAndDocument(leafReaderContext, docID); + List objects = valueFetcher.fetchValues(sourceLookup); + Mapper inferredMapper = null; + if (objects != null && !objects.isEmpty()) { + // always using first value in case of multi value field + inferredMapper = inferTypeFromObject(objects.get(0)); + } + if (inferredMapper != null) { + return inferredMapper; + } + iter++; + if (iter >= docs.length) { + break; + } + docID = docs[iter] - offset; + } + offset += leafReader.numDocs(); + } + return null; + } + + private static int[] getSortedRandomNum(int k, int n, int attempts) { + Set generatedNumbers = new HashSet<>(); + Random random = new Random(); + int itr = 0; + while (generatedNumbers.size() < k && itr++ < attempts) { + int randomNumber = random.nextInt(n); + generatedNumbers.add(randomNumber); + } + int[] result = new int[generatedNumbers.size()]; + int i = 0; + for (int number : generatedNumbers) { + result[i++] = number; + } + Arrays.sort(result); + return result; + } + + private Mapper inferTypeFromObject(Object o) throws IOException { + if (o == null) { + return null; + } + DocumentMapper mapper = mapperService.documentMapper(); + XContentBuilder builder = XContentFactory.jsonBuilder().startObject().field("field", o).endObject(); + BytesReference bytesReference = BytesReference.bytes(builder); + SourceToParse sourceToParse = new SourceToParse(indexName, "_id", bytesReference, JsonXContent.jsonXContent.mediaType()); + ParsedDocument parsedDocument = mapper.parse(sourceToParse); + Mapping mapping = parsedDocument.dynamicMappingsUpdate(); + return mapping.root.getMapper("field"); + } +} diff --git a/server/src/test/java/org/opensearch/index/mapper/FieldTypeInferenceTests.java b/server/src/test/java/org/opensearch/index/mapper/FieldTypeInferenceTests.java new file mode 100644 index 0000000000000..97ae855e455cf --- /dev/null +++ b/server/src/test/java/org/opensearch/index/mapper/FieldTypeInferenceTests.java @@ -0,0 +1,142 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.index.mapper; + +import org.apache.lucene.document.Document; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.store.Directory; +import org.opensearch.common.lucene.Lucene; +import org.opensearch.core.index.Index; +import org.opensearch.index.query.QueryShardContext; +import org.opensearch.search.lookup.SourceLookup; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.Mockito.when; + +public class FieldTypeInferenceTests extends MapperServiceTestCase { + + private static final Map> documentMap; + static { + List listWithNull = new ArrayList<>(); + listWithNull.add(null); + documentMap = new HashMap<>(); + documentMap.put("text_field", List.of("The quick brown fox jumps over the lazy dog.")); + documentMap.put("int_field", List.of(789)); + documentMap.put("float_field", List.of(123.45)); + documentMap.put("date_field_1", List.of("2024-05-12T15:45:00Z")); + documentMap.put("date_field_2", List.of("2024-05-12")); + documentMap.put("boolean_field", List.of(true)); + documentMap.put("null_field", listWithNull); + documentMap.put("array_field_int", List.of(100, 200, 300, 400, 500)); + documentMap.put("array_field_text", List.of("100", "200")); + documentMap.put("object_type", List.of(Map.of("foo", Map.of("bar", 10)))); + } + + public void testJsonSupportedTypes() throws IOException { + MapperService mapperService = createMapperService(topMapping(b -> {})); + QueryShardContext queryShardContext = createQueryShardContext(mapperService); + when(queryShardContext.index()).thenReturn(new Index("test_index", "uuid")); + int totalDocs = 10000; + int docsPerLeafCount = 1000; + int leaves = 0; + try (Directory dir = newDirectory()) { + IndexWriter iw = new IndexWriter(dir, new IndexWriterConfig(Lucene.STANDARD_ANALYZER)); + Document d = new Document(); + for (int i = 0; i < totalDocs; i++) { + iw.addDocument(d); + if ((i + 1) % docsPerLeafCount == 0) { + iw.commit(); + leaves++; + } + } + try (IndexReader reader = DirectoryReader.open(iw)) { + iw.close(); + FieldTypeInference typeInference = new FieldTypeInference("test_index", queryShardContext.getMapperService(), reader); + String[] fieldName = { "text_field" }; + Mapper mapper = typeInference.infer(lookup -> documentMap.get(fieldName[0])); + assertEquals("text", mapper.typeName()); + + fieldName[0] = "int_field"; + mapper = typeInference.infer(lookup -> documentMap.get(fieldName[0])); + assertEquals("long", mapper.typeName()); + + fieldName[0] = "float_field"; + mapper = typeInference.infer(lookup -> documentMap.get(fieldName[0])); + assertEquals("float", mapper.typeName()); + + fieldName[0] = "date_field_1"; + mapper = typeInference.infer(lookup -> documentMap.get(fieldName[0])); + assertEquals("date", mapper.typeName()); + + fieldName[0] = "date_field_2"; + mapper = typeInference.infer(lookup -> documentMap.get(fieldName[0])); + assertEquals("date", mapper.typeName()); + + fieldName[0] = "boolean_field"; + mapper = typeInference.infer(lookup -> documentMap.get(fieldName[0])); + assertEquals("boolean", mapper.typeName()); + + fieldName[0] = "array_field_int"; + mapper = typeInference.infer(lookup -> documentMap.get(fieldName[0])); + assertEquals("long", mapper.typeName()); + + fieldName[0] = "array_field_text"; + mapper = typeInference.infer(lookup -> documentMap.get(fieldName[0])); + assertEquals("text", mapper.typeName()); + + fieldName[0] = "object_type"; + mapper = typeInference.infer(lookup -> documentMap.get(fieldName[0])); + assertEquals("object", mapper.typeName()); + + fieldName[0] = "null_field"; + mapper = typeInference.infer(lookup -> documentMap.get(fieldName[0])); + assertNull(mapper); + + // If field is missing ensure that sample docIDs generated for inference are ordered and are in bounds + fieldName[0] = "missing_field"; + List> docsEvaluated = new ArrayList<>(); + int[] totalDocsEvaluated = { 0 }; + typeInference.setSampleSize(50); + mapper = typeInference.infer(new ValueFetcher() { + @Override + public List fetchValues(SourceLookup lookup) throws IOException { + docsEvaluated.get(docsEvaluated.size() - 1).add(lookup.docId()); + totalDocsEvaluated[0]++; + return documentMap.get(fieldName[0]); + } + + @Override + public void setNextReader(LeafReaderContext leafReaderContext) { + docsEvaluated.add(new ArrayList<>()); + } + }); + assertNull(mapper); + // assertEquals(leaves, docsEvaluated.size()); + assertEquals(typeInference.getSampleSize(), totalDocsEvaluated[0]); + for (List docsPerLeaf : docsEvaluated) { + for (int j = 0; j < docsPerLeaf.size() - 1; j++) { + assertTrue(docsPerLeaf.get(j) < docsPerLeaf.get(j + 1)); + } + if (!docsPerLeaf.isEmpty()) { + assertTrue(docsPerLeaf.get(0) >= 0 && docsPerLeaf.get(docsPerLeaf.size() - 1) < docsPerLeafCount); + } + } + } + } + } +} diff --git a/server/src/test/java/org/opensearch/search/fetch/subphase/highlight/DerivedFieldFetchAndHighlightTests.java b/server/src/test/java/org/opensearch/search/fetch/subphase/highlight/DerivedFieldFetchAndHighlightTests.java index 0bfbb7fcde27e..78b3a2ced292a 100644 --- a/server/src/test/java/org/opensearch/search/fetch/subphase/highlight/DerivedFieldFetchAndHighlightTests.java +++ b/server/src/test/java/org/opensearch/search/fetch/subphase/highlight/DerivedFieldFetchAndHighlightTests.java @@ -223,13 +223,19 @@ public void testDerivedFieldFromSearchMapping() throws IOException { mockShardContext.setDerivedFieldResolver( new DerivedFieldResolver( mockShardContext, - Map.of( - DERIVED_FIELD_1, - createDerivedFieldType(DERIVED_FIELD_1, "keyword", DERIVED_FIELD_SCRIPT_1), - DERIVED_FIELD_2, - createDerivedFieldType(DERIVED_FIELD_2, "keyword", DERIVED_FIELD_SCRIPT_2) - ), - null + null, + List.of( + new DerivedField( + DERIVED_FIELD_1, + "keyword", + new Script(ScriptType.INLINE, "mockscript", DERIVED_FIELD_SCRIPT_1, emptyMap()) + ), + new DerivedField( + DERIVED_FIELD_2, + "keyword", + new Script(ScriptType.INLINE, "mockscript", DERIVED_FIELD_SCRIPT_2, emptyMap()) + ) + ) ) );