diff --git a/README.asciidoc b/README.asciidoc index c1945e56b025b..a1ccbefe3bdbc 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -33,9 +33,11 @@ https://www.elastic.co/downloads/elasticsearch[elastic.co/downloads/elasticsearc === Run Elasticsearch locally //// -IMPORTANT: This content is replicated in the Elasticsearch guide. See `run-elasticsearch-locally.asciidoc`. -Both will soon be replaced by a quickstart script. -//// +IMPORTANT: This content is replicated in `run-elasticsearch-locally.asciidoc`. +Ensure both files are in sync. + +https://github.com/elastic/start-local is the source of truth. +//// [WARNING] ==== @@ -44,89 +46,67 @@ DO NOT USE THESE INSTRUCTIONS FOR PRODUCTION DEPLOYMENTS. This setup is intended for local development and testing only. ==== -The following commands help you very quickly spin up a single-node Elasticsearch cluster, together with Kibana in Docker. -Use this setup for local development or testing. +Quickly set up Elasticsearch and Kibana in Docker for local development or testing, using the https://github.com/elastic/start-local?tab=readme-ov-file#-try-elasticsearch-and-kibana-locally[`start-local` script]. -==== Prerequisites +ℹ️ For more detailed information about the `start-local` setup, refer to the https://github.com/elastic/start-local[README on GitHub]. -If you don't have Docker installed, https://www.docker.com/products/docker-desktop[download and install Docker Desktop] for your operating system. +==== Prerequisites -==== Set environment variables +- If you don't have Docker installed, https://www.docker.com/products/docker-desktop[download and install Docker Desktop] for your operating system. +- If you're using Microsoft Windows, then install https://learn.microsoft.com/en-us/windows/wsl/install[Windows Subsystem for Linux (WSL)]. -Configure the following environment variables. +==== Trial license -[source,sh] ----- -export ELASTIC_PASSWORD="" # password for "elastic" username -export KIBANA_PASSWORD="" # Used internally by Kibana, must be at least 6 characters long ----- +This setup comes with a one-month trial of the Elastic *Platinum* license. +After the trial period, the license reverts to *Free and open - Basic*. +Refer to https://www.elastic.co/subscriptions[Elastic subscriptions] for more information. -==== Create a Docker network +==== Run `start-local` -To run both Elasticsearch and Kibana, you'll need to create a Docker network: +To set up Elasticsearch and Kibana locally, run the `start-local` script: [source,sh] ---- -docker network create elastic-net +curl -fsSL https://elastic.co/start-local | sh ---- +// NOTCONSOLE -==== Run Elasticsearch +This script creates an `elastic-start-local` folder containing configuration files and starts both Elasticsearch and Kibana using Docker. -Start the Elasticsearch container with the following command: +After running the script, you can access Elastic services at the following endpoints: -[source,sh] ----- -docker run -p 127.0.0.1:9200:9200 -d --name elasticsearch --network elastic-net \ - -e ELASTIC_PASSWORD=$ELASTIC_PASSWORD \ - -e "discovery.type=single-node" \ - -e "xpack.security.http.ssl.enabled=false" \ - -e "xpack.license.self_generated.type=trial" \ - docker.elastic.co/elasticsearch/elasticsearch:{version} ----- +* *Elasticsearch*: http://localhost:9200 +* *Kibana*: http://localhost:5601 -==== Run Kibana (optional) +The script generates a random password for the `elastic` user, which is displayed at the end of the installation and stored in the `.env` file. -To run Kibana, you must first set the `kibana_system` password in the Elasticsearch container. +[CAUTION] +==== +This setup is for local testing only. HTTPS is disabled, and Basic authentication is used for Elasticsearch. For security, Elasticsearch and Kibana are accessible only through `localhost`. +==== -[source,sh] ----- -# configure the Kibana password in the ES container -curl -u elastic:$ELASTIC_PASSWORD \ - -X POST \ - http://localhost:9200/_security/user/kibana_system/_password \ - -d '{"password":"'"$KIBANA_PASSWORD"'"}' \ - -H 'Content-Type: application/json' ----- -// NOTCONSOLE +==== API access -Start the Kibana container with the following command: +An API key for Elasticsearch is generated and stored in the `.env` file as `ES_LOCAL_API_KEY`. +Use this key to connect to Elasticsearch with a https://www.elastic.co/guide/en/elasticsearch/client/index.html[programming language client] or the https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html[REST API]. + +From the `elastic-start-local` folder, check the connection to Elasticsearch using `curl`: [source,sh] +---- +source .env +curl $ES_LOCAL_URL -H "Authorization: ApiKey ${ES_LOCAL_API_KEY}" ---- -docker run -p 127.0.0.1:5601:5601 -d --name kibana --network elastic-net \ - -e ELASTICSEARCH_URL=http://elasticsearch:9200 \ - -e ELASTICSEARCH_HOSTS=http://elasticsearch:9200 \ - -e ELASTICSEARCH_USERNAME=kibana_system \ - -e ELASTICSEARCH_PASSWORD=$KIBANA_PASSWORD \ - -e "xpack.security.enabled=false" \ - -e "xpack.license.self_generated.type=trial" \ - docker.elastic.co/kibana/kibana:{version} ----- - -.Trial license -[%collapsible] -==== -The service is started with a trial license. The trial license enables all features of Elasticsearch for a trial period of 30 days. After the trial period expires, the license is downgraded to a basic license, which is free forever. If you prefer to skip the trial and use the basic license, set the value of the `xpack.license.self_generated.type` variable to basic instead. For a detailed feature comparison between the different licenses, refer to our https://www.elastic.co/subscriptions[subscriptions page]. -==== +// NOTCONSOLE -==== Send requests to Elasticsearch +=== Send requests to Elasticsearch You send data and other requests to Elasticsearch through REST APIs. You can interact with Elasticsearch using any client that sends HTTP requests, such as the https://www.elastic.co/guide/en/elasticsearch/client/index.html[Elasticsearch language clients] and https://curl.se[curl]. -===== Using curl +==== Using curl Here's an example curl command to create a new Elasticsearch index, using basic auth: @@ -139,7 +119,7 @@ curl -u elastic:$ELASTIC_PASSWORD \ ---- // NOTCONSOLE -===== Using a language client +==== Using a language client To connect to your local dev Elasticsearch cluster with a language client, you can use basic authentication with the `elastic` username and the password you set in the environment variable. @@ -167,7 +147,7 @@ client = Elasticsearch( print(client.info()) ---- -===== Using the Dev Tools Console +==== Using the Dev Tools Console Kibana's developer console provides an easy way to experiment and test requests. To access the console, open Kibana, then go to **Management** > **Dev Tools**. diff --git a/docs/reference/run-elasticsearch-locally.asciidoc b/docs/reference/run-elasticsearch-locally.asciidoc index 64bcd3d066529..1a115ae926ea2 100644 --- a/docs/reference/run-elasticsearch-locally.asciidoc +++ b/docs/reference/run-elasticsearch-locally.asciidoc @@ -1,5 +1,11 @@ +//// +IMPORTANT: This content is replicated in the Elasticsearch repo root readme. Ensure both files are in sync. + +https://github.com/elastic/start-local is the source of truth. +//// + [[run-elasticsearch-locally]] -== Run {es} locally in Docker +== Run {es} locally ++++ Run {es} locally ++++ @@ -8,164 +14,74 @@ ==== *DO NOT USE THESE INSTRUCTIONS FOR PRODUCTION DEPLOYMENTS* -The instructions on this page are for *local development only*. Do not use these instructions for production deployments, because they are not secure. -While this approach is convenient for experimenting and learning, you should never run Elasticsearch in this way in a production environment. +The instructions on this page are for *local development only*. Do not use this configuration for production deployments, because it is not secure. +Refer to <> for a list of production deployment options. ==== -Follow this tutorial if you want to quickly set up {es} in Docker for local development or testing. +Quickly set up {es} and {kib} in Docker for local development or testing, using the https://github.com/elastic/start-local?tab=readme-ov-file#-try-elasticsearch-and-kibana-locally[`start-local` script]. -This tutorial also includes instructions for installing {kib}. - If you don't need access to the {kib} UI, then you can skip those instructions. +This setup comes with a one-month trial of the Elastic *Platinum* license. +After the trial period, the license reverts to *Free and open - Basic*. +Refer to https://www.elastic.co/subscriptions[Elastic subscriptions] for more information. [discrete] [[local-dev-prerequisites]] === Prerequisites -If you don't have Docker installed, https://www.docker.com/products/docker-desktop[download and install Docker Desktop] for your operating system. +- If you don't have Docker installed, https://www.docker.com/products/docker-desktop[download and install Docker Desktop] for your operating system. +- If you're using Microsoft Windows, then install https://learn.microsoft.com/en-us/windows/wsl/install[Windows Subsystem for Linux (WSL)]. [discrete] -[[local-dev-env-vars]] -=== Set environment variables +[[local-dev-quick-start]] +=== Run `start-local` -Configure the following environment variables. +To set up {es} and {kib} locally, run the `start-local` script: [source,sh] ---- -export ELASTIC_PASSWORD="" # password for "elastic" username -export KIBANA_PASSWORD="" # Used _internally_ by Kibana, must be at least 6 characters long ----- - -[discrete] -[[local-dev-create-docker-network]] -=== Create a Docker network - -To run both {es} and {kib}, you'll need to create a Docker network: - -[source,sh] ----- -docker network create elastic-net ----- - -[discrete] -[[local-dev-run-es]] -=== Run {es} - -Start the {es} container with the following command: - -ifeval::["{release-state}"=="unreleased"] -WARNING: Version {version} has not yet been released. -No Docker image is currently available for {es} {version}. -endif::[] - -[source,sh,subs="attributes"] ----- -docker run -p 127.0.0.1:9200:9200 -d --name elasticsearch --network elastic-net \ - -e ELASTIC_PASSWORD=$ELASTIC_PASSWORD \ - -e "discovery.type=single-node" \ - -e "xpack.security.http.ssl.enabled=false" \ - -e "xpack.license.self_generated.type=trial" \ - {docker-image} ----- - -[discrete] -[[local-dev-run-kib]] -=== Run {kib} (optional) - -To run {kib}, you must first set the `kibana_system` password in the {es} container. - -[source,sh,subs="attributes"] ----- -# configure the Kibana password in the ES container -curl -u elastic:$ELASTIC_PASSWORD \ - -X POST \ - http://localhost:9200/_security/user/kibana_system/_password \ - -d '{"password":"'"$KIBANA_PASSWORD"'"}' \ - -H 'Content-Type: application/json' +curl -fsSL https://elastic.co/start-local | sh ---- // NOTCONSOLE -Start the {kib} container with the following command: +This script creates an `elastic-start-local` folder containing configuration files and starts both {es} and {kib} using Docker. -ifeval::["{release-state}"=="unreleased"] -WARNING: Version {version} has not yet been released. -No Docker image is currently available for {es} {version}. -endif::[] +After running the script, you can access Elastic services at the following endpoints: -[source,sh,subs="attributes"] ----- -docker run -p 127.0.0.1:5601:5601 -d --name kibana --network elastic-net \ - -e ELASTICSEARCH_URL=http://elasticsearch:9200 \ - -e ELASTICSEARCH_HOSTS=http://elasticsearch:9200 \ - -e ELASTICSEARCH_USERNAME=kibana_system \ - -e ELASTICSEARCH_PASSWORD=$KIBANA_PASSWORD \ - -e "xpack.security.enabled=false" \ - -e "xpack.license.self_generated.type=trial" \ - {kib-docker-image} ----- +* *{es}*: http://localhost:9200 +* *{kib}*: http://localhost:5601 -When you access {kib}, use `elastic` as the username and the password you set earlier for the `ELASTIC_PASSWORD` environment variable. +The script generates a random password for the `elastic` user, which is displayed at the end of the installation and stored in the `.env` file. -[NOTE] +[CAUTION] ==== -The service is started with a trial license. The trial license enables all features of Elasticsearch for a trial period of 30 days. After the trial period expires, the license is downgraded to a basic license, which is free forever. +This setup is for local testing only. HTTPS is disabled, and Basic authentication is used for {es}. For security, {es} and {kib} are accessible only through `localhost`. ==== [discrete] -[[local-dev-connecting-clients]] -=== Connect to {es} with language clients - -To connect to the {es} cluster from a language client, you can use basic authentication with the `elastic` username and the password you set in the environment variable. - -.*Expand* for details -[%collapsible] -============== - -You'll use the following connection details: - -* **{es} endpoint**: `http://localhost:9200` -* **Username**: `elastic` -* **Password**: `$ELASTIC_PASSWORD` (Value you set in the environment variable) - -For example, to connect with the Python `elasticsearch` client: - -[source,python] ----- -import os -from elasticsearch import Elasticsearch +[[api-access]] +=== API access -username = 'elastic' -password = os.getenv('ELASTIC_PASSWORD') # Value you set in the environment variable +An API key for {es} is generated and stored in the `.env` file as `ES_LOCAL_API_KEY`. +Use this key to connect to {es} with a https://www.elastic.co/guide/en/elasticsearch/client/index.html[programming language client] or the <>. -client = Elasticsearch( - "http://localhost:9200", - basic_auth=(username, password) -) +From the `elastic-start-local` folder, check the connection to Elasticsearch using `curl`: -print(client.info()) ----- - -Here's an example curl command using basic authentication: - -[source,sh,subs="attributes"] ----- -curl -u elastic:$ELASTIC_PASSWORD \ - -X PUT \ - http://localhost:9200/my-new-index \ - -H 'Content-Type: application/json' +[source,sh] +---- +source .env +curl $ES_LOCAL_URL -H "Authorization: ApiKey ${ES_LOCAL_API_KEY}" ---- // NOTCONSOLE -============== - [discrete] -[[local-dev-next-steps]] -=== Next steps +[[local-dev-additional-info]] +=== Learn more -Use our <> to learn the basics of {es}. +For more detailed information about the `start-local` setup, refer to the https://github.com/elastic/start-local[README on GitHub]. +Learn about customizing the setup, logging, and more. [discrete] -[[local-dev-production]] -=== Moving to production +[[local-dev-next-steps]] +=== Next steps -This setup is not suitable for production use. -Refer to <> for more information. \ No newline at end of file +Use our <> to learn the basics of {es}. \ No newline at end of file diff --git a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/TokenCountFieldMapperTests.java b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/TokenCountFieldMapperTests.java index 614093f010895..1a4619e00f826 100644 --- a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/TokenCountFieldMapperTests.java +++ b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/TokenCountFieldMapperTests.java @@ -220,7 +220,7 @@ public SyntheticSourceExample example(int maxValues) { var textArray = values.stream().map(Value::text).toList(); - var blockExpectedList = values.stream().map(Value::tokenCount).filter(Objects::nonNull).toList(); + var blockExpectedList = values.stream().map(Value::tokenCount).filter(Objects::nonNull).sorted().toList(); var blockExpected = blockExpectedList.size() == 1 ? blockExpectedList.get(0) : blockExpectedList; return new SyntheticSourceExample(textArray, textArray, blockExpected, this::mapping); @@ -230,7 +230,7 @@ private record Value(String text, Integer tokenCount) {} private Value generateValue() { if (rarely()) { - return new Value(null, null); + return new Value(null, nullValue); } var text = randomList(0, 10, () -> randomAlphaOfLengthBetween(0, 10)).stream().collect(Collectors.joining(" ")); diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml index b5a9146bc54a6..a999bb7816065 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml @@ -535,6 +535,8 @@ object array in object with dynamic override: _source: mode: synthetic properties: + id: + type: integer path_no: dynamic: false properties: @@ -552,19 +554,25 @@ object array in object with dynamic override: refresh: true body: - '{ "create": { } }' - - '{ "path_no": [ { "some_int": 10 }, {"name": "foo"} ], "path_runtime": [ { "some_int": 20 }, {"name": "bar"} ], "name": "baz" }' + - '{ "id": 1, "path_no": [ { "some_int": 30 }, {"name": "baz"}, { "some_int": 20 }, {"name": "bar"} ], "name": "A" }' + - '{ "create": { } }' + - '{ "id": 2, "path_runtime": [ { "some_int": 30 }, {"name": "baz"}, { "some_int": 20 }, {"name": "bar"} ], "name": "B" }' - match: { errors: false } - do: search: index: test + sort: id - - match: { hits.total.value: 1 } - - match: { hits.hits.0._source.name: baz } - - match: { hits.hits.0._source.path_no.0.some_int: 10 } - - match: { hits.hits.0._source.path_no.1.name: foo } - - match: { hits.hits.0._source.path_runtime.0.some_int: 20 } - - match: { hits.hits.0._source.path_runtime.1.name: bar } + - match: { hits.hits.0._source.id: 1 } + - match: { hits.hits.0._source.name: A } + - match: { hits.hits.0._source.path_no.some_int: [ 30, 20 ] } + - match: { hits.hits.0._source.path_no.name: [ bar, baz ] } + + - match: { hits.hits.1._source.id: 2 } + - match: { hits.hits.1._source.name: B } + - match: { hits.hits.1._source.path_runtime.some_int: [ 30, 20 ] } + - match: { hits.hits.1._source.path_runtime.name: [ bar, baz ] } --- diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java index 1153ba65211dc..a7372f54d2c3b 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java @@ -401,7 +401,7 @@ static void parseObjectOrNested(DocumentParserContext context) throws IOExceptio context.addIgnoredField( new IgnoredSourceFieldMapper.NameValue( context.parent().fullPath(), - context.parent().fullPath().lastIndexOf(currentFieldName), + context.parent().fullPath().lastIndexOf(context.parent().leafName()), XContentDataHelper.encodeToken(parser), context.doc() ) @@ -803,27 +803,25 @@ private static void parseNonDynamicArray( boolean objectRequiresStoringSource = mapper instanceof ObjectMapper objectMapper && (objectMapper.storeArraySource() || (context.sourceKeepModeFromIndexSettings() == Mapper.SourceKeepMode.ARRAYS - && objectMapper instanceof NestedObjectMapper == false) - || objectMapper.dynamic == ObjectMapper.Dynamic.RUNTIME); + && objectMapper instanceof NestedObjectMapper == false)); boolean fieldWithFallbackSyntheticSource = mapper instanceof FieldMapper fieldMapper && fieldMapper.syntheticSourceMode() == FieldMapper.SyntheticSourceMode.FALLBACK; boolean fieldWithStoredArraySource = mapper instanceof FieldMapper fieldMapper && getSourceKeepMode(context, fieldMapper.sourceKeepMode()) != Mapper.SourceKeepMode.NONE; - boolean dynamicRuntimeContext = context.dynamic() == ObjectMapper.Dynamic.RUNTIME; boolean copyToFieldHasValuesInDocument = context.isWithinCopyTo() == false && context.isCopyToDestinationField(fullPath); if (objectRequiresStoringSource || fieldWithFallbackSyntheticSource - || dynamicRuntimeContext || fieldWithStoredArraySource || copyToFieldHasValuesInDocument) { context = context.addIgnoredFieldFromContext(IgnoredSourceFieldMapper.NameValue.fromContext(context, fullPath, null)); - } else if (mapper instanceof ObjectMapper objectMapper - && (objectMapper.isEnabled() == false || objectMapper.dynamic == ObjectMapper.Dynamic.FALSE)) { - context.addIgnoredField( - IgnoredSourceFieldMapper.NameValue.fromContext(context, fullPath, XContentDataHelper.encodeToken(context.parser())) - ); - return; - } + } else if (mapper instanceof ObjectMapper objectMapper && (objectMapper.isEnabled() == false)) { + // No need to call #addIgnoredFieldFromContext as both singleton and array instances of this object + // get tracked through ignored source. + context.addIgnoredField( + IgnoredSourceFieldMapper.NameValue.fromContext(context, fullPath, XContentDataHelper.encodeToken(context.parser())) + ); + return; + } } // In synthetic source, if any array element requires storing its source as-is, it takes precedence over diff --git a/server/src/test/java/org/elasticsearch/index/mapper/IgnoredSourceFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/IgnoredSourceFieldMapperTests.java index eaa7bf6528203..4bc33558e104b 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/IgnoredSourceFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/IgnoredSourceFieldMapperTests.java @@ -902,7 +902,6 @@ public void testObjectArrayAndValue() throws IOException { } b.endObject(); })).documentMapper(); - // { "path": [ { "stored":[ { "leaf": 10 } ] }, { "stored": { "leaf": 20 } } ] } var syntheticSource = syntheticSource(documentMapper, b -> { b.startArray("path"); { @@ -927,6 +926,91 @@ public void testObjectArrayAndValue() throws IOException { {"path":{"stored":[{"leaf":10},{"leaf":20}]}}""", syntheticSource); } + public void testObjectArrayAndValueDisabledObject() throws IOException { + DocumentMapper documentMapper = createMapperService(syntheticSourceMapping(b -> { + b.startObject("path").field("type", "object").startObject("properties"); + { + b.startObject("regular"); + { + b.startObject("properties").startObject("leaf").field("type", "integer").endObject().endObject(); + } + b.endObject(); + b.startObject("disabled").field("type", "object").field("enabled", false); + { + b.startObject("properties").startObject("leaf").field("type", "integer").endObject().endObject(); + } + b.endObject(); + } + b.endObject().endObject(); + })).documentMapper(); + var syntheticSource = syntheticSource(documentMapper, b -> { + b.startArray("path"); + { + b.startObject().startArray("disabled").startObject().field("leaf", 10).endObject().endArray().endObject(); + b.startObject().startObject("disabled").field("leaf", 20).endObject().endObject(); + b.startObject().startArray("regular").startObject().field("leaf", 10).endObject().endArray().endObject(); + b.startObject().startObject("regular").field("leaf", 20).endObject().endObject(); + } + b.endArray(); + }); + assertEquals(""" + {"path":{"disabled":[{"leaf":10},{"leaf":20}],"regular":{"leaf":[10,20]}}}""", syntheticSource); + } + + public void testObjectArrayAndValueNonDynamicObject() throws IOException { + DocumentMapper documentMapper = createMapperService(syntheticSourceMapping(b -> { + b.startObject("path").field("type", "object").startObject("properties"); + { + b.startObject("regular"); + { + b.startObject("properties").startObject("leaf").field("type", "integer").endObject().endObject(); + } + b.endObject(); + b.startObject("disabled").field("type", "object").field("dynamic", "false").endObject(); + } + b.endObject().endObject(); + })).documentMapper(); + var syntheticSource = syntheticSource(documentMapper, b -> { + b.startArray("path"); + { + b.startObject().startArray("disabled").startObject().field("leaf", 10).endObject().endArray().endObject(); + b.startObject().startObject("disabled").field("leaf", 20).endObject().endObject(); + b.startObject().startArray("regular").startObject().field("leaf", 10).endObject().endArray().endObject(); + b.startObject().startObject("regular").field("leaf", 20).endObject().endObject(); + } + b.endArray(); + }); + assertEquals(""" + {"path":{"disabled":{"leaf":[10,20]},"regular":{"leaf":[10,20]}}}""", syntheticSource); + } + + public void testObjectArrayAndValueDynamicRuntimeObject() throws IOException { + DocumentMapper documentMapper = createMapperService(syntheticSourceMapping(b -> { + b.startObject("path").field("type", "object").startObject("properties"); + { + b.startObject("regular"); + { + b.startObject("properties").startObject("leaf").field("type", "integer").endObject().endObject(); + } + b.endObject(); + b.startObject("runtime").field("type", "object").field("dynamic", "runtime").endObject(); + } + b.endObject().endObject(); + })).documentMapper(); + var syntheticSource = syntheticSource(documentMapper, b -> { + b.startArray("path"); + { + b.startObject().startArray("runtime").startObject().field("leaf", 10).endObject().endArray().endObject(); + b.startObject().startObject("runtime").field("leaf", 20).endObject().endObject(); + b.startObject().startArray("regular").startObject().field("leaf", 10).endObject().endArray().endObject(); + b.startObject().startObject("regular").field("leaf", 20).endObject().endObject(); + } + b.endArray(); + }); + assertEquals(""" + {"path":{"regular":{"leaf":[10,20]},"runtime":{"leaf":[10,20]}}}""", syntheticSource); + } + public void testDisabledObjectWithinHigherLevelArray() throws IOException { DocumentMapper documentMapper = createMapperService(syntheticSourceMapping(b -> { b.startObject("path"); @@ -1337,7 +1421,7 @@ public void testNoDynamicObjectSimpleArray() throws IOException { b.endArray(); }); assertEquals(""" - {"path":[{"name":"foo"},{"name":"bar"}]}""", syntheticSource); + {"path":{"name":["foo","bar"]}}""", syntheticSource); } public void testNoDynamicObjectSimpleValueArray() throws IOException { @@ -1365,7 +1449,20 @@ public void testNoDynamicObjectNestedArray() throws IOException { b.endArray(); }); assertEquals(""" - {"path":[{"to":{"foo":"A","bar":"B"}},{"to":{"foo":"C","bar":"D"}}]}""", syntheticSource); + {"path":{"to":[{"foo":"A","bar":"B"},{"foo":"C","bar":"D"}]}}""", syntheticSource); + } + + public void testNoDynamicRootObject() throws IOException { + DocumentMapper documentMapper = createMapperService(topMapping(b -> { + b.startObject("_source").field("mode", "synthetic").endObject().field("dynamic", "false"); + })).documentMapper(); + var syntheticSource = syntheticSource(documentMapper, b -> { + b.field("foo", "bar"); + b.startObject("path").field("X", "Y").endObject(); + b.array("name", "A", "D", "C", "B"); + }); + assertEquals(""" + {"foo":"bar","name":["A","D","C","B"],"path":{"X":"Y"}}""", syntheticSource); } public void testRuntimeDynamicObjectSingleField() throws IOException { @@ -1445,7 +1542,7 @@ public void testRuntimeDynamicObjectSimpleArray() throws IOException { b.endArray(); }); assertEquals(""" - {"path":[{"name":"foo"},{"name":"bar"}]}""", syntheticSource); + {"path":{"name":["foo","bar"]}}""", syntheticSource); } public void testRuntimeDynamicObjectSimpleValueArray() throws IOException { @@ -1473,7 +1570,7 @@ public void testRuntimeDynamicObjectNestedArray() throws IOException { b.endArray(); }); assertEquals(""" - {"path":[{"to":{"foo":"A","bar":"B"}},{"to":{"foo":"C","bar":"D"}}]}""", syntheticSource); + {"path":{"to":[{"foo":"A","bar":"B"},{"foo":"C","bar":"D"}]}}""", syntheticSource); } public void testDisabledSubObjectWithNameOverlappingParentName() throws IOException {