From 31fc5a32ac94cadc1b989727a622beb83ff2c7b8 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Thu, 11 Apr 2024 17:01:43 +0000
Subject: [PATCH] [Derived Fields] PR4: Capability to define derived fields in
 search request (#12850)

* Support derived fields definition in search request
* adds support for fetch phase on derived fields
* adds support for highlighting on derived fields

---------

Signed-off-by: Rishabh Maurya <rishabhmaurya05@gmail.com>
(cherry picked from commit 645b1f1b6c0738ef2b4a0de364b6bcd42af239b9)
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
---
 .../java/org/opensearch/client/SearchIT.java  | 229 +++++++++++
 .../search/fields/SearchFieldsIT.java         | 157 ++++++++
 .../action/search/SearchRequestBuilder.java   |  15 +
 .../opensearch/index/mapper/DerivedField.java |  90 +++++
 .../index/mapper/DerivedFieldMapper.java      |  37 +-
 .../mapper/DerivedFieldSupportedTypes.java    |  40 +-
 .../index/mapper/DerivedFieldType.java        | 197 +++-------
 .../mapper/DerivedFieldValueFetcher.java      |  38 +-
 .../index/mapper/DocumentMapperParser.java    |   2 +-
 .../index/query/DerivedFieldQuery.java        |  29 +-
 .../index/query/QueryShardContext.java        |  30 +-
 .../VectorGeoPointShapeQueryProcessor.java    |   5 +-
 .../opensearch/script/DerivedFieldScript.java |   7 -
 .../org/opensearch/search/SearchService.java  |  25 ++
 .../search/builder/SearchSourceBuilder.java   |  76 +++-
 .../subphase/highlight/HighlightPhase.java    |   8 +-
 .../subphase/highlight/HighlightUtils.java    |   4 +
 .../highlight/UnifiedHighlighter.java         |   5 +
 .../mapper/DerivedFieldMapperQueryTests.java  |  48 ++-
 .../index/mapper/DerivedFieldTypeTests.java   |   7 +-
 .../index/query/DerivedFieldQueryTests.java   |   8 +-
 .../index/query/QueryShardContextTests.java   |  31 ++
 .../opensearch/search/SearchServiceTests.java |  44 +++
 .../builder/SearchSourceBuilderTests.java     |  46 +++
 .../DerivedFieldFetchAndHighlightTests.java   | 366 ++++++++++++++++++
 .../opensearch/script/MockScriptEngine.java   |  44 ++-
 26 files changed, 1373 insertions(+), 215 deletions(-)
 create mode 100644 server/src/main/java/org/opensearch/index/mapper/DerivedField.java
 create mode 100644 server/src/test/java/org/opensearch/search/fetch/subphase/highlight/DerivedFieldFetchAndHighlightTests.java

diff --git a/client/rest-high-level/src/test/java/org/opensearch/client/SearchIT.java b/client/rest-high-level/src/test/java/org/opensearch/client/SearchIT.java
index bb705aebd2fcf..b962fa8ff415e 100644
--- a/client/rest-high-level/src/test/java/org/opensearch/client/SearchIT.java
+++ b/client/rest-high-level/src/test/java/org/opensearch/client/SearchIT.java
@@ -54,15 +54,19 @@
 import org.opensearch.action.search.SearchScrollRequest;
 import org.opensearch.client.core.CountRequest;
 import org.opensearch.client.core.CountResponse;
+import org.opensearch.common.geo.ShapeRelation;
 import org.opensearch.common.unit.TimeValue;
 import org.opensearch.common.xcontent.XContentFactory;
 import org.opensearch.core.common.bytes.BytesReference;
 import org.opensearch.core.rest.RestStatus;
 import org.opensearch.core.xcontent.MediaTypeRegistry;
 import org.opensearch.core.xcontent.XContentBuilder;
+import org.opensearch.geometry.Rectangle;
+import org.opensearch.index.query.GeoShapeQueryBuilder;
 import org.opensearch.index.query.MatchQueryBuilder;
 import org.opensearch.index.query.QueryBuilder;
 import org.opensearch.index.query.QueryBuilders;
+import org.opensearch.index.query.RangeQueryBuilder;
 import org.opensearch.index.query.ScriptQueryBuilder;
 import org.opensearch.index.query.TermsQueryBuilder;
 import org.opensearch.join.aggregations.Children;
@@ -102,6 +106,8 @@
 import org.opensearch.search.suggest.Suggest;
 import org.opensearch.search.suggest.SuggestBuilder;
 import org.opensearch.search.suggest.phrase.PhraseSuggestionBuilder;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 import org.hamcrest.Matchers;
 import org.junit.Before;
 
@@ -116,6 +122,7 @@
 import java.util.concurrent.TimeUnit;
 
 import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder;
+import static org.opensearch.index.query.QueryBuilders.geoShapeQuery;
 import static org.opensearch.test.hamcrest.OpenSearchAssertions.assertToXContentEquivalent;
 import static org.hamcrest.Matchers.arrayContaining;
 import static org.hamcrest.Matchers.both;
@@ -764,6 +771,228 @@ public void testSearchWithWeirdScriptFields() throws Exception {
         }
     }
 
+    public void testSearchWithDerivedFields() throws Exception {
+        // Just testing DerivedField definition from SearchSourceBuilder derivedField()
+        // We are not testing the full functionality here
+        Request doc = new Request("PUT", "test/_doc/1");
+        doc.setJsonEntity("{\"field\":\"value\"}");
+        client().performRequest(doc);
+        client().performRequest(new Request("POST", "/test/_refresh"));
+        // Keyword field
+        {
+            SearchRequest searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource()
+                    .derivedField("result", "keyword", new Script("emit(params._source[\"field\"])"))
+                    .fetchField("result")
+                    .query(new TermsQueryBuilder("result", "value"))
+            );
+            SearchResponse searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            SearchHit searchHit = searchResponse.getHits().getAt(0);
+            List<Object> values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(1, values.size());
+            assertEquals("value", values.get(0));
+
+            // multi valued
+            searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource()
+                    .derivedField(
+                        "result",
+                        "keyword",
+                        new Script("emit(params._source[\"field\"]);emit(params._source[\"field\"] + \"_2\")")
+                    )
+                    .query(new TermsQueryBuilder("result", "value_2"))
+                    .fetchField("result")
+            );
+            searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            searchHit = searchResponse.getHits().getAt(0);
+            values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(2, values.size());
+            assertEquals("value", values.get(0));
+            assertEquals("value_2", values.get(1));
+        }
+        // Boolean field
+        {
+            SearchRequest searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource()
+                    .derivedField("result", "boolean", new Script("emit(((String)params._source[\"field\"]).equals(\"value\"))"))
+                    .query(new TermsQueryBuilder("result", "true"))
+                    .fetchField("result")
+            );
+            SearchResponse searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            SearchHit searchHit = searchResponse.getHits().getAt(0);
+            List<Object> values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(1, values.size());
+            assertEquals(true, values.get(0));
+        }
+        // Long field
+        {
+            SearchRequest searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource()
+                    .derivedField("result", "long", new Script("emit(Long.MAX_VALUE)"))
+                    .query(new RangeQueryBuilder("result").from(Long.MAX_VALUE - 1).to(Long.MAX_VALUE))
+                    .fetchField("result")
+            );
+
+            SearchResponse searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            SearchHit searchHit = searchResponse.getHits().getAt(0);
+            List<Object> values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(1, values.size());
+            assertEquals(Long.MAX_VALUE, values.get(0));
+
+            // multi-valued
+            searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource()
+                    .derivedField("result", "long", new Script("emit(Long.MAX_VALUE); emit(Long.MIN_VALUE);"))
+                    .query(new RangeQueryBuilder("result").from(Long.MIN_VALUE).to(Long.MIN_VALUE + 1))
+                    .fetchField("result")
+            );
+
+            searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            searchHit = searchResponse.getHits().getAt(0);
+            values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(2, values.size());
+            assertEquals(Long.MAX_VALUE, values.get(0));
+            assertEquals(Long.MIN_VALUE, values.get(1));
+        }
+        // Double field
+        {
+            SearchRequest searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource()
+                    .derivedField("result", "double", new Script("emit(Double.MAX_VALUE)"))
+                    .query(new RangeQueryBuilder("result").from(Double.MAX_VALUE - 1).to(Double.MAX_VALUE))
+                    .fetchField("result")
+            );
+            SearchResponse searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            SearchHit searchHit = searchResponse.getHits().getAt(0);
+            List<Object> values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(1, values.size());
+            assertEquals(Double.MAX_VALUE, values.get(0));
+
+            // multi-valued
+            searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource()
+                    .derivedField("result", "double", new Script("emit(Double.MAX_VALUE); emit(Double.MIN_VALUE);"))
+                    .query(new RangeQueryBuilder("result").from(Double.MIN_VALUE).to(Double.MIN_VALUE + 1))
+                    .fetchField("result")
+            );
+
+            searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            searchHit = searchResponse.getHits().getAt(0);
+            values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(2, values.size());
+            assertEquals(Double.MAX_VALUE, values.get(0));
+            assertEquals(Double.MIN_VALUE, values.get(1));
+        }
+        // Date field
+        {
+            DateTime date1 = new DateTime(1990, 12, 29, 0, 0, DateTimeZone.UTC);
+            DateTime date2 = new DateTime(1990, 12, 30, 0, 0, DateTimeZone.UTC);
+            SearchRequest searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource()
+                    .derivedField("result", "date", new Script("emit(" + date1.getMillis() + "L)"))
+                    .query(new RangeQueryBuilder("result").from(date1.toString()).to(date2.toString()))
+                    .fetchField("result")
+            );
+
+            SearchResponse searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            SearchHit searchHit = searchResponse.getHits().getAt(0);
+            List<Object> values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(1, values.size());
+            assertEquals(date1.toString(), values.get(0));
+
+            // multi-valued
+            searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource()
+                    .derivedField("result", "date", new Script("emit(" + date1.getMillis() + "L); " + "emit(" + date2.getMillis() + "L)"))
+                    .query(new RangeQueryBuilder("result").from(date1.toString()).to(date2.toString()))
+                    .fetchField("result")
+            );
+
+            searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            searchHit = searchResponse.getHits().getAt(0);
+            values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(2, values.size());
+            assertEquals(date1.toString(), values.get(0));
+            assertEquals(date2.toString(), values.get(1));
+        }
+        // Geo field
+        {
+            GeoShapeQueryBuilder qb = geoShapeQuery("result", new Rectangle(-35, 35, 35, -35));
+            qb.relation(ShapeRelation.INTERSECTS);
+            SearchRequest searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource()
+                    .derivedField("result", "geo_point", new Script("emit(10.0, 20.0)"))
+                    .query(qb)
+                    .fetchField("result")
+            );
+
+            SearchResponse searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            SearchHit searchHit = searchResponse.getHits().getAt(0);
+            List<Object> values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(1, values.size());
+            assertEquals(10.0, ((HashMap) values.get(0)).get("lat"));
+            assertEquals(20.0, ((HashMap) values.get(0)).get("lon"));
+
+            // multi-valued
+            searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource()
+                    .derivedField("result", "geo_point", new Script("emit(10.0, 20.0); emit(20.0, 30.0);"))
+                    .query(qb)
+                    .fetchField("result")
+            );
+
+            searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            searchHit = searchResponse.getHits().getAt(0);
+            values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(2, values.size());
+            assertEquals(10.0, ((HashMap) values.get(0)).get("lat"));
+            assertEquals(20.0, ((HashMap) values.get(0)).get("lon"));
+            assertEquals(20.0, ((HashMap) values.get(1)).get("lat"));
+            assertEquals(30.0, ((HashMap) values.get(1)).get("lon"));
+        }
+        // IP field
+        {
+            SearchRequest searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource().derivedField("result", "ip", new Script("emit(\"10.0.0.1\")")).fetchField("result")
+            );
+
+            SearchResponse searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            SearchHit searchHit = searchResponse.getHits().getAt(0);
+            List<Object> values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(1, values.size());
+            assertEquals("10.0.0.1", values.get(0));
+
+            // multi-valued
+            searchRequest = new SearchRequest("test").source(
+                SearchSourceBuilder.searchSource()
+                    .derivedField("result", "ip", new Script("emit(\"10.0.0.1\"); emit(\"10.0.0.2\");"))
+                    .fetchField("result")
+            );
+
+            searchResponse = execute(searchRequest, highLevelClient()::search, highLevelClient()::searchAsync);
+            searchHit = searchResponse.getHits().getAt(0);
+            values = searchHit.getFields().get("result").getValues();
+            assertNotNull(values);
+            assertEquals(2, values.size());
+            assertEquals("10.0.0.1", values.get(0));
+            assertEquals("10.0.0.2", values.get(1));
+
+        }
+
+    }
+
     public void testSearchScroll() throws Exception {
         for (int i = 0; i < 100; i++) {
             XContentBuilder builder = jsonBuilder().startObject().field("field", i).endObject();
diff --git a/server/src/internalClusterTest/java/org/opensearch/search/fields/SearchFieldsIT.java b/server/src/internalClusterTest/java/org/opensearch/search/fields/SearchFieldsIT.java
index 906d45ef84b3f..2ce96092203e8 100644
--- a/server/src/internalClusterTest/java/org/opensearch/search/fields/SearchFieldsIT.java
+++ b/server/src/internalClusterTest/java/org/opensearch/search/fields/SearchFieldsIT.java
@@ -40,6 +40,7 @@
 import org.opensearch.common.Numbers;
 import org.opensearch.common.collect.MapBuilder;
 import org.opensearch.common.document.DocumentField;
+import org.opensearch.common.geo.GeoPoint;
 import org.opensearch.common.settings.Settings;
 import org.opensearch.common.time.DateFormatter;
 import org.opensearch.common.time.DateUtils;
@@ -51,6 +52,7 @@
 import org.opensearch.core.xcontent.MediaTypeRegistry;
 import org.opensearch.core.xcontent.XContentBuilder;
 import org.opensearch.index.fielddata.ScriptDocValues;
+import org.opensearch.index.mapper.DateFieldMapper;
 import org.opensearch.index.mapper.MapperService;
 import org.opensearch.index.query.QueryBuilders;
 import org.opensearch.plugins.Plugin;
@@ -189,6 +191,20 @@ protected Map<String, Function<Map<String, Object>, Object>> pluginScripts() {
             scripts.put("doc['s']", vars -> docScript(vars, "s"));
             scripts.put("doc['ms']", vars -> docScript(vars, "ms"));
 
+            scripts.put("doc['keyword_field']", vars -> sourceScript(vars, "keyword_field"));
+            scripts.put("doc['multi_keyword_field']", vars -> sourceScript(vars, "multi_keyword_field"));
+            scripts.put("doc['long_field']", vars -> sourceScript(vars, "long_field"));
+            scripts.put("doc['multi_long_field']", vars -> sourceScript(vars, "multi_long_field"));
+            scripts.put("doc['double_field']", vars -> sourceScript(vars, "double_field"));
+            scripts.put("doc['multi_double_field']", vars -> sourceScript(vars, "multi_double_field"));
+            scripts.put("doc['date_field']", vars -> sourceScript(vars, "date_field"));
+            scripts.put("doc['multi_date_field']", vars -> sourceScript(vars, "multi_date_field"));
+            scripts.put("doc['ip_field']", vars -> sourceScript(vars, "ip_field"));
+            scripts.put("doc['multi_ip_field']", vars -> sourceScript(vars, "multi_ip_field"));
+            scripts.put("doc['boolean_field']", vars -> sourceScript(vars, "boolean_field"));
+            scripts.put("doc['geo_field']", vars -> sourceScript(vars, "geo_field"));
+            scripts.put("doc['multi_geo_field']", vars -> sourceScript(vars, "multi_geo_field"));
+
             return scripts;
         }
 
@@ -1299,6 +1315,147 @@ public void testScriptFields() throws Exception {
         }
     }
 
+    public void testDerivedFields() throws Exception {
+        assertAcked(
+            prepareCreate("index").setMapping(
+                "keyword_field",
+                "type=keyword",
+                "multi_keyword_field",
+                "type=keyword",
+                "long_field",
+                "type=long",
+                "multi_long_field",
+                "type=long",
+                "double_field",
+                "type=double",
+                "multi_double_field",
+                "type=double",
+                "date_field",
+                "type=date",
+                "multi_date_field",
+                "type=date",
+                "ip_field",
+                "type=ip",
+                "multi_ip_field",
+                "type=ip",
+                "boolean_field",
+                "type=boolean",
+                "geo_field",
+                "type=geo_point",
+                "multi_geo_field",
+                "type=geo_point"
+            ).get()
+        );
+        final int numDocs = randomIntBetween(3, 8);
+        List<IndexRequestBuilder> reqs = new ArrayList<>();
+
+        DateTime date1 = new DateTime(1990, 12, 29, 0, 0, DateTimeZone.UTC);
+        DateTime date2 = new DateTime(1990, 12, 30, 0, 0, DateTimeZone.UTC);
+
+        for (int i = 0; i < numDocs; ++i) {
+            reqs.add(
+                client().prepareIndex("index")
+                    .setId(Integer.toString(i))
+                    .setSource(
+                        "keyword_field",
+                        Integer.toString(i),
+                        "multi_keyword_field",
+                        new String[] { Integer.toString(i), Integer.toString(i + 1) },
+                        "long_field",
+                        (long) i,
+                        "multi_long_field",
+                        new long[] { i, i + 1 },
+                        "double_field",
+                        (double) i,
+                        "multi_double_field",
+                        new double[] { i, i + 1 },
+                        "date_field",
+                        date1.getMillis(),
+                        "multi_date_field",
+                        new Long[] { date1.getMillis(), date2.getMillis() },
+                        "ip_field",
+                        "172.16.1.10",
+                        "multi_ip_field",
+                        new String[] { "172.16.1.10", "172.16.1.11" },
+                        "boolean_field",
+                        true,
+                        "geo_field",
+                        new GeoPoint(12.0, 10.0),
+                        "multi_geo_field",
+                        new GeoPoint[] { new GeoPoint(12.0, 10.0), new GeoPoint(13.0, 10.0) }
+                    )
+            );
+        }
+        indexRandom(true, reqs);
+        indexRandomForConcurrentSearch("index");
+        ensureSearchable();
+        SearchRequestBuilder req = client().prepareSearch("index");
+        String[][] fieldLookup = new String[][] {
+            { "keyword_field", "keyword" },
+            { "multi_keyword_field", "keyword" },
+            { "long_field", "long" },
+            { "multi_long_field", "long" },
+            { "double_field", "double" },
+            { "multi_double_field", "double" },
+            { "date_field", "date" },
+            { "multi_date_field", "date" },
+            { "ip_field", "ip" },
+            { "multi_ip_field", "ip" },
+            { "boolean_field", "boolean" },
+            { "geo_field", "geo_point" },
+            { "multi_geo_field", "geo_point" } };
+        for (String[] field : fieldLookup) {
+            req.addDerivedField(
+                "derived_" + field[0],
+                field[1],
+                new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "doc['" + field[0] + "']", Collections.emptyMap())
+            );
+        }
+        req.addFetchField("derived_*");
+        SearchResponse resp = req.get();
+        assertSearchResponse(resp);
+        for (SearchHit hit : resp.getHits().getHits()) {
+            final int id = Integer.parseInt(hit.getId());
+            Map<String, DocumentField> fields = hit.getFields();
+
+            assertEquals(fields.get("derived_keyword_field").getValues().get(0), Integer.toString(id));
+            assertEquals(fields.get("derived_multi_keyword_field").getValues().get(0), Integer.toString(id));
+            assertEquals(fields.get("derived_multi_keyword_field").getValues().get(1), Integer.toString(id + 1));
+
+            assertEquals(fields.get("derived_long_field").getValues().get(0), id);
+            assertEquals(fields.get("derived_multi_long_field").getValues().get(0), id);
+            assertEquals(fields.get("derived_multi_long_field").getValues().get(1), (id + 1));
+
+            assertEquals(fields.get("derived_double_field").getValues().get(0), (double) id);
+            assertEquals(fields.get("derived_multi_double_field").getValues().get(0), (double) id);
+            assertEquals(fields.get("derived_multi_double_field").getValues().get(1), (double) (id + 1));
+
+            assertEquals(
+                fields.get("derived_date_field").getValues().get(0),
+                DateFieldMapper.getDefaultDateTimeFormatter().formatJoda(date1)
+            );
+            assertEquals(
+                fields.get("derived_multi_date_field").getValues().get(0),
+                DateFieldMapper.getDefaultDateTimeFormatter().formatJoda(date1)
+            );
+            assertEquals(
+                fields.get("derived_multi_date_field").getValues().get(1),
+                DateFieldMapper.getDefaultDateTimeFormatter().formatJoda(date2)
+            );
+
+            assertEquals(fields.get("derived_ip_field").getValues().get(0), "172.16.1.10");
+            assertEquals(fields.get("derived_multi_ip_field").getValues().get(0), "172.16.1.10");
+            assertEquals(fields.get("derived_multi_ip_field").getValues().get(1), "172.16.1.11");
+
+            assertEquals(fields.get("derived_boolean_field").getValues().get(0), true);
+
+            assertEquals(fields.get("derived_geo_field").getValues().get(0), new GeoPoint(12.0, 10.0));
+            assertEquals(fields.get("derived_multi_geo_field").getValues().get(0), new GeoPoint(12.0, 10.0));
+            assertEquals(fields.get("derived_multi_geo_field").getValues().get(1), new GeoPoint(13.0, 10.0));
+
+        }
+    }
+
     public void testDocValueFieldsWithFieldAlias() throws Exception {
         XContentBuilder mapping = XContentFactory.jsonBuilder()
             .startObject()
diff --git a/server/src/main/java/org/opensearch/action/search/SearchRequestBuilder.java b/server/src/main/java/org/opensearch/action/search/SearchRequestBuilder.java
index 9dac827e7d518..4a547ee2c82bd 100644
--- a/server/src/main/java/org/opensearch/action/search/SearchRequestBuilder.java
+++ b/server/src/main/java/org/opensearch/action/search/SearchRequestBuilder.java
@@ -363,6 +363,21 @@ public SearchRequestBuilder addScriptField(String name, Script script) {
         return this;
     }
 
+    /**
+     * Adds a derived field of a given type. The script provided will be used to derive the value
+     * of a given type. Thereafter, it can be treated as regular field of a given type to perform
+     * query on them.
+     *
+     * @param name   The name of the field to be used in various parts of the query. The name will also represent
+     *               the field value in the return hit.
+     * @param type   The type of derived field. All values emitted by script must be of this type
+     * @param script The script to use
+     */
+    public SearchRequestBuilder addDerivedField(String name, String type, Script script) {
+        sourceBuilder().derivedField(name, type, script);
+        return this;
+    }
+
     /**
      * Adds a sort against the given field name and the sort ordering.
      *
diff --git a/server/src/main/java/org/opensearch/index/mapper/DerivedField.java b/server/src/main/java/org/opensearch/index/mapper/DerivedField.java
new file mode 100644
index 0000000000000..7ebe4e5f0b0e8
--- /dev/null
+++ b/server/src/main/java/org/opensearch/index/mapper/DerivedField.java
@@ -0,0 +1,90 @@
+/*
+ * 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.opensearch.common.annotation.PublicApi;
+import org.opensearch.core.common.io.stream.StreamInput;
+import org.opensearch.core.common.io.stream.StreamOutput;
+import org.opensearch.core.common.io.stream.Writeable;
+import org.opensearch.core.xcontent.ToXContent;
+import org.opensearch.core.xcontent.ToXContentFragment;
+import org.opensearch.core.xcontent.XContentBuilder;
+import org.opensearch.script.Script;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * DerivedField representation: expects a name, type and script.
+ */
+@PublicApi(since = "2.14.0")
+public class DerivedField implements Writeable, ToXContentFragment {
+
+    private final String name;
+    private final String type;
+    private final Script script;
+
+    public DerivedField(String name, String type, Script script) {
+        this.name = name;
+        this.type = type;
+        this.script = script;
+    }
+
+    public DerivedField(StreamInput in) throws IOException {
+        name = in.readString();
+        type = in.readString();
+        script = new Script(in);
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeString(name);
+        out.writeString(type);
+        script.writeTo(out);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
+        builder.startObject(name);
+        builder.field("type", type);
+        builder.field("script", script);
+        builder.endObject();
+        return builder;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public Script getScript() {
+        return script;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, type, script);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        DerivedField other = (DerivedField) obj;
+        return Objects.equals(name, other.name) && Objects.equals(type, other.type) && Objects.equals(script, other.script);
+    }
+
+}
diff --git a/server/src/main/java/org/opensearch/index/mapper/DerivedFieldMapper.java b/server/src/main/java/org/opensearch/index/mapper/DerivedFieldMapper.java
index b448487a4f810..c6ae71320c35c 100644
--- a/server/src/main/java/org/opensearch/index/mapper/DerivedFieldMapper.java
+++ b/server/src/main/java/org/opensearch/index/mapper/DerivedFieldMapper.java
@@ -14,7 +14,9 @@
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Function;
 
 /**
@@ -37,7 +39,7 @@ private static DerivedFieldMapper toType(FieldMapper in) {
      */
     public static class Builder extends ParametrizedFieldMapper.Builder {
         // TODO: The type of parameter may change here if the actual underlying FieldType object is needed
-        private final Parameter<String> type = Parameter.stringParam("type", false, m -> toType(m).type, "text");
+        private final Parameter<String> type = Parameter.stringParam("type", false, m -> toType(m).type, "");
 
         private final Parameter<Script> script = new Parameter<>(
             "script",
@@ -51,6 +53,12 @@ public Builder(String name) {
             super(name);
         }
 
+        public Builder(DerivedField derivedField) {
+            super(derivedField.getName());
+            this.type.setValue(derivedField.getType());
+            this.script.setValue(derivedField.getScript());
+        }
+
         @Override
         protected List<Parameter<?>> getParameters() {
             return Arrays.asList(type, script);
@@ -64,9 +72,7 @@ public DerivedFieldMapper build(BuilderContext context) {
                 name
             );
             DerivedFieldType ft = new DerivedFieldType(
-                buildFullName(context),
-                type.getValue(),
-                script.getValue(),
+                new DerivedField(buildFullName(context), type.getValue(), script.getValue()),
                 fieldMapper,
                 fieldFunction
             );
@@ -126,4 +132,27 @@ public String getType() {
     public Script getScript() {
         return script;
     }
+
+    public static Map<String, DerivedFieldType> getAllDerivedFieldTypeFromObject(
+        Map<String, Object> derivedFieldObject,
+        MapperService mapperService
+    ) {
+        Map<String, DerivedFieldType> derivedFieldTypes = new HashMap<>();
+        DocumentMapper documentMapper = mapperService.documentMapperParser().parse(DerivedFieldMapper.CONTENT_TYPE, derivedFieldObject);
+        if (documentMapper != null && documentMapper.mappers() != null) {
+            for (Mapper mapper : documentMapper.mappers()) {
+                if (mapper instanceof DerivedFieldMapper) {
+                    DerivedFieldType derivedFieldType = ((DerivedFieldMapper) mapper).fieldType();
+                    derivedFieldTypes.put(derivedFieldType.name(), derivedFieldType);
+                }
+            }
+        }
+        return derivedFieldTypes;
+    }
+
+    public static DerivedFieldType getDerivedFieldType(DerivedField derivedField, MapperService mapperService) {
+        BuilderContext builderContext = new Mapper.BuilderContext(mapperService.getIndexSettings().getSettings(), new ContentPath(1));
+        Builder builder = new Builder(derivedField);
+        return builder.build(builderContext).fieldType();
+    }
 }
diff --git a/server/src/main/java/org/opensearch/index/mapper/DerivedFieldSupportedTypes.java b/server/src/main/java/org/opensearch/index/mapper/DerivedFieldSupportedTypes.java
index 10b5c4a0f7157..aa6936bf6529a 100644
--- a/server/src/main/java/org/opensearch/index/mapper/DerivedFieldSupportedTypes.java
+++ b/server/src/main/java/org/opensearch/index/mapper/DerivedFieldSupportedTypes.java
@@ -20,12 +20,13 @@
 import org.apache.lucene.index.IndexableField;
 import org.opensearch.Version;
 import org.opensearch.common.Booleans;
+import org.opensearch.common.collect.Tuple;
+import org.opensearch.common.geo.GeoPoint;
 import org.opensearch.common.lucene.Lucene;
 import org.opensearch.common.network.InetAddresses;
 
 import java.net.InetAddress;
 import java.util.Arrays;
-import java.util.List;
 import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.Function;
@@ -36,7 +37,7 @@
  * it is used to create an IndexableField for the provided type and object. It is useful when indexing into
  * lucene MemoryIndex in {@link org.opensearch.index.query.DerivedFieldQuery}.
  */
-enum DerivedFieldSupportedTypes {
+public enum DerivedFieldSupportedTypes {
 
     BOOLEAN("boolean", (name, context) -> {
         BooleanFieldMapper.Builder builder = new BooleanFieldMapper.Builder(name);
@@ -51,7 +52,7 @@ enum DerivedFieldSupportedTypes {
             value = Booleans.parseBooleanStrict(textValue, false);
         }
         return new Field(name, value ? "T" : "F", BooleanFieldMapper.Defaults.FIELD_TYPE);
-    }),
+    }, o -> o),
     DATE("date", (name, context) -> {
         // TODO: should we support mapping settings exposed by a given field type from derived fields too?
         // for example, support `format` for date type?
@@ -63,17 +64,17 @@ enum DerivedFieldSupportedTypes {
             Version.CURRENT
         );
         return builder.build(context);
-    }, name -> o -> new LongPoint(name, (long) o)),
+    }, name -> o -> new LongPoint(name, (long) o), o -> DateFieldMapper.getDefaultDateTimeFormatter().formatMillis((long) o)),
     GEO_POINT("geo_point", (name, context) -> {
         GeoPointFieldMapper.Builder builder = new GeoPointFieldMapper.Builder(name);
         return builder.build(context);
     }, name -> o -> {
         // convert o to array of double
-        if (!(o instanceof List) || ((List<?>) o).size() != 2 || !(((List<?>) o).get(0) instanceof Double)) {
+        if (!(o instanceof Tuple) || !(((Tuple<?, ?>) o).v1() instanceof Double || !(((Tuple<?, ?>) o).v2() instanceof Double))) {
             throw new ClassCastException("geo_point should be in format emit(double lat, double lon) for derived fields");
         }
-        return new LatLonPoint(name, (Double) ((List<?>) o).get(0), (Double) ((List<?>) o).get(1));
-    }),
+        return new LatLonPoint(name, (double) ((Tuple<?, ?>) o).v1(), (double) ((Tuple<?, ?>) o).v2());
+    }, o -> new GeoPoint((double) ((Tuple) o).v1(), (double) ((Tuple) o).v2())),
     IP("ip", (name, context) -> {
         IpFieldMapper.Builder builder = new IpFieldMapper.Builder(name, false, Version.CURRENT);
         return builder.build(context);
@@ -85,7 +86,7 @@ enum DerivedFieldSupportedTypes {
             address = InetAddresses.forString(o.toString());
         }
         return new InetAddressPoint(name, address);
-    }),
+    }, o -> o),
     KEYWORD("keyword", (name, context) -> {
         FieldType dummyFieldType = new FieldType();
         dummyFieldType.setIndexOptions(IndexOptions.DOCS_AND_FREQS);
@@ -100,29 +101,33 @@ enum DerivedFieldSupportedTypes {
             keywordBuilder.copyTo.build(),
             keywordBuilder
         );
-    }, name -> o -> new KeywordField(name, (String) o, Field.Store.NO)),
+    }, name -> o -> new KeywordField(name, (String) o, Field.Store.NO), o -> o),
     LONG("long", (name, context) -> {
         NumberFieldMapper.Builder longBuilder = new NumberFieldMapper.Builder(name, NumberFieldMapper.NumberType.LONG, false, false);
         return longBuilder.build(context);
-    }, name -> o -> new LongField(name, Long.parseLong(o.toString()), Field.Store.NO)),
+    }, name -> o -> new LongField(name, Long.parseLong(o.toString()), Field.Store.NO), o -> o),
     DOUBLE("double", (name, context) -> {
         NumberFieldMapper.Builder doubleBuilder = new NumberFieldMapper.Builder(name, NumberFieldMapper.NumberType.DOUBLE, false, false);
         return doubleBuilder.build(context);
-    }, name -> o -> new DoubleField(name, Double.parseDouble(o.toString()), Field.Store.NO));
+    }, name -> o -> new DoubleField(name, Double.parseDouble(o.toString()), Field.Store.NO), o -> o);
 
     final String name;
     private final BiFunction<String, Mapper.BuilderContext, FieldMapper> builder;
 
     private final Function<String, Function<Object, IndexableField>> indexableFieldBuilder;
 
+    private final Function<Object, Object> valueForDisplay;
+
     DerivedFieldSupportedTypes(
         String name,
         BiFunction<String, Mapper.BuilderContext, FieldMapper> builder,
-        Function<String, Function<Object, IndexableField>> indexableFieldBuilder
+        Function<String, Function<Object, IndexableField>> indexableFieldBuilder,
+        Function<Object, Object> valueForDisplay
     ) {
         this.name = name;
         this.builder = builder;
         this.indexableFieldBuilder = indexableFieldBuilder;
+        this.valueForDisplay = valueForDisplay;
     }
 
     public String getName() {
@@ -137,6 +142,10 @@ private Function<Object, IndexableField> getIndexableFieldGenerator(String name)
         return indexableFieldBuilder.apply(name);
     }
 
+    private Function<Object, Object> getValueForDisplayGenerator() {
+        return valueForDisplay;
+    }
+
     private static final Map<String, DerivedFieldSupportedTypes> enumMap = Arrays.stream(DerivedFieldSupportedTypes.values())
         .collect(Collectors.toMap(DerivedFieldSupportedTypes::getName, enumValue -> enumValue));
 
@@ -153,4 +162,11 @@ public static Function<Object, IndexableField> getIndexableFieldGeneratorType(St
         }
         return enumMap.get(type).getIndexableFieldGenerator(name);
     }
+
+    public static Function<Object, Object> getValueForDisplayGenerator(String type) {
+        if (!enumMap.containsKey(type)) {
+            throw new IllegalArgumentException("Type [" + type + "] isn't supported in Derived field context.");
+        }
+        return enumMap.get(type).getValueForDisplayGenerator();
+    }
 }
diff --git a/server/src/main/java/org/opensearch/index/mapper/DerivedFieldType.java b/server/src/main/java/org/opensearch/index/mapper/DerivedFieldType.java
index abdca7879cc94..8b480819acd0e 100644
--- a/server/src/main/java/org/opensearch/index/mapper/DerivedFieldType.java
+++ b/server/src/main/java/org/opensearch/index/mapper/DerivedFieldType.java
@@ -18,10 +18,11 @@
 import org.opensearch.common.geo.ShapeRelation;
 import org.opensearch.common.time.DateMathParser;
 import org.opensearch.common.unit.Fuzziness;
+import org.opensearch.geometry.Geometry;
+import org.opensearch.index.analysis.NamedAnalyzer;
 import org.opensearch.index.query.DerivedFieldQuery;
 import org.opensearch.index.query.QueryShardContext;
 import org.opensearch.script.DerivedFieldScript;
-import org.opensearch.script.Script;
 import org.opensearch.search.lookup.SearchLookup;
 
 import java.io.IOException;
@@ -36,19 +37,16 @@
  * Contains logic to execute different type of queries on a derived field of given type.
  * @opensearch.internal
  */
-public final class DerivedFieldType extends MappedFieldType {
-    private final String type;
+public final class DerivedFieldType extends MappedFieldType implements GeoShapeQueryable {
 
-    private final Script script;
+    private final DerivedField derivedField;
 
     FieldMapper typeFieldMapper;
 
     final Function<Object, IndexableField> indexableFieldGenerator;
 
     public DerivedFieldType(
-        String name,
-        String type,
-        Script script,
+        DerivedField derivedField,
         boolean isIndexed,
         boolean isStored,
         boolean hasDocValues,
@@ -56,21 +54,14 @@ public DerivedFieldType(
         FieldMapper typeFieldMapper,
         Function<Object, IndexableField> fieldFunction
     ) {
-        super(name, isIndexed, isStored, hasDocValues, typeFieldMapper.fieldType().getTextSearchInfo(), meta);
-        this.type = type;
-        this.script = script;
+        super(derivedField.getName(), isIndexed, isStored, hasDocValues, typeFieldMapper.fieldType().getTextSearchInfo(), meta);
+        this.derivedField = derivedField;
         this.typeFieldMapper = typeFieldMapper;
         this.indexableFieldGenerator = fieldFunction;
     }
 
-    public DerivedFieldType(
-        String name,
-        String type,
-        Script script,
-        FieldMapper typeFieldMapper,
-        Function<Object, IndexableField> fieldFunction
-    ) {
-        this(name, type, script, false, false, false, Collections.emptyMap(), typeFieldMapper, fieldFunction);
+    public DerivedFieldType(DerivedField derivedField, FieldMapper typeFieldMapper, Function<Object, IndexableField> fieldFunction) {
+        this(derivedField, false, false, false, Collections.emptyMap(), typeFieldMapper, fieldFunction);
     }
 
     @Override
@@ -79,7 +70,15 @@ public String typeName() {
     }
 
     public String getType() {
-        return type;
+        return derivedField.getType();
+    }
+
+    public MappedFieldType getTypeMappedFieldType() {
+        return typeFieldMapper.mappedFieldType;
+    }
+
+    public NamedAnalyzer getIndexAnalyzer() {
+        return typeFieldMapper.mappedFieldType.indexAnalyzer();
     }
 
     @Override
@@ -87,46 +86,33 @@ public DerivedFieldValueFetcher valueFetcher(QueryShardContext context, SearchLo
         if (format != null) {
             throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() + "] doesn't support formats.");
         }
-        return new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
+        Function<Object, Object> valueForDisplay = DerivedFieldSupportedTypes.getValueForDisplayGenerator(getType());
+        return new DerivedFieldValueFetcher(
+            getDerivedFieldLeafFactory(context, searchLookup == null ? context.lookup() : searchLookup),
+            valueForDisplay,
+            indexableFieldGenerator
+        );
     }
 
     @Override
     public Query termQuery(Object value, QueryShardContext context) {
         Query query = typeFieldMapper.mappedFieldType.termQuery(value, context);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
     public Query termQueryCaseInsensitive(Object value, @Nullable QueryShardContext context) {
         Query query = typeFieldMapper.mappedFieldType.termQueryCaseInsensitive(value, context);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
     public Query termsQuery(List<?> values, @Nullable QueryShardContext context) {
         Query query = typeFieldMapper.mappedFieldType.termsQuery(values, context);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
@@ -150,14 +136,8 @@ public Query rangeQuery(
             parser,
             context
         );
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
@@ -170,14 +150,8 @@ public Query fuzzyQuery(
         QueryShardContext context
     ) {
         Query query = typeFieldMapper.mappedFieldType.fuzzyQuery(value, fuzziness, prefixLength, maxExpansions, transpositions, context);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
@@ -199,14 +173,8 @@ public Query fuzzyQuery(
             method,
             context
         );
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
@@ -217,14 +185,8 @@ public Query prefixQuery(
         QueryShardContext context
     ) {
         Query query = typeFieldMapper.mappedFieldType.prefixQuery(value, method, caseInsensitive, context);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
@@ -235,27 +197,15 @@ public Query wildcardQuery(
         QueryShardContext context
     ) {
         Query query = typeFieldMapper.mappedFieldType.wildcardQuery(value, method, caseInsensitive, context);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
     public Query normalizedWildcardQuery(String value, @Nullable MultiTermQuery.RewriteMethod method, QueryShardContext context) {
         Query query = typeFieldMapper.mappedFieldType.normalizedWildcardQuery(value, method, context);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
@@ -268,54 +218,30 @@ public Query regexpQuery(
         QueryShardContext context
     ) {
         Query query = typeFieldMapper.mappedFieldType.regexpQuery(value, syntaxFlags, matchFlags, maxDeterminizedStates, method, context);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
     public Query phraseQuery(TokenStream stream, int slop, boolean enablePositionIncrements, QueryShardContext context) throws IOException {
         Query query = typeFieldMapper.mappedFieldType.phraseQuery(stream, slop, enablePositionIncrements, context);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
     public Query multiPhraseQuery(TokenStream stream, int slop, boolean enablePositionIncrements, QueryShardContext context)
         throws IOException {
         Query query = typeFieldMapper.mappedFieldType.multiPhraseQuery(stream, slop, enablePositionIncrements, context);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
     public Query phrasePrefixQuery(TokenStream stream, int slop, int maxExpansions, QueryShardContext context) throws IOException {
         Query query = typeFieldMapper.mappedFieldType.phrasePrefixQuery(stream, slop, maxExpansions, context);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
@@ -328,14 +254,15 @@ public SpanQuery spanPrefixQuery(String value, SpanMultiTermQueryWrapper.SpanRew
     @Override
     public Query distanceFeatureQuery(Object origin, String pivot, float boost, QueryShardContext context) {
         Query query = typeFieldMapper.mappedFieldType.distanceFeatureQuery(origin, pivot, boost, context);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(getDerivedFieldLeafFactory(context));
-        return new DerivedFieldQuery(
-            query,
-            valueFetcher,
-            context.lookup(),
-            indexableFieldGenerator,
-            typeFieldMapper.mappedFieldType.indexAnalyzer()
-        );
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
+    }
+
+    @Override
+    public Query geoShapeQuery(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) {
+        Query query = ((GeoShapeQueryable) (typeFieldMapper.mappedFieldType)).geoShapeQuery(shape, fieldName, relation, context);
+        DerivedFieldValueFetcher valueFetcher = valueFetcher(context, context.lookup(), null);
+        return new DerivedFieldQuery(query, valueFetcher, context.lookup(), getIndexAnalyzer());
     }
 
     @Override
@@ -348,7 +275,7 @@ public boolean isAggregatable() {
         return false;
     }
 
-    private DerivedFieldScript.LeafFactory getDerivedFieldLeafFactory(QueryShardContext context) {
+    private DerivedFieldScript.LeafFactory getDerivedFieldLeafFactory(QueryShardContext context, SearchLookup searchLookup) {
         if (!context.documentMapper("").sourceMapper().enabled()) {
             throw new IllegalArgumentException(
                 "DerivedFieldQuery error: unable to fetch fields from _source field: _source is disabled in the mappings "
@@ -357,7 +284,7 @@ private DerivedFieldScript.LeafFactory getDerivedFieldLeafFactory(QueryShardCont
                     + "]"
             );
         }
-        DerivedFieldScript.Factory factory = context.compile(script, DerivedFieldScript.CONTEXT);
-        return factory.newFactory(script.getParams(), context.lookup());
+        DerivedFieldScript.Factory factory = context.compile(derivedField.getScript(), DerivedFieldScript.CONTEXT);
+        return factory.newFactory(derivedField.getScript().getParams(), searchLookup);
     }
 }
diff --git a/server/src/main/java/org/opensearch/index/mapper/DerivedFieldValueFetcher.java b/server/src/main/java/org/opensearch/index/mapper/DerivedFieldValueFetcher.java
index 40aa2f9890965..2d9379e04c512 100644
--- a/server/src/main/java/org/opensearch/index/mapper/DerivedFieldValueFetcher.java
+++ b/server/src/main/java/org/opensearch/index/mapper/DerivedFieldValueFetcher.java
@@ -8,33 +8,69 @@
 
 package org.opensearch.index.mapper;
 
+import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.index.LeafReaderContext;
+import org.opensearch.common.annotation.PublicApi;
 import org.opensearch.script.DerivedFieldScript;
 import org.opensearch.search.lookup.SourceLookup;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Function;
 
 /**
  * The value fetcher contains logic to execute script and fetch the value in form of list of object.
  * It expects DerivedFieldScript.LeafFactory as an input and sets the contract with consumer to call
  * {@link #setNextReader(LeafReaderContext)} whenever a segment is switched.
  */
+@PublicApi(since = "2.14.0")
 public final class DerivedFieldValueFetcher implements ValueFetcher {
     private DerivedFieldScript derivedFieldScript;
     private final DerivedFieldScript.LeafFactory derivedFieldScriptFactory;
 
-    public DerivedFieldValueFetcher(DerivedFieldScript.LeafFactory derivedFieldScriptFactory) {
+    private final Function<Object, Object> valueForDisplay;
+    private final Function<Object, IndexableField> indexableFieldFunction;
+
+    public DerivedFieldValueFetcher(
+        DerivedFieldScript.LeafFactory derivedFieldScriptFactory,
+        Function<Object, Object> valueForDisplay,
+        Function<Object, IndexableField> indexableFieldFunction
+    ) {
         this.derivedFieldScriptFactory = derivedFieldScriptFactory;
+        this.valueForDisplay = valueForDisplay;
+        this.indexableFieldFunction = indexableFieldFunction;
     }
 
     @Override
     public List<Object> fetchValues(SourceLookup lookup) {
+        List<Object> values = fetchValuesInternal(lookup);
+        if (values.isEmpty()) {
+            return values;
+        }
+        List<Object> result = new ArrayList<>();
+        for (Object v : values) {
+            result.add(valueForDisplay.apply(v));
+        }
+        return result;
+    }
+
+    private List<Object> fetchValuesInternal(SourceLookup lookup) {
         derivedFieldScript.setDocument(lookup.docId());
         derivedFieldScript.execute();
         return derivedFieldScript.getEmittedValues();
     }
 
+    public List<IndexableField> getIndexableField(SourceLookup lookup) {
+        List<Object> values = fetchValuesInternal(lookup);
+        List<IndexableField> indexableFields = new ArrayList<>();
+        for (Object v : values) {
+            indexableFields.add(indexableFieldFunction.apply(v));
+        }
+        return indexableFields;
+    }
+
+    @Override
     public void setNextReader(LeafReaderContext context) {
         try {
             derivedFieldScript = derivedFieldScriptFactory.newInstance(context);
diff --git a/server/src/main/java/org/opensearch/index/mapper/DocumentMapperParser.java b/server/src/main/java/org/opensearch/index/mapper/DocumentMapperParser.java
index 2e85cdccb6b0d..72cb370d2d362 100644
--- a/server/src/main/java/org/opensearch/index/mapper/DocumentMapperParser.java
+++ b/server/src/main/java/org/opensearch/index/mapper/DocumentMapperParser.java
@@ -136,7 +136,7 @@ public DocumentMapper parse(@Nullable String type, CompressedXContent source) th
     }
 
     @SuppressWarnings({ "unchecked" })
-    private DocumentMapper parse(String type, Map<String, Object> mapping) throws MapperParsingException {
+    public DocumentMapper parse(String type, Map<String, Object> mapping) throws MapperParsingException {
         if (type == null) {
             throw new MapperParsingException("Failed to derive type");
         }
diff --git a/server/src/main/java/org/opensearch/index/query/DerivedFieldQuery.java b/server/src/main/java/org/opensearch/index/query/DerivedFieldQuery.java
index 8beef0bf46be0..42ac61bf98f73 100644
--- a/server/src/main/java/org/opensearch/index/query/DerivedFieldQuery.java
+++ b/server/src/main/java/org/opensearch/index/query/DerivedFieldQuery.java
@@ -29,7 +29,6 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.Objects;
-import java.util.function.Function;
 
 /**
  * DerivedFieldQuery used for querying derived fields. It contains the logic to execute an input lucene query against
@@ -39,7 +38,6 @@ public final class DerivedFieldQuery extends Query {
     private final Query query;
     private final DerivedFieldValueFetcher valueFetcher;
     private final SearchLookup searchLookup;
-    private final Function<Object, IndexableField> indexableFieldGenerator;
     private final Analyzer indexAnalyzer;
 
     /**
@@ -47,20 +45,11 @@ public final class DerivedFieldQuery extends Query {
      * @param valueFetcher DerivedFieldValueFetcher ValueFetcher to fetch the value of a derived field from _source
      *                     using LeafSearchLookup
      * @param searchLookup SearchLookup to get the LeafSearchLookup look used by valueFetcher to fetch the _source
-     * @param indexableFieldGenerator used to generate lucene IndexableField from a given object fetched by valueFetcher
-     *                                to be used in lucene memory index.
      */
-    public DerivedFieldQuery(
-        Query query,
-        DerivedFieldValueFetcher valueFetcher,
-        SearchLookup searchLookup,
-        Function<Object, IndexableField> indexableFieldGenerator,
-        Analyzer indexAnalyzer
-    ) {
+    public DerivedFieldQuery(Query query, DerivedFieldValueFetcher valueFetcher, SearchLookup searchLookup, Analyzer indexAnalyzer) {
         this.query = query;
         this.valueFetcher = valueFetcher;
         this.searchLookup = searchLookup;
-        this.indexableFieldGenerator = indexableFieldGenerator;
         this.indexAnalyzer = indexAnalyzer;
     }
 
@@ -75,7 +64,7 @@ public Query rewrite(IndexSearcher indexSearcher) throws IOException {
         if (rewritten == query) {
             return this;
         }
-        return new DerivedFieldQuery(rewritten, valueFetcher, searchLookup, indexableFieldGenerator, indexAnalyzer);
+        return new DerivedFieldQuery(rewritten, valueFetcher, searchLookup, indexAnalyzer);
     }
 
     @Override
@@ -91,12 +80,12 @@ public Scorer scorer(LeafReaderContext context) {
                     @Override
                     public boolean matches() {
                         leafSearchLookup.source().setSegmentAndDocument(context, approximation.docID());
-                        List<Object> values = valueFetcher.fetchValues(leafSearchLookup.source());
+                        List<IndexableField> indexableFields = valueFetcher.getIndexableField(leafSearchLookup.source());
                         // TODO: in case of errors from script, should it be ignored and treated as missing field
                         // by using a configurable setting?
                         MemoryIndex memoryIndex = new MemoryIndex();
-                        for (Object value : values) {
-                            memoryIndex.addField(indexableFieldGenerator.apply(value), indexAnalyzer);
+                        for (IndexableField indexableField : indexableFields) {
+                            memoryIndex.addField(indexableField, indexAnalyzer);
                         }
                         float score = memoryIndex.search(query);
                         return score > 0.0f;
@@ -127,16 +116,12 @@ public boolean equals(Object o) {
             return false;
         }
         DerivedFieldQuery other = (DerivedFieldQuery) o;
-        return Objects.equals(this.query, other.query)
-            && Objects.equals(this.valueFetcher, other.valueFetcher)
-            && Objects.equals(this.searchLookup, other.searchLookup)
-            && Objects.equals(this.indexableFieldGenerator, other.indexableFieldGenerator)
-            && Objects.equals(this.indexAnalyzer, other.indexAnalyzer);
+        return Objects.equals(this.query, other.query) && Objects.equals(this.indexAnalyzer, other.indexAnalyzer);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(classHash(), query, valueFetcher, searchLookup, indexableFieldGenerator, indexableFieldGenerator);
+        return Objects.hash(classHash(), query, indexAnalyzer);
     }
 
     @Override
diff --git a/server/src/main/java/org/opensearch/index/query/QueryShardContext.java b/server/src/main/java/org/opensearch/index/query/QueryShardContext.java
index f3b392559d33e..64643ad6d2c94 100644
--- a/server/src/main/java/org/opensearch/index/query/QueryShardContext.java
+++ b/server/src/main/java/org/opensearch/index/query/QueryShardContext.java
@@ -45,6 +45,7 @@
 import org.opensearch.common.TriFunction;
 import org.opensearch.common.annotation.PublicApi;
 import org.opensearch.common.lucene.search.Queries;
+import org.opensearch.common.regex.Regex;
 import org.opensearch.common.util.BigArrays;
 import org.opensearch.core.action.ActionListener;
 import org.opensearch.core.common.ParsingException;
@@ -77,6 +78,7 @@
 
 import java.io.IOException;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -119,6 +121,8 @@ public class QueryShardContext extends QueryRewriteContext {
     private final ValuesSourceRegistry valuesSourceRegistry;
     private BitSetProducer parentFilter;
 
+    private Map<String, MappedFieldType> derivedFieldTypeMap = new HashMap<>();
+
     public QueryShardContext(
         int shardId,
         IndexSettings indexSettings,
@@ -329,7 +333,17 @@ public Map<String, Query> copyNamedQueries() {
      * type then the fields will be returned with a type prefix.
      */
     public Set<String> simpleMatchToIndexNames(String pattern) {
-        return mapperService.simpleMatchToFullName(pattern);
+        Set<String> matchingFields = mapperService.simpleMatchToFullName(pattern);
+        if (derivedFieldTypeMap != null && !derivedFieldTypeMap.isEmpty()) {
+            Set<String> matchingDerivedFields = new HashSet<>(matchingFields);
+            for (String fieldName : derivedFieldTypeMap.keySet()) {
+                if (!matchingDerivedFields.contains(fieldName) && Regex.simpleMatch(pattern, fieldName)) {
+                    matchingDerivedFields.add(fieldName);
+                }
+            }
+            return matchingDerivedFields;
+        }
+        return matchingFields;
     }
 
     /**
@@ -395,6 +409,14 @@ public ValuesSourceRegistry getValuesSourceRegistry() {
         return valuesSourceRegistry;
     }
 
+    public void setDerivedFieldTypes(Map<String, MappedFieldType> derivedFieldTypeMap) {
+        this.derivedFieldTypeMap = derivedFieldTypeMap;
+    }
+
+    public MappedFieldType getDerivedFieldType(String fieldName) {
+        return derivedFieldTypeMap == null ? null : derivedFieldTypeMap.get(fieldName);
+    }
+
     public void setAllowUnmappedFields(boolean allowUnmappedFields) {
         this.allowUnmappedFields = allowUnmappedFields;
     }
@@ -404,7 +426,11 @@ public void setMapUnmappedFieldAsString(boolean mapUnmappedFieldAsString) {
     }
 
     MappedFieldType failIfFieldMappingNotFound(String name, MappedFieldType fieldMapping) {
-        if (fieldMapping != null || allowUnmappedFields) {
+        if (fieldMapping != null) {
+            return fieldMapping;
+        } else if (getDerivedFieldType(name) != null) {
+            return getDerivedFieldType(name);
+        } else if (allowUnmappedFields) {
             return fieldMapping;
         } else if (mapUnmappedFieldAsString) {
             TextFieldMapper.Builder builder = new TextFieldMapper.Builder(name, mapperService.getIndexAnalyzers());
diff --git a/server/src/main/java/org/opensearch/index/query/VectorGeoPointShapeQueryProcessor.java b/server/src/main/java/org/opensearch/index/query/VectorGeoPointShapeQueryProcessor.java
index eb7923a5b5212..c55d88439b11f 100644
--- a/server/src/main/java/org/opensearch/index/query/VectorGeoPointShapeQueryProcessor.java
+++ b/server/src/main/java/org/opensearch/index/query/VectorGeoPointShapeQueryProcessor.java
@@ -54,6 +54,7 @@
 import org.opensearch.geometry.Polygon;
 import org.opensearch.geometry.Rectangle;
 import org.opensearch.geometry.ShapeType;
+import org.opensearch.index.mapper.DerivedFieldType;
 import org.opensearch.index.mapper.GeoPointFieldMapper;
 import org.opensearch.index.mapper.MappedFieldType;
 
@@ -78,7 +79,9 @@ public Query geoShapeQuery(Geometry shape, String fieldName, ShapeRelation relat
 
     private void validateIsGeoPointFieldType(String fieldName, QueryShardContext context) {
         MappedFieldType fieldType = context.fieldMapper(fieldName);
-        if (fieldType instanceof GeoPointFieldMapper.GeoPointFieldType == false) {
+        if (fieldType instanceof GeoPointFieldMapper.GeoPointFieldType == false
+            && !(fieldType instanceof DerivedFieldType
+                && (((DerivedFieldType) fieldType).getTypeMappedFieldType() instanceof GeoPointFieldMapper.GeoPointFieldType))) {
             throw new QueryShardException(
                 context,
                 "Expected "
diff --git a/server/src/main/java/org/opensearch/script/DerivedFieldScript.java b/server/src/main/java/org/opensearch/script/DerivedFieldScript.java
index e9988ec5aeef2..0a2b7cf691283 100644
--- a/server/src/main/java/org/opensearch/script/DerivedFieldScript.java
+++ b/server/src/main/java/org/opensearch/script/DerivedFieldScript.java
@@ -68,13 +68,6 @@ public DerivedFieldScript(Map<String, Object> params, SearchLookup lookup, LeafR
         this.totalByteSize = 0;
     }
 
-    public DerivedFieldScript() {
-        this.params = null;
-        this.leafLookup = null;
-        this.emittedValues = new ArrayList<>();
-        this.totalByteSize = 0;
-    }
-
     /**
      * Return the parameters for this script.
      */
diff --git a/server/src/main/java/org/opensearch/search/SearchService.java b/server/src/main/java/org/opensearch/search/SearchService.java
index d48f6f6522ca5..13369d298e2d5 100644
--- a/server/src/main/java/org/opensearch/search/SearchService.java
+++ b/server/src/main/java/org/opensearch/search/SearchService.java
@@ -78,6 +78,9 @@
 import org.opensearch.index.IndexService;
 import org.opensearch.index.IndexSettings;
 import org.opensearch.index.engine.Engine;
+import org.opensearch.index.mapper.DerivedField;
+import org.opensearch.index.mapper.DerivedFieldMapper;
+import org.opensearch.index.mapper.MappedFieldType;
 import org.opensearch.index.query.InnerHitContextBuilder;
 import org.opensearch.index.query.MatchAllQueryBuilder;
 import org.opensearch.index.query.MatchNoneQueryBuilder;
@@ -1067,6 +1070,28 @@ private DefaultSearchContext createSearchContext(ReaderContext reader, ShardSear
             // might end up with incorrect state since we are using now() or script services
             // during rewrite and normalized / evaluate templates etc.
             QueryShardContext context = new QueryShardContext(searchContext.getQueryShardContext());
+            if (request.source() != null
+                && request.source().size() != 0
+                && (request.source().getDerivedFieldsObject() != null || request.source().getDerivedFields() != null)) {
+                Map<String, MappedFieldType> derivedFieldTypeMap = new HashMap<>();
+                if (request.source().getDerivedFieldsObject() != null) {
+                    Map<String, Object> derivedFieldObject = new HashMap<>();
+                    derivedFieldObject.put(DerivedFieldMapper.CONTENT_TYPE, request.source().getDerivedFieldsObject());
+                    derivedFieldTypeMap.putAll(
+                        DerivedFieldMapper.getAllDerivedFieldTypeFromObject(derivedFieldObject, searchContext.mapperService())
+                    );
+                }
+                if (request.source().getDerivedFields() != null) {
+                    for (DerivedField derivedField : request.source().getDerivedFields()) {
+                        derivedFieldTypeMap.put(
+                            derivedField.getName(),
+                            DerivedFieldMapper.getDerivedFieldType(derivedField, searchContext.mapperService())
+                        );
+                    }
+                }
+                context.setDerivedFieldTypes(derivedFieldTypeMap);
+                searchContext.getQueryShardContext().setDerivedFieldTypes(derivedFieldTypeMap);
+            }
             Rewriteable.rewrite(request.getRewriteable(), context, true);
             assert searchContext.getQueryShardContext().isCacheable();
             success = true;
diff --git a/server/src/main/java/org/opensearch/search/builder/SearchSourceBuilder.java b/server/src/main/java/org/opensearch/search/builder/SearchSourceBuilder.java
index c930cd5752ecd..b40710146c672 100644
--- a/server/src/main/java/org/opensearch/search/builder/SearchSourceBuilder.java
+++ b/server/src/main/java/org/opensearch/search/builder/SearchSourceBuilder.java
@@ -52,6 +52,8 @@
 import org.opensearch.core.xcontent.XContentBuilder;
 import org.opensearch.core.xcontent.XContentHelper;
 import org.opensearch.core.xcontent.XContentParser;
+import org.opensearch.index.mapper.DerivedField;
+import org.opensearch.index.mapper.DerivedFieldMapper;
 import org.opensearch.index.query.QueryBuilder;
 import org.opensearch.index.query.QueryRewriteContext;
 import org.opensearch.index.query.Rewriteable;
@@ -114,6 +116,7 @@ public final class SearchSourceBuilder implements Writeable, ToXContentObject, R
     public static final ParseField DOCVALUE_FIELDS_FIELD = new ParseField("docvalue_fields");
     public static final ParseField FETCH_FIELDS_FIELD = new ParseField("fields");
     public static final ParseField SCRIPT_FIELDS_FIELD = new ParseField("script_fields");
+    public static final ParseField DERIVED_FIELDS_FIELD = new ParseField(DerivedFieldMapper.CONTENT_TYPE);
     public static final ParseField SCRIPT_FIELD = new ParseField("script");
     public static final ParseField IGNORE_FAILURE_FIELD = new ParseField("ignore_failure");
     public static final ParseField SORT_FIELD = new ParseField("sort");
@@ -193,6 +196,10 @@ public static HighlightBuilder highlight() {
     private StoredFieldsContext storedFieldsContext;
     private List<FieldAndFormat> docValueFields;
     private List<ScriptField> scriptFields;
+    private Map<String, Object> derivedFieldsObject;
+
+    private List<DerivedField> derivedFields;
+
     private FetchSourceContext fetchSourceContext;
     private List<FieldAndFormat> fetchFields;
 
@@ -291,6 +298,14 @@ public SearchSourceBuilder(StreamInput in) throws IOException {
         if (in.getVersion().onOrAfter(Version.V_2_13_0)) {
             includeNamedQueriesScore = in.readOptionalBoolean();
         }
+        if (in.getVersion().onOrAfter(Version.V_3_0_0)) {
+            if (in.readBoolean()) {
+                derivedFieldsObject = in.readMap();
+            }
+            if (in.readBoolean()) {
+                derivedFields = in.readList(DerivedField::new);
+            }
+        }
     }
 
     @Override
@@ -367,6 +382,18 @@ public void writeTo(StreamOutput out) throws IOException {
         if (out.getVersion().onOrAfter(Version.V_2_13_0)) {
             out.writeOptionalBoolean(includeNamedQueriesScore);
         }
+        if (out.getVersion().onOrAfter(Version.V_3_0_0)) {
+            boolean hasDerivedFieldsObject = derivedFieldsObject != null;
+            out.writeBoolean(hasDerivedFieldsObject);
+            if (hasDerivedFieldsObject) {
+                out.writeMap(derivedFieldsObject);
+            }
+            boolean hasDerivedFields = derivedFields != null;
+            out.writeBoolean(hasDerivedFields);
+            if (hasDerivedFields) {
+                out.writeList(derivedFields);
+            }
+        }
     }
 
     /**
@@ -972,6 +999,28 @@ public List<ScriptField> scriptFields() {
         return scriptFields;
     }
 
+    public Map<String, Object> getDerivedFieldsObject() {
+        return derivedFieldsObject;
+    }
+
+    public List<DerivedField> getDerivedFields() {
+        return derivedFields;
+    }
+
+    /**
+     * Adds a derived field with the given name with provided type and script
+     * @param name name of the derived field
+     * @param type type of the derived field
+     * @param script script associated with derived field
+     */
+    public SearchSourceBuilder derivedField(String name, String type, Script script) {
+        if (derivedFields == null) {
+            derivedFields = new ArrayList<>();
+        }
+        derivedFields.add(new DerivedField(name, type, script));
+        return this;
+    }
+
     /**
      * Sets the boost a specific index or alias will receive when the query is executed
      * against it.
@@ -1151,6 +1200,8 @@ private SearchSourceBuilder shallowCopy(
         rewrittenBuilder.seqNoAndPrimaryTerm = seqNoAndPrimaryTerm;
         rewrittenBuilder.collapse = collapse;
         rewrittenBuilder.pointInTimeBuilder = pointInTimeBuilder;
+        rewrittenBuilder.derivedFieldsObject = derivedFieldsObject;
+        rewrittenBuilder.derivedFields = derivedFields;
         return rewrittenBuilder;
     }
 
@@ -1298,6 +1349,8 @@ public void parseXContent(XContentParser parser, boolean checkTrailingTokens) th
                         pointInTimeBuilder = PointInTimeBuilder.fromXContent(parser);
                     } else if (SEARCH_PIPELINE.match(currentFieldName, parser.getDeprecationHandler())) {
                         searchPipelineSource = parser.mapOrdered();
+                    } else if (DERIVED_FIELDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
+                        derivedFieldsObject = parser.map();
                     } else {
                         throw new ParsingException(
                             parser.getTokenLocation(),
@@ -1530,6 +1583,21 @@ public XContentBuilder innerToXContent(XContentBuilder builder, Params params) t
         if (searchPipelineSource != null) {
             builder.field(SEARCH_PIPELINE.getPreferredName(), searchPipelineSource);
         }
+
+        if (derivedFieldsObject != null || derivedFields != null) {
+            builder.startObject(DERIVED_FIELDS_FIELD.getPreferredName());
+            if (derivedFieldsObject != null) {
+                builder.mapContents(derivedFieldsObject);
+            }
+            if (derivedFields != null) {
+                for (DerivedField derivedField : derivedFields) {
+                    derivedField.toXContent(builder, params);
+                }
+            }
+            builder.endObject();
+
+        }
+
         return builder;
     }
 
@@ -1805,7 +1873,9 @@ public int hashCode() {
             extBuilders,
             collapse,
             trackTotalHitsUpTo,
-            pointInTimeBuilder
+            pointInTimeBuilder,
+            derivedFieldsObject,
+            derivedFields
         );
     }
 
@@ -1848,7 +1918,9 @@ public boolean equals(Object obj) {
             && Objects.equals(extBuilders, other.extBuilders)
             && Objects.equals(collapse, other.collapse)
             && Objects.equals(trackTotalHitsUpTo, other.trackTotalHitsUpTo)
-            && Objects.equals(pointInTimeBuilder, other.pointInTimeBuilder);
+            && Objects.equals(pointInTimeBuilder, other.pointInTimeBuilder)
+            && Objects.equals(derivedFieldsObject, other.derivedFieldsObject)
+            && Objects.equals(derivedFields, other.derivedFields);
     }
 
     @Override
diff --git a/server/src/main/java/org/opensearch/search/fetch/subphase/highlight/HighlightPhase.java b/server/src/main/java/org/opensearch/search/fetch/subphase/highlight/HighlightPhase.java
index 2fc9b214e3ebb..b16f06e7e3989 100644
--- a/server/src/main/java/org/opensearch/search/fetch/subphase/highlight/HighlightPhase.java
+++ b/server/src/main/java/org/opensearch/search/fetch/subphase/highlight/HighlightPhase.java
@@ -145,6 +145,9 @@ private Map<String, Function<HitContext, FieldHighlightContext>> contextBuilders
             boolean fieldNameContainsWildcards = field.field().contains("*");
             for (String fieldName : fieldNamesToHighlight) {
                 MappedFieldType fieldType = context.mapperService().fieldType(fieldName);
+                if (fieldType == null && context.getQueryShardContext().getDerivedFieldType(fieldName) != null) {
+                    fieldType = context.getQueryShardContext().getDerivedFieldType(fieldName);
+                }
                 if (fieldType == null) {
                     continue;
                 }
@@ -170,12 +173,13 @@ private Map<String, Function<HitContext, FieldHighlightContext>> contextBuilders
                 Query highlightQuery = field.fieldOptions().highlightQuery();
 
                 boolean forceSource = highlightContext.forceSource(field);
+                MappedFieldType finalFieldType = fieldType;
                 builders.put(
                     fieldName,
                     hc -> new FieldHighlightContext(
-                        fieldType.name(),
+                        finalFieldType.name(),
                         field,
-                        fieldType,
+                        finalFieldType,
                         context,
                         hc,
                         highlightQuery == null ? query : highlightQuery,
diff --git a/server/src/main/java/org/opensearch/search/fetch/subphase/highlight/HighlightUtils.java b/server/src/main/java/org/opensearch/search/fetch/subphase/highlight/HighlightUtils.java
index 2238554a12149..55df80e7d7aa9 100644
--- a/server/src/main/java/org/opensearch/search/fetch/subphase/highlight/HighlightUtils.java
+++ b/server/src/main/java/org/opensearch/search/fetch/subphase/highlight/HighlightUtils.java
@@ -35,6 +35,7 @@
 import org.apache.lucene.search.highlight.Encoder;
 import org.apache.lucene.search.highlight.SimpleHTMLEncoder;
 import org.opensearch.index.fieldvisitor.CustomFieldsVisitor;
+import org.opensearch.index.mapper.DerivedFieldValueFetcher;
 import org.opensearch.index.mapper.MappedFieldType;
 import org.opensearch.index.mapper.ValueFetcher;
 import org.opensearch.index.query.QueryShardContext;
@@ -77,6 +78,9 @@ public static List<Object> loadFieldValues(
             return textsToHighlight != null ? textsToHighlight : Collections.emptyList();
         }
         ValueFetcher fetcher = fieldType.valueFetcher(context, null, null);
+        if (fetcher instanceof DerivedFieldValueFetcher) {
+            fetcher.setNextReader(hitContext.reader().getContext());
+        }
         return fetcher.fetchValues(hitContext.sourceLookup());
     }
 
diff --git a/server/src/main/java/org/opensearch/search/fetch/subphase/highlight/UnifiedHighlighter.java b/server/src/main/java/org/opensearch/search/fetch/subphase/highlight/UnifiedHighlighter.java
index df85246a84d54..c791c8bc05054 100644
--- a/server/src/main/java/org/opensearch/search/fetch/subphase/highlight/UnifiedHighlighter.java
+++ b/server/src/main/java/org/opensearch/search/fetch/subphase/highlight/UnifiedHighlighter.java
@@ -48,6 +48,7 @@
 import org.opensearch.common.CheckedSupplier;
 import org.opensearch.core.common.Strings;
 import org.opensearch.core.common.text.Text;
+import org.opensearch.index.mapper.DerivedFieldType;
 import org.opensearch.index.mapper.DocumentMapper;
 import org.opensearch.index.mapper.IdFieldMapper;
 import org.opensearch.index.mapper.MappedFieldType;
@@ -159,6 +160,10 @@ CustomUnifiedHighlighter buildHighlighter(FieldHighlightContext fieldContext) th
         Integer fieldMaxAnalyzedOffset = fieldContext.field.fieldOptions().maxAnalyzerOffset();
         int numberOfFragments = fieldContext.field.fieldOptions().numberOfFragments();
         Analyzer analyzer = getAnalyzer(fieldContext.context.mapperService().documentMapper());
+        if (fieldContext.context.getQueryShardContext().getDerivedFieldType(fieldContext.fieldName) != null) {
+            analyzer = ((DerivedFieldType) fieldContext.context.getQueryShardContext().getDerivedFieldType(fieldContext.fieldName))
+                .getIndexAnalyzer();
+        }
         if (fieldMaxAnalyzedOffset != null) {
             analyzer = getLimitedOffsetAnalyzer(analyzer, fieldMaxAnalyzedOffset);
         }
diff --git a/server/src/test/java/org/opensearch/index/mapper/DerivedFieldMapperQueryTests.java b/server/src/test/java/org/opensearch/index/mapper/DerivedFieldMapperQueryTests.java
index bd6d7b88ade28..1307028dd27b0 100644
--- a/server/src/test/java/org/opensearch/index/mapper/DerivedFieldMapperQueryTests.java
+++ b/server/src/test/java/org/opensearch/index/mapper/DerivedFieldMapperQueryTests.java
@@ -19,8 +19,10 @@
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.TopDocs;
 import org.apache.lucene.store.Directory;
+import org.opensearch.common.collect.Tuple;
 import org.opensearch.common.lucene.Lucene;
 import org.opensearch.core.index.Index;
+import org.opensearch.geometry.Rectangle;
 import org.opensearch.index.query.QueryBuilders;
 import org.opensearch.index.query.QueryShardContext;
 import org.opensearch.script.DerivedFieldScript;
@@ -32,6 +34,7 @@
 
 import org.mockito.Mockito;
 
+import static org.opensearch.index.query.QueryBuilders.geoShapeQuery;
 import static org.mockito.Mockito.when;
 
 public class DerivedFieldMapperQueryTests extends MapperServiceTestCase {
@@ -40,13 +43,14 @@ public class DerivedFieldMapperQueryTests extends MapperServiceTestCase {
     // Raw Message, Request Succeeded (boolean), Timestamp (long), Client IP, Method, Request Size (double), Duration (long)
     private static final Object[][] raw_requests = new Object[][] {
         {
-            "40.135.0.0 GET /images/hm_bg.jpg?size=1.5KB HTTP/1.0 200 2024-03-20T08:30:45 1500",
+            "40.135.0.0 GET /images/hm_bg.jpg?size=1.5KB loc 10.0 20.0 HTTP/1.0 200 2024-03-20T08:30:45 1500",
             true,
             1710923445000L,
             "40.135.0.0",
             "GET",
             1.5,
-            1500L },
+            1500L,
+            new Tuple<>(10.0, 20.0) },
         {
             "232.0.0.0 GET /images/hm_bg.jpg?size=2.3KB HTTP/1.0 400 2024-03-20T09:15:20 2300",
             false,
@@ -54,7 +58,8 @@ public class DerivedFieldMapperQueryTests extends MapperServiceTestCase {
             "232.0.0.0",
             "GET",
             2.3,
-            2300L },
+            2300L,
+            new Tuple<>(20.0, 30.0) },
         {
             "26.1.0.0 DELETE /images/hm_bg.jpg?size=3.7KB HTTP/1.0 200 2024-03-20T10:05:55 3700",
             true,
@@ -62,7 +67,8 @@ public class DerivedFieldMapperQueryTests extends MapperServiceTestCase {
             "26.1.0.0",
             "DELETE",
             3.7,
-            3700L },
+            3700L,
+            new Tuple<>(30.0, 40.0) },
         {
             "247.37.0.0 GET /french/splash_inet.html?size=4.1KB HTTP/1.0 400 2024-03-20T11:20:10 4100",
             false,
@@ -70,7 +76,8 @@ public class DerivedFieldMapperQueryTests extends MapperServiceTestCase {
             "247.37.0.0",
             "GET",
             4.1,
-            4100L },
+            4100L,
+            new Tuple<>(40.0, 50.0) },
         {
             "247.37.0.0 DELETE /french/splash_inet.html?size=5.8KB HTTP/1.0 400 2024-03-20T12:45:30 5800",
             false,
@@ -78,7 +85,8 @@ public class DerivedFieldMapperQueryTests extends MapperServiceTestCase {
             "247.37.0.0",
             "DELETE",
             5.8,
-            5800L },
+            5800L,
+            new Tuple<>(50.0, 60.0) },
         {
             "10.20.30.40 GET /path/to/resource?size=6.3KB HTTP/1.0 200 2024-03-20T13:10:15 6300",
             true,
@@ -86,7 +94,8 @@ public class DerivedFieldMapperQueryTests extends MapperServiceTestCase {
             "10.20.30.40",
             "GET",
             6.3,
-            6300L },
+            6300L,
+            new Tuple<>(60.0, 70.0) },
         {
             "50.60.70.80 GET /path/to/resource?size=7.2KB HTTP/1.0 404 2024-03-20T14:20:50 7200",
             false,
@@ -94,7 +103,8 @@ public class DerivedFieldMapperQueryTests extends MapperServiceTestCase {
             "50.60.70.80",
             "GET",
             7.2,
-            7200L },
+            7200L,
+            new Tuple<>(70.0, 80.0) },
         {
             "127.0.0.1 PUT /path/to/resource?size=8.9KB HTTP/1.0 500 2024-03-20T15:30:25 8900",
             false,
@@ -102,7 +112,8 @@ public class DerivedFieldMapperQueryTests extends MapperServiceTestCase {
             "127.0.0.1",
             "PUT",
             8.9,
-            8900L },
+            8900L,
+            new Tuple<>(80.0, 90.0) },
         {
             "127.0.0.1 GET /path/to/resource?size=9.4KB HTTP/1.0 200 2024-03-20T16:40:15 9400",
             true,
@@ -110,7 +121,8 @@ public class DerivedFieldMapperQueryTests extends MapperServiceTestCase {
             "127.0.0.1",
             "GET",
             9.4,
-            9400L },
+            9400L,
+            new Tuple<>(85.0, 90.0) },
         {
             "192.168.1.1 GET /path/to/resource?size=10.7KB HTTP/1.0 400 2024-03-20T17:50:40 10700",
             false,
@@ -118,7 +130,8 @@ public class DerivedFieldMapperQueryTests extends MapperServiceTestCase {
             "192.168.1.1",
             "GET",
             10.7,
-            10700L } };
+            10700L,
+            new Tuple<>(90.0, 90.0) } };
 
     public void testAllPossibleQueriesOnDerivedFields() throws IOException {
         MapperService mapperService = createMapperService(topMapping(b -> {
@@ -169,6 +182,12 @@ public void testAllPossibleQueriesOnDerivedFields() throws IOException {
                     b.field("script", "");
                 }
                 b.endObject();
+                b.startObject("geopoint");
+                {
+                    b.field("type", "geo_point");
+                    b.field("script", "");
+                }
+                b.endObject();
             }
             b.endObject();
         }));
@@ -273,6 +292,13 @@ public void execute() {
                 query = QueryBuilders.regexpQuery("method", ".*LET.*").toQuery(queryShardContext);
                 topDocs = searcher.search(query, 10);
                 assertEquals(2, topDocs.totalHits.value);
+
+                // GeoPoint Query
+                scriptIndex[0] = 7;
+
+                query = geoShapeQuery("geopoint", new Rectangle(0.0, 55.0, 55.0, 0.0)).toQuery(queryShardContext);
+                topDocs = searcher.search(query, 10);
+                assertEquals(4, topDocs.totalHits.value);
             }
         }
     }
diff --git a/server/src/test/java/org/opensearch/index/mapper/DerivedFieldTypeTests.java b/server/src/test/java/org/opensearch/index/mapper/DerivedFieldTypeTests.java
index 72fb7c88cc478..897848008fd5f 100644
--- a/server/src/test/java/org/opensearch/index/mapper/DerivedFieldTypeTests.java
+++ b/server/src/test/java/org/opensearch/index/mapper/DerivedFieldTypeTests.java
@@ -15,6 +15,7 @@
 import org.apache.lucene.document.LatLonPoint;
 import org.apache.lucene.document.LongField;
 import org.apache.lucene.document.LongPoint;
+import org.opensearch.common.collect.Tuple;
 import org.opensearch.script.Script;
 
 import java.util.List;
@@ -28,9 +29,7 @@ private DerivedFieldType createDerivedFieldType(String type) {
         Mapper.BuilderContext context = mock(Mapper.BuilderContext.class);
         when(context.path()).thenReturn(new ContentPath());
         return new DerivedFieldType(
-            type + " _derived_field",
-            type,
-            new Script(""),
+            new DerivedField(type + " _derived_field", type, new Script("")),
             DerivedFieldSupportedTypes.getFieldMapperFromType(type, type + "_derived_field", context),
             DerivedFieldSupportedTypes.getIndexableFieldGeneratorType(type, type + "_derived_field")
         );
@@ -53,7 +52,7 @@ public void testDateType() {
     public void testGeoPointType() {
         DerivedFieldType dft = createDerivedFieldType("geo_point");
         assertTrue(dft.typeFieldMapper instanceof GeoPointFieldMapper);
-        assertTrue(dft.indexableFieldGenerator.apply(List.of(10.0, 20.0)) instanceof LatLonPoint);
+        assertTrue(dft.indexableFieldGenerator.apply(new Tuple<>(10.0, 20.0)) instanceof LatLonPoint);
         expectThrows(ClassCastException.class, () -> dft.indexableFieldGenerator.apply(List.of(10.0)));
         expectThrows(ClassCastException.class, () -> dft.indexableFieldGenerator.apply(List.of()));
         expectThrows(ClassCastException.class, () -> dft.indexableFieldGenerator.apply(List.of("10")));
diff --git a/server/src/test/java/org/opensearch/index/query/DerivedFieldQueryTests.java b/server/src/test/java/org/opensearch/index/query/DerivedFieldQueryTests.java
index 1bb303a874b9a..5a11ebebb312e 100644
--- a/server/src/test/java/org/opensearch/index/query/DerivedFieldQueryTests.java
+++ b/server/src/test/java/org/opensearch/index/query/DerivedFieldQueryTests.java
@@ -22,6 +22,7 @@
 import org.apache.lucene.search.TopDocs;
 import org.apache.lucene.store.Directory;
 import org.opensearch.common.lucene.Lucene;
+import org.opensearch.index.mapper.DerivedFieldSupportedTypes;
 import org.opensearch.index.mapper.DerivedFieldValueFetcher;
 import org.opensearch.script.DerivedFieldScript;
 import org.opensearch.script.Script;
@@ -75,14 +76,17 @@ public void execute() {
 
         // Create ValueFetcher from mocked DerivedFieldScript.Factory
         DerivedFieldScript.LeafFactory leafFactory = factory.newFactory((new Script("")).getParams(), searchLookup);
-        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(leafFactory);
+        DerivedFieldValueFetcher valueFetcher = new DerivedFieldValueFetcher(
+            leafFactory,
+            null,
+            DerivedFieldSupportedTypes.getIndexableFieldGeneratorType("keyword", "ip_from_raw_request")
+        );
 
         // Create DerivedFieldQuery
         DerivedFieldQuery derivedFieldQuery = new DerivedFieldQuery(
             new TermQuery(new Term("ip_from_raw_request", "247.37.0.0")),
             valueFetcher,
             searchLookup,
-            (o -> new KeywordField("ip_from_raw_request", (String) o, Field.Store.NO)),
             Lucene.STANDARD_ANALYZER
         );
 
diff --git a/server/src/test/java/org/opensearch/index/query/QueryShardContextTests.java b/server/src/test/java/org/opensearch/index/query/QueryShardContextTests.java
index 1a2ad49a3f334..6a7bf10835ddd 100644
--- a/server/src/test/java/org/opensearch/index/query/QueryShardContextTests.java
+++ b/server/src/test/java/org/opensearch/index/query/QueryShardContextTests.java
@@ -31,6 +31,7 @@
 
 package org.opensearch.index.query;
 
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
 import org.apache.lucene.document.Field;
 import org.apache.lucene.document.StringField;
 import org.apache.lucene.index.DirectoryReader;
@@ -63,10 +64,17 @@
 import org.opensearch.index.fielddata.LeafFieldData;
 import org.opensearch.index.fielddata.ScriptDocValues;
 import org.opensearch.index.fielddata.plain.AbstractLeafOrdinalsFieldData;
+import org.opensearch.index.mapper.ContentPath;
+import org.opensearch.index.mapper.DerivedField;
+import org.opensearch.index.mapper.DerivedFieldMapper;
+import org.opensearch.index.mapper.DocumentMapper;
 import org.opensearch.index.mapper.IndexFieldMapper;
 import org.opensearch.index.mapper.MappedFieldType;
+import org.opensearch.index.mapper.Mapper;
 import org.opensearch.index.mapper.MapperService;
+import org.opensearch.index.mapper.MappingLookup;
 import org.opensearch.index.mapper.TextFieldMapper;
+import org.opensearch.script.Script;
 import org.opensearch.search.lookup.LeafDocLookup;
 import org.opensearch.search.lookup.LeafSearchLookup;
 import org.opensearch.search.lookup.SearchLookup;
@@ -77,6 +85,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.Supplier;
 
@@ -118,6 +127,28 @@ public void testFailIfFieldMappingNotFound() {
         assertThat(result.name(), equalTo("name"));
     }
 
+    public void testDerivedFieldMapping() {
+        QueryShardContext context = createQueryShardContext(IndexMetadata.INDEX_UUID_NA_VALUE, null);
+        assertNull(context.failIfFieldMappingNotFound("test_derived", null));
+        context.setDerivedFieldTypes(null);
+        assertNull(context.failIfFieldMappingNotFound("test_derived", null));
+        DocumentMapper documentMapper = mock(DocumentMapper.class);
+        Mapper.BuilderContext builderContext = new Mapper.BuilderContext(Settings.EMPTY, new ContentPath(0));
+        DerivedFieldMapper derivedFieldMapper = new DerivedFieldMapper.Builder(new DerivedField("test_derived", "keyword", new Script("")))
+            .build(builderContext);
+        MappingLookup mappingLookup = new MappingLookup(
+            Collections.singletonList(derivedFieldMapper),
+            Collections.emptyList(),
+            Collections.emptyList(),
+            0,
+            new StandardAnalyzer()
+        );
+        when(documentMapper.mappers()).thenReturn(mappingLookup);
+        context.setDerivedFieldTypes(Map.of("test_derived", derivedFieldMapper.fieldType()));
+        context.setAllowUnmappedFields(false);
+        assertEquals(derivedFieldMapper.fieldType(), context.failIfFieldMappingNotFound("test_derived", null));
+    }
+
     public void testToQueryFails() {
         QueryShardContext context = createQueryShardContext(IndexMetadata.INDEX_UUID_NA_VALUE, null);
         Exception exc = expectThrows(Exception.class, () -> context.toQuery(new AbstractQueryBuilder() {
diff --git a/server/src/test/java/org/opensearch/search/SearchServiceTests.java b/server/src/test/java/org/opensearch/search/SearchServiceTests.java
index bdbec62ab3e71..ba1047d3bd349 100644
--- a/server/src/test/java/org/opensearch/search/SearchServiceTests.java
+++ b/server/src/test/java/org/opensearch/search/SearchServiceTests.java
@@ -70,6 +70,7 @@
 import org.opensearch.index.IndexService;
 import org.opensearch.index.IndexSettings;
 import org.opensearch.index.engine.Engine;
+import org.opensearch.index.mapper.DerivedFieldType;
 import org.opensearch.index.query.AbstractQueryBuilder;
 import org.opensearch.index.query.MatchAllQueryBuilder;
 import org.opensearch.index.query.MatchNoneQueryBuilder;
@@ -546,6 +547,49 @@ public void testMaxDocvalueFieldsSearch() throws IOException {
         }
     }
 
+    public void testDerivedFieldsSearch() throws IOException {
+        createIndex("index");
+        final SearchService service = getInstanceFromNode(SearchService.class);
+        final IndicesService indicesService = getInstanceFromNode(IndicesService.class);
+        final IndexService indexService = indicesService.indexServiceSafe(resolveIndex("index"));
+        final IndexShard indexShard = indexService.getShard(0);
+
+        SearchRequest searchRequest = new SearchRequest().allowPartialSearchResults(true);
+        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
+        searchRequest.source(searchSourceBuilder);
+
+        for (int i = 0; i < 5; i++) {
+            searchSourceBuilder.derivedField(
+                "field" + i,
+                "date",
+                new Script(ScriptType.INLINE, MockScriptEngine.NAME, CustomScriptPlugin.DUMMY_SCRIPT, Collections.emptyMap())
+            );
+        }
+        final ShardSearchRequest request = new ShardSearchRequest(
+            OriginalIndices.NONE,
+            searchRequest,
+            indexShard.shardId(),
+            1,
+            new AliasFilter(null, Strings.EMPTY_ARRAY),
+            1.0f,
+            -1,
+            null,
+            null
+        );
+
+        try (ReaderContext reader = createReaderContext(indexService, indexShard)) {
+            try (SearchContext context = service.createContext(reader, request, null, randomBoolean())) {
+                assertNotNull(context);
+                for (int i = 0; i < 5; i++) {
+                    DerivedFieldType derivedFieldType = (DerivedFieldType) context.getQueryShardContext().getDerivedFieldType("field" + i);
+                    assertEquals("field" + i, derivedFieldType.name());
+                    assertEquals("date", derivedFieldType.getType());
+                }
+                assertNull(context.getQueryShardContext().getDerivedFieldType("field" + 5));
+            }
+        }
+    }
+
     /**
      * test that getting more than the allowed number of script_fields throws an exception
      */
diff --git a/server/src/test/java/org/opensearch/search/builder/SearchSourceBuilderTests.java b/server/src/test/java/org/opensearch/search/builder/SearchSourceBuilderTests.java
index 3d0e5c3eaf1c0..eea7b1829e9b0 100644
--- a/server/src/test/java/org/opensearch/search/builder/SearchSourceBuilderTests.java
+++ b/server/src/test/java/org/opensearch/search/builder/SearchSourceBuilderTests.java
@@ -53,6 +53,7 @@
 import org.opensearch.index.query.QueryRewriteContext;
 import org.opensearch.index.query.RandomQueryBuilder;
 import org.opensearch.index.query.Rewriteable;
+import org.opensearch.script.Script;
 import org.opensearch.search.AbstractSearchTestCase;
 import org.opensearch.search.rescore.QueryRescorerBuilder;
 import org.opensearch.search.sort.FieldSortBuilder;
@@ -311,6 +312,51 @@ public void testParseSort() throws IOException {
         }
     }
 
+    public void testDerivedFieldsParsingAndSerialization() throws IOException {
+        {
+            String restContent = "{\n"
+                + "  \"derived\": {\n"
+                + "    \"duration\": {\n"
+                + "      \"type\": \"long\",\n"
+                + "      \"script\": \"emit(doc['test'])\"\n"
+                + "    },\n"
+                + "    \"ip_from_message\": {\n"
+                + "      \"type\": \"keyword\",\n"
+                + "      \"script\": \"emit(doc['message'])\"\n"
+                + "    }\n"
+                + "  },\n"
+                + "    \"query\" : {\n"
+                + "        \"match\": { \"content\": { \"query\": \"foo bar\" }}\n"
+                + "     }\n"
+                + "}";
+
+            String expectedContent =
+                "{\"query\":{\"match\":{\"content\":{\"query\":\"foo bar\",\"operator\":\"OR\",\"prefix_length\":0,\"max_expansions\":50,\"fuzzy_transpositions\":true,\"lenient\":false,\"zero_terms_query\":\"NONE\",\"auto_generate_synonyms_phrase_query\":true,\"boost\":1.0}}},"
+                    + "\"derived\":{"
+                    + "\"duration\":{\"type\":\"long\",\"script\":\"emit(doc['test'])\"},\"ip_from_message\":{\"type\":\"keyword\",\"script\":\"emit(doc['message'])\"},\"derived_field\":{\"type\":\"keyword\",\"script\":{\"source\":\"emit(doc['message']\",\"lang\":\"painless\"}}}}";
+
+            try (XContentParser parser = createParser(JsonXContent.jsonXContent, restContent)) {
+                SearchSourceBuilder searchSourceBuilder = SearchSourceBuilder.fromXContent(parser);
+                searchSourceBuilder.derivedField("derived_field", "keyword", new Script("emit(doc['message']"));
+                searchSourceBuilder = rewrite(searchSourceBuilder);
+                assertEquals(2, searchSourceBuilder.getDerivedFieldsObject().size());
+                assertEquals(1, searchSourceBuilder.getDerivedFields().size());
+
+                try (BytesStreamOutput output = new BytesStreamOutput()) {
+                    searchSourceBuilder.writeTo(output);
+                    try (StreamInput in = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), namedWriteableRegistry)) {
+                        SearchSourceBuilder deserializedBuilder = new SearchSourceBuilder(in);
+                        String actualContent = deserializedBuilder.toString();
+                        assertEquals(expectedContent, actualContent);
+                        assertEquals(searchSourceBuilder.hashCode(), deserializedBuilder.hashCode());
+                        assertNotSame(searchSourceBuilder, deserializedBuilder);
+                    }
+                }
+            }
+        }
+
+    }
+
     public void testAggsParsing() throws IOException {
         {
             String restContent = "{\n"
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
new file mode 100644
index 0000000000000..28d97c74d9445
--- /dev/null
+++ b/server/src/test/java/org/opensearch/search/fetch/subphase/highlight/DerivedFieldFetchAndHighlightTests.java
@@ -0,0 +1,366 @@
+/*
+ * 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.search.fetch.subphase.highlight;
+
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.tests.index.RandomIndexWriter;
+import org.opensearch.Version;
+import org.opensearch.cluster.metadata.IndexMetadata;
+import org.opensearch.common.document.DocumentField;
+import org.opensearch.common.settings.Settings;
+import org.opensearch.common.xcontent.XContentFactory;
+import org.opensearch.core.common.bytes.BytesReference;
+import org.opensearch.core.xcontent.MediaTypeRegistry;
+import org.opensearch.core.xcontent.XContentBuilder;
+import org.opensearch.index.IndexService;
+import org.opensearch.index.IndexSettings;
+import org.opensearch.index.mapper.ContentPath;
+import org.opensearch.index.mapper.DerivedField;
+import org.opensearch.index.mapper.DerivedFieldSupportedTypes;
+import org.opensearch.index.mapper.DerivedFieldType;
+import org.opensearch.index.mapper.Mapper;
+import org.opensearch.index.mapper.MapperService;
+import org.opensearch.index.mapper.SourceToParse;
+import org.opensearch.index.query.QueryShardContext;
+import org.opensearch.index.query.Rewriteable;
+import org.opensearch.script.MockScriptEngine;
+import org.opensearch.script.Script;
+import org.opensearch.script.ScriptEngine;
+import org.opensearch.script.ScriptModule;
+import org.opensearch.script.ScriptService;
+import org.opensearch.script.ScriptType;
+import org.opensearch.search.SearchHit;
+import org.opensearch.search.fetch.FetchContext;
+import org.opensearch.search.fetch.FetchSubPhase;
+import org.opensearch.search.fetch.FetchSubPhaseProcessor;
+import org.opensearch.search.fetch.subphase.FieldAndFormat;
+import org.opensearch.search.fetch.subphase.FieldFetcher;
+import org.opensearch.search.internal.ContextIndexSearcher;
+import org.opensearch.test.OpenSearchSingleNodeTestCase;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonMap;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DerivedFieldFetchAndHighlightTests extends OpenSearchSingleNodeTestCase {
+    private static String DERIVED_FIELD_SCRIPT_1 = "derived_field_script_1";
+    private static String DERIVED_FIELD_SCRIPT_2 = "derived_field_script_2";
+
+    private static String DERIVED_FIELD_1 = "derived_1";
+    private static String DERIVED_FIELD_2 = "derived_2";
+
+    public void testDerivedFieldFromIndexMapping() throws IOException {
+        // Create index and mapper service
+        // Define mapping for derived fields, create 2 derived fields derived_1 and derived_2
+        XContentBuilder mapping = XContentFactory.jsonBuilder()
+            .startObject()
+            .startObject("derived")
+            .startObject(DERIVED_FIELD_1)
+            .field("type", "keyword")
+            .startObject("script")
+            .field("source", DERIVED_FIELD_SCRIPT_1)
+            .field("lang", "mockscript")
+            .endObject()
+            .endObject()
+            .startObject(DERIVED_FIELD_2)
+            .field("type", "keyword")
+            .startObject("script")
+            .field("source", DERIVED_FIELD_SCRIPT_2)
+            .field("lang", "mockscript")
+            .endObject()
+            .endObject()
+            .endObject()
+            .endObject();
+
+        // Create source document with 2 fields field1 and field2.
+        // derived_1 will act on field1 and derived_2 will act on derived_2. DERIVED_FIELD_SCRIPT_1 substitutes whitespaces with _
+        XContentBuilder source = XContentFactory.jsonBuilder()
+            .startObject()
+            .field("field1", "some_text_1")
+            .field("field2", "some_text_2")
+            .endObject();
+
+        int docId = 0;
+        IndexService indexService = createIndex("test_index", Settings.EMPTY, MapperService.SINGLE_MAPPING_NAME, mapping);
+        MapperService mapperService = indexService.mapperService();
+
+        try (
+            Directory dir = newDirectory();
+            RandomIndexWriter iw = new RandomIndexWriter(random(), dir, new IndexWriterConfig(mapperService.indexAnalyzer()));
+        ) {
+            iw.addDocument(
+                mapperService.documentMapper()
+                    .parse(new SourceToParse("test_index", "0", BytesReference.bytes(source), MediaTypeRegistry.JSON))
+                    .rootDoc()
+            );
+            try (IndexReader reader = iw.getReader()) {
+                IndexSearcher searcher = newSearcher(reader);
+                LeafReaderContext context = searcher.getIndexReader().leaves().get(0);
+                QueryShardContext mockShardContext = createQueryShardContext(mapperService, searcher);
+                mockShardContext.lookup().source().setSegmentAndDocument(context, docId);
+                // mockShardContext.setDerivedFieldTypes(Map.of("derived_2", createDerivedFieldType("derived_1", "keyword"), "derived_1",
+
+                // Assert the fetch phase works for both of the derived fields
+                Map<String, DocumentField> fields = fetchFields(mockShardContext, context, "*");
+
+                // Validate FetchPhase
+                {
+                    assertEquals(fields.size(), 2);
+                    assertEquals(1, fields.get(DERIVED_FIELD_1).getValues().size());
+                    assertEquals(1, fields.get(DERIVED_FIELD_2).getValues().size());
+                    assertEquals("some_text_1", fields.get(DERIVED_FIELD_1).getValue());
+                    assertEquals("some_text_2", fields.get(DERIVED_FIELD_2).getValue());
+                }
+
+                // Create a HighlightBuilder of type unified, set its fields as derived_1 and derived_2
+                HighlightBuilder highlightBuilder = new HighlightBuilder();
+                highlightBuilder.highlighterType("unified");
+                highlightBuilder.field(DERIVED_FIELD_1);
+                highlightBuilder.field(DERIVED_FIELD_2);
+                highlightBuilder = Rewriteable.rewrite(highlightBuilder, mockShardContext);
+                SearchHighlightContext searchHighlightContext = highlightBuilder.build(mockShardContext);
+
+                // Create a HighlightPhase with highlighter defined above
+                HighlightPhase highlightPhase = new HighlightPhase(Collections.singletonMap("unified", new UnifiedHighlighter()));
+
+                // create a fetch context to be used by HighlightPhase processor
+                FetchContext fetchContext = mock(FetchContext.class);
+                when(fetchContext.mapperService()).thenReturn(mockShardContext.getMapperService());
+                when(fetchContext.getQueryShardContext()).thenReturn(mockShardContext);
+                when(fetchContext.getIndexSettings()).thenReturn(indexService.getIndexSettings());
+                when(fetchContext.searcher()).thenReturn(
+                    new ContextIndexSearcher(
+                        searcher.getIndexReader(),
+                        searcher.getSimilarity(),
+                        searcher.getQueryCache(),
+                        searcher.getQueryCachingPolicy(),
+                        true,
+                        searcher.getExecutor(),
+                        null
+                    )
+                );
+
+                // The query used by FetchSubPhaseProcessor to highlight is a term query on DERIVED_FIELD_1
+                FetchSubPhaseProcessor subPhaseProcessor = highlightPhase.getProcessor(
+                    fetchContext,
+                    searchHighlightContext,
+                    new TermQuery(new Term(DERIVED_FIELD_1, "some_text_1"))
+                );
+
+                // Create a search hit using the derived fields fetched above in fetch phase
+                SearchHit searchHit = new SearchHit(docId, "0", null, fields, null);
+
+                // Create a HitContext of search hit
+                FetchSubPhase.HitContext hitContext = new FetchSubPhase.HitContext(
+                    searchHit,
+                    context,
+                    docId,
+                    mockShardContext.lookup().source()
+                );
+                hitContext.sourceLookup().loadSourceIfNeeded();
+                // process the HitContext using the highlightPhase subPhaseProcessor
+                subPhaseProcessor.process(hitContext);
+
+                // Validate that 1 highlight field is present
+                assertEquals(hitContext.hit().getHighlightFields().size(), 1);
+            }
+        }
+    }
+
+    public void testDerivedFieldFromSearchMapping() throws IOException {
+        // Create source document with 2 fields field1 and field2.
+        // derived_1 will act on field1 and derived_2 will act on derived_2. DERIVED_FIELD_SCRIPT_1 substitutes whitespaces with _
+        XContentBuilder source = XContentFactory.jsonBuilder()
+            .startObject()
+            .field("field1", "some_text_1")
+            .field("field2", "some_text_2")
+            .endObject();
+
+        int docId = 0;
+
+        // Create index and mapper service
+        // We are not defining derived fields in index mapping here
+        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().endObject();
+        IndexService indexService = createIndex("test_index", Settings.EMPTY, MapperService.SINGLE_MAPPING_NAME, mapping);
+        MapperService mapperService = indexService.mapperService();
+
+        try (
+            Directory dir = newDirectory();
+            RandomIndexWriter iw = new RandomIndexWriter(random(), dir, new IndexWriterConfig(mapperService.indexAnalyzer()));
+        ) {
+            iw.addDocument(
+                mapperService.documentMapper()
+                    .parse(new SourceToParse("test_index", "0", BytesReference.bytes(source), MediaTypeRegistry.JSON))
+                    .rootDoc()
+            );
+            try (IndexReader reader = iw.getReader()) {
+                IndexSearcher searcher = newSearcher(reader);
+                LeafReaderContext context = searcher.getIndexReader().leaves().get(0);
+                QueryShardContext mockShardContext = createQueryShardContext(mapperService, searcher);
+                mockShardContext.lookup().source().setSegmentAndDocument(context, docId);
+
+                // This mock behavior is similar to adding derived fields in search request
+                mockShardContext.setDerivedFieldTypes(
+                    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)
+                    )
+                );
+
+                // Assert the fetch phase works for both of the derived fields
+                Map<String, DocumentField> fields = fetchFields(mockShardContext, context, "derived_*");
+
+                // Validate FetchPhase
+                {
+                    assertEquals(fields.size(), 2);
+                    assertEquals(1, fields.get(DERIVED_FIELD_1).getValues().size());
+                    assertEquals(1, fields.get(DERIVED_FIELD_2).getValues().size());
+                    assertEquals("some_text_1", fields.get(DERIVED_FIELD_1).getValue());
+                    assertEquals("some_text_2", fields.get(DERIVED_FIELD_2).getValue());
+                }
+
+                // Create a HighlightBuilder of type unified, set its fields as derived_1 and derived_2
+                HighlightBuilder highlightBuilder = new HighlightBuilder();
+                highlightBuilder.highlighterType("unified");
+                highlightBuilder.field(DERIVED_FIELD_1);
+                highlightBuilder.field(DERIVED_FIELD_2);
+                highlightBuilder = Rewriteable.rewrite(highlightBuilder, mockShardContext);
+                SearchHighlightContext searchHighlightContext = highlightBuilder.build(mockShardContext);
+
+                // Create a HighlightPhase with highlighter defined above
+                HighlightPhase highlightPhase = new HighlightPhase(Collections.singletonMap("unified", new UnifiedHighlighter()));
+
+                // create a fetch context to be used by HighlightPhase processor
+                FetchContext fetchContext = mock(FetchContext.class);
+                when(fetchContext.mapperService()).thenReturn(mockShardContext.getMapperService());
+                when(fetchContext.getQueryShardContext()).thenReturn(mockShardContext);
+                when(fetchContext.getIndexSettings()).thenReturn(indexService.getIndexSettings());
+                when(fetchContext.searcher()).thenReturn(
+                    new ContextIndexSearcher(
+                        searcher.getIndexReader(),
+                        searcher.getSimilarity(),
+                        searcher.getQueryCache(),
+                        searcher.getQueryCachingPolicy(),
+                        true,
+                        searcher.getExecutor(),
+                        null
+                    )
+                );
+
+                // The query used by FetchSubPhaseProcessor to highlight is a term query on DERIVED_FIELD_1
+                FetchSubPhaseProcessor subPhaseProcessor = highlightPhase.getProcessor(
+                    fetchContext,
+                    searchHighlightContext,
+                    new TermQuery(new Term(DERIVED_FIELD_1, "some_text_1"))
+                );
+
+                // Create a search hit using the derived fields fetched above in fetch phase
+                SearchHit searchHit = new SearchHit(docId, "0", null, fields, null);
+
+                // Create a HitContext of search hit
+                FetchSubPhase.HitContext hitContext = new FetchSubPhase.HitContext(
+                    searchHit,
+                    context,
+                    docId,
+                    mockShardContext.lookup().source()
+                );
+                hitContext.sourceLookup().loadSourceIfNeeded();
+                // process the HitContext using the highlightPhase subPhaseProcessor
+                subPhaseProcessor.process(hitContext);
+
+                // Validate that 1 highlight field is present
+                assertEquals(hitContext.hit().getHighlightFields().size(), 1);
+            }
+        }
+    }
+
+    public static Map<String, DocumentField> fetchFields(
+        QueryShardContext queryShardContext,
+        LeafReaderContext context,
+        String fieldPattern
+    ) throws IOException {
+        List<FieldAndFormat> fields = List.of(new FieldAndFormat(fieldPattern, null));
+        FieldFetcher fieldFetcher = FieldFetcher.create(queryShardContext, queryShardContext.lookup(), fields);
+        fieldFetcher.setNextReader(context);
+        return fieldFetcher.fetch(queryShardContext.lookup().source(), Set.of());
+    }
+
+    private static QueryShardContext createQueryShardContext(MapperService mapperService, IndexSearcher indexSearcher) {
+        Settings settings = Settings.builder()
+            .put("index.version.created", Version.CURRENT)
+            .put("index.number_of_shards", 1)
+            .put("index.number_of_replicas", 0)
+            .put(IndexMetadata.SETTING_INDEX_UUID, "uuid")
+            .build();
+        IndexMetadata indexMetadata = new IndexMetadata.Builder("index").settings(settings).build();
+        IndexSettings indexSettings = new IndexSettings(indexMetadata, settings);
+
+        ScriptService scriptService = getScriptService();
+        return new QueryShardContext(
+            0,
+            indexSettings,
+            null,
+            null,
+            null,
+            mapperService,
+            null,
+            scriptService,
+            null,
+            null,
+            null,
+            indexSearcher,
+            null,
+            null,
+            null,
+            null,
+            null
+        );
+    }
+
+    private static ScriptService getScriptService() {
+        final MockScriptEngine engine = new MockScriptEngine(
+            MockScriptEngine.NAME,
+            Map.of(
+                DERIVED_FIELD_SCRIPT_1,
+                (script) -> ((String) ((Map<String, Object>) script.get("_source")).get("field1")).replace(" ", "_"),
+                DERIVED_FIELD_SCRIPT_2,
+                (script) -> ((String) ((Map<String, Object>) script.get("_source")).get("field2")).replace(" ", "_")
+            ),
+            Collections.emptyMap()
+        );
+        final Map<String, ScriptEngine> engines = singletonMap(engine.getType(), engine);
+        ScriptService scriptService = new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS);
+        return scriptService;
+    }
+
+    private DerivedFieldType createDerivedFieldType(String name, String type, String script) {
+        Mapper.BuilderContext context = mock(Mapper.BuilderContext.class);
+        when(context.path()).thenReturn(new ContentPath());
+        return new DerivedFieldType(
+            new DerivedField(name, type, new Script(ScriptType.INLINE, "mockscript", script, emptyMap())),
+            DerivedFieldSupportedTypes.getFieldMapperFromType(type, name, context),
+            DerivedFieldSupportedTypes.getIndexableFieldGeneratorType(type, name)
+        );
+    }
+}
diff --git a/test/framework/src/main/java/org/opensearch/script/MockScriptEngine.java b/test/framework/src/main/java/org/opensearch/script/MockScriptEngine.java
index 456b55883f91e..048f0acb60cde 100644
--- a/test/framework/src/main/java/org/opensearch/script/MockScriptEngine.java
+++ b/test/framework/src/main/java/org/opensearch/script/MockScriptEngine.java
@@ -35,6 +35,7 @@
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Scorable;
+import org.opensearch.common.collect.Tuple;
 import org.opensearch.index.query.IntervalFilterScript;
 import org.opensearch.index.similarity.ScriptedSimilarity.Doc;
 import org.opensearch.index.similarity.ScriptedSimilarity.Field;
@@ -43,8 +44,10 @@
 import org.opensearch.search.aggregations.pipeline.MovingFunctionScript;
 import org.opensearch.search.lookup.LeafSearchLookup;
 import org.opensearch.search.lookup.SearchLookup;
+import org.opensearch.search.lookup.SourceLookup;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -282,16 +285,39 @@ public double execute(Map<String, Object> params1, double[] values) {
             IntervalFilterScript.Factory factory = mockCompiled::createIntervalFilterScript;
             return context.factoryClazz.cast(factory);
         } else if (context.instanceClazz.equals(DerivedFieldScript.class)) {
-            DerivedFieldScript.Factory factory = (derivedFieldsParams, lookup) -> ctx -> new DerivedFieldScript(
-                derivedFieldsParams,
-                lookup,
-                ctx
-            ) {
+            DerivedFieldScript.Factory factory = new DerivedFieldScript.Factory() {
                 @Override
-                public void execute() {
-                    Map<String, Object> vars = new HashMap<>(derivedFieldsParams);
-                    vars.put("params", derivedFieldsParams);
-                    script.apply(vars);
+                public boolean isResultDeterministic() {
+                    return true;
+                }
+
+                @Override
+                public DerivedFieldScript.LeafFactory newFactory(Map<String, Object> derivedFieldParams, SearchLookup lookup) {
+                    return ctx -> new DerivedFieldScript(derivedFieldParams, lookup, ctx) {
+                        @Override
+                        public void execute() {
+                            Map<String, Object> vars = new HashMap<>(derivedFieldParams);
+                            SourceLookup sourceLookup = lookup.source();
+                            vars.put("params", derivedFieldParams);
+                            vars.put("_source", sourceLookup.loadSourceIfNeeded());
+                            Object result = script.apply(vars);
+                            if (result instanceof ArrayList) {
+                                for (Object v : ((ArrayList<?>) result)) {
+                                    if (v instanceof HashMap) {
+                                        addEmittedValue(new Tuple(((HashMap<?, ?>) v).get("lat"), ((HashMap<?, ?>) v).get("lon")));
+                                    } else {
+                                        addEmittedValue(v);
+                                    }
+                                }
+                            } else {
+                                if (result instanceof HashMap) {
+                                    addEmittedValue(new Tuple(((HashMap<?, ?>) result).get("lat"), ((HashMap<?, ?>) result).get("lon")));
+                                } else {
+                                    addEmittedValue(result);
+                                }
+                            }
+                        }
+                    };
                 }
             };
             return context.factoryClazz.cast(factory);