diff --git a/.backportrc.json b/.backportrc.json index 31698460c282..50cdda5b752e 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -1,9 +1,9 @@ { "upstream" : "elastic/elasticsearch", - "targetBranchChoices" : [ "main", "9.0", "8.19", "8.18", "8.17", "8.16", "8.15", "8.14", "8.13", "8.12", "8.11", "8.10", "8.9", "8.8", "8.7", "8.6", "8.5", "8.4", "8.3", "8.2", "8.1", "8.0", "7.17", "6.8" ], + "targetBranchChoices" : [ "main", "9.1", "9.0", "8.19", "8.18", "8.17", "8.16", "8.15", "8.14", "8.13", "8.12", "8.11", "8.10", "8.9", "8.8", "8.7", "8.6", "8.5", "8.4", "8.3", "8.2", "8.1", "8.0", "7.17", "6.8" ], "targetPRLabels" : [ "backport" ], "branchLabelMapping" : { - "^v9.1.0$" : "main", + "^v9.2.0$" : "main", "^v(\\d+).(\\d+).\\d+(?:-(?:alpha|beta|rc)\\d+)?$" : "$1.$2" } } \ No newline at end of file diff --git a/.buildkite/pipelines/intake.yml b/.buildkite/pipelines/intake.yml index 93c5c8eecc3f..18dafe732a10 100644 --- a/.buildkite/pipelines/intake.yml +++ b/.buildkite/pipelines/intake.yml @@ -65,7 +65,7 @@ steps: timeout_in_minutes: 300 matrix: setup: - BWC_VERSION: ["8.17.9", "8.18.4", "8.19.0", "9.0.4", "9.1.0"] + BWC_VERSION: ["8.17.9", "8.18.4", "8.19.0", "9.0.4", "9.1.0", "9.2.0"] agents: provider: gcp image: family/elasticsearch-ubuntu-2404 diff --git a/.buildkite/pipelines/periodic-packaging.yml b/.buildkite/pipelines/periodic-packaging.yml index cac74ac6ebaa..b3663ca0fce9 100644 --- a/.buildkite/pipelines/periodic-packaging.yml +++ b/.buildkite/pipelines/periodic-packaging.yml @@ -382,6 +382,22 @@ steps: env: BWC_VERSION: 9.1.0 + - label: "{{matrix.image}} / 9.2.0 / packaging-tests-upgrade" + command: ./.ci/scripts/packaging-test.sh -Dbwc.checkout.align=true destructiveDistroUpgradeTest.v9.2.0 + timeout_in_minutes: 300 + matrix: + setup: + image: + - rocky-8 + - ubuntu-2404 + agents: + provider: gcp + image: family/elasticsearch-{{matrix.image}} + machineType: custom-16-32768 + buildDirectory: /dev/shm/bk + env: + BWC_VERSION: 9.2.0 + - group: packaging-tests-windows steps: - label: "{{matrix.image}} / packaging-tests-windows" diff --git a/.buildkite/pipelines/periodic.yml b/.buildkite/pipelines/periodic.yml index 5c6af6ba91a5..fd1217fd1d31 100644 --- a/.buildkite/pipelines/periodic.yml +++ b/.buildkite/pipelines/periodic.yml @@ -420,6 +420,25 @@ steps: - signal_reason: agent_stop limit: 3 + - label: 9.2.0 / bwc + command: .ci/scripts/run-gradle.sh -Dbwc.checkout.align=true v9.2.0#bwcTest + timeout_in_minutes: 300 + agents: + provider: gcp + image: family/elasticsearch-ubuntu-2404 + machineType: n1-standard-32 + buildDirectory: /dev/shm/bk + preemptible: true + env: + BWC_VERSION: 9.2.0 + retry: + automatic: + - exit_status: "-1" + limit: 3 + signal_reason: none + - signal_reason: agent_stop + limit: 3 + - label: concurrent-search-tests command: .ci/scripts/run-gradle.sh -Dbwc.checkout.align=true -Dtests.jvm.argline=-Des.concurrent_search=true -Des.concurrent_search=true functionalTests timeout_in_minutes: 420 @@ -487,7 +506,7 @@ steps: setup: ES_RUNTIME_JAVA: - openjdk21 - BWC_VERSION: ["8.17.9", "8.18.4", "8.19.0", "9.0.4", "9.1.0"] + BWC_VERSION: ["8.17.9", "8.18.4", "8.19.0", "9.0.4", "9.1.0", "9.2.0"] agents: provider: gcp image: family/elasticsearch-ubuntu-2404 @@ -531,7 +550,7 @@ steps: ES_RUNTIME_JAVA: - openjdk21 - openjdk23 - BWC_VERSION: ["8.17.9", "8.18.4", "8.19.0", "9.0.4", "9.1.0"] + BWC_VERSION: ["8.17.9", "8.18.4", "8.19.0", "9.0.4", "9.1.0", "9.2.0"] agents: provider: gcp image: family/elasticsearch-ubuntu-2404 diff --git a/.ci/bwcVersions b/.ci/bwcVersions index 33f7091cad16..bb383b759d09 100644 --- a/.ci/bwcVersions +++ b/.ci/bwcVersions @@ -21,3 +21,4 @@ BWC_VERSION: - "8.19.0" - "9.0.4" - "9.1.0" + - "9.2.0" diff --git a/.ci/scripts/resolve-dra-manifest.sh b/.ci/scripts/resolve-dra-manifest.sh index 37ede297fdc0..67d86dbe5aba 100755 --- a/.ci/scripts/resolve-dra-manifest.sh +++ b/.ci/scripts/resolve-dra-manifest.sh @@ -6,7 +6,8 @@ strip_version() { } fetch_build() { - curl -sS https://artifacts-$1.elastic.co/$2/latest/$3.json \ + >&2 echo "Checking for build id: https://artifacts-$1.elastic.co/$2/latest/$3.json" + curl -sSf https://artifacts-$1.elastic.co/$2/latest/$3.json \ | jq -r '.build_id' } @@ -15,7 +16,15 @@ BRANCH="${BRANCH:-$2}" ES_VERSION="${ES_VERSION:-$3}" WORKFLOW=${WORKFLOW:-$4} -LATEST_BUILD=$(fetch_build $WORKFLOW $ARTIFACT $BRANCH) +if [[ "$WORKFLOW" == "staging" ]]; then + LATEST_BUILD=$(fetch_build $WORKFLOW $ARTIFACT $ES_VERSION) +elif [[ "$WORKFLOW" == "snapshot" ]]; then + LATEST_BUILD=$(fetch_build $WORKFLOW $ARTIFACT $BRANCH) +else + echo "Unknown workflow: $WORKFLOW" + exit 1 +fi + LATEST_VERSION=$(strip_version $LATEST_BUILD) # If the latest artifact version doesn't match what we expect, try the corresponding version branch. diff --git a/.ci/snapshotBwcVersions b/.ci/snapshotBwcVersions index 2df77ed9ede1..2092e0da165c 100644 --- a/.ci/snapshotBwcVersions +++ b/.ci/snapshotBwcVersions @@ -4,3 +4,4 @@ BWC_VERSION: - "8.19.0" - "9.0.4" - "9.1.0" + - "9.2.0" diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/ValuesSourceReaderBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/ValuesSourceReaderBenchmark.java index 2796bb5c6de1..a4504bedb364 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/ValuesSourceReaderBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/ValuesSourceReaderBenchmark.java @@ -40,6 +40,7 @@ import org.elasticsearch.compute.data.LongBlock; import org.elasticsearch.compute.data.LongVector; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.lucene.LuceneSourceOperator; +import org.elasticsearch.compute.lucene.ShardRefCounted; import org.elasticsearch.compute.lucene.ValuesSourceReaderOperator; import org.elasticsearch.compute.operator.topn.TopNOperator; import org.elasticsearch.core.IOUtils; @@ -477,6 +478,7 @@ public class ValuesSourceReaderBenchmark { pages.add( new Page( new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, blockFactory.newConstantIntBlockWith(0, end - begin).asVector(), blockFactory.newConstantIntBlockWith(ctx.ord, end - begin).asVector(), docs.build(), @@ -512,7 +514,14 @@ public class ValuesSourceReaderBenchmark { if (size >= BLOCK_LENGTH) { pages.add( new Page( - new DocVector(blockFactory.newConstantIntVector(0, size), leafs.build(), docs.build(), null).asBlock() + new DocVector( + + ShardRefCounted.ALWAYS_REFERENCED, + blockFactory.newConstantIntVector(0, size), + leafs.build(), + docs.build(), + null + ).asBlock() ) ); docs = blockFactory.newIntVectorBuilder(BLOCK_LENGTH); @@ -525,6 +534,8 @@ public class ValuesSourceReaderBenchmark { pages.add( new Page( new DocVector( + + ShardRefCounted.ALWAYS_REFERENCED, blockFactory.newConstantIntBlockWith(0, size).asVector(), leafs.build().asBlock().asVector(), docs.build(), @@ -551,6 +562,8 @@ public class ValuesSourceReaderBenchmark { pages.add( new Page( new DocVector( + + ShardRefCounted.ALWAYS_REFERENCED, blockFactory.newConstantIntVector(0, 1), blockFactory.newConstantIntVector(next.ord, 1), blockFactory.newConstantIntVector(next.itr.nextInt(), 1), diff --git a/build-tools-internal/src/main/resources/checkstyle_suppressions.xml b/build-tools-internal/src/main/resources/checkstyle_suppressions.xml index 5fdfebf6849e..98ded638773c 100644 --- a/build-tools-internal/src/main/resources/checkstyle_suppressions.xml +++ b/build-tools-internal/src/main/resources/checkstyle_suppressions.xml @@ -37,6 +37,7 @@ + diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 8694c73ad099..c443b280e3dd 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,4 +1,4 @@ -elasticsearch = 9.1.0 +elasticsearch = 9.2.0 lucene = 10.2.2 bundled_jdk_vendor = openjdk diff --git a/docs/changelog/122497.yaml b/docs/changelog/122497.yaml new file mode 100644 index 000000000000..46c385ea4ed4 --- /dev/null +++ b/docs/changelog/122497.yaml @@ -0,0 +1,5 @@ +pr: 122497 +summary: Check if index patterns conform to valid format before validation +area: CCS +type: enhancement +issues: [] diff --git a/docs/changelog/129370.yaml b/docs/changelog/129370.yaml new file mode 100644 index 000000000000..73d1c25f4b34 --- /dev/null +++ b/docs/changelog/129370.yaml @@ -0,0 +1,7 @@ +pr: 129370 +summary: Avoid dropping aggregate groupings in local plans +area: ES|QL +type: bug +issues: + - 129811 + - 128054 diff --git a/docs/changelog/129454.yaml b/docs/changelog/129454.yaml new file mode 100644 index 000000000000..538c5266c616 --- /dev/null +++ b/docs/changelog/129454.yaml @@ -0,0 +1,5 @@ +pr: 129454 +summary: Aggressive release of shard contexts +area: ES|QL +type: enhancement +issues: [] diff --git a/docs/changelog/129967.yaml b/docs/changelog/129967.yaml new file mode 100644 index 000000000000..8a7ea868aaeb --- /dev/null +++ b/docs/changelog/129967.yaml @@ -0,0 +1,6 @@ +pr: 129967 +summary: Support returning default `index_options` for `semantic_text` fields when + `include_defaults` is true +area: Search +type: bug +issues: [] diff --git a/docs/changelog/130027.yaml b/docs/changelog/130027.yaml new file mode 100644 index 000000000000..e00ce036ae08 --- /dev/null +++ b/docs/changelog/130027.yaml @@ -0,0 +1,6 @@ +pr: 130027 +summary: "Fix: prevent duplication of \"invalid index name\" string in the final exception\ + \ error message" +area: ES|QL +type: bug +issues: [] diff --git a/docs/changelog/130032.yaml b/docs/changelog/130032.yaml new file mode 100644 index 000000000000..69140cdc3f83 --- /dev/null +++ b/docs/changelog/130032.yaml @@ -0,0 +1,12 @@ +pr: 130032 +summary: ES|QL cross-cluster querying is now generally available +area: ES|QL +type: feature +issues: [] +highlight: + title: ES|QL cross-cluster querying is now generally available + body: |- + The ES|QL Cross-Cluster querying feature has been in technical preview since 8.13. + As of releases 8.19.0 and 9.1.0 this is now generally available. + This feature allows you to run ES|QL queries across multiple clusters. + notable: true diff --git a/docs/changelog/130083.yaml b/docs/changelog/130083.yaml new file mode 100644 index 000000000000..1b3288165953 --- /dev/null +++ b/docs/changelog/130083.yaml @@ -0,0 +1,5 @@ +pr: 130083 +summary: Fix timeout bug in DBQ deletion of unused and orphan ML data +area: Machine Learning +type: bug +issues: [] diff --git a/docs/reference/elasticsearch/configuration-reference/thread-pool-settings.md b/docs/reference/elasticsearch/configuration-reference/thread-pool-settings.md index 8314ff41bdc5..b7253ba5f9d3 100644 --- a/docs/reference/elasticsearch/configuration-reference/thread-pool-settings.md +++ b/docs/reference/elasticsearch/configuration-reference/thread-pool-settings.md @@ -33,7 +33,7 @@ $$$search-throttled$$$`search_throttled` : For analyze requests. Thread pool type is `fixed` with a size of `1`, queue size of `16`. `write` -: For write operations and ingest processors. Thread pool type is `fixed` with a size of [`# of allocated processors`](#node.processors), queue_size of `10000`. The maximum size for this pool is `1 + `[`# of allocated processors`](#node.processors). +: For write operations and ingest processors. Thread pool type is `fixed` with a size of [`# of allocated processors`](#node.processors), queue_size of `max(10000, (`[`# of allocated processors`](#node.processors)`* 750))`. The maximum size for this pool is `1 + `[`# of allocated processors`](#node.processors). `write_coordination` : For bulk request coordination operations. Thread pool type is `fixed` with a size of [`# of allocated processors`](#node.processors), queue_size of `10000`. The maximum size for this pool is `1 + `[`# of allocated processors`](#node.processors). diff --git a/docs/reference/elasticsearch/index-settings/slow-log.md b/docs/reference/elasticsearch/index-settings/slow-log.md index 1477348b0ca1..20b416360a1c 100644 --- a/docs/reference/elasticsearch/index-settings/slow-log.md +++ b/docs/reference/elasticsearch/index-settings/slow-log.md @@ -13,7 +13,7 @@ applies_to: The slow log records database searching and indexing events that have execution durations above specified thresholds. You can use these logs to investigate analyze or troubleshoot your cluster’s historical search and indexing performance. -Slow logs report task duration at the shard level for searches, and at the index level for indexing, but might not encompass the full task execution time observed on the client. For example, slow logs don’t surface HTTP network delays or the impact of [task queues](docs-content://troubleshoot/elasticsearch/task-queue-backlog.md). +Slow logs report task duration at the shard level for searches, and at the index level for indexing, but might not encompass the full task execution time observed on the client. For example, slow logs don’t surface HTTP network delays or the impact of [task queues](docs-content://troubleshoot/elasticsearch/task-queue-backlog.md). For more information about the higher-level operations affecting response times, refer to [Reading and writing documents](docs-content://deploy-manage/distributed-architecture/reading-and-writing-documents.md). Events that meet the specified threshold are emitted into [{{es}} logging](docs-content://deploy-manage/monitor/logging-configuration/update-elasticsearch-logging-levels.md) under the `fileset.name` of `slowlog`. These logs can be viewed in the following locations: diff --git a/docs/reference/elasticsearch/mapping-reference/semantic-text.md b/docs/reference/elasticsearch/mapping-reference/semantic-text.md index 0e71155b94ce..06ea9cc9156e 100644 --- a/docs/reference/elasticsearch/mapping-reference/semantic-text.md +++ b/docs/reference/elasticsearch/mapping-reference/semantic-text.md @@ -112,32 +112,11 @@ to create the endpoint. If not specified, the {{infer}} endpoint defined by `inference_id` will be used at both index and query time. `index_options` -: (Optional, string) Specifies the index options to override default values +: (Optional, object) Specifies the index options to override default values for the field. Currently, `dense_vector` index options are supported. For text embeddings, `index_options` may match any allowed [dense_vector index options](/reference/elasticsearch/mapping-reference/dense-vector.md#dense-vector-index-options). -An example of how to set index_options for a `semantic_text` field: - -```console -PUT my-index-000004 -{ - "mappings": { - "properties": { - "inference_field": { - "type": "semantic_text", - "inference_id": "my-text-embedding-endpoint", - "index_options": { - "dense_vector": { - "type": "int4_flat" - } - } - } - } - } -} -``` - `chunking_settings` : (Optional, object) Settings for chunking text into smaller passages. If specified, these will override the chunking settings set in the {{infer-cap}} @@ -165,7 +144,7 @@ To completely disable chunking, use the `none` chunking strategy. or `1`. Required for `sentence` type chunking settings ::::{warning} -If the input exceeds the maximum token limit of the underlying model, some +When using the `none` chunking strategy, if the input exceeds the maximum token limit of the underlying model, some services (such as OpenAI) may return an error. In contrast, the `elastic` and `elasticsearch` services will automatically truncate the input to fit within the @@ -315,18 +294,38 @@ specified. It enables you to quickstart your semantic search by providing automatic {{infer}} and a dedicated query so you don’t need to provide further details. -In case you want to customize data indexing, use the [ -`sparse_vector`](/reference/elasticsearch/mapping-reference/sparse-vector.md) -or [`dense_vector`](/reference/elasticsearch/mapping-reference/dense-vector.md) -field types and create an ingest pipeline with -an [{{infer}} processor](/reference/enrich-processor/inference-processor.md) to -generate the -embeddings. [This tutorial](docs-content://solutions/search/semantic-search/semantic-search-inference.md) -walks you through the process. In these cases - when you use `sparse_vector` or -`dense_vector` field types instead of the `semantic_text` field type to -customize indexing - using the [ -`semantic_query`](/reference/query-languages/query-dsl/query-dsl-semantic-query.md) -is not supported for querying the field data. +If you want to override those defaults and customize the embeddings that +`semantic_text` indexes, you can do so by modifying <>: + +- Use `index_options` to specify alternate index options such as specific + `dense_vector` quantization methods +- Use `chunking_settings` to override the chunking strategy associated with the + {{infer}} endpoint, or completely disable chunking using the `none` type + +Here is an example of how to set these parameters for a text embedding endpoint: + +```console +PUT my-index-000004 +{ + "mappings": { + "properties": { + "inference_field": { + "type": "semantic_text", + "inference_id": "my-text-embedding-endpoint", + "index_options": { + "dense_vector": { + "type": "int4_flat" + } + }, + "chunking_settings": { + "type": "none" + } + } + } + } +} +``` ## Updates to `semantic_text` fields [update-script] diff --git a/docs/release-notes/breaking-changes.md b/docs/release-notes/breaking-changes.md index b1651728012e..1fe3c4c0d8fc 100644 --- a/docs/release-notes/breaking-changes.md +++ b/docs/release-notes/breaking-changes.md @@ -12,23 +12,15 @@ If you are migrating from a version prior to version 9.0, you must first upgrade % ## Next version [elasticsearch-nextversion-breaking-changes] -```{applies_to} -stack: coming 9.0.3 -``` ## 9.0.3 [elasticsearch-9.0.3-breaking-changes] No breaking changes in this version. -```{applies_to} -stack: coming 9.0.2 -``` ## 9.0.2 [elasticsearch-9.0.2-breaking-changes] Snapshot/Restore: * Make S3 custom query parameter optional [#128043](https://github.com/elastic/elasticsearch/pull/128043) - - ## 9.0.1 [elasticsearch-9.0.1-breaking-changes] No breaking changes in this version. diff --git a/docs/release-notes/changelog-bundles/9.0.3.yml b/docs/release-notes/changelog-bundles/9.0.3.yml index 1ed8fa4052e3..86b4b9fe06a0 100644 --- a/docs/release-notes/changelog-bundles/9.0.3.yml +++ b/docs/release-notes/changelog-bundles/9.0.3.yml @@ -1,6 +1,6 @@ version: 9.0.3 -released: false -generated: 2025-06-21T00:06:16.346021604Z +released: true +generated: 2025-06-24T15:19:29.859630035Z changelogs: - pr: 120869 summary: Threadpool merge scheduler diff --git a/docs/release-notes/deprecations.md b/docs/release-notes/deprecations.md index d9273321ed05..be1029c187cc 100644 --- a/docs/release-notes/deprecations.md +++ b/docs/release-notes/deprecations.md @@ -16,19 +16,11 @@ To give you insight into what deprecated features you’re using, {{es}}: % ## Next version [elasticsearch-nextversion-deprecations] -```{applies_to} -stack: coming 9.0.3 -``` ## 9.0.3 [elasticsearch-9.0.3-deprecations] Engine: * Deprecate `indices.merge.scheduler.use_thread_pool` setting [#129464](https://github.com/elastic/elasticsearch/pull/129464) - - -```{applies_to} -stack: coming 9.0.2 -``` ## 9.0.2 [elasticsearch-9.0.2-deprecations] No deprecations in this version. diff --git a/docs/release-notes/index.md b/docs/release-notes/index.md index 3e94a2de8a92..477f840abc08 100644 --- a/docs/release-notes/index.md +++ b/docs/release-notes/index.md @@ -21,9 +21,6 @@ To check for security updates, go to [Security announcements for the Elastic sta % * ## 9.0.3 [elasticsearch-9.0.3-release-notes] -```{applies_to} -stack: coming 9.0.3 -``` ### Features and enhancements [elasticsearch-9.0.3-features-enhancements] @@ -92,7 +89,6 @@ Searchable Snapshots: Security: * Fix error message when changing the password for a user in the file realm [#127621](https://github.com/elastic/elasticsearch/pull/127621) - ## 9.0.2 [elasticsearch-9.0.2-release-notes] ### Features and enhancements [elasticsearch-9.0.2-features-enhancements] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index db8e7b415ee6..7e147eff76db 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1153,16 +1153,16 @@ - - - - - + + + + + @@ -4601,6 +4601,11 @@ + + + + + @@ -4621,6 +4626,11 @@ + + + + + @@ -4686,6 +4696,11 @@ + + + + + @@ -4696,6 +4711,11 @@ + + + + + diff --git a/libs/core/src/main/java/org/elasticsearch/core/NotMultiProjectCapable.java b/libs/core/src/main/java/org/elasticsearch/core/NotMultiProjectCapable.java new file mode 100644 index 000000000000..051ed4eb2c91 --- /dev/null +++ b/libs/core/src/main/java/org/elasticsearch/core/NotMultiProjectCapable.java @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.core; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to identify a block of code (a whole class, a method, a field, or a local variable) that is intentionally not fully + * project-aware because it's not intended to be used in a serverless environment. Some features are unavailable in serverless and are + * thus not worth the investment to make fully project-aware. This annotation makes it easier to identify blocks of code that require + * attention in case those features are revisited from a multi-project POV. + */ +@Retention(RetentionPolicy.SOURCE) +@Target( + { ElementType.LOCAL_VARIABLE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.MODULE } +) +public @interface NotMultiProjectCapable { + + /** + * Some explanation on why the block of code would not work in a multi-project context and/or what would need to be done to make it + * properly project-aware. + */ + String description() default ""; +} diff --git a/libs/core/src/main/java/org/elasticsearch/core/Releasables.java b/libs/core/src/main/java/org/elasticsearch/core/Releasables.java index 12417e0971c0..8eee84050ca3 100644 --- a/libs/core/src/main/java/org/elasticsearch/core/Releasables.java +++ b/libs/core/src/main/java/org/elasticsearch/core/Releasables.java @@ -202,6 +202,11 @@ public enum Releasables { } } + /** Creates a {@link Releasable} that calls {@link RefCounted#decRef()} when closed. */ + public static Releasable fromRefCounted(RefCounted refCounted) { + return () -> refCounted.decRef(); + } + private static class ReleaseOnce extends AtomicReference implements Releasable { ReleaseOnce(Releasable releasable) { super(releasable); diff --git a/libs/entitlement/asm-provider/build.gradle b/libs/entitlement/asm-provider/build.gradle index d992792cd96d..e6016a429497 100644 --- a/libs/entitlement/asm-provider/build.gradle +++ b/libs/entitlement/asm-provider/build.gradle @@ -13,10 +13,10 @@ dependencies { compileOnly project(':libs:entitlement') compileOnly project(':libs:core') compileOnly project(':libs:logging') - implementation 'org.ow2.asm:asm:9.7.1' - implementation 'org.ow2.asm:asm-util:9.7.1' - implementation 'org.ow2.asm:asm-tree:9.7.1' - implementation 'org.ow2.asm:asm-analysis:9.7.1' + implementation 'org.ow2.asm:asm:9.8' + implementation 'org.ow2.asm:asm-util:9.8' + implementation 'org.ow2.asm:asm-tree:9.8' + implementation 'org.ow2.asm:asm-analysis:9.8' testImplementation project(":test:framework") testImplementation project(":libs:entitlement:bridge") } diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java index 81f61aefdfb8..be9e8254f464 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java @@ -98,6 +98,10 @@ public class EntitlementBootstrap { ); exportInitializationToAgent(); loadAgent(findAgentJar(), EntitlementInitialization.class.getName()); + + if (EntitlementInitialization.getError() != null) { + throw EntitlementInitialization.getError(); + } } private static Path getUserHome() { diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/DynamicInstrumentation.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/DynamicInstrumentation.java index b7d92d351884..1802925d9625 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/DynamicInstrumentation.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/DynamicInstrumentation.java @@ -117,6 +117,10 @@ class DynamicInstrumentation { // We should have failed already in the loop above, but just in case we did not, rethrow. throw e; } + + if (transformer.hadErrors()) { + throw new RuntimeException("Failed to transform JDK classes for entitlements"); + } } private static Map getMethodsToInstrument(Class checkerInterface) throws ClassNotFoundException, diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java index fccb84a0d908..871e4ade9748 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java @@ -16,11 +16,14 @@ import org.elasticsearch.entitlement.runtime.policy.PathLookup; import org.elasticsearch.entitlement.runtime.policy.PolicyChecker; import org.elasticsearch.entitlement.runtime.policy.PolicyCheckerImpl; import org.elasticsearch.entitlement.runtime.policy.PolicyManager; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; import java.lang.instrument.Instrumentation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import static java.util.Objects.requireNonNull; @@ -32,17 +35,26 @@ import static java.util.Objects.requireNonNull; * to begin injecting our instrumentation. */ public class EntitlementInitialization { + private static final Logger logger = LogManager.getLogger(EntitlementInitialization.class); private static final Module ENTITLEMENTS_MODULE = PolicyManager.class.getModule(); public static InitializeArgs initializeArgs; private static ElasticsearchEntitlementChecker checker; + private static AtomicReference error = new AtomicReference<>(); // Note: referenced by bridge reflectively public static EntitlementChecker checker() { return checker; } + /** + * Return any exception that occurred during initialization + */ + public static RuntimeException getError() { + return error.get(); + } + /** * Initializes the Entitlement system: *
    @@ -62,10 +74,16 @@ public class EntitlementInitialization { * * @param inst the JVM instrumentation class instance */ - public static void initialize(Instrumentation inst) throws Exception { - // the checker _MUST_ be set before _any_ instrumentation is done - checker = initChecker(initializeArgs.policyManager()); - initInstrumentation(inst); + public static void initialize(Instrumentation inst) { + try { + // the checker _MUST_ be set before _any_ instrumentation is done + checker = initChecker(initializeArgs.policyManager()); + initInstrumentation(inst); + } catch (Exception e) { + // exceptions thrown within the agent will be swallowed, so capture it here + // instead so that it can be retrieved by bootstrap + error.set(new RuntimeException("Failed to initialize entitlements", e)); + } } /** diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/Transformer.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/Transformer.java index 6d4d4edaae16..bd9c5a06910f 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/Transformer.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/Transformer.java @@ -9,16 +9,22 @@ package org.elasticsearch.entitlement.instrumentation; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; + import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; /** * A {@link ClassFileTransformer} that applies an {@link Instrumenter} to the appropriate classes. */ public class Transformer implements ClassFileTransformer { + private static final Logger logger = LogManager.getLogger(Transformer.class); private final Instrumenter instrumenter; private final Set classesToTransform; + private final AtomicBoolean hadErrors = new AtomicBoolean(false); private boolean verifyClasses; @@ -33,6 +39,10 @@ public class Transformer implements ClassFileTransformer { this.verifyClasses = true; } + public boolean hadErrors() { + return hadErrors.get(); + } + @Override public byte[] transform( ClassLoader loader, @@ -42,13 +52,19 @@ public class Transformer implements ClassFileTransformer { byte[] classfileBuffer ) { if (classesToTransform.contains(className)) { - // System.out.println("Transforming " + className); - return instrumenter.instrumentClass(className, classfileBuffer, verifyClasses); + logger.debug("Transforming " + className); + try { + return instrumenter.instrumentClass(className, classfileBuffer, verifyClasses); + } catch (Throwable t) { + hadErrors.set(true); + logger.error("Failed to instrument class " + className, t); + // throwing an exception from a transformer results in the exception being swallowed, + // effectively the same as returning null anyways, so we instead log it here completely + return null; + } } else { - // System.out.println("Not transforming " + className); + logger.trace("Not transforming " + className); return null; } } - - // private static final Logger LOGGER = LogManager.getLogger(Transformer.class); } diff --git a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/action/TransportGetDataStreamsAction.java b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/action/TransportGetDataStreamsAction.java index 4411f8f1ee4e..2500326e102d 100644 --- a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/action/TransportGetDataStreamsAction.java +++ b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/action/TransportGetDataStreamsAction.java @@ -54,6 +54,7 @@ import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import java.io.IOException; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; @@ -261,13 +262,17 @@ public class TransportGetDataStreamsAction extends TransportLocalProjectMetadata Settings settings = dataStream.getEffectiveSettings(state.metadata()); ilmPolicyName = settings.get(IndexMetadata.LIFECYCLE_NAME); if (indexMode == null && state.metadata().templatesV2().get(indexTemplate) != null) { - indexMode = resolveMode( - state, - indexSettingProviders, - dataStream, - settings, - dataStream.getEffectiveIndexTemplate(state.metadata()) - ); + try { + indexMode = resolveMode( + state, + indexSettingProviders, + dataStream, + settings, + dataStream.getEffectiveIndexTemplate(state.metadata()) + ); + } catch (IOException e) { + throw new RuntimeException(e); + } } indexTemplatePreferIlmValue = PREFER_ILM_SETTING.get(settings); } else { diff --git a/modules/data-streams/src/test/java/org/elasticsearch/datastreams/UpdateTimeSeriesRangeServiceTests.java b/modules/data-streams/src/test/java/org/elasticsearch/datastreams/UpdateTimeSeriesRangeServiceTests.java index 0169b1b7da8c..54232799a3c7 100644 --- a/modules/data-streams/src/test/java/org/elasticsearch/datastreams/UpdateTimeSeriesRangeServiceTests.java +++ b/modules/data-streams/src/test/java/org/elasticsearch/datastreams/UpdateTimeSeriesRangeServiceTests.java @@ -258,6 +258,7 @@ public class UpdateTimeSeriesRangeServiceTests extends ESTestCase { 2, ds2.getMetadata(), ds2.getSettings(), + ds2.getMappings(), ds2.isHidden(), ds2.isReplicated(), ds2.isSystem(), diff --git a/modules/ingest-geoip/qa/multi-project/build.gradle b/modules/ingest-geoip/qa/multi-project/build.gradle index c67fa94e2763..bcfe4f22599a 100644 --- a/modules/ingest-geoip/qa/multi-project/build.gradle +++ b/modules/ingest-geoip/qa/multi-project/build.gradle @@ -22,3 +22,8 @@ dependencies { tasks.withType(Test).configureEach { it.systemProperty "tests.multi_project.enabled", true } + +// Exclude multi-project tests from release build +tasks.named { it == "javaRestTest" || it == "yamlRestTest" }.configureEach { + it.onlyIf("snapshot build") { buildParams.snapshotBuild } +} diff --git a/modules/mapper-extras/src/main/java/module-info.java b/modules/mapper-extras/src/main/java/module-info.java index f89224813379..8bdda994e3e5 100644 --- a/modules/mapper-extras/src/main/java/module-info.java +++ b/modules/mapper-extras/src/main/java/module-info.java @@ -14,4 +14,6 @@ module org.elasticsearch.mapper.extras { requires org.apache.lucene.core; requires org.apache.lucene.memory; requires org.apache.lucene.queries; + + exports org.elasticsearch.index.mapper.extras; } diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapper.java index 3333b004df40..59f43360b09b 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapper.java @@ -173,7 +173,7 @@ public class MatchOnlyTextFieldMapper extends FieldMapper { super(name, true, false, false, tsi, meta); this.indexAnalyzer = Objects.requireNonNull(indexAnalyzer); this.textFieldType = new TextFieldType(name, isSyntheticSource); - this.originalName = isSyntheticSource ? name() + "._original" : null; + this.originalName = isSyntheticSource ? name + "._original" : null; } public MatchOnlyTextFieldType(String name) { @@ -362,10 +362,38 @@ public class MatchOnlyTextFieldMapper extends FieldMapper { return toQuery(query, queryShardContext); } + private static class BytesFromMixedStringsBytesRefBlockLoader extends BlockStoredFieldsReader.StoredFieldsBlockLoader { + BytesFromMixedStringsBytesRefBlockLoader(String field) { + super(field); + } + + @Override + public Builder builder(BlockFactory factory, int expectedCount) { + return factory.bytesRefs(expectedCount); + } + + @Override + public RowStrideReader rowStrideReader(LeafReaderContext context) throws IOException { + return new BlockStoredFieldsReader.Bytes(field) { + private final BytesRef scratch = new BytesRef(); + + @Override + protected BytesRef toBytesRef(Object v) { + if (v instanceof BytesRef b) { + return b; + } else { + assert v instanceof String; + return BlockSourceReader.toBytesRef(scratch, v.toString()); + } + } + }; + } + } + @Override public BlockLoader blockLoader(BlockLoaderContext blContext) { if (textFieldType.isSyntheticSource()) { - return new BlockStoredFieldsReader.BytesFromBytesRefsBlockLoader(storedFieldNameForSyntheticSource()); + return new BytesFromMixedStringsBytesRefBlockLoader(storedFieldNameForSyntheticSource()); } SourceValueFetcher fetcher = SourceValueFetcher.toString(blContext.sourcePaths(name())); // MatchOnlyText never has norms, so we have to use the field names field @@ -386,7 +414,12 @@ public class MatchOnlyTextFieldMapper extends FieldMapper { ) { @Override protected BytesRef storedToBytesRef(Object stored) { - return (BytesRef) stored; + if (stored instanceof BytesRef storedBytes) { + return storedBytes; + } else { + assert stored instanceof String; + return new BytesRef(stored.toString()); + } } }; } @@ -477,7 +510,12 @@ public class MatchOnlyTextFieldMapper extends FieldMapper { () -> new StringStoredFieldFieldLoader(fieldType().storedFieldNameForSyntheticSource(), fieldType().name(), leafName()) { @Override protected void write(XContentBuilder b, Object value) throws IOException { - b.value(((BytesRef) value).utf8ToString()); + if (value instanceof BytesRef valueBytes) { + b.value(valueBytes.utf8ToString()); + } else { + assert value instanceof String; + b.value(value.toString()); + } } } ); diff --git a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapperTests.java b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapperTests.java index f427c6c1b7c0..cfbf3a338f69 100644 --- a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapperTests.java +++ b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapperTests.java @@ -10,6 +10,9 @@ package org.elasticsearch.index.mapper.extras; import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.document.StringField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.DocValuesType; import org.apache.lucene.index.IndexOptions; @@ -21,6 +24,7 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.tests.analysis.CannedTokenStream; import org.apache.lucene.tests.analysis.Token; import org.apache.lucene.tests.index.RandomIndexWriter; +import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.Strings; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.IndexSettings; @@ -350,4 +354,29 @@ public class MatchOnlyTextFieldMapperTests extends MapperTestCase { assertThat(fields, empty()); } } + + public void testLoadSyntheticSourceFromStringOrBytesRef() throws IOException { + DocumentMapper mapper = createSytheticSourceMapperService(mapping(b -> { + b.startObject("field1").field("type", "match_only_text").endObject(); + b.startObject("field2").field("type", "match_only_text").endObject(); + })).documentMapper(); + try (Directory directory = newDirectory()) { + RandomIndexWriter iw = indexWriterForSyntheticSource(directory); + + LuceneDocument document = new LuceneDocument(); + document.add(new StringField("field1", "foo", Field.Store.NO)); + document.add(new StoredField("field1._original", "foo")); + + document.add(new StringField("field2", "bar", Field.Store.NO)); + document.add(new StoredField("field2._original", new BytesRef("bar"))); + + iw.addDocument(document); + iw.close(); + + try (DirectoryReader indexReader = wrapInMockESDirectoryReader(DirectoryReader.open(directory))) { + String syntheticSource = syntheticSource(mapper, null, indexReader, 0); + assertEquals("{\"field1\":\"foo\",\"field2\":\"bar\"}", syntheticSource); + } + } + } } diff --git a/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/rank_feature/10_basic.yml b/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/rank_feature/10_basic.yml index fcdf3f5a5fdf..34b654068c01 100644 --- a/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/rank_feature/10_basic.yml +++ b/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/rank_feature/10_basic.yml @@ -3,8 +3,6 @@ setup: indices.create: index: test body: - settings: - number_of_replicas: 0 mappings: properties: pagerank: diff --git a/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/rank_feature/20_null_value.yml b/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/rank_feature/20_null_value.yml index 926ac3e77040..c6200635d5ae 100644 --- a/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/rank_feature/20_null_value.yml +++ b/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/rank_feature/20_null_value.yml @@ -10,8 +10,6 @@ indices.create: index: test2 body: - settings: - number_of_replicas: 0 mappings: properties: pagerank: @@ -29,8 +27,6 @@ indices.create: index: test1 body: - settings: - number_of_replicas: 0 mappings: properties: pagerank: diff --git a/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/rank_features/10_basic.yml b/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/rank_features/10_basic.yml index 5806b492bb05..c64f82e0f0fe 100644 --- a/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/rank_features/10_basic.yml +++ b/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/rank_features/10_basic.yml @@ -3,8 +3,6 @@ setup: indices.create: index: test body: - settings: - number_of_replicas: 0 mappings: properties: tags: diff --git a/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/scaled_float/10_basic.yml b/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/scaled_float/10_basic.yml index b7f810fa4820..395943d010f5 100644 --- a/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/scaled_float/10_basic.yml +++ b/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/scaled_float/10_basic.yml @@ -3,8 +3,6 @@ setup: indices.create: index: test body: - settings: - number_of_replicas: 0 mappings: "properties": "number": diff --git a/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/search-as-you-type/10_basic.yml b/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/search-as-you-type/10_basic.yml index 07e0a3585256..b36134a79842 100644 --- a/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/search-as-you-type/10_basic.yml +++ b/modules/mapper-extras/src/yamlRestTest/resources/rest-api-spec/test/search-as-you-type/10_basic.yml @@ -7,8 +7,6 @@ setup: indices.create: index: test body: - settings: - number_of_replicas: 0 mappings: properties: a_field: diff --git a/muted-tests.yml b/muted-tests.yml index 6e416b4e6fa6..589a6e9a58fd 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -182,9 +182,6 @@ tests: - class: org.elasticsearch.blocks.SimpleBlocksIT method: testConcurrentAddBlock issue: https://github.com/elastic/elasticsearch/issues/122324 -- class: org.elasticsearch.xpack.ilm.TimeSeriesLifecycleActionsIT - method: testHistoryIsWrittenWithFailure - issue: https://github.com/elastic/elasticsearch/issues/123203 - class: org.elasticsearch.packaging.test.DockerTests method: test151MachineDependentHeapWithSizeOverride issue: https://github.com/elastic/elasticsearch/issues/123437 @@ -281,9 +278,6 @@ tests: - class: org.elasticsearch.search.basic.SearchWithRandomDisconnectsIT method: testSearchWithRandomDisconnects issue: https://github.com/elastic/elasticsearch/issues/122707 -- class: org.elasticsearch.index.engine.ThreadPoolMergeSchedulerTests - method: testSchedulerCloseWaitsForRunningMerge - issue: https://github.com/elastic/elasticsearch/issues/125236 - class: org.elasticsearch.packaging.test.DockerTests method: test020PluginsListWithNoPlugins issue: https://github.com/elastic/elasticsearch/issues/126232 @@ -473,12 +467,6 @@ tests: - class: org.elasticsearch.packaging.test.DockerTests method: test081SymlinksAreFollowedWithEnvironmentVariableFiles issue: https://github.com/elastic/elasticsearch/issues/128867 -- class: org.elasticsearch.index.engine.ThreadPoolMergeExecutorServiceDiskSpaceTests - method: testAvailableDiskSpaceMonitorWhenFileSystemStatErrors - issue: https://github.com/elastic/elasticsearch/issues/129149 -- class: org.elasticsearch.index.engine.ThreadPoolMergeExecutorServiceDiskSpaceTests - method: testUnavailableBudgetBlocksNewMergeTasksFromStartingExecution - issue: https://github.com/elastic/elasticsearch/issues/129148 - class: org.elasticsearch.xpack.esql.qa.single_node.GenerativeForkIT method: test {lookup-join.EnrichLookupStatsBug ASYNC} issue: https://github.com/elastic/elasticsearch/issues/129228 @@ -536,9 +524,6 @@ tests: - class: org.elasticsearch.search.query.VectorIT method: testFilteredQueryStrategy issue: https://github.com/elastic/elasticsearch/issues/129517 -- class: org.elasticsearch.test.apmintegration.TracesApmIT - method: testApmIntegration - issue: https://github.com/elastic/elasticsearch/issues/129651 - class: org.elasticsearch.snapshots.SnapshotShutdownIT method: testSnapshotShutdownProgressTracker issue: https://github.com/elastic/elasticsearch/issues/129752 @@ -548,9 +533,6 @@ tests: - class: org.elasticsearch.qa.verify_version_constants.VerifyVersionConstantsIT method: testLuceneVersionConstant issue: https://github.com/elastic/elasticsearch/issues/125638 -- class: org.elasticsearch.xpack.esql.qa.single_node.GenerativeIT - method: test - issue: https://github.com/elastic/elasticsearch/issues/129819 - class: org.elasticsearch.index.store.FsDirectoryFactoryTests method: testPreload issue: https://github.com/elastic/elasticsearch/issues/129852 @@ -564,7 +546,26 @@ tests: method: "builds distribution from branches via archives extractedAssemble [bwcDistVersion: 8.2.1, bwcProject: bugfix, expectedAssembleTaskName: extractedAssemble, #2]" issue: https://github.com/elastic/elasticsearch/issues/119871 - +- class: org.elasticsearch.xpack.inference.qa.mixed.CohereServiceMixedIT + method: testRerank + issue: https://github.com/elastic/elasticsearch/issues/130009 +- class: org.elasticsearch.xpack.inference.qa.mixed.CohereServiceMixedIT + method: testCohereEmbeddings + issue: https://github.com/elastic/elasticsearch/issues/130010 +- class: org.elasticsearch.xpack.esql.qa.multi_node.GenerativeIT + method: test + issue: https://github.com/elastic/elasticsearch/issues/130067 +- class: geoip.GeoIpMultiProjectIT + issue: https://github.com/elastic/elasticsearch/issues/130073 +- class: org.elasticsearch.xpack.esql.qa.single_node.GenerativeIT + method: test + issue: https://github.com/elastic/elasticsearch/issues/130067 +- class: org.elasticsearch.xpack.esql.action.EnrichIT + method: testTopN + issue: https://github.com/elastic/elasticsearch/issues/130122 +- class: org.elasticsearch.action.support.ThreadedActionListenerTests + method: testRejectionHandling + issue: https://github.com/elastic/elasticsearch/issues/130129 # Examples: # diff --git a/qa/mixed-cluster/build.gradle b/qa/mixed-cluster/build.gradle index d893eb6233e6..dfe4052b09af 100644 --- a/qa/mixed-cluster/build.gradle +++ b/qa/mixed-cluster/build.gradle @@ -23,6 +23,7 @@ apply plugin: 'elasticsearch.rest-resources' dependencies { restTestConfig project(path: ':modules:aggregations', configuration: 'restTests') + restTestConfig project(path: ':modules:mapper-extras', configuration: 'restTests') } restResources { diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index 9ac259cfa46e..07bf5e3d5026 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -208,6 +208,7 @@ public class TransportVersions { public static final TransportVersion SPARSE_VECTOR_FIELD_PRUNING_OPTIONS_8_19 = def(8_841_0_58); public static final TransportVersion ML_INFERENCE_ELASTIC_DENSE_TEXT_EMBEDDINGS_ADDED_8_19 = def(8_841_0_59); public static final TransportVersion ML_INFERENCE_COHERE_API_VERSION_8_19 = def(8_841_0_60); + public static final TransportVersion ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED_8_19 = def(8_841_0_61); public static final TransportVersion V_9_0_0 = def(9_000_0_09); public static final TransportVersion INITIAL_ELASTICSEARCH_9_0_1 = def(9_000_0_10); public static final TransportVersion INITIAL_ELASTICSEARCH_9_0_2 = def(9_000_0_11); @@ -323,6 +324,7 @@ public class TransportVersions { public static final TransportVersion ML_INFERENCE_ELASTIC_DENSE_TEXT_EMBEDDINGS_ADDED = def(9_109_00_0); public static final TransportVersion ML_INFERENCE_COHERE_API_VERSION = def(9_110_0_00); public static final TransportVersion ESQL_PROFILE_INCLUDE_PLAN = def(9_111_0_00); + public static final TransportVersion MAPPINGS_IN_DATA_STREAMS = def(9_112_0_00); /* * STOP! READ THIS FIRST! No, really, diff --git a/server/src/main/java/org/elasticsearch/Version.java b/server/src/main/java/org/elasticsearch/Version.java index 925370623a88..6b8959f0c775 100644 --- a/server/src/main/java/org/elasticsearch/Version.java +++ b/server/src/main/java/org/elasticsearch/Version.java @@ -217,7 +217,8 @@ public class Version implements VersionId, ToXContentFragment { public static final Version V_9_0_3 = new Version(9_00_03_99); public static final Version V_9_0_4 = new Version(9_00_04_99); public static final Version V_9_1_0 = new Version(9_01_00_99); - public static final Version CURRENT = V_9_1_0; + public static final Version V_9_2_0 = new Version(9_02_00_99); + public static final Version CURRENT = V_9_2_0; private static final NavigableMap VERSION_IDS; private static final Map VERSION_STRINGS; diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplate.java b/server/src/main/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplate.java index 4ceb807adece..70ec6678ea16 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplate.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplate.java @@ -19,18 +19,24 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.Nullable; import org.elasticsearch.index.mapper.DataStreamTimestampFieldMapper; import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.XContentType; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -51,6 +57,14 @@ public class ComposableIndexTemplate implements SimpleDiffable PARSER = new ConstructingObjectParser<>( @@ -338,6 +352,64 @@ public class ComposableIndexTemplate implements SimpleDiffable mappingAdditionMap = XContentHelper.convertToMap(mappingAddition.uncompressed(), true, XContentType.JSON).v2(); + Map combinedMappingMap = new HashMap<>(); + if (originalMapping != null) { + Map originalMappingMap = XContentHelper.convertToMap(originalMapping.uncompressed(), true, XContentType.JSON) + .v2(); + if (originalMappingMap.containsKey(MapperService.SINGLE_MAPPING_NAME)) { + combinedMappingMap.putAll((Map) originalMappingMap.get(MapperService.SINGLE_MAPPING_NAME)); + } else { + combinedMappingMap.putAll(originalMappingMap); + } + } + XContentHelper.update(combinedMappingMap, mappingAdditionMap, true); + return convertMappingMapToXContent(combinedMappingMap); + } + + private static CompressedXContent convertMappingMapToXContent(Map rawAdditionalMapping) throws IOException { + CompressedXContent compressedXContent; + if (rawAdditionalMapping.isEmpty()) { + compressedXContent = EMPTY_MAPPINGS; + } else { + try (var parser = XContentHelper.mapToXContentParser(XContentParserConfiguration.EMPTY, rawAdditionalMapping)) { + compressedXContent = mappingFromXContent(parser); + } + } + return compressedXContent; + } + + private static CompressedXContent mappingFromXContent(XContentParser parser) throws IOException { + XContentParser.Token token = parser.nextToken(); + if (token == XContentParser.Token.START_OBJECT) { + return new CompressedXContent(Strings.toString(XContentFactory.jsonBuilder().map(parser.mapOrdered()))); + } else { + throw new IllegalArgumentException("Unexpected token: " + token); + } + } + @Override public int hashCode() { return Objects.hash( diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java index cd3d5d262161..69e15a2b9888 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java @@ -29,6 +29,7 @@ import org.elasticsearch.cluster.metadata.DataStreamLifecycle.DownsamplingRound; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.Settings; @@ -47,9 +48,11 @@ import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.indices.SystemIndices; import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; @@ -58,6 +61,7 @@ import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Base64; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -70,6 +74,7 @@ import java.util.function.LongSupplier; import java.util.function.Predicate; import java.util.stream.Collectors; +import static org.elasticsearch.cluster.metadata.ComposableIndexTemplate.EMPTY_MAPPINGS; import static org.elasticsearch.cluster.metadata.MetadataCreateDataStreamService.lookupTemplateForDataStream; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; import static org.elasticsearch.index.IndexSettings.LIFECYCLE_ORIGINATION_DATE; @@ -89,6 +94,7 @@ public final class DataStream implements SimpleDiffable, ToXContentO public static final String FAILURE_STORE_PREFIX = ".fs-"; public static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("uuuu.MM.dd"); public static final String TIMESTAMP_FIELD_NAME = "@timestamp"; + // Timeseries indices' leaf readers should be sorted by desc order of their timestamp field, as it allows search time optimizations public static final Comparator TIMESERIES_LEAF_READERS_SORTER = Comparator.comparingLong((LeafReader r) -> { try { @@ -120,6 +126,7 @@ public final class DataStream implements SimpleDiffable, ToXContentO @Nullable private final Map metadata; private final Settings settings; + private final CompressedXContent mappings; private final boolean hidden; private final boolean replicated; private final boolean system; @@ -156,6 +163,7 @@ public final class DataStream implements SimpleDiffable, ToXContentO generation, metadata, Settings.EMPTY, + EMPTY_MAPPINGS, hidden, replicated, system, @@ -176,6 +184,7 @@ public final class DataStream implements SimpleDiffable, ToXContentO long generation, Map metadata, Settings settings, + CompressedXContent mappings, boolean hidden, boolean replicated, boolean system, @@ -192,6 +201,7 @@ public final class DataStream implements SimpleDiffable, ToXContentO generation, metadata, settings, + mappings, hidden, replicated, system, @@ -210,6 +220,7 @@ public final class DataStream implements SimpleDiffable, ToXContentO long generation, Map metadata, Settings settings, + CompressedXContent mappings, boolean hidden, boolean replicated, boolean system, @@ -225,6 +236,7 @@ public final class DataStream implements SimpleDiffable, ToXContentO this.generation = generation; this.metadata = metadata; this.settings = Objects.requireNonNull(settings); + this.mappings = Objects.requireNonNull(mappings); assert system == false || hidden; // system indices must be hidden this.hidden = hidden; this.replicated = replicated; @@ -286,11 +298,18 @@ public final class DataStream implements SimpleDiffable, ToXContentO } else { settings = Settings.EMPTY; } + CompressedXContent mappings; + if (in.getTransportVersion().onOrAfter(TransportVersions.MAPPINGS_IN_DATA_STREAMS)) { + mappings = CompressedXContent.readCompressedString(in); + } else { + mappings = EMPTY_MAPPINGS; + } return new DataStream( name, generation, metadata, settings, + mappings, hidden, replicated, system, @@ -381,8 +400,8 @@ public final class DataStream implements SimpleDiffable, ToXContentO return backingIndices.rolloverOnWrite; } - public ComposableIndexTemplate getEffectiveIndexTemplate(ProjectMetadata projectMetadata) { - return getMatchingIndexTemplate(projectMetadata).mergeSettings(settings); + public ComposableIndexTemplate getEffectiveIndexTemplate(ProjectMetadata projectMetadata) throws IOException { + return getMatchingIndexTemplate(projectMetadata).mergeSettings(settings).mergeMappings(mappings); } public Settings getEffectiveSettings(ProjectMetadata projectMetadata) { @@ -391,6 +410,10 @@ public final class DataStream implements SimpleDiffable, ToXContentO return templateSettings.merge(settings); } + public CompressedXContent getEffectiveMappings(ProjectMetadata projectMetadata) throws IOException { + return getMatchingIndexTemplate(projectMetadata).mergeMappings(mappings).template().mappings(); + } + private ComposableIndexTemplate getMatchingIndexTemplate(ProjectMetadata projectMetadata) { return lookupTemplateForDataStream(name, projectMetadata); } @@ -510,6 +533,10 @@ public final class DataStream implements SimpleDiffable, ToXContentO return settings; } + public CompressedXContent getMappings() { + return mappings; + } + @Override public boolean isHidden() { return hidden; @@ -1354,6 +1381,9 @@ public final class DataStream implements SimpleDiffable, ToXContentO || out.getTransportVersion().isPatchFrom(TransportVersions.SETTINGS_IN_DATA_STREAMS_8_19)) { settings.writeTo(out); } + if (out.getTransportVersion().onOrAfter(TransportVersions.MAPPINGS_IN_DATA_STREAMS)) { + mappings.writeTo(out); + } } public static final ParseField NAME_FIELD = new ParseField("name"); @@ -1376,6 +1406,7 @@ public final class DataStream implements SimpleDiffable, ToXContentO public static final ParseField FAILURE_AUTO_SHARDING_FIELD = new ParseField("failure_auto_sharding"); public static final ParseField DATA_STREAM_OPTIONS_FIELD = new ParseField("options"); public static final ParseField SETTINGS_FIELD = new ParseField("settings"); + public static final ParseField MAPPINGS_FIELD = new ParseField("mappings"); @SuppressWarnings("unchecked") private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( @@ -1385,6 +1416,7 @@ public final class DataStream implements SimpleDiffable, ToXContentO (Long) args[2], (Map) args[3], args[17] == null ? Settings.EMPTY : (Settings) args[17], + args[18] == null ? EMPTY_MAPPINGS : (CompressedXContent) args[18], args[4] != null && (boolean) args[4], args[5] != null && (boolean) args[5], args[6] != null && (boolean) args[6], @@ -1456,6 +1488,18 @@ public final class DataStream implements SimpleDiffable, ToXContentO DATA_STREAM_OPTIONS_FIELD ); PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> Settings.fromXContent(p), SETTINGS_FIELD); + PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> { + XContentParser.Token token = p.currentToken(); + if (token == XContentParser.Token.VALUE_STRING) { + return new CompressedXContent(Base64.getDecoder().decode(p.text())); + } else if (token == XContentParser.Token.VALUE_EMBEDDED_OBJECT) { + return new CompressedXContent(p.binaryValue()); + } else if (token == XContentParser.Token.START_OBJECT) { + return new CompressedXContent(Strings.toString(XContentFactory.jsonBuilder().map(p.mapOrdered()))); + } else { + throw new IllegalArgumentException("Unexpected token: " + token); + } + }, MAPPINGS_FIELD, ObjectParser.ValueType.VALUE_OBJECT_ARRAY); } public static DataStream fromXContent(XContentParser parser) throws IOException { @@ -1520,6 +1564,20 @@ public final class DataStream implements SimpleDiffable, ToXContentO builder.startObject(SETTINGS_FIELD.getPreferredName()); this.settings.toXContent(builder, params); builder.endObject(); + + String context = params.param(Metadata.CONTEXT_MODE_PARAM, Metadata.CONTEXT_MODE_API); + boolean binary = params.paramAsBoolean("binary", false); + if (Metadata.CONTEXT_MODE_API.equals(context) || binary == false) { + Map uncompressedMapping = XContentHelper.convertToMap(this.mappings.uncompressed(), true, XContentType.JSON) + .v2(); + if (uncompressedMapping.isEmpty() == false) { + builder.field(MAPPINGS_FIELD.getPreferredName()); + builder.map(uncompressedMapping); + } + } else { + builder.field(MAPPINGS_FIELD.getPreferredName(), mappings.compressed()); + } + builder.endObject(); return builder; } @@ -1864,6 +1922,7 @@ public final class DataStream implements SimpleDiffable, ToXContentO @Nullable private Map metadata = null; private Settings settings = Settings.EMPTY; + private CompressedXContent mappings = EMPTY_MAPPINGS; private boolean hidden = false; private boolean replicated = false; private boolean system = false; @@ -1892,6 +1951,7 @@ public final class DataStream implements SimpleDiffable, ToXContentO generation = dataStream.generation; metadata = dataStream.metadata; settings = dataStream.settings; + mappings = dataStream.mappings; hidden = dataStream.hidden; replicated = dataStream.replicated; system = dataStream.system; @@ -1928,6 +1988,11 @@ public final class DataStream implements SimpleDiffable, ToXContentO return this; } + public Builder setMappings(CompressedXContent mappings) { + this.mappings = mappings; + return this; + } + public Builder setHidden(boolean hidden) { this.hidden = hidden; return this; @@ -1989,6 +2054,7 @@ public final class DataStream implements SimpleDiffable, ToXContentO generation, metadata, settings, + mappings, hidden, replicated, system, diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateDataStreamService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateDataStreamService.java index ba462a941652..3be5df4077cc 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateDataStreamService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateDataStreamService.java @@ -332,6 +332,7 @@ public class MetadataCreateDataStreamService { initialGeneration, template.metadata() != null ? Map.copyOf(template.metadata()) : null, Settings.EMPTY, + ComposableIndexTemplate.EMPTY_MAPPINGS, hidden, false, isSystem, diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/WriteLoadConstraintSettings.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/WriteLoadConstraintSettings.java new file mode 100644 index 000000000000..cba02ed207b8 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/WriteLoadConstraintSettings.java @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.cluster.routing.allocation; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.routing.RerouteService; +import org.elasticsearch.common.Priority; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.unit.RatioValue; +import org.elasticsearch.core.TimeValue; + +/** + * Settings definitions for the write load allocation decider and associated infrastructure + */ +public class WriteLoadConstraintSettings { + + private static final String SETTING_PREFIX = "cluster.routing.allocation.write_load_decider."; + + public enum WriteLoadDeciderStatus { + /** + * The decider is disabled + */ + DISABLED, + /** + * Only the low-threshold is enabled (write-load will not trigger rebalance) + */ + LOW_ONLY, + /** + * The decider is enabled + */ + ENABLED + } + + public static final Setting WRITE_LOAD_DECIDER_ENABLED_SETTING = Setting.enumSetting( + WriteLoadDeciderStatus.class, + SETTING_PREFIX + "enabled", + WriteLoadDeciderStatus.DISABLED, + Setting.Property.Dynamic, + Setting.Property.NodeScope + ); + + /** + * The threshold over which we consider write thread pool utilization "high" + */ + public static final Setting WRITE_LOAD_DECIDER_HIGH_UTILIZATION_THRESHOLD_SETTING = new Setting<>( + SETTING_PREFIX + "high_utilization_threshold", + "90%", + RatioValue::parseRatioValue, + Setting.Property.Dynamic, + Setting.Property.NodeScope + ); + + /** + * The duration for which we need to see "high" utilization before we consider the low threshold exceeded + */ + public static final Setting WRITE_LOAD_DECIDER_HIGH_UTILIZATION_DURATION_SETTING = Setting.timeSetting( + SETTING_PREFIX + "high_utilization_duration", + TimeValue.timeValueMinutes(10), + Setting.Property.Dynamic, + Setting.Property.NodeScope + ); + + /** + * When the decider is {@link WriteLoadDeciderStatus#ENABLED}, the write-load monitor will call + * {@link RerouteService#reroute(String, Priority, ActionListener)} when we see tasks being delayed by this amount of time + * (but no more often than {@link #WRITE_LOAD_DECIDER_REROUTE_INTERVAL_SETTING}) + */ + public static final Setting WRITE_LOAD_DECIDER_QUEUE_LATENCY_THRESHOLD_SETTING = Setting.timeSetting( + SETTING_PREFIX + "queue_latency_threshold", + TimeValue.timeValueSeconds(30), + Setting.Property.Dynamic, + Setting.Property.NodeScope + ); + + /** + * How often the data node calculates the write-loads for the individual shards + */ + public static final Setting WRITE_LOAD_DECIDER_SHARD_WRITE_LOAD_POLLING_INTERVAL_SETTING = Setting.timeSetting( + SETTING_PREFIX + "shard_write_load_polling_interval", + TimeValue.timeValueSeconds(60), + Setting.Property.Dynamic, + Setting.Property.NodeScope + ); + + /** + * The minimum amount of time between successive calls to reroute to address write load hot-spots + */ + public static final Setting WRITE_LOAD_DECIDER_REROUTE_INTERVAL_SETTING = Setting.timeSetting( + SETTING_PREFIX + "reroute_interval", + TimeValue.timeValueSeconds(60), + Setting.Property.Dynamic, + Setting.Property.NodeScope + ); +} diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index fd6da4fadae6..1fbc8993cc5a 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -46,6 +46,7 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.routing.OperationRouting; import org.elasticsearch.cluster.routing.allocation.DataTier; import org.elasticsearch.cluster.routing.allocation.DiskThresholdSettings; +import org.elasticsearch.cluster.routing.allocation.WriteLoadConstraintSettings; import org.elasticsearch.cluster.routing.allocation.allocator.AllocationBalancingRoundSummaryService; import org.elasticsearch.cluster.routing.allocation.allocator.BalancedShardsAllocator; import org.elasticsearch.cluster.routing.allocation.allocator.DesiredBalanceComputer; @@ -643,6 +644,12 @@ public final class ClusterSettings extends AbstractScopedSettings { DataStreamFailureStoreSettings.DATA_STREAM_FAILURE_STORED_ENABLED_SETTING, IndexingStatsSettings.RECENT_WRITE_LOAD_HALF_LIFE_SETTING, SearchStatsSettings.RECENT_READ_LOAD_HALF_LIFE_SETTING, - TransportGetAllocationStatsAction.CACHE_TTL_SETTING + TransportGetAllocationStatsAction.CACHE_TTL_SETTING, + WriteLoadConstraintSettings.WRITE_LOAD_DECIDER_ENABLED_SETTING, + WriteLoadConstraintSettings.WRITE_LOAD_DECIDER_HIGH_UTILIZATION_THRESHOLD_SETTING, + WriteLoadConstraintSettings.WRITE_LOAD_DECIDER_HIGH_UTILIZATION_DURATION_SETTING, + WriteLoadConstraintSettings.WRITE_LOAD_DECIDER_QUEUE_LATENCY_THRESHOLD_SETTING, + WriteLoadConstraintSettings.WRITE_LOAD_DECIDER_SHARD_WRITE_LOAD_POLLING_INTERVAL_SETTING, + WriteLoadConstraintSettings.WRITE_LOAD_DECIDER_REROUTE_INTERVAL_SETTING ); } diff --git a/server/src/main/java/org/elasticsearch/index/engine/Engine.java b/server/src/main/java/org/elasticsearch/index/engine/Engine.java index 423ef199e03b..881de29c435d 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -2199,7 +2199,7 @@ public abstract class Engine implements Closeable { awaitPendingClose(); } - private void awaitPendingClose() { + protected final void awaitPendingClose() { try { closedLatch.await(); } catch (InterruptedException e) { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/BlockSourceReader.java b/server/src/main/java/org/elasticsearch/index/mapper/BlockSourceReader.java index 72d39b7f59ca..9d65f0ed8ba2 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/BlockSourceReader.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/BlockSourceReader.java @@ -469,7 +469,7 @@ public abstract class BlockSourceReader implements BlockLoader.RowStrideReader { /** * Convert a {@link String} into a utf-8 {@link BytesRef}. */ - static BytesRef toBytesRef(BytesRef scratch, String v) { + public static BytesRef toBytesRef(BytesRef scratch, String v) { int len = UnicodeUtil.maxUTF8Length(v.length()); if (scratch.bytes.length < len) { scratch.bytes = new byte[len]; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/BlockStoredFieldsReader.java b/server/src/main/java/org/elasticsearch/index/mapper/BlockStoredFieldsReader.java index f56bfb098ed8..a1f5dc4381f5 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/BlockStoredFieldsReader.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/BlockStoredFieldsReader.java @@ -35,10 +35,10 @@ public abstract class BlockStoredFieldsReader implements BlockLoader.RowStrideRe return true; } - private abstract static class StoredFieldsBlockLoader implements BlockLoader { + public abstract static class StoredFieldsBlockLoader implements BlockLoader { protected final String field; - StoredFieldsBlockLoader(String field) { + public StoredFieldsBlockLoader(String field) { this.field = field; } @@ -112,10 +112,10 @@ public abstract class BlockStoredFieldsReader implements BlockLoader.RowStrideRe } } - private abstract static class Bytes extends BlockStoredFieldsReader { + public abstract static class Bytes extends BlockStoredFieldsReader { private final String field; - Bytes(String field) { + public Bytes(String field) { this.field = field; } diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightPhase.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightPhase.java index 54c265deb948..cf9e8fbf7ded 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightPhase.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightPhase.java @@ -124,7 +124,8 @@ public class HighlightPhase implements FetchSubPhase { if (fieldNameContainsWildcards) { if (fieldType.typeName().equals(TextFieldMapper.CONTENT_TYPE) == false && fieldType.typeName().equals(KeywordFieldMapper.CONTENT_TYPE) == false - && fieldType.typeName().equals("match_only_text") == false) { + && fieldType.typeName().equals("match_only_text") == false + && fieldType.typeName().equals("patterned_text") == false) { continue; } if (highlighter.canHighlight(fieldType) == false) { diff --git a/server/src/main/java/org/elasticsearch/search/internal/SearchContext.java b/server/src/main/java/org/elasticsearch/search/internal/SearchContext.java index 580fb5efc722..7d018a7ef4ba 100644 --- a/server/src/main/java/org/elasticsearch/search/internal/SearchContext.java +++ b/server/src/main/java/org/elasticsearch/search/internal/SearchContext.java @@ -115,6 +115,10 @@ public abstract class SearchContext implements Releasable { closeFuture.onResponse(null); } + public final boolean isClosed() { + return closeFuture.isDone(); + } + /** * Should be called before executing the main query and after all other parameters have been set. */ diff --git a/server/src/main/java/org/elasticsearch/threadpool/DefaultBuiltInExecutorBuilders.java b/server/src/main/java/org/elasticsearch/threadpool/DefaultBuiltInExecutorBuilders.java index 336d978358b9..46d64b9713fa 100644 --- a/server/src/main/java/org/elasticsearch/threadpool/DefaultBuiltInExecutorBuilders.java +++ b/server/src/main/java/org/elasticsearch/threadpool/DefaultBuiltInExecutorBuilders.java @@ -54,7 +54,8 @@ public class DefaultBuiltInExecutorBuilders implements BuiltInExecutorBuilders { settings, ThreadPool.Names.WRITE, allocatedProcessors, - 10000, + // 10,000 for all nodes with 8 cores or fewer. Scale up once we have more than 8 cores. + Math.max(allocatedProcessors * 750, 10000), new EsExecutors.TaskTrackingConfig(true, indexAutoscalingEWMA) ) ); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateTests.java index e75d8dd9fbca..6f3c30292029 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateTests.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import static org.elasticsearch.cluster.metadata.ComposableIndexTemplate.EMPTY_MAPPINGS; import static org.elasticsearch.cluster.metadata.DataStream.TIMESTAMP_FIELD_NAME; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -283,9 +284,7 @@ public class ComposableIndexTemplateTests extends SimpleDiffableSerializationTes } public void testMergeEmptySettingsIntoTemplateWithNonEmptySettings() { - // We only have settings from the template, so the effective template will just be the original template - Settings templateSettings = randomSettings(); - Template.Builder templateBuilder = Template.builder().settings(templateSettings).mappings(randomMappings(null)); + // Attempting to merge in null settings ought to fail ComposableIndexTemplate indexTemplate = randomInstance(); expectThrows(NullPointerException.class, () -> indexTemplate.mergeSettings(null)); assertThat(indexTemplate.mergeSettings(Settings.EMPTY), equalTo(indexTemplate)); @@ -325,12 +324,14 @@ public class ComposableIndexTemplateTests extends SimpleDiffableSerializationTes .put("index.setting3", "templateValue") .put("index.setting4", "templateValue") .build(); + List componentTemplates = List.of("component_template_1"); CompressedXContent templateMappings = randomMappings(randomDataStreamTemplate()); Template.Builder templateBuilder = Template.builder().settings(templateSettings).mappings(templateMappings); ComposableIndexTemplate indexTemplate = ComposableIndexTemplate.builder() .indexPatterns(List.of(dataStreamName)) .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) .template(templateBuilder) + .componentTemplates(componentTemplates) .build(); Settings mergedSettings = Settings.builder() .put("index.setting1", "dataStreamValue") @@ -342,7 +343,67 @@ public class ComposableIndexTemplateTests extends SimpleDiffableSerializationTes .indexPatterns(List.of(dataStreamName)) .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) .template(expectedTemplateBuilder) + .componentTemplates(componentTemplates) .build(); assertThat(indexTemplate.mergeSettings(dataStreamSettings), equalTo(expectedEffectiveTemplate)); } + + public void testMergeEmptyMappingsIntoTemplateWithNonEmptySettings() throws IOException { + // Attempting to merge in null mappings ought to fail + ComposableIndexTemplate indexTemplate = randomInstance(); + expectThrows(NullPointerException.class, () -> indexTemplate.mergeMappings(null)); + ComposableIndexTemplate mergedTemplate = indexTemplate.mergeMappings(EMPTY_MAPPINGS); + if (indexTemplate.template() == null || indexTemplate.template().mappings() == null) { + assertThat(mergedTemplate.template().mappings(), equalTo(EMPTY_MAPPINGS)); + } else { + assertThat(mergedTemplate, equalTo(indexTemplate)); + } + assertThat(indexTemplate.mergeSettings(Settings.EMPTY), equalTo(indexTemplate)); + } + + public void testMergeNonEmptyMappingsIntoTemplateWithEmptyMappings() throws IOException { + // We only have settings from the data stream, so we expect to get only those back in the effective template + CompressedXContent dataStreamMappings = randomMappings(randomDataStreamTemplate()); + String dataStreamName = randomAlphaOfLength(10).toLowerCase(Locale.ROOT); + Settings templateSettings = Settings.EMPTY; + CompressedXContent templateMappings = new CompressedXContent(Map.of("_doc", Map.of())); + Template.Builder templateBuilder = Template.builder().settings(templateSettings).mappings(templateMappings); + ComposableIndexTemplate indexTemplate = ComposableIndexTemplate.builder() + .indexPatterns(List.of(dataStreamName)) + .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) + .template(templateBuilder) + .build(); + Template.Builder expectedTemplateBuilder = Template.builder().settings(templateSettings).mappings(dataStreamMappings); + ComposableIndexTemplate expectedEffectiveTemplate = ComposableIndexTemplate.builder() + .indexPatterns(List.of(dataStreamName)) + .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) + .template(expectedTemplateBuilder) + .build(); + assertThat(indexTemplate.mergeMappings(dataStreamMappings), equalTo(expectedEffectiveTemplate)); + } + + public void testMergeMappings() throws IOException { + // Here we have settings from both the template and the data stream, so we expect the data stream settings to take precedence + CompressedXContent dataStreamMappings = new CompressedXContent(Map.of()); + String dataStreamName = randomAlphaOfLength(10).toLowerCase(Locale.ROOT); + CompressedXContent templateMappings = new CompressedXContent(Map.of("_doc", Map.of())); + Settings templateSettings = randomSettings(); + List componentTemplates = List.of("component_template_1"); + Template.Builder templateBuilder = Template.builder().settings(templateSettings).mappings(templateMappings); + ComposableIndexTemplate indexTemplate = ComposableIndexTemplate.builder() + .indexPatterns(List.of(dataStreamName)) + .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) + .template(templateBuilder) + .componentTemplates(componentTemplates) + .build(); + Template.Builder expectedTemplateBuilder = Template.builder().settings(templateSettings).mappings(EMPTY_MAPPINGS); + ComposableIndexTemplate expectedEffectiveTemplate = ComposableIndexTemplate.builder() + .indexPatterns(List.of(dataStreamName)) + .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) + .template(expectedTemplateBuilder) + .componentTemplates(componentTemplates) + .build(); + ComposableIndexTemplate merged = indexTemplate.mergeMappings(dataStreamMappings); + assertThat(merged, equalTo(expectedEffectiveTemplate)); + } } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java index 7c68f49b42d1..eaf3d63490c3 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java @@ -53,7 +53,6 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; -import static org.elasticsearch.cluster.metadata.ComponentTemplateTests.randomMappings; import static org.elasticsearch.cluster.metadata.DataStream.getDefaultBackingIndexName; import static org.elasticsearch.cluster.metadata.DataStream.getDefaultFailureStoreName; import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.newInstance; @@ -98,6 +97,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase name = randomAlphaOfLength(10); case 1 -> indices = randomNonEmptyIndexInstances(); case 2 -> generation = instance.getGeneration() + randomIntBetween(1, 10); @@ -179,6 +179,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase settings = randomValueOtherThan(settings, DataStreamTestHelper::randomSettings); + case 17 -> mappings = randomValueOtherThan(mappings, ComponentTemplateTests::randomMappings); } return new DataStream( @@ -186,6 +187,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase dataStream.getEffectiveIndexTemplate(projectMetadataBuilder.build())); } - public void testGetEffectiveIndexTemplateTemplateSettingsOnly() { - // We only have settings from the template, so the effective template will just be the original template - DataStream dataStream = createDataStream(Settings.EMPTY); + public void testGetEffectiveIndexTemplateTemplateNoOverrides() throws IOException { + // We only have settings and mappings from the template, so the effective template will just be the original template + DataStream dataStream = createDataStream(Settings.EMPTY, ComposableIndexTemplate.EMPTY_MAPPINGS); Settings templateSettings = randomSettings(); Template.Builder templateBuilder = Template.builder().settings(templateSettings).mappings(randomMappings()); ComposableIndexTemplate indexTemplate = ComposableIndexTemplate.builder() .indexPatterns(List.of(dataStream.getName())) .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) .template(templateBuilder) + .componentTemplates(List.of("component-template-1")) .build(); ProjectMetadata.Builder projectMetadataBuilder = ProjectMetadata.builder(randomProjectIdOrDefault()) - .indexTemplates(Map.of(dataStream.getName(), indexTemplate)); + .indexTemplates(Map.of(dataStream.getName(), indexTemplate)) + .componentTemplates( + Map.of( + "component-template-1", + new ComponentTemplate( + Template.builder().settings(Settings.builder().put("index.setting5", "componentTemplateValue")).build(), + 1L, + Map.of() + ) + ) + ); assertThat(dataStream.getEffectiveIndexTemplate(projectMetadataBuilder.build()), equalTo(indexTemplate)); } - public void testGetEffectiveIndexTemplateDataStreamSettingsOnly() { + public void testGetEffectiveIndexTemplateDataStreamSettingsOnly() throws IOException { // We only have settings from the data stream, so we expect to get only those back in the effective template Settings dataStreamSettings = randomSettings(); - DataStream dataStream = createDataStream(dataStreamSettings); + DataStream dataStream = createDataStream(dataStreamSettings, ComposableIndexTemplate.EMPTY_MAPPINGS); Settings templateSettings = Settings.EMPTY; CompressedXContent templateMappings = randomMappings(); Template.Builder templateBuilder = Template.builder().settings(templateSettings).mappings(templateMappings); + ComposableIndexTemplate indexTemplate = ComposableIndexTemplate.builder() .indexPatterns(List.of(dataStream.getName())) .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) @@ -2585,20 +2624,80 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase componentTemplates = List.of("component-template-1"); + ComposableIndexTemplate indexTemplate = ComposableIndexTemplate.builder() + .indexPatterns(List.of(dataStream.getName())) + .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) + .template(templateBuilder) + .componentTemplates(componentTemplates) + .build(); + ProjectMetadata.Builder projectMetadataBuilder = ProjectMetadata.builder(randomProjectIdOrDefault()) + .indexTemplates(Map.of(dataStream.getName(), indexTemplate)) + .componentTemplates( + Map.of( + "component-template-1", + new ComponentTemplate( + Template.builder().settings(Settings.builder().put("index.setting5", "componentTemplateValue")).build(), + 1L, + Map.of() + ) + ) + ); + Settings mergedSettings = Settings.builder() + .put("index.setting1", "dataStreamValue") + .put("index.setting2", "dataStreamValue") + .put("index.setting4", "templateValue") + .build(); + CompressedXContent mergedMappings = new CompressedXContent( + Map.of( + "properties", + Map.of("field1", Map.of("type", "keyword"), "field2", Map.of("type", "text"), "field3", Map.of("type", "keyword")) + ) + ); + Template.Builder expectedTemplateBuilder = Template.builder().settings(mergedSettings).mappings(mergedMappings); + ComposableIndexTemplate expectedEffectiveTemplate = ComposableIndexTemplate.builder() + .indexPatterns(List.of(dataStream.getName())) + .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) + .template(expectedTemplateBuilder) + .componentTemplates(componentTemplates) + .build(); + assertThat(dataStream.getEffectiveIndexTemplate(projectMetadataBuilder.build()), equalTo(expectedEffectiveTemplate)); + } + + public void testGetEffectiveMappingsNoMatchingTemplate() { + // No matching template, so we expect an IllegalArgumentException + DataStream dataStream = createTestInstance(); + ProjectMetadata.Builder projectMetadataBuilder = ProjectMetadata.builder(randomProjectIdOrDefault()); + assertThrows(IllegalArgumentException.class, () -> dataStream.getEffectiveMappings(projectMetadataBuilder.build())); + } + + public void testGetEffectiveIndexTemplateDataStreamMappingsOnly() throws IOException { + // We only have mappings from the data stream, so we expect to get only those back in the effective template + CompressedXContent dataStreamMappings = randomMappings(); + DataStream dataStream = createDataStream(Settings.EMPTY, dataStreamMappings); + Settings templateSettings = Settings.EMPTY; + CompressedXContent templateMappings = new CompressedXContent(Map.of("_doc", Map.of())); + ; Template.Builder templateBuilder = Template.builder().settings(templateSettings).mappings(templateMappings); ComposableIndexTemplate indexTemplate = ComposableIndexTemplate.builder() .indexPatterns(List.of(dataStream.getName())) @@ -2607,12 +2706,7 @@ public class DataStreamTests extends AbstractXContentSerializingTestCase { synchronized (availableDiskSpaceUpdates) { + // the updates are different values assertThat(availableDiskSpaceUpdates.size(), is(3)); if (aErrorsFirst) { // uses the stats from "a" - assertThat( - availableDiskSpaceUpdates.getLast().getBytes(), - // the default 5% (same as flood stage level) - is(Math.max(aFileStore.usableSpace - aFileStore.totalSpace / 20, 0L)) - ); + assertThat(availableDiskSpaceUpdates.getLast().getBytes(), is(finalAUsableSpace)); } else { // uses the stats from "b" - assertThat( - availableDiskSpaceUpdates.getLast().getBytes(), - // the default 5% (same as flood stage level) - is(Math.max(bFileStore.usableSpace - bFileStore.totalSpace / 20, 0L)) - ); + assertThat(availableDiskSpaceUpdates.getLast().getBytes(), is(finalBUsableSpace)); } } }); @@ -847,6 +841,7 @@ public class ThreadPoolMergeExecutorServiceDiskSpaceTests extends ESTestCase { when(mergeTask2.schedule()).thenReturn(RUN); boolean task1Runs = randomBoolean(); long currentAvailableBudget = expectedAvailableBudget.get(); + // the over-budget here can be larger than the total initial available budget long overBudget = randomLongBetween(currentAvailableBudget + 1L, currentAvailableBudget + 100L); long underBudget = randomLongBetween(0L, currentAvailableBudget); if (task1Runs) { @@ -882,11 +877,18 @@ public class ThreadPoolMergeExecutorServiceDiskSpaceTests extends ESTestCase { // update the expected budget given that one task now finished expectedAvailableBudget.set(expectedAvailableBudget.get() + completedMergeTask.estimatedRemainingMergeSize()); } - // let the test finish cleanly - assertBusy(() -> { - assertThat(threadPoolMergeExecutorService.getDiskSpaceAvailableForNewMergeTasks(), is(aHasMoreSpace ? 112_500L : 103_000L)); - assertThat(threadPoolMergeExecutorService.allDone(), is(true)); - }); + assertBusy( + () -> assertThat( + threadPoolMergeExecutorService.getDiskSpaceAvailableForNewMergeTasks(), + is(aHasMoreSpace ? 112_500L : 103_000L) + ) + ); + // let the test finish cleanly (some tasks can be over budget even if all the other tasks finished running) + aFileStore.totalSpace = Long.MAX_VALUE; + bFileStore.totalSpace = Long.MAX_VALUE; + aFileStore.usableSpace = Long.MAX_VALUE; + bFileStore.usableSpace = Long.MAX_VALUE; + assertBusy(() -> assertThat(threadPoolMergeExecutorService.allDone(), is(true))); } if (setThreadPoolMergeSchedulerSetting) { assertWarnings( diff --git a/server/src/test/java/org/elasticsearch/index/engine/ThreadPoolMergeSchedulerTests.java b/server/src/test/java/org/elasticsearch/index/engine/ThreadPoolMergeSchedulerTests.java index d88f7c67b0bb..bea7697fd51c 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/ThreadPoolMergeSchedulerTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/ThreadPoolMergeSchedulerTests.java @@ -612,11 +612,12 @@ public class ThreadPoolMergeSchedulerTests extends ESTestCase { fail(e); } }); + // test expects that there definitely is a running merge before closing the merge scheduler + mergeRunningLatch.await(); + // closes the merge scheduler t.start(); try { assertTrue(t.isAlive()); - // wait for the merge to actually run - mergeRunningLatch.await(); // ensure the merge scheduler is effectively "closed" assertBusy(() -> { MergeSource mergeSource2 = mock(MergeSource.class); diff --git a/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/RecordingApmServer.java b/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/RecordingApmServer.java index db2ce9fb83a5..d20f2d08719e 100644 --- a/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/RecordingApmServer.java +++ b/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/RecordingApmServer.java @@ -15,7 +15,6 @@ import com.sun.net.httpserver.HttpServer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.core.SuppressForbidden; -import org.elasticsearch.xcontent.spi.XContentProvider; import org.junit.rules.ExternalResource; import java.io.BufferedReader; @@ -25,7 +24,6 @@ import java.io.InputStreamReader; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; @@ -35,14 +33,12 @@ import java.util.function.Consumer; public class RecordingApmServer extends ExternalResource { private static final Logger logger = LogManager.getLogger(RecordingApmServer.class); - private static final XContentProvider.FormatProvider XCONTENT = XContentProvider.provider().getJsonXContent(); - final ArrayBlockingQueue received = new ArrayBlockingQueue<>(1000); private static HttpServer server; private final Thread messageConsumerThread = consumerThread(); private volatile Consumer consumer; - private volatile boolean consumerRunning = true; + private volatile boolean running = true; @Override protected void before() throws Throwable { @@ -56,7 +52,7 @@ public class RecordingApmServer extends ExternalResource { private Thread consumerThread() { return new Thread(() -> { - while (consumerRunning) { + while (running) { if (consumer != null) { try { String msg = received.poll(1L, TimeUnit.SECONDS); @@ -74,28 +70,38 @@ public class RecordingApmServer extends ExternalResource { @Override protected void after() { + running = false; server.stop(1); - consumerRunning = false; + consumer = null; } private void handle(HttpExchange exchange) throws IOException { try (exchange) { - try { - try (InputStream requestBody = exchange.getRequestBody()) { - if (requestBody != null) { - var read = readJsonMessages(requestBody); - received.addAll(read); + if (running) { + try { + try (InputStream requestBody = exchange.getRequestBody()) { + if (requestBody != null) { + var read = readJsonMessages(requestBody); + received.addAll(read); + } } - } - } catch (RuntimeException e) { - logger.warn("failed to parse request", e); + } catch (Throwable t) { + // The lifetime of HttpServer makes message handling "brittle": we need to start handling and recording received + // messages before the test starts running. We should also stop handling them before the test ends (and the test + // cluster is torn down), or we may run into IOException as the communication channel is interrupted. + // Coordinating the lifecycle of the mock HttpServer and of the test ES cluster is difficult and error-prone, so + // we just handle Throwable and don't care (log, but don't care): if we have an error in communicating to/from + // the mock server while the test is running, the test would fail anyway as the expected messages will not arrive, and + // if we have an error outside the test scope (before or after) that is OK. + logger.warn("failed to parse request", t); + } } exchange.sendResponseHeaders(201, 0); } } - private List readJsonMessages(InputStream input) throws IOException { + private List readJsonMessages(InputStream input) { // parse NDJSON return new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)).lines().toList(); } @@ -104,14 +110,7 @@ public class RecordingApmServer extends ExternalResource { return server.getAddress().getPort(); } - public List getMessages() { - List list = new ArrayList<>(received.size()); - received.drainTo(list); - return list; - } - public void addMessageConsumer(Consumer messageConsumer) { this.consumer = messageConsumer; } - } diff --git a/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/TracesApmIT.java b/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/TracesApmIT.java index 6b10140bd80e..afb9243e0f3e 100644 --- a/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/TracesApmIT.java +++ b/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/TracesApmIT.java @@ -91,7 +91,8 @@ public class TracesApmIT extends ESRestTestCase { client().performRequest(nodeStatsRequest); - finished.await(30, TimeUnit.SECONDS); + var completed = finished.await(30, TimeUnit.SECONDS); + assertTrue("Timeout when waiting for assertions to complete", completed); assertThat(assertions, equalTo(Collections.emptySet())); } @@ -143,5 +144,4 @@ public class TracesApmIT extends ESRestTestCase { return Collections.emptyMap(); } } - } diff --git a/test/framework/src/main/java/org/elasticsearch/cluster/metadata/DataStreamTestHelper.java b/test/framework/src/main/java/org/elasticsearch/cluster/metadata/DataStreamTestHelper.java index 19d4efdff133..7c78c82f75f0 100644 --- a/test/framework/src/main/java/org/elasticsearch/cluster/metadata/DataStreamTestHelper.java +++ b/test/framework/src/main/java/org/elasticsearch/cluster/metadata/DataStreamTestHelper.java @@ -17,6 +17,7 @@ import org.elasticsearch.cluster.routing.allocation.AllocationService; import org.elasticsearch.cluster.routing.allocation.WriteLoadForecaster; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.CheckedFunction; @@ -58,6 +59,7 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; +import java.io.IOException; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; @@ -85,6 +87,7 @@ import static org.elasticsearch.test.ESTestCase.randomIntBetween; import static org.elasticsearch.test.ESTestCase.randomMap; import static org.elasticsearch.test.ESTestCase.randomMillisUpToYear9999; import static org.elasticsearch.test.ESTestCase.randomPositiveTimeValue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -365,6 +368,7 @@ public final class DataStreamTestHelper { generation, metadata, randomSettings(), + randomMappings(), system ? true : randomBoolean(), replicated, system, @@ -400,6 +404,15 @@ public final class DataStreamTestHelper { ); } + private static CompressedXContent randomMappings() { + try { + return new CompressedXContent("{\"properties\":{\"" + randomAlphaOfLength(5) + "\":{\"type\":\"keyword\"}}}"); + } catch (IOException e) { + fail("got an IO exception creating fake mappings: " + e); + return null; + } + } + public static DataStreamAlias randomAliasInstance() { List dataStreams = List.of(generateRandomStringArray(5, 5, false, false)); return new DataStreamAlias( diff --git a/test/framework/src/main/java/org/elasticsearch/search/MockSearchService.java b/test/framework/src/main/java/org/elasticsearch/search/MockSearchService.java index 14c75a01e5b6..07ffb3ab9a4e 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/MockSearchService.java +++ b/test/framework/src/main/java/org/elasticsearch/search/MockSearchService.java @@ -152,7 +152,12 @@ public class MockSearchService extends SearchService { @Override public SearchContext createSearchContext(ShardSearchRequest request, TimeValue timeout) throws IOException { SearchContext searchContext = super.createSearchContext(request, timeout); - onPutContext.accept(searchContext.readerContext()); + try { + onCreateSearchContext.accept(searchContext); + } catch (Exception e) { + searchContext.close(); + throw e; + } searchContext.addReleasable(() -> onRemoveContext.accept(searchContext.readerContext())); return searchContext; } diff --git a/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/storage/AutoscalingStorageIntegTestCase.java b/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/storage/AutoscalingStorageIntegTestCase.java index 83bd9399274d..331d3438eafd 100644 --- a/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/storage/AutoscalingStorageIntegTestCase.java +++ b/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/storage/AutoscalingStorageIntegTestCase.java @@ -14,6 +14,7 @@ import org.elasticsearch.cluster.InternalClusterInfoService; import org.elasticsearch.cluster.routing.allocation.DiskThresholdSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.datastreams.DataStreamsPlugin; +import org.elasticsearch.index.engine.ThreadPoolMergeExecutorService; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.xpack.autoscaling.LocalStateAutoscaling; import org.elasticsearch.xpack.autoscaling.action.GetAutoscalingCapacityAction; @@ -39,7 +40,10 @@ public abstract class AutoscalingStorageIntegTestCase extends DiskUsageIntegTest builder.put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_LOW_DISK_WATERMARK_SETTING.getKey(), LOW_WATERMARK_BYTES + "b") .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK_SETTING.getKey(), HIGH_WATERMARK_BYTES + "b") .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_DISK_FLOOD_STAGE_WATERMARK_SETTING.getKey(), "0b") - .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_REROUTE_INTERVAL_SETTING.getKey(), "0ms"); + .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_REROUTE_INTERVAL_SETTING.getKey(), "0ms") + // the periodicity for the checker for the available disk space as well as the merge tasks' aborting status + // the default of 5 seconds might timeout some tests + .put(ThreadPoolMergeExecutorService.INDICES_MERGE_DISK_CHECK_INTERVAL_SETTING.getKey(), "100ms"); return builder.build(); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleOperationMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleOperationMetadata.java index 878eb9f9b17d..23d032c2ddbf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleOperationMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleOperationMetadata.java @@ -80,6 +80,11 @@ public class LifecycleOperationMetadata implements Metadata.ProjectCustom { ); } + @Deprecated(forRemoval = true) + public static OperationMode currentSLMMode(final ClusterState state) { + return currentSLMMode(state.metadata().getProject()); + } + /** * Returns the current ILM mode based on the given cluster state. It first checks the newer * storage mechanism ({@link LifecycleOperationMetadata#getSLMOperationMode()}) before falling @@ -87,9 +92,9 @@ public class LifecycleOperationMetadata implements Metadata.ProjectCustom { * value for an empty state is used. */ @SuppressWarnings("deprecated") - public static OperationMode currentSLMMode(final ClusterState state) { - SnapshotLifecycleMetadata oldMetadata = state.metadata().getProject().custom(SnapshotLifecycleMetadata.TYPE); - LifecycleOperationMetadata currentMetadata = state.metadata().getProject().custom(LifecycleOperationMetadata.TYPE); + public static OperationMode currentSLMMode(ProjectMetadata project) { + SnapshotLifecycleMetadata oldMetadata = project.custom(SnapshotLifecycleMetadata.TYPE); + LifecycleOperationMetadata currentMetadata = project.custom(LifecycleOperationMetadata.TYPE); return Optional.ofNullable(currentMetadata) .map(LifecycleOperationMetadata::getSLMOperationMode) .orElse( diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTask.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTask.java index 5a708dbf95fb..09cd368645c7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTask.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTask.java @@ -14,7 +14,10 @@ import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.cluster.AckedClusterStateUpdateTask; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateUpdateTask; +import org.elasticsearch.cluster.metadata.ProjectId; +import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.common.Priority; +import org.elasticsearch.core.FixForMultiProject; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Strings; @@ -29,6 +32,8 @@ import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.curren */ public class OperationModeUpdateTask extends ClusterStateUpdateTask { private static final Logger logger = LogManager.getLogger(OperationModeUpdateTask.class); + + private final ProjectId projectId; @Nullable private final OperationMode ilmMode; @Nullable @@ -47,18 +52,21 @@ public class OperationModeUpdateTask extends ClusterStateUpdateTask { }; } - private OperationModeUpdateTask(Priority priority, OperationMode ilmMode, OperationMode slmMode) { + private OperationModeUpdateTask(Priority priority, ProjectId projectId, OperationMode ilmMode, OperationMode slmMode) { super(priority); + this.projectId = projectId; this.ilmMode = ilmMode; this.slmMode = slmMode; } - public static OperationModeUpdateTask ilmMode(OperationMode mode) { - return new OperationModeUpdateTask(getPriority(mode), mode, null); + public static OperationModeUpdateTask ilmMode(ProjectId projectId, OperationMode mode) { + return new OperationModeUpdateTask(getPriority(mode), projectId, mode, null); } public static OperationModeUpdateTask slmMode(OperationMode mode) { - return new OperationModeUpdateTask(getPriority(mode), null, mode); + @FixForMultiProject // Use non-default ID when SLM has been made project-aware + final var projectId = ProjectId.DEFAULT; + return new OperationModeUpdateTask(getPriority(mode), projectId, null, mode); } private static Priority getPriority(OperationMode mode) { @@ -79,22 +87,24 @@ public class OperationModeUpdateTask extends ClusterStateUpdateTask { @Override public ClusterState execute(ClusterState currentState) { - ClusterState newState = currentState; - newState = updateILMState(newState); - newState = updateSLMState(newState); - return newState; - } - - private ClusterState updateILMState(final ClusterState currentState) { - if (ilmMode == null) { + ProjectMetadata oldProject = currentState.metadata().getProject(projectId); + ProjectMetadata newProject = updateILMState(oldProject); + newProject = updateSLMState(newProject); + if (newProject == oldProject) { return currentState; } + return ClusterState.builder(currentState).putProjectMetadata(newProject).build(); + } - final var project = currentState.metadata().getProject(); - final OperationMode currentMode = currentILMMode(project); + private ProjectMetadata updateILMState(final ProjectMetadata currentProject) { + if (ilmMode == null) { + return currentProject; + } + + final OperationMode currentMode = currentILMMode(currentProject); if (currentMode.equals(ilmMode)) { // No need for a new state - return currentState; + return currentProject; } final OperationMode newMode; @@ -102,24 +112,23 @@ public class OperationModeUpdateTask extends ClusterStateUpdateTask { newMode = ilmMode; } else { // The transition is invalid, return the current state - return currentState; + return currentProject; } logger.info("updating ILM operation mode to {}", newMode); - final var updatedMetadata = new LifecycleOperationMetadata(newMode, currentSLMMode(currentState)); - return currentState.copyAndUpdateProject(project.id(), b -> b.putCustom(LifecycleOperationMetadata.TYPE, updatedMetadata)); + final var updatedMetadata = new LifecycleOperationMetadata(newMode, currentSLMMode(currentProject)); + return currentProject.copyAndUpdate(b -> b.putCustom(LifecycleOperationMetadata.TYPE, updatedMetadata)); } - private ClusterState updateSLMState(final ClusterState currentState) { + private ProjectMetadata updateSLMState(final ProjectMetadata currentProject) { if (slmMode == null) { - return currentState; + return currentProject; } - final var project = currentState.metadata().getProject(); - final OperationMode currentMode = currentSLMMode(currentState); + final OperationMode currentMode = currentSLMMode(currentProject); if (currentMode.equals(slmMode)) { // No need for a new state - return currentState; + return currentProject; } final OperationMode newMode; @@ -127,12 +136,12 @@ public class OperationModeUpdateTask extends ClusterStateUpdateTask { newMode = slmMode; } else { // The transition is invalid, return the current state - return currentState; + return currentProject; } logger.info("updating SLM operation mode to {}", newMode); - final var updatedMetadata = new LifecycleOperationMetadata(currentILMMode(project), newMode); - return currentState.copyAndUpdateProject(project.id(), b -> b.putCustom(LifecycleOperationMetadata.TYPE, updatedMetadata)); + final var updatedMetadata = new LifecycleOperationMetadata(currentILMMode(currentProject), newMode); + return currentProject.copyAndUpdate(b -> b.putCustom(LifecycleOperationMetadata.TYPE, updatedMetadata)); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTaskTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTaskTests.java index 585cf7325489..524d977318b0 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTaskTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTaskTests.java @@ -9,6 +9,7 @@ package org.elasticsearch.xpack.core.ilm; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; @@ -102,23 +103,23 @@ public class OperationModeUpdateTaskTests extends ESTestCase { currentMode, new SnapshotLifecycleStats() ); - Metadata.Builder metadata = Metadata.builder().persistentSettings(settings(IndexVersion.current()).build()); + ProjectMetadata.Builder project = ProjectMetadata.builder(randomProjectIdOrDefault()); if (metadataInstalled) { - metadata.projectCustoms( + project.customs( Map.of(IndexLifecycleMetadata.TYPE, indexLifecycleMetadata, SnapshotLifecycleMetadata.TYPE, snapshotLifecycleMetadata) ); } - ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(metadata).build(); - OperationModeUpdateTask task = OperationModeUpdateTask.ilmMode(requestMode); + ClusterState state = ClusterState.builder(ClusterName.DEFAULT).putProjectMetadata(project).build(); + OperationModeUpdateTask task = OperationModeUpdateTask.ilmMode(project.getId(), requestMode); ClusterState newState = task.execute(state); if (assertSameClusterState) { assertSame("expected the same state instance but they were different", state, newState); } else { assertThat("expected a different state instance but they were the same", state, not(equalTo(newState))); } - LifecycleOperationMetadata newMetadata = newState.metadata().getProject().custom(LifecycleOperationMetadata.TYPE); + LifecycleOperationMetadata newMetadata = newState.metadata().getProject(project.getId()).custom(LifecycleOperationMetadata.TYPE); IndexLifecycleMetadata oldMetadata = newState.metadata() - .getProject() + .getProject(project.getId()) .custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY); return Optional.ofNullable(newMetadata) .map(LifecycleOperationMetadata::getILMOperationMode) diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DocBlock.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DocBlock.java index 7d1360c5102d..dcf91bb3db7e 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DocBlock.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DocBlock.java @@ -9,6 +9,8 @@ package org.elasticsearch.compute.data; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.compute.lucene.ShardRefCounted; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.ReleasableIterator; import org.elasticsearch.core.Releasables; @@ -17,7 +19,7 @@ import java.io.IOException; /** * Wrapper around {@link DocVector} to make a valid {@link Block}. */ -public class DocBlock extends AbstractVectorBlock implements Block { +public class DocBlock extends AbstractVectorBlock implements Block, RefCounted { private final DocVector vector; @@ -96,6 +98,12 @@ public class DocBlock extends AbstractVectorBlock implements Block { private final IntVector.Builder shards; private final IntVector.Builder segments; private final IntVector.Builder docs; + private ShardRefCounted shardRefCounters = ShardRefCounted.ALWAYS_REFERENCED; + + public Builder setShardRefCounted(ShardRefCounted shardRefCounters) { + this.shardRefCounters = shardRefCounters; + return this; + } private Builder(BlockFactory blockFactory, int estimatedSize) { IntVector.Builder shards = null; @@ -183,7 +191,7 @@ public class DocBlock extends AbstractVectorBlock implements Block { shards = this.shards.build(); segments = this.segments.build(); docs = this.docs.build(); - result = new DocVector(shards, segments, docs, null); + result = new DocVector(shardRefCounters, shards, segments, docs, null); return result.asBlock(); } finally { if (result == null) { diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DocVector.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DocVector.java index 5c8d23c6a296..20ca4ed70e3f 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DocVector.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/DocVector.java @@ -10,10 +10,13 @@ package org.elasticsearch.compute.data; import org.apache.lucene.util.IntroSorter; import org.apache.lucene.util.RamUsageEstimator; import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.compute.lucene.ShardRefCounted; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.ReleasableIterator; import org.elasticsearch.core.Releasables; import java.util.Objects; +import java.util.function.Consumer; /** * {@link Vector} where each entry references a lucene document. @@ -48,8 +51,21 @@ public final class DocVector extends AbstractVector implements Vector { */ private int[] shardSegmentDocMapBackwards; - public DocVector(IntVector shards, IntVector segments, IntVector docs, Boolean singleSegmentNonDecreasing) { + private final ShardRefCounted shardRefCounters; + + public ShardRefCounted shardRefCounted() { + return shardRefCounters; + } + + public DocVector( + ShardRefCounted shardRefCounters, + IntVector shards, + IntVector segments, + IntVector docs, + Boolean singleSegmentNonDecreasing + ) { super(shards.getPositionCount(), shards.blockFactory()); + this.shardRefCounters = shardRefCounters; this.shards = shards; this.segments = segments; this.docs = docs; @@ -65,10 +81,19 @@ public final class DocVector extends AbstractVector implements Vector { ); } blockFactory().adjustBreaker(BASE_RAM_BYTES_USED); + + forEachShardRefCounter(RefCounted::mustIncRef); } - public DocVector(IntVector shards, IntVector segments, IntVector docs, int[] docMapForwards, int[] docMapBackwards) { - this(shards, segments, docs, null); + public DocVector( + ShardRefCounted shardRefCounters, + IntVector shards, + IntVector segments, + IntVector docs, + int[] docMapForwards, + int[] docMapBackwards + ) { + this(shardRefCounters, shards, segments, docs, null); this.shardSegmentDocMapForwards = docMapForwards; this.shardSegmentDocMapBackwards = docMapBackwards; } @@ -238,7 +263,7 @@ public final class DocVector extends AbstractVector implements Vector { filteredShards = shards.filter(positions); filteredSegments = segments.filter(positions); filteredDocs = docs.filter(positions); - result = new DocVector(filteredShards, filteredSegments, filteredDocs, null); + result = new DocVector(shardRefCounters, filteredShards, filteredSegments, filteredDocs, null); return result; } finally { if (result == null) { @@ -317,5 +342,20 @@ public final class DocVector extends AbstractVector implements Vector { segments, docs ); + forEachShardRefCounter(RefCounted::decRef); + } + + private void forEachShardRefCounter(Consumer consumer) { + switch (shards) { + case ConstantIntVector constantIntVector -> consumer.accept(shardRefCounters.get(constantIntVector.getInt(0))); + case ConstantNullVector ignored -> { + // Noop + } + default -> { + for (int i = 0; i < shards.getPositionCount(); i++) { + consumer.accept(shardRefCounters.get(shards.getInt(i))); + } + } + } } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneCountOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneCountOperator.java index fb733e0cb057..626f0b00f0e2 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneCountOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneCountOperator.java @@ -18,6 +18,7 @@ import org.elasticsearch.compute.data.LongBlock; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.DriverContext; import org.elasticsearch.compute.operator.SourceOperator; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.Releasables; import java.io.IOException; @@ -40,6 +41,7 @@ public class LuceneCountOperator extends LuceneOperator { private final LeafCollector leafCollector; public static class Factory extends LuceneOperator.Factory { + private final List shardRefCounters; public Factory( List contexts, @@ -58,11 +60,12 @@ public class LuceneCountOperator extends LuceneOperator { false, ScoreMode.COMPLETE_NO_SCORES ); + this.shardRefCounters = contexts; } @Override public SourceOperator get(DriverContext driverContext) { - return new LuceneCountOperator(driverContext.blockFactory(), sliceQueue, limit); + return new LuceneCountOperator(shardRefCounters, driverContext.blockFactory(), sliceQueue, limit); } @Override @@ -71,8 +74,13 @@ public class LuceneCountOperator extends LuceneOperator { } } - public LuceneCountOperator(BlockFactory blockFactory, LuceneSliceQueue sliceQueue, int limit) { - super(blockFactory, PAGE_SIZE, sliceQueue); + public LuceneCountOperator( + List shardRefCounters, + BlockFactory blockFactory, + LuceneSliceQueue sliceQueue, + int limit + ) { + super(shardRefCounters, blockFactory, PAGE_SIZE, sliceQueue); this.remainingDocs = limit; this.leafCollector = new LeafCollector() { @Override diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneMaxFactory.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneMaxFactory.java index 49a6471b3e70..82d766349ce9 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneMaxFactory.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneMaxFactory.java @@ -108,6 +108,7 @@ public final class LuceneMaxFactory extends LuceneOperator.Factory { abstract long bytesToLong(byte[] bytes); } + private final List contexts; private final String fieldName; private final NumberType numberType; @@ -130,13 +131,14 @@ public final class LuceneMaxFactory extends LuceneOperator.Factory { false, ScoreMode.COMPLETE_NO_SCORES ); + this.contexts = contexts; this.fieldName = fieldName; this.numberType = numberType; } @Override public SourceOperator get(DriverContext driverContext) { - return new LuceneMinMaxOperator(driverContext.blockFactory(), sliceQueue, fieldName, numberType, limit, Long.MIN_VALUE); + return new LuceneMinMaxOperator(contexts, driverContext.blockFactory(), sliceQueue, fieldName, numberType, limit, Long.MIN_VALUE); } @Override diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneMinFactory.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneMinFactory.java index 1abb2e7f8085..505e5cd3f0d7 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneMinFactory.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneMinFactory.java @@ -16,6 +16,7 @@ import org.elasticsearch.compute.data.Block; import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.operator.DriverContext; import org.elasticsearch.compute.operator.SourceOperator; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.search.MultiValueMode; import java.io.IOException; @@ -108,6 +109,7 @@ public final class LuceneMinFactory extends LuceneOperator.Factory { abstract long bytesToLong(byte[] bytes); } + private final List shardRefCounters; private final String fieldName; private final NumberType numberType; @@ -130,13 +132,22 @@ public final class LuceneMinFactory extends LuceneOperator.Factory { false, ScoreMode.COMPLETE_NO_SCORES ); + this.shardRefCounters = contexts; this.fieldName = fieldName; this.numberType = numberType; } @Override public SourceOperator get(DriverContext driverContext) { - return new LuceneMinMaxOperator(driverContext.blockFactory(), sliceQueue, fieldName, numberType, limit, Long.MAX_VALUE); + return new LuceneMinMaxOperator( + shardRefCounters, + driverContext.blockFactory(), + sliceQueue, + fieldName, + numberType, + limit, + Long.MAX_VALUE + ); } @Override diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneMinMaxOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneMinMaxOperator.java index d0b508f14025..b9e05567411f 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneMinMaxOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneMinMaxOperator.java @@ -20,10 +20,12 @@ import org.elasticsearch.compute.data.Block; import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.BooleanBlock; import org.elasticsearch.compute.data.Page; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.Releasables; import org.elasticsearch.search.MultiValueMode; import java.io.IOException; +import java.util.List; /** * Operator that finds the min or max value of a field using Lucene searches @@ -65,6 +67,7 @@ final class LuceneMinMaxOperator extends LuceneOperator { private final String fieldName; LuceneMinMaxOperator( + List shardRefCounters, BlockFactory blockFactory, LuceneSliceQueue sliceQueue, String fieldName, @@ -72,7 +75,7 @@ final class LuceneMinMaxOperator extends LuceneOperator { int limit, long initialResult ) { - super(blockFactory, PAGE_SIZE, sliceQueue); + super(shardRefCounters, blockFactory, PAGE_SIZE, sliceQueue); this.remainingDocs = limit; this.numberType = numberType; this.fieldName = fieldName; diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneOperator.java index 0da3915c9ad0..366715530f66 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneOperator.java @@ -25,6 +25,7 @@ import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.Operator; import org.elasticsearch.compute.operator.SourceOperator; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.TimeValue; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; @@ -52,6 +53,7 @@ public abstract class LuceneOperator extends SourceOperator { public static final int NO_LIMIT = Integer.MAX_VALUE; + protected final List shardContextCounters; protected final BlockFactory blockFactory; /** @@ -77,7 +79,14 @@ public abstract class LuceneOperator extends SourceOperator { */ long rowsEmitted; - protected LuceneOperator(BlockFactory blockFactory, int maxPageSize, LuceneSliceQueue sliceQueue) { + protected LuceneOperator( + List shardContextCounters, + BlockFactory blockFactory, + int maxPageSize, + LuceneSliceQueue sliceQueue + ) { + this.shardContextCounters = shardContextCounters; + shardContextCounters.forEach(RefCounted::mustIncRef); this.blockFactory = blockFactory; this.maxPageSize = maxPageSize; this.sliceQueue = sliceQueue; @@ -138,7 +147,12 @@ public abstract class LuceneOperator extends SourceOperator { protected abstract Page getCheckedOutput() throws IOException; @Override - public void close() {} + public final void close() { + shardContextCounters.forEach(RefCounted::decRef); + additionalClose(); + } + + protected void additionalClose() { /* Override this method to add any additional cleanup logic if needed */ } LuceneScorer getCurrentOrLoadNextScorer() { while (currentScorer == null || currentScorer.isDone()) { diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneSourceOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneSourceOperator.java index 26339d2bdb10..9fedc595641b 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneSourceOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneSourceOperator.java @@ -28,6 +28,7 @@ import org.elasticsearch.compute.lucene.LuceneSliceQueue.PartitioningStrategy; import org.elasticsearch.compute.operator.DriverContext; import org.elasticsearch.compute.operator.Limiter; import org.elasticsearch.compute.operator.SourceOperator; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.Releasables; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; @@ -59,7 +60,7 @@ public class LuceneSourceOperator extends LuceneOperator { private final int minPageSize; public static class Factory extends LuceneOperator.Factory { - + private final List contexts; private final int maxPageSize; private final Limiter limiter; @@ -82,6 +83,7 @@ public class LuceneSourceOperator extends LuceneOperator { needsScore, needsScore ? COMPLETE : COMPLETE_NO_SCORES ); + this.contexts = contexts; this.maxPageSize = maxPageSize; // TODO: use a single limiter for multiple stage execution this.limiter = limit == NO_LIMIT ? Limiter.NO_LIMIT : new Limiter(limit); @@ -89,7 +91,7 @@ public class LuceneSourceOperator extends LuceneOperator { @Override public SourceOperator get(DriverContext driverContext) { - return new LuceneSourceOperator(driverContext.blockFactory(), maxPageSize, sliceQueue, limit, limiter, needsScore); + return new LuceneSourceOperator(contexts, driverContext.blockFactory(), maxPageSize, sliceQueue, limit, limiter, needsScore); } public int maxPageSize() { @@ -216,6 +218,7 @@ public class LuceneSourceOperator extends LuceneOperator { @SuppressWarnings("this-escape") public LuceneSourceOperator( + List shardContextCounters, BlockFactory blockFactory, int maxPageSize, LuceneSliceQueue sliceQueue, @@ -223,7 +226,7 @@ public class LuceneSourceOperator extends LuceneOperator { Limiter limiter, boolean needsScore ) { - super(blockFactory, maxPageSize, sliceQueue); + super(shardContextCounters, blockFactory, maxPageSize, sliceQueue); this.minPageSize = Math.max(1, maxPageSize / 2); this.remainingDocs = limit; this.limiter = limiter; @@ -324,12 +327,14 @@ public class LuceneSourceOperator extends LuceneOperator { Block[] blocks = new Block[1 + (scoreBuilder == null ? 0 : 1) + scorer.tags().size()]; currentPagePos -= discardedDocs; try { - shard = blockFactory.newConstantIntVector(scorer.shardContext().index(), currentPagePos); + int shardId = scorer.shardContext().index(); + shard = blockFactory.newConstantIntVector(shardId, currentPagePos); leaf = blockFactory.newConstantIntVector(scorer.leafReaderContext().ord, currentPagePos); docs = buildDocsVector(currentPagePos); docsBuilder = blockFactory.newIntVectorBuilder(Math.min(remainingDocs, maxPageSize)); int b = 0; - blocks[b++] = new DocVector(shard, leaf, docs, true).asBlock(); + ShardRefCounted refCounted = ShardRefCounted.single(shardId, shardContextCounters.get(shardId)); + blocks[b++] = new DocVector(refCounted, shard, leaf, docs, true).asBlock(); shard = null; leaf = null; docs = null; @@ -387,7 +392,7 @@ public class LuceneSourceOperator extends LuceneOperator { } @Override - public void close() { + public void additionalClose() { Releasables.close(docsBuilder, scoreBuilder); } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneTopNSourceOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneTopNSourceOperator.java index 5457caa25e15..d93a5493a3ab 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneTopNSourceOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneTopNSourceOperator.java @@ -53,6 +53,7 @@ import static org.apache.lucene.search.ScoreMode.TOP_DOCS_WITH_SCORES; public final class LuceneTopNSourceOperator extends LuceneOperator { public static class Factory extends LuceneOperator.Factory { + private final List contexts; private final int maxPageSize; private final List> sorts; @@ -76,13 +77,14 @@ public final class LuceneTopNSourceOperator extends LuceneOperator { needsScore, needsScore ? TOP_DOCS_WITH_SCORES : TOP_DOCS ); + this.contexts = contexts; this.maxPageSize = maxPageSize; this.sorts = sorts; } @Override public SourceOperator get(DriverContext driverContext) { - return new LuceneTopNSourceOperator(driverContext.blockFactory(), maxPageSize, sorts, limit, sliceQueue, needsScore); + return new LuceneTopNSourceOperator(contexts, driverContext.blockFactory(), maxPageSize, sorts, limit, sliceQueue, needsScore); } public int maxPageSize() { @@ -116,11 +118,13 @@ public final class LuceneTopNSourceOperator extends LuceneOperator { private int offset = 0; private PerShardCollector perShardCollector; + private final List contexts; private final List> sorts; private final int limit; private final boolean needsScore; public LuceneTopNSourceOperator( + List contexts, BlockFactory blockFactory, int maxPageSize, List> sorts, @@ -128,7 +132,8 @@ public final class LuceneTopNSourceOperator extends LuceneOperator { LuceneSliceQueue sliceQueue, boolean needsScore ) { - super(blockFactory, maxPageSize, sliceQueue); + super(contexts, blockFactory, maxPageSize, sliceQueue); + this.contexts = contexts; this.sorts = sorts; this.limit = limit; this.needsScore = needsScore; @@ -236,10 +241,12 @@ public final class LuceneTopNSourceOperator extends LuceneOperator { } } - shard = blockFactory.newConstantIntBlockWith(perShardCollector.shardContext.index(), size); + int shardId = perShardCollector.shardContext.index(); + shard = blockFactory.newConstantIntBlockWith(shardId, size); segments = currentSegmentBuilder.build(); docs = currentDocsBuilder.build(); - docBlock = new DocVector(shard.asVector(), segments, docs, null).asBlock(); + ShardRefCounted shardRefCounted = ShardRefCounted.single(shardId, contexts.get(shardId)); + docBlock = new DocVector(shardRefCounted, shard.asVector(), segments, docs, null).asBlock(); shard = null; segments = null; docs = null; diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ShardContext.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ShardContext.java index 8d1656899617..d20a002407be 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ShardContext.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ShardContext.java @@ -9,6 +9,7 @@ package org.elasticsearch.compute.lucene; import org.apache.lucene.search.IndexSearcher; import org.elasticsearch.compute.data.Block; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.index.mapper.BlockLoader; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.SourceLoader; @@ -22,7 +23,7 @@ import java.util.Optional; /** * Context of each shard we're operating against. */ -public interface ShardContext { +public interface ShardContext extends RefCounted { /** * The index of this shard in the list of shards being processed. */ diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ShardRefCounted.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ShardRefCounted.java new file mode 100644 index 000000000000..e63d4ab0641f --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ShardRefCounted.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.lucene; + +import org.elasticsearch.core.RefCounted; + +import java.util.List; + +/** Manages reference counting for {@link ShardContext}. */ +public interface ShardRefCounted { + /** + * @param shardId The shard index used by {@link org.elasticsearch.compute.data.DocVector}. + * @return the {@link RefCounted} for the given shard. In production, this will almost always be a {@link ShardContext}. + */ + RefCounted get(int shardId); + + static ShardRefCounted fromList(List refCounters) { + return shardId -> refCounters.get(shardId); + } + + static ShardRefCounted fromShardContext(ShardContext shardContext) { + return single(shardContext.index(), shardContext); + } + + static ShardRefCounted single(int index, RefCounted refCounted) { + return shardId -> { + if (shardId != index) { + throw new IllegalArgumentException("Invalid shardId: " + shardId + ", expected: " + index); + } + return refCounted; + }; + } + + ShardRefCounted ALWAYS_REFERENCED = shardId -> RefCounted.ALWAYS_REFERENCED; +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/TimeSeriesSourceOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/TimeSeriesSourceOperator.java index d0f1a5ee5fcd..089846f9939a 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/TimeSeriesSourceOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/TimeSeriesSourceOperator.java @@ -36,12 +36,12 @@ import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; import java.io.UncheckedIOException; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; public final class TimeSeriesSourceOperator extends LuceneOperator { - private final int maxPageSize; private final BlockFactory blockFactory; private final LuceneSliceQueue sliceQueue; @@ -55,8 +55,14 @@ public final class TimeSeriesSourceOperator extends LuceneOperator { private DocIdCollector docCollector; private long tsidsLoaded; - TimeSeriesSourceOperator(BlockFactory blockFactory, LuceneSliceQueue sliceQueue, int maxPageSize, int limit) { - super(blockFactory, maxPageSize, sliceQueue); + TimeSeriesSourceOperator( + List contexts, + BlockFactory blockFactory, + LuceneSliceQueue sliceQueue, + int maxPageSize, + int limit + ) { + super(contexts, blockFactory, maxPageSize, sliceQueue); this.maxPageSize = maxPageSize; this.blockFactory = blockFactory; this.remainingDocs = limit; @@ -131,7 +137,7 @@ public final class TimeSeriesSourceOperator extends LuceneOperator { } @Override - public void close() { + public void additionalClose() { Releasables.closeExpectNoException(timestampsBuilder, tsHashesBuilder, docCollector); } @@ -382,7 +388,7 @@ public final class TimeSeriesSourceOperator extends LuceneOperator { segments = segmentsBuilder.build(); segmentsBuilder = null; shards = blockFactory.newConstantIntVector(shardContext.index(), docs.getPositionCount()); - docVector = new DocVector(shards, segments, docs, segments.isConstant()); + docVector = new DocVector(ShardRefCounted.fromShardContext(shardContext), shards, segments, docs, segments.isConstant()); return docVector; } finally { if (docVector == null) { diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/TimeSeriesSourceOperatorFactory.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/TimeSeriesSourceOperatorFactory.java index 7ee13b3e6e0f..97286761b7bc 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/TimeSeriesSourceOperatorFactory.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/TimeSeriesSourceOperatorFactory.java @@ -27,7 +27,7 @@ import java.util.function.Function; * in order to read tsdb indices in parallel. */ public class TimeSeriesSourceOperatorFactory extends LuceneOperator.Factory { - + private final List contexts; private final int maxPageSize; private TimeSeriesSourceOperatorFactory( @@ -47,12 +47,13 @@ public class TimeSeriesSourceOperatorFactory extends LuceneOperator.Factory { false, ScoreMode.COMPLETE_NO_SCORES ); + this.contexts = contexts; this.maxPageSize = maxPageSize; } @Override public SourceOperator get(DriverContext driverContext) { - return new TimeSeriesSourceOperator(driverContext.blockFactory(), sliceQueue, maxPageSize, limit); + return new TimeSeriesSourceOperator(contexts, driverContext.blockFactory(), sliceQueue, maxPageSize, limit); } @Override diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperator.java index d9f56340b458..500bef6d2a59 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperator.java @@ -11,6 +11,7 @@ import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.SortedDocValues; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.TransportVersion; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; @@ -47,6 +48,7 @@ import java.util.function.IntFunction; import java.util.function.Supplier; import static org.elasticsearch.TransportVersions.ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED; +import static org.elasticsearch.TransportVersions.ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED_8_19; /** * Operator that extracts doc_values from a Lucene index out of pages that have been produced by {@link LuceneSourceOperator} @@ -529,7 +531,7 @@ public class ValuesSourceReaderOperator extends AbstractPageMappingOperator { } private LeafReaderContext ctx(int shard, int segment) { - return shardContexts.get(shard).reader.leaves().get(segment); + return shardContexts.get(shard).reader().leaves().get(segment); } @Override @@ -617,18 +619,23 @@ public class ValuesSourceReaderOperator extends AbstractPageMappingOperator { Status(StreamInput in) throws IOException { super(in); readersBuilt = in.readOrderedMap(StreamInput::readString, StreamInput::readVInt); - valuesLoaded = in.getTransportVersion().onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED) ? in.readVLong() : 0; + valuesLoaded = supportsValuesLoaded(in.getTransportVersion()) ? in.readVLong() : 0; } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeMap(readersBuilt, StreamOutput::writeVInt); - if (out.getTransportVersion().onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED)) { + if (supportsValuesLoaded(out.getTransportVersion())) { out.writeVLong(valuesLoaded); } } + private static boolean supportsValuesLoaded(TransportVersion version) { + return version.onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED) + || version.isPatchFrom(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED_8_19); + } + @Override public String getWriteableName() { return ENTRY.name; diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/Driver.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/Driver.java index ef7eef4c111b..775ac401cd91 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/Driver.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/Driver.java @@ -24,6 +24,7 @@ import org.elasticsearch.tasks.TaskCancelledException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.ListIterator; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -75,7 +76,7 @@ public class Driver implements Releasable, Describable { private final long startNanos; private final DriverContext driverContext; private final Supplier description; - private final List activeOperators; + private List activeOperators; private final List statusOfCompletedOperators = new ArrayList<>(); private final Releasable releasable; private final long statusNanos; @@ -184,7 +185,7 @@ public class Driver implements Releasable, Describable { assert driverContext.assertBeginRunLoop(); isBlocked = runSingleLoopIteration(); } catch (DriverEarlyTerminationException unused) { - closeEarlyFinishedOperators(); + closeEarlyFinishedOperators(activeOperators.listIterator(activeOperators.size())); assert isFinished() : "not finished after early termination"; } finally { assert driverContext.assertEndRunLoop(); @@ -251,9 +252,13 @@ public class Driver implements Releasable, Describable { driverContext.checkForEarlyTermination(); boolean movedPage = false; - for (int i = 0; i < activeOperators.size() - 1; i++) { - Operator op = activeOperators.get(i); - Operator nextOp = activeOperators.get(i + 1); + ListIterator iterator = activeOperators.listIterator(); + while (iterator.hasNext()) { + Operator op = iterator.next(); + if (iterator.hasNext() == false) { + break; + } + Operator nextOp = activeOperators.get(iterator.nextIndex()); // skip blocked operator if (op.isBlocked().listener().isDone() == false) { @@ -262,6 +267,7 @@ public class Driver implements Releasable, Describable { if (op.isFinished() == false && nextOp.needsInput()) { driverContext.checkForEarlyTermination(); + assert nextOp.isFinished() == false : "next operator should not be finished yet: " + nextOp; Page page = op.getOutput(); if (page == null) { // No result, just move to the next iteration @@ -283,11 +289,15 @@ public class Driver implements Releasable, Describable { if (op.isFinished()) { driverContext.checkForEarlyTermination(); - nextOp.finish(); + var originalIndex = iterator.previousIndex(); + var index = closeEarlyFinishedOperators(iterator); + if (index >= 0) { + iterator = new ArrayList<>(activeOperators).listIterator(originalIndex - index); + } } } - closeEarlyFinishedOperators(); + closeEarlyFinishedOperators(activeOperators.listIterator(activeOperators.size())); if (movedPage == false) { return oneOf( @@ -300,22 +310,24 @@ public class Driver implements Releasable, Describable { return Operator.NOT_BLOCKED; } - private void closeEarlyFinishedOperators() { - for (int index = activeOperators.size() - 1; index >= 0; index--) { - if (activeOperators.get(index).isFinished()) { + // Returns the index of the last operator that was closed, -1 if no operator was closed. + private int closeEarlyFinishedOperators(ListIterator operators) { + var iterator = activeOperators.listIterator(operators.nextIndex()); + while (iterator.hasPrevious()) { + if (iterator.previous().isFinished()) { + var index = iterator.nextIndex(); /* * Close and remove this operator and all source operators in the * most paranoid possible way. Closing operators shouldn't throw, * but if it does, this will make sure we don't try to close any * that succeed twice. */ - List finishedOperators = this.activeOperators.subList(0, index + 1); - Iterator itr = finishedOperators.iterator(); - while (itr.hasNext()) { - Operator op = itr.next(); + Iterator finishedOperators = this.activeOperators.subList(0, index + 1).iterator(); + while (finishedOperators.hasNext()) { + Operator op = finishedOperators.next(); statusOfCompletedOperators.add(new OperatorStatus(op.toString(), op.status())); op.close(); - itr.remove(); + finishedOperators.remove(); } // Finish the next operator, which is now the first operator. @@ -323,9 +335,10 @@ public class Driver implements Releasable, Describable { Operator newRootOperator = activeOperators.get(0); newRootOperator.finish(); } - break; + return index; } } + return -1; } public void cancel(String reason) { diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/OrdinalsGroupingOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/OrdinalsGroupingOperator.java index c030b329dd2d..9c15b0f3fc7d 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/OrdinalsGroupingOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/OrdinalsGroupingOperator.java @@ -33,6 +33,7 @@ import org.elasticsearch.compute.data.IntBlock; import org.elasticsearch.compute.data.IntVector; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.lucene.ValuesSourceReaderOperator; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.Releasables; import org.elasticsearch.index.mapper.BlockLoader; @@ -136,6 +137,7 @@ public class OrdinalsGroupingOperator implements Operator { requireNonNull(page, "page is null"); DocVector docVector = page.getBlock(docChannel).asVector(); final int shardIndex = docVector.shards().getInt(0); + RefCounted shardRefCounter = docVector.shardRefCounted().get(shardIndex); final var blockLoader = blockLoaders.apply(shardIndex); boolean pagePassed = false; try { @@ -150,7 +152,8 @@ public class OrdinalsGroupingOperator implements Operator { driverContext.blockFactory(), this::createGroupingAggregators, () -> blockLoader.ordinals(shardContexts.get(k.shardIndex).reader().leaves().get(k.segmentIndex)), - driverContext.bigArrays() + driverContext.bigArrays(), + shardRefCounter ); } catch (IOException e) { throw new UncheckedIOException(e); @@ -343,15 +346,19 @@ public class OrdinalsGroupingOperator implements Operator { private final List aggregators; private final CheckedSupplier docValuesSupplier; private final BitArray visitedOrds; + private final RefCounted shardRefCounted; private BlockOrdinalsReader currentReader; OrdinalSegmentAggregator( BlockFactory blockFactory, Supplier> aggregatorsSupplier, CheckedSupplier docValuesSupplier, - BigArrays bigArrays + BigArrays bigArrays, + RefCounted shardRefCounted ) throws IOException { boolean success = false; + this.shardRefCounted = shardRefCounted; + this.shardRefCounted.mustIncRef(); List groupingAggregators = null; BitArray bitArray = null; try { @@ -368,6 +375,9 @@ public class OrdinalsGroupingOperator implements Operator { if (success == false) { if (bitArray != null) Releasables.close(bitArray); if (groupingAggregators != null) Releasables.close(groupingAggregators); + // There is no danger of double decRef here, since this decRef is called only if the constructor throws, so it would be + // impossible to call close on the instance. + shardRefCounted.decRef(); } } } @@ -447,7 +457,7 @@ public class OrdinalsGroupingOperator implements Operator { @Override public void close() { - Releasables.close(visitedOrds, () -> Releasables.close(aggregators)); + Releasables.close(visitedOrds, () -> Releasables.close(aggregators), Releasables.fromRefCounted(shardRefCounted)); } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/lookup/EnrichQuerySourceOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/lookup/EnrichQuerySourceOperator.java index 0cd34d2ad406..214e7197b2c8 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/lookup/EnrichQuerySourceOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/lookup/EnrichQuerySourceOperator.java @@ -21,6 +21,8 @@ import org.elasticsearch.compute.data.DocVector; import org.elasticsearch.compute.data.IntBlock; import org.elasticsearch.compute.data.IntVector; import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.lucene.ShardContext; +import org.elasticsearch.compute.lucene.ShardRefCounted; import org.elasticsearch.compute.operator.SourceOperator; import org.elasticsearch.compute.operator.Warnings; import org.elasticsearch.core.Releasables; @@ -37,6 +39,7 @@ public final class EnrichQuerySourceOperator extends SourceOperator { private final BlockFactory blockFactory; private final QueryList queryList; private int queryPosition = -1; + private final ShardContext shardContext; private final IndexReader indexReader; private final IndexSearcher searcher; private final Warnings warnings; @@ -49,14 +52,16 @@ public final class EnrichQuerySourceOperator extends SourceOperator { BlockFactory blockFactory, int maxPageSize, QueryList queryList, - IndexReader indexReader, + ShardContext shardContext, Warnings warnings ) { this.blockFactory = blockFactory; this.maxPageSize = maxPageSize; this.queryList = queryList; - this.indexReader = indexReader; - this.searcher = new IndexSearcher(indexReader); + this.shardContext = shardContext; + this.shardContext.incRef(); + this.searcher = shardContext.searcher(); + this.indexReader = searcher.getIndexReader(); this.warnings = warnings; } @@ -142,7 +147,10 @@ public final class EnrichQuerySourceOperator extends SourceOperator { segmentsVector = segmentsBuilder.build(); } docsVector = docsBuilder.build(); - page = new Page(new DocVector(shardsVector, segmentsVector, docsVector, null).asBlock(), positionsVector.asBlock()); + page = new Page( + new DocVector(ShardRefCounted.fromShardContext(shardContext), shardsVector, segmentsVector, docsVector, null).asBlock(), + positionsVector.asBlock() + ); } finally { if (page == null) { Releasables.close(positionsBuilder, segmentsVector, docsBuilder, positionsVector, shardsVector, docsVector); @@ -185,6 +193,6 @@ public final class EnrichQuerySourceOperator extends SourceOperator { @Override public void close() { - + this.shardContext.decRef(); } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ResultBuilder.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ResultBuilder.java index 6ad550c439ec..c3da40254c09 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ResultBuilder.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ResultBuilder.java @@ -11,6 +11,8 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.compute.data.Block; import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.Releasable; /** @@ -33,6 +35,12 @@ interface ResultBuilder extends Releasable { */ void decodeValue(BytesRef values); + /** + * Sets the RefCounted value, which was extracted by {@link ValueExtractor#getRefCountedForShard(int)}. By default, this is a no-op, + * since most builders do not the shard ref counter. + */ + default void setNextRefCounted(@Nullable RefCounted nextRefCounted) { /* no-op */ } + /** * Build the result block. */ diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ResultBuilderForDoc.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ResultBuilderForDoc.java index 779e1dece2b3..cb659e8921aa 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ResultBuilderForDoc.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ResultBuilderForDoc.java @@ -12,14 +12,22 @@ import org.elasticsearch.compute.data.Block; import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.DocVector; import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.lucene.ShardRefCounted; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.Releasables; +import java.util.HashMap; +import java.util.Map; + class ResultBuilderForDoc implements ResultBuilder { private final BlockFactory blockFactory; private final int[] shards; private final int[] segments; private final int[] docs; private int position; + private @Nullable RefCounted nextRefCounted; + private final Map refCounted = new HashMap<>(); ResultBuilderForDoc(BlockFactory blockFactory, int positions) { // TODO use fixed length builders @@ -34,12 +42,24 @@ class ResultBuilderForDoc implements ResultBuilder { throw new AssertionError("_doc can't be a key"); } + @Override + public void setNextRefCounted(RefCounted nextRefCounted) { + this.nextRefCounted = nextRefCounted; + // Since rows can be closed before build is called, we need to increment the ref count to ensure the shard context isn't closed. + this.nextRefCounted.mustIncRef(); + } + @Override public void decodeValue(BytesRef values) { + if (nextRefCounted == null) { + throw new IllegalStateException("setNextRefCounted must be set before each decodeValue call"); + } shards[position] = TopNEncoder.DEFAULT_UNSORTABLE.decodeInt(values); segments[position] = TopNEncoder.DEFAULT_UNSORTABLE.decodeInt(values); docs[position] = TopNEncoder.DEFAULT_UNSORTABLE.decodeInt(values); + refCounted.putIfAbsent(shards[position], nextRefCounted); position++; + nextRefCounted = null; } @Override @@ -51,16 +71,26 @@ class ResultBuilderForDoc implements ResultBuilder { shardsVector = blockFactory.newIntArrayVector(shards, position); segmentsVector = blockFactory.newIntArrayVector(segments, position); var docsVector = blockFactory.newIntArrayVector(docs, position); - var docsBlock = new DocVector(shardsVector, segmentsVector, docsVector, null).asBlock(); + var docsBlock = new DocVector(new ShardRefCountedMap(refCounted), shardsVector, segmentsVector, docsVector, null).asBlock(); success = true; return docsBlock; } finally { + // The DocVector constructor already incremented the relevant RefCounted, so we can now decrement them since we incremented them + // in setNextRefCounted. + refCounted.values().forEach(RefCounted::decRef); if (success == false) { Releasables.closeExpectNoException(shardsVector, segmentsVector); } } } + private record ShardRefCountedMap(Map refCounters) implements ShardRefCounted { + @Override + public RefCounted get(int shardId) { + return refCounters.get(shardId); + } + } + @Override public String toString() { return "ValueExtractorForDoc"; diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/TopNOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/TopNOperator.java index 0489be58fade..fdf88cf8f55b 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/TopNOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/TopNOperator.java @@ -15,11 +15,14 @@ import org.elasticsearch.common.breaker.CircuitBreaker; import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.compute.data.Block; import org.elasticsearch.compute.data.BlockFactory; +import org.elasticsearch.compute.data.DocVector; import org.elasticsearch.compute.data.ElementType; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; import org.elasticsearch.compute.operator.DriverContext; import org.elasticsearch.compute.operator.Operator; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.Releasables; @@ -71,6 +74,21 @@ public class TopNOperator implements Operator, Accountable { */ final BreakingBytesRefBuilder values; + /** + * Reference counter for the shard this row belongs to, used for rows containing a {@link DocVector} to ensure that the shard + * context before we build the final result. + */ + @Nullable + RefCounted shardRefCounter; + + void setShardRefCountersAndShard(RefCounted shardRefCounter) { + if (this.shardRefCounter != null) { + this.shardRefCounter.decRef(); + } + this.shardRefCounter = shardRefCounter; + this.shardRefCounter.mustIncRef(); + } + Row(CircuitBreaker breaker, List sortOrders, int preAllocatedKeysSize, int preAllocatedValueSize) { boolean success = false; try { @@ -92,8 +110,16 @@ public class TopNOperator implements Operator, Accountable { @Override public void close() { + clearRefCounters(); Releasables.closeExpectNoException(keys, values, bytesOrder); } + + public void clearRefCounters() { + if (shardRefCounter != null) { + shardRefCounter.decRef(); + } + shardRefCounter = null; + } } static final class BytesOrder implements Releasable, Accountable { @@ -174,7 +200,7 @@ public class TopNOperator implements Operator, Accountable { */ void row(int position, Row destination) { writeKey(position, destination); - writeValues(position, destination.values); + writeValues(position, destination); } private void writeKey(int position, Row row) { @@ -187,9 +213,13 @@ public class TopNOperator implements Operator, Accountable { } } - private void writeValues(int position, BreakingBytesRefBuilder values) { + private void writeValues(int position, Row destination) { for (ValueExtractor e : valueExtractors) { - e.writeValue(values, position); + var refCounted = e.getRefCountedForShard(position); + if (refCounted != null) { + destination.setShardRefCountersAndShard(refCounted); + } + e.writeValue(destination.values, position); } } } @@ -376,6 +406,7 @@ public class TopNOperator implements Operator, Accountable { } else { spare.keys.clear(); spare.values.clear(); + spare.clearRefCounters(); } rowFiller.row(i, spare); @@ -456,6 +487,7 @@ public class TopNOperator implements Operator, Accountable { BytesRef values = row.values.bytesRefView(); for (ResultBuilder builder : builders) { + builder.setNextRefCounted(row.shardRefCounter); builder.decodeValue(values); } if (values.length != 0) { @@ -463,7 +495,6 @@ public class TopNOperator implements Operator, Accountable { } list.set(i, null); - row.close(); p++; if (p == size) { @@ -481,6 +512,8 @@ public class TopNOperator implements Operator, Accountable { Releasables.closeExpectNoException(builders); builders = null; } + // It's important to close the row only after we build the new block, so we don't pre-release any shard counter. + row.close(); } assert builders == null; success = true; diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ValueExtractor.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ValueExtractor.java index ccf36a08c280..b6f3a1198d1f 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ValueExtractor.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ValueExtractor.java @@ -18,6 +18,8 @@ import org.elasticsearch.compute.data.FloatBlock; import org.elasticsearch.compute.data.IntBlock; import org.elasticsearch.compute.data.LongBlock; import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.RefCounted; /** * Extracts values into a {@link BreakingBytesRefBuilder}. @@ -25,6 +27,15 @@ import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; interface ValueExtractor { void writeValue(BreakingBytesRefBuilder values, int position); + /** + * This should return a non-null value if the row is supposed to hold a temporary reference to a shard (including incrementing and + * decrementing it) in between encoding and decoding the row values. + */ + @Nullable + default RefCounted getRefCountedForShard(int position) { + return null; + } + static ValueExtractor extractorFor(ElementType elementType, TopNEncoder encoder, boolean inKey, Block block) { if (false == (elementType == block.elementType() || ElementType.NULL == block.elementType())) { // While this maybe should be an IllegalArgumentException, it's important to throw an exception that causes a 500 response. diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ValueExtractorForDoc.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ValueExtractorForDoc.java index b6fc30e221cd..e0d7cffabdfb 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ValueExtractorForDoc.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/ValueExtractorForDoc.java @@ -9,15 +9,25 @@ package org.elasticsearch.compute.operator.topn; import org.elasticsearch.compute.data.DocVector; import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; +import org.elasticsearch.core.RefCounted; class ValueExtractorForDoc implements ValueExtractor { private final DocVector vector; + @Override + public RefCounted getRefCountedForShard(int position) { + return vector().shardRefCounted().get(vector().shards().getInt(position)); + } + ValueExtractorForDoc(TopNEncoder encoder, DocVector vector) { assert encoder == TopNEncoder.DEFAULT_UNSORTABLE; this.vector = vector; } + DocVector vector() { + return vector; + } + @Override public void writeValue(BreakingBytesRefBuilder values, int position) { TopNEncoder.DEFAULT_UNSORTABLE.encodeInt(vector.shards().getInt(position), values); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/CountDistinctLongGroupingAggregatorFunctionTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/CountDistinctLongGroupingAggregatorFunctionTests.java index db08fd0428e7..d3b374a4d487 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/CountDistinctLongGroupingAggregatorFunctionTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/CountDistinctLongGroupingAggregatorFunctionTests.java @@ -14,7 +14,7 @@ import org.elasticsearch.compute.data.LongBlock; import org.elasticsearch.compute.data.LongVector; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.SourceOperator; -import org.elasticsearch.compute.operator.TupleBlockSourceOperator; +import org.elasticsearch.compute.operator.TupleLongLongBlockSourceOperator; import org.elasticsearch.core.Tuple; import java.util.List; @@ -36,7 +36,7 @@ public class CountDistinctLongGroupingAggregatorFunctionTests extends GroupingAg @Override protected SourceOperator simpleInput(BlockFactory blockFactory, int size) { - return new TupleBlockSourceOperator( + return new TupleLongLongBlockSourceOperator( blockFactory, LongStream.range(0, size).mapToObj(l -> Tuple.tuple(randomGroupId(size), randomLongBetween(0, 100_000))) ); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/CountGroupingAggregatorFunctionTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/CountGroupingAggregatorFunctionTests.java index 06a066658629..d0dcf39029d8 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/CountGroupingAggregatorFunctionTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/CountGroupingAggregatorFunctionTests.java @@ -15,7 +15,7 @@ import org.elasticsearch.compute.data.LongVector; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.LongDoubleTupleBlockSourceOperator; import org.elasticsearch.compute.operator.SourceOperator; -import org.elasticsearch.compute.operator.TupleBlockSourceOperator; +import org.elasticsearch.compute.operator.TupleLongLongBlockSourceOperator; import org.elasticsearch.core.Tuple; import java.util.List; @@ -37,7 +37,7 @@ public class CountGroupingAggregatorFunctionTests extends GroupingAggregatorFunc @Override protected SourceOperator simpleInput(BlockFactory blockFactory, int size) { if (randomBoolean()) { - return new TupleBlockSourceOperator( + return new TupleLongLongBlockSourceOperator( blockFactory, LongStream.range(0, size).mapToObj(l -> Tuple.tuple(randomLongBetween(0, 4), randomLong())) ); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MaxLongGroupingAggregatorFunctionTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MaxLongGroupingAggregatorFunctionTests.java index 6d6c37fb306a..b6223e36597d 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MaxLongGroupingAggregatorFunctionTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MaxLongGroupingAggregatorFunctionTests.java @@ -12,7 +12,7 @@ import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.LongBlock; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.SourceOperator; -import org.elasticsearch.compute.operator.TupleBlockSourceOperator; +import org.elasticsearch.compute.operator.TupleLongLongBlockSourceOperator; import org.elasticsearch.core.Tuple; import java.util.List; @@ -34,7 +34,7 @@ public class MaxLongGroupingAggregatorFunctionTests extends GroupingAggregatorFu @Override protected SourceOperator simpleInput(BlockFactory blockFactory, int size) { - return new TupleBlockSourceOperator( + return new TupleLongLongBlockSourceOperator( blockFactory, LongStream.range(0, size).mapToObj(l -> Tuple.tuple(randomLongBetween(0, 4), randomLong())) ); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MedianAbsoluteDeviationLongGroupingAggregatorFunctionTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MedianAbsoluteDeviationLongGroupingAggregatorFunctionTests.java index 55895ceadd52..fbd41d8ab06b 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MedianAbsoluteDeviationLongGroupingAggregatorFunctionTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MedianAbsoluteDeviationLongGroupingAggregatorFunctionTests.java @@ -13,7 +13,7 @@ import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.DoubleBlock; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.SourceOperator; -import org.elasticsearch.compute.operator.TupleBlockSourceOperator; +import org.elasticsearch.compute.operator.TupleLongLongBlockSourceOperator; import org.elasticsearch.core.Tuple; import java.util.ArrayList; @@ -42,7 +42,7 @@ public class MedianAbsoluteDeviationLongGroupingAggregatorFunctionTests extends values.add(Tuple.tuple((long) i, v)); } } - return new TupleBlockSourceOperator(blockFactory, values.subList(0, Math.min(values.size(), end))); + return new TupleLongLongBlockSourceOperator(blockFactory, values.subList(0, Math.min(values.size(), end))); } @Override diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MinLongGroupingAggregatorFunctionTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MinLongGroupingAggregatorFunctionTests.java index da8a63a42920..82095553fdd5 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MinLongGroupingAggregatorFunctionTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/MinLongGroupingAggregatorFunctionTests.java @@ -12,7 +12,7 @@ import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.LongBlock; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.SourceOperator; -import org.elasticsearch.compute.operator.TupleBlockSourceOperator; +import org.elasticsearch.compute.operator.TupleLongLongBlockSourceOperator; import org.elasticsearch.core.Tuple; import java.util.List; @@ -34,7 +34,7 @@ public class MinLongGroupingAggregatorFunctionTests extends GroupingAggregatorFu @Override protected SourceOperator simpleInput(BlockFactory blockFactory, int size) { - return new TupleBlockSourceOperator( + return new TupleLongLongBlockSourceOperator( blockFactory, LongStream.range(0, size).mapToObj(l -> Tuple.tuple(randomLongBetween(0, 4), randomLong())) ); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/PercentileLongGroupingAggregatorFunctionTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/PercentileLongGroupingAggregatorFunctionTests.java index 55065129df0c..74f6b20a9f9f 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/PercentileLongGroupingAggregatorFunctionTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/PercentileLongGroupingAggregatorFunctionTests.java @@ -13,7 +13,7 @@ import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.DoubleBlock; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.SourceOperator; -import org.elasticsearch.compute.operator.TupleBlockSourceOperator; +import org.elasticsearch.compute.operator.TupleLongLongBlockSourceOperator; import org.elasticsearch.core.Tuple; import org.elasticsearch.search.aggregations.metrics.TDigestState; import org.junit.Before; @@ -45,7 +45,7 @@ public class PercentileLongGroupingAggregatorFunctionTests extends GroupingAggre @Override protected SourceOperator simpleInput(BlockFactory blockFactory, int size) { long max = randomLongBetween(1, Long.MAX_VALUE / size / 5); - return new TupleBlockSourceOperator( + return new TupleLongLongBlockSourceOperator( blockFactory, LongStream.range(0, size).mapToObj(l -> Tuple.tuple(randomLongBetween(0, 4), randomLongBetween(-0, max))) ); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/SumLongGroupingAggregatorFunctionTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/SumLongGroupingAggregatorFunctionTests.java index f289686f8e84..f39df0071aab 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/SumLongGroupingAggregatorFunctionTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/SumLongGroupingAggregatorFunctionTests.java @@ -12,7 +12,7 @@ import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.LongBlock; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.SourceOperator; -import org.elasticsearch.compute.operator.TupleBlockSourceOperator; +import org.elasticsearch.compute.operator.TupleLongLongBlockSourceOperator; import org.elasticsearch.core.Tuple; import java.util.List; @@ -34,7 +34,7 @@ public class SumLongGroupingAggregatorFunctionTests extends GroupingAggregatorFu @Override protected SourceOperator simpleInput(BlockFactory blockFactory, int size) { long max = randomLongBetween(1, Long.MAX_VALUE / size / 5); - return new TupleBlockSourceOperator( + return new TupleLongLongBlockSourceOperator( blockFactory, LongStream.range(0, size).mapToObj(l -> Tuple.tuple(randomLongBetween(0, 4), randomLongBetween(-max, max))) ); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/ValuesLongGroupingAggregatorFunctionTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/ValuesLongGroupingAggregatorFunctionTests.java index 3180ac53f6ef..bb00541f24fe 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/ValuesLongGroupingAggregatorFunctionTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/ValuesLongGroupingAggregatorFunctionTests.java @@ -12,7 +12,7 @@ import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.BlockUtils; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.SourceOperator; -import org.elasticsearch.compute.operator.TupleBlockSourceOperator; +import org.elasticsearch.compute.operator.TupleLongLongBlockSourceOperator; import org.elasticsearch.core.Tuple; import java.util.Arrays; @@ -38,7 +38,7 @@ public class ValuesLongGroupingAggregatorFunctionTests extends GroupingAggregato @Override protected SourceOperator simpleInput(BlockFactory blockFactory, int size) { - return new TupleBlockSourceOperator( + return new TupleLongLongBlockSourceOperator( blockFactory, LongStream.range(0, size).mapToObj(l -> Tuple.tuple(randomLongBetween(0, 4), randomLong())) ); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/data/BasicBlockTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/data/BasicBlockTests.java index 7192146939ec..d077d8d2160b 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/data/BasicBlockTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/data/BasicBlockTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.util.IntArray; import org.elasticsearch.common.util.LongArray; import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.compute.lucene.ShardRefCounted; import org.elasticsearch.compute.test.BlockTestUtils; import org.elasticsearch.compute.test.TestBlockFactory; import org.elasticsearch.core.RefCounted; @@ -1394,7 +1395,13 @@ public class BasicBlockTests extends ESTestCase { public void testRefCountingDocBlock() { int positionCount = randomIntBetween(0, 100); - DocBlock block = new DocVector(intVector(positionCount), intVector(positionCount), intVector(positionCount), true).asBlock(); + DocBlock block = new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, + intVector(positionCount), + intVector(positionCount), + intVector(positionCount), + true + ).asBlock(); assertThat(breaker.getUsed(), greaterThan(0L)); assertRefCountingBehavior(block); assertThat(breaker.getUsed(), is(0L)); @@ -1430,7 +1437,13 @@ public class BasicBlockTests extends ESTestCase { public void testRefCountingDocVector() { int positionCount = randomIntBetween(0, 100); - DocVector vector = new DocVector(intVector(positionCount), intVector(positionCount), intVector(positionCount), true); + DocVector vector = new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, + intVector(positionCount), + intVector(positionCount), + intVector(positionCount), + true + ); assertThat(breaker.getUsed(), greaterThan(0L)); assertRefCountingBehavior(vector); assertThat(breaker.getUsed(), is(0L)); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/data/DocVectorTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/data/DocVectorTests.java index 78192d6363d4..59520a25c523 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/data/DocVectorTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/data/DocVectorTests.java @@ -10,6 +10,7 @@ package org.elasticsearch.compute.data; import org.elasticsearch.common.Randomness; import org.elasticsearch.common.breaker.CircuitBreakingException; import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.compute.lucene.ShardRefCounted; import org.elasticsearch.compute.test.ComputeTestCase; import org.elasticsearch.compute.test.TestBlockFactory; import org.elasticsearch.core.Releasables; @@ -28,27 +29,51 @@ import static org.hamcrest.Matchers.is; public class DocVectorTests extends ComputeTestCase { public void testNonDecreasingSetTrue() { int length = between(1, 100); - DocVector docs = new DocVector(intRange(0, length), intRange(0, length), intRange(0, length), true); + DocVector docs = new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, + intRange(0, length), + intRange(0, length), + intRange(0, length), + true + ); assertTrue(docs.singleSegmentNonDecreasing()); } public void testNonDecreasingSetFalse() { BlockFactory blockFactory = blockFactory(); - DocVector docs = new DocVector(intRange(0, 2), intRange(0, 2), blockFactory.newIntArrayVector(new int[] { 1, 0 }, 2), false); + DocVector docs = new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, + intRange(0, 2), + intRange(0, 2), + blockFactory.newIntArrayVector(new int[] { 1, 0 }, 2), + false + ); assertFalse(docs.singleSegmentNonDecreasing()); docs.close(); } public void testNonDecreasingNonConstantShard() { BlockFactory blockFactory = blockFactory(); - DocVector docs = new DocVector(intRange(0, 2), blockFactory.newConstantIntVector(0, 2), intRange(0, 2), null); + DocVector docs = new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, + intRange(0, 2), + blockFactory.newConstantIntVector(0, 2), + intRange(0, 2), + null + ); assertFalse(docs.singleSegmentNonDecreasing()); docs.close(); } public void testNonDecreasingNonConstantSegment() { BlockFactory blockFactory = blockFactory(); - DocVector docs = new DocVector(blockFactory.newConstantIntVector(0, 2), intRange(0, 2), intRange(0, 2), null); + DocVector docs = new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, + blockFactory.newConstantIntVector(0, 2), + intRange(0, 2), + intRange(0, 2), + null + ); assertFalse(docs.singleSegmentNonDecreasing()); docs.close(); } @@ -56,6 +81,7 @@ public class DocVectorTests extends ComputeTestCase { public void testNonDecreasingDescendingDocs() { BlockFactory blockFactory = blockFactory(); DocVector docs = new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, blockFactory.newConstantIntVector(0, 2), blockFactory.newConstantIntVector(0, 2), blockFactory.newIntArrayVector(new int[] { 1, 0 }, 2), @@ -209,7 +235,13 @@ public class DocVectorTests extends ComputeTestCase { public void testCannotDoubleRelease() { BlockFactory blockFactory = blockFactory(); - var block = new DocVector(intRange(0, 2), blockFactory.newConstantIntBlockWith(0, 2).asVector(), intRange(0, 2), null).asBlock(); + var block = new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, + intRange(0, 2), + blockFactory.newConstantIntBlockWith(0, 2).asVector(), + intRange(0, 2), + null + ).asBlock(); assertThat(block.isReleased(), is(false)); Page page = new Page(block); @@ -229,6 +261,7 @@ public class DocVectorTests extends ComputeTestCase { public void testRamBytesUsedWithout() { BlockFactory blockFactory = blockFactory(); DocVector docs = new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, blockFactory.newConstantIntBlockWith(0, 1).asVector(), blockFactory.newConstantIntBlockWith(0, 1).asVector(), blockFactory.newConstantIntBlockWith(0, 1).asVector(), @@ -243,6 +276,7 @@ public class DocVectorTests extends ComputeTestCase { BlockFactory factory = blockFactory(); try ( DocVector docs = new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, factory.newConstantIntVector(0, 10), factory.newConstantIntVector(0, 10), factory.newIntArrayVector(new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 10), @@ -250,6 +284,7 @@ public class DocVectorTests extends ComputeTestCase { ); DocVector filtered = docs.filter(1, 2, 3); DocVector expected = new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, factory.newConstantIntVector(0, 3), factory.newConstantIntVector(0, 3), factory.newIntArrayVector(new int[] { 1, 2, 3 }, 3), @@ -270,7 +305,7 @@ public class DocVectorTests extends ComputeTestCase { shards = factory.newConstantIntVector(0, 10); segments = factory.newConstantIntVector(0, 10); docs = factory.newConstantIntVector(0, 10); - result = new DocVector(shards, segments, docs, false); + result = new DocVector(ShardRefCounted.ALWAYS_REFERENCED, shards, segments, docs, false); return result; } finally { if (result == null) { diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneQueryEvaluatorTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneQueryEvaluatorTests.java index eb7cb32fd0e7..4828f70e51dc 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneQueryEvaluatorTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneQueryEvaluatorTests.java @@ -191,6 +191,7 @@ public abstract class LuceneQueryEvaluatorTests { IndexSearcher searcher = new IndexSearcher(reader); + var shardContext = new LuceneSourceOperatorTests.MockShardContext(reader, 0); LuceneQueryEvaluator.ShardConfig shard = new LuceneQueryEvaluator.ShardConfig(searcher.rewrite(query), searcher); List operators = new ArrayList<>(); if (shuffleDocs) { diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneSourceOperatorTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneSourceOperatorTests.java index 4c5c860d244a..a8cb202f2be2 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneSourceOperatorTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneSourceOperatorTests.java @@ -405,6 +405,11 @@ public class LuceneSourceOperatorTests extends AnyOperatorTestCase { private final int index; private final ContextIndexSearcher searcher; + // TODO Reuse this overload in the places that pass 0. + public MockShardContext(IndexReader reader) { + this(reader, 0); + } + public MockShardContext(IndexReader reader, int index) { this.index = index; try { @@ -458,5 +463,22 @@ public class LuceneSourceOperatorTests extends AnyOperatorTestCase { public MappedFieldType fieldType(String name) { throw new UnsupportedOperationException(); } + + public void incRef() {} + + @Override + public boolean tryIncRef() { + return true; + } + + @Override + public boolean decRef() { + return false; + } + + @Override + public boolean hasReferences() { + return true; + } } } diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverContextTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverContextTests.java index 563e88ab4eeb..29ec46bc3440 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverContextTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverContextTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.compute.test.NoOpReleasable; import org.elasticsearch.compute.test.TestBlockFactory; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.Releasables; @@ -267,14 +268,6 @@ public class DriverContextTests extends ESTestCase { public void close() {} } - static class NoOpReleasable implements Releasable { - - @Override - public void close() { - // no-op - } - } - static class CheckableReleasable implements Releasable { boolean closed; diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/EvalOperatorTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/EvalOperatorTests.java index 544541ef49d2..189ccdb402f9 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/EvalOperatorTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/EvalOperatorTests.java @@ -30,7 +30,7 @@ import static org.hamcrest.Matchers.equalTo; public class EvalOperatorTests extends OperatorTestCase { @Override protected SourceOperator simpleInput(BlockFactory blockFactory, int end) { - return new TupleBlockSourceOperator(blockFactory, LongStream.range(0, end).mapToObj(l -> Tuple.tuple(l, end - l))); + return new TupleLongLongBlockSourceOperator(blockFactory, LongStream.range(0, end).mapToObj(l -> Tuple.tuple(l, end - l))); } record Addition(DriverContext driverContext, int lhs, int rhs) implements EvalOperator.ExpressionEvaluator { diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/FilterOperatorTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/FilterOperatorTests.java index a0de030bf4c9..fb1f7b542230 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/FilterOperatorTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/FilterOperatorTests.java @@ -29,7 +29,7 @@ import static org.hamcrest.Matchers.equalTo; public class FilterOperatorTests extends OperatorTestCase { @Override protected SourceOperator simpleInput(BlockFactory blockFactory, int end) { - return new TupleBlockSourceOperator(blockFactory, LongStream.range(0, end).mapToObj(l -> Tuple.tuple(l, end - l))); + return new TupleLongLongBlockSourceOperator(blockFactory, LongStream.range(0, end).mapToObj(l -> Tuple.tuple(l, end - l))); } record SameLastDigit(DriverContext context, int lhs, int rhs) implements EvalOperator.ExpressionEvaluator { diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/HashAggregationOperatorTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/HashAggregationOperatorTests.java index ec84d17045af..106b9613d7bb 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/HashAggregationOperatorTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/HashAggregationOperatorTests.java @@ -38,7 +38,7 @@ public class HashAggregationOperatorTests extends ForkingOperatorTestCase { @Override protected SourceOperator simpleInput(BlockFactory blockFactory, int size) { long max = randomLongBetween(1, Long.MAX_VALUE / size); - return new TupleBlockSourceOperator( + return new TupleLongLongBlockSourceOperator( blockFactory, LongStream.range(0, size).mapToObj(l -> Tuple.tuple(l % 5, randomLongBetween(-max, max))) ); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/ProjectOperatorTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/ProjectOperatorTests.java index de32b51f93ed..88b664533dbb 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/ProjectOperatorTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/ProjectOperatorTests.java @@ -62,7 +62,7 @@ public class ProjectOperatorTests extends OperatorTestCase { @Override protected SourceOperator simpleInput(BlockFactory blockFactory, int end) { - return new TupleBlockSourceOperator(blockFactory, LongStream.range(0, end).mapToObj(l -> Tuple.tuple(l, end - l))); + return new TupleLongLongBlockSourceOperator(blockFactory, LongStream.range(0, end).mapToObj(l -> Tuple.tuple(l, end - l))); } @Override diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/RowInTableLookupOperatorTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/RowInTableLookupOperatorTests.java index 63f8239073c2..441d125c5608 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/RowInTableLookupOperatorTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/RowInTableLookupOperatorTests.java @@ -105,7 +105,7 @@ public class RowInTableLookupOperatorTests extends OperatorTestCase { public void testSelectBlocks() { DriverContext context = driverContext(); List input = CannedSourceOperator.collectPages( - new TupleBlockSourceOperator( + new TupleLongLongBlockSourceOperator( context.blockFactory(), LongStream.range(0, 1000).mapToObj(l -> Tuple.tuple(randomLong(), randomFrom(1L, 7L, 14L, 20L))) ) diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/ShuffleDocsOperator.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/ShuffleDocsOperator.java index 955d0237c65f..2f0f86ee19ad 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/ShuffleDocsOperator.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/ShuffleDocsOperator.java @@ -12,6 +12,7 @@ import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.DocVector; import org.elasticsearch.compute.data.IntVector; import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.lucene.ShardRefCounted; import org.elasticsearch.core.Releasables; import java.util.ArrayList; @@ -60,7 +61,7 @@ public class ShuffleDocsOperator extends AbstractPageMappingOperator { } } Block[] blocks = new Block[page.getBlockCount()]; - blocks[0] = new DocVector(shards, segments, docs, false).asBlock(); + blocks[0] = new DocVector(ShardRefCounted.ALWAYS_REFERENCED, shards, segments, docs, false).asBlock(); for (int i = 1; i < blocks.length; i++) { blocks[i] = page.getBlock(i); } diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/TupleAbstractBlockSourceOperator.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/TupleAbstractBlockSourceOperator.java new file mode 100644 index 000000000000..739c54e6e8ee --- /dev/null +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/TupleAbstractBlockSourceOperator.java @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.operator; + +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BlockFactory; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.test.AbstractBlockSourceOperator; +import org.elasticsearch.core.Tuple; + +import java.util.List; + +/** + * A source operator whose output is the given tuple values. This operator produces pages + * with two Blocks. The returned pages preserve the order of values as given in the in initial list. + */ +public abstract class TupleAbstractBlockSourceOperator extends AbstractBlockSourceOperator { + private static final int DEFAULT_MAX_PAGE_POSITIONS = 8 * 1024; + + private final List> values; + private final ElementType firstElementType; + private final ElementType secondElementType; + + public TupleAbstractBlockSourceOperator( + BlockFactory blockFactory, + List> values, + ElementType firstElementType, + ElementType secondElementType + ) { + this(blockFactory, values, DEFAULT_MAX_PAGE_POSITIONS, firstElementType, secondElementType); + } + + public TupleAbstractBlockSourceOperator( + BlockFactory blockFactory, + List> values, + int maxPagePositions, + ElementType firstElementType, + ElementType secondElementType + ) { + super(blockFactory, maxPagePositions); + this.values = values; + this.firstElementType = firstElementType; + this.secondElementType = secondElementType; + } + + @Override + protected Page createPage(int positionOffset, int length) { + try (var blockBuilder1 = firstElementBlockBuilder(length); var blockBuilder2 = secondElementBlockBuilder(length)) { + for (int i = 0; i < length; i++) { + Tuple item = values.get(positionOffset + i); + if (item.v1() == null) { + blockBuilder1.appendNull(); + } else { + consumeFirstElement(item.v1(), blockBuilder1); + } + if (item.v2() == null) { + blockBuilder2.appendNull(); + } else { + consumeSecondElement(item.v2(), blockBuilder2); + } + } + currentPosition += length; + return new Page(Block.Builder.buildAll(blockBuilder1, blockBuilder2)); + } + } + + protected abstract void consumeFirstElement(T t, Block.Builder blockBuilder1); + + protected Block.Builder firstElementBlockBuilder(int length) { + return firstElementType.newBlockBuilder(length, blockFactory); + } + + protected Block.Builder secondElementBlockBuilder(int length) { + return secondElementType.newBlockBuilder(length, blockFactory); + } + + protected abstract void consumeSecondElement(S t, Block.Builder blockBuilder1); + + @Override + protected int remaining() { + return values.size() - currentPosition; + } + + public List elementTypes() { + return List.of(firstElementType, secondElementType); + } + + public List> values() { + return values; + } +} diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/TupleBlockSourceOperator.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/TupleBlockSourceOperator.java deleted file mode 100644 index b905de17608c..000000000000 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/TupleBlockSourceOperator.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.compute.operator; - -import org.elasticsearch.compute.data.Block; -import org.elasticsearch.compute.data.BlockFactory; -import org.elasticsearch.compute.data.Page; -import org.elasticsearch.compute.test.AbstractBlockSourceOperator; -import org.elasticsearch.core.Tuple; - -import java.util.List; -import java.util.stream.Stream; - -/** - * A source operator whose output is the given tuple values. This operator produces pages - * with two Blocks. The returned pages preserve the order of values as given in the in initial list. - */ -public class TupleBlockSourceOperator extends AbstractBlockSourceOperator { - - private static final int DEFAULT_MAX_PAGE_POSITIONS = 8 * 1024; - - private final List> values; - - public TupleBlockSourceOperator(BlockFactory blockFactory, Stream> values) { - this(blockFactory, values, DEFAULT_MAX_PAGE_POSITIONS); - } - - public TupleBlockSourceOperator(BlockFactory blockFactory, Stream> values, int maxPagePositions) { - super(blockFactory, maxPagePositions); - this.values = values.toList(); - } - - public TupleBlockSourceOperator(BlockFactory blockFactory, List> values) { - this(blockFactory, values, DEFAULT_MAX_PAGE_POSITIONS); - } - - public TupleBlockSourceOperator(BlockFactory blockFactory, List> values, int maxPagePositions) { - super(blockFactory, maxPagePositions); - this.values = values; - } - - @Override - protected Page createPage(int positionOffset, int length) { - try (var blockBuilder1 = blockFactory.newLongBlockBuilder(length); var blockBuilder2 = blockFactory.newLongBlockBuilder(length)) { - for (int i = 0; i < length; i++) { - Tuple item = values.get(positionOffset + i); - if (item.v1() == null) { - blockBuilder1.appendNull(); - } else { - blockBuilder1.appendLong(item.v1()); - } - if (item.v2() == null) { - blockBuilder2.appendNull(); - } else { - blockBuilder2.appendLong(item.v2()); - } - } - currentPosition += length; - return new Page(Block.Builder.buildAll(blockBuilder1, blockBuilder2)); - } - } - - @Override - protected int remaining() { - return values.size() - currentPosition; - } -} diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/TupleDocLongBlockSourceOperator.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/TupleDocLongBlockSourceOperator.java new file mode 100644 index 000000000000..26e84fe46d01 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/TupleDocLongBlockSourceOperator.java @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.operator; + +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BlockFactory; +import org.elasticsearch.compute.data.BlockUtils; +import org.elasticsearch.compute.data.DocBlock; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.index.mapper.BlockLoader; + +import java.util.List; + +import static org.elasticsearch.compute.data.ElementType.DOC; +import static org.elasticsearch.compute.data.ElementType.LONG; + +/** + * A source operator whose output is the given tuple values. This operator produces pages + * with two Blocks. The returned pages preserve the order of values as given in the in initial list. + */ +public class TupleDocLongBlockSourceOperator extends TupleAbstractBlockSourceOperator { + public TupleDocLongBlockSourceOperator(BlockFactory blockFactory, List> values) { + super(blockFactory, values, DOC, LONG); + } + + public TupleDocLongBlockSourceOperator(BlockFactory blockFactory, List> values, int maxPagePositions) { + super(blockFactory, values, maxPagePositions, DOC, LONG); + } + + @Override + protected void consumeFirstElement(BlockUtils.Doc doc, Block.Builder builder) { + var docBuilder = (DocBlock.Builder) builder; + docBuilder.appendShard(doc.shard()); + docBuilder.appendSegment(doc.segment()); + docBuilder.appendDoc(doc.doc()); + } + + @Override + protected void consumeSecondElement(Long l, Block.Builder blockBuilder) { + ((BlockLoader.LongBuilder) blockBuilder).appendLong(l); + } +} diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/TupleLongLongBlockSourceOperator.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/TupleLongLongBlockSourceOperator.java new file mode 100644 index 000000000000..ae5045f04c9b --- /dev/null +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/TupleLongLongBlockSourceOperator.java @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.operator; + +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BlockFactory; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.index.mapper.BlockLoader; + +import java.util.List; +import java.util.stream.Stream; + +import static org.elasticsearch.compute.data.ElementType.LONG; + +/** + * A source operator whose output is the given tuple values. This operator produces pages + * with two Blocks. The returned pages preserve the order of values as given in the in initial list. + */ +public class TupleLongLongBlockSourceOperator extends TupleAbstractBlockSourceOperator { + + public TupleLongLongBlockSourceOperator(BlockFactory blockFactory, Stream> values) { + super(blockFactory, values.toList(), LONG, LONG); + } + + public TupleLongLongBlockSourceOperator(BlockFactory blockFactory, Stream> values, int maxPagePositions) { + super(blockFactory, values.toList(), maxPagePositions, LONG, LONG); + } + + public TupleLongLongBlockSourceOperator(BlockFactory blockFactory, List> values) { + super(blockFactory, values, LONG, LONG); + } + + public TupleLongLongBlockSourceOperator(BlockFactory blockFactory, List> values, int maxPagePositions) { + super(blockFactory, values, maxPagePositions, LONG, LONG); + } + + @Override + protected void consumeFirstElement(Long l, Block.Builder blockBuilder) { + ((BlockLoader.LongBuilder) blockBuilder).appendLong(l); + } + + @Override + protected void consumeSecondElement(Long l, Block.Builder blockBuilder) { + consumeFirstElement(l, blockBuilder); + } +} diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/lookup/EnrichQuerySourceOperatorTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/lookup/EnrichQuerySourceOperatorTests.java index 2aadb81a8b08..d1a3b408c41a 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/lookup/EnrichQuerySourceOperatorTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/lookup/EnrichQuerySourceOperatorTests.java @@ -32,6 +32,7 @@ import org.elasticsearch.compute.data.DocBlock; import org.elasticsearch.compute.data.IntBlock; import org.elasticsearch.compute.data.IntVector; import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.lucene.LuceneSourceOperatorTests; import org.elasticsearch.compute.operator.DriverContext; import org.elasticsearch.compute.operator.Warnings; import org.elasticsearch.core.IOUtils; @@ -104,7 +105,8 @@ public class EnrichQuerySourceOperatorTests extends ESTestCase { blockFactory, 128, queryList, - directoryData.reader, + + new LuceneSourceOperatorTests.MockShardContext(directoryData.reader), warnings() ); Page page = queryOperator.getOutput(); @@ -165,7 +167,7 @@ public class EnrichQuerySourceOperatorTests extends ESTestCase { blockFactory, maxPageSize, queryList, - directoryData.reader, + new LuceneSourceOperatorTests.MockShardContext(directoryData.reader), warnings() ); Map> actualPositions = new HashMap<>(); @@ -214,7 +216,7 @@ public class EnrichQuerySourceOperatorTests extends ESTestCase { blockFactory, 128, queryList, - directoryData.reader, + new LuceneSourceOperatorTests.MockShardContext(directoryData.reader), warnings() ); Page page = queryOperator.getOutput(); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/ExtractorTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/ExtractorTests.java index 101c129e7720..b345d8c0b196 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/ExtractorTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/ExtractorTests.java @@ -18,9 +18,11 @@ import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.data.BlockUtils; import org.elasticsearch.compute.data.DocVector; import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.lucene.ShardRefCounted; import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; import org.elasticsearch.compute.test.BlockTestUtils; import org.elasticsearch.compute.test.TestBlockFactory; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; @@ -94,7 +96,9 @@ public class ExtractorTests extends ESTestCase { e, TopNEncoder.DEFAULT_UNSORTABLE, () -> new DocVector( - blockFactory.newConstantIntBlockWith(randomInt(), 1).asVector(), + ShardRefCounted.ALWAYS_REFERENCED, + // Shard ID should be small and non-negative. + blockFactory.newConstantIntBlockWith(randomIntBetween(0, 255), 1).asVector(), blockFactory.newConstantIntBlockWith(randomInt(), 1).asVector(), blockFactory.newConstantIntBlockWith(randomInt(), 1).asVector(), randomBoolean() ? null : randomBoolean() @@ -172,6 +176,9 @@ public class ExtractorTests extends ESTestCase { 1 ); BytesRef values = valuesBuilder.bytesRefView(); + if (result instanceof ResultBuilderForDoc fd) { + fd.setNextRefCounted(RefCounted.ALWAYS_REFERENCED); + } result.decodeValue(values); assertThat(values.length, equalTo(0)); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/TopNOperatorTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/TopNOperatorTests.java index 8561ce84744a..1180cdca6456 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/TopNOperatorTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/TopNOperatorTests.java @@ -17,23 +17,30 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.compute.data.Block; import org.elasticsearch.compute.data.BlockFactory; +import org.elasticsearch.compute.data.BlockUtils; +import org.elasticsearch.compute.data.DocBlock; import org.elasticsearch.compute.data.ElementType; import org.elasticsearch.compute.data.IntBlock; import org.elasticsearch.compute.data.LongBlock; import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.lucene.ShardRefCounted; import org.elasticsearch.compute.operator.CountingCircuitBreaker; import org.elasticsearch.compute.operator.Driver; import org.elasticsearch.compute.operator.DriverContext; import org.elasticsearch.compute.operator.Operator; import org.elasticsearch.compute.operator.PageConsumerOperator; import org.elasticsearch.compute.operator.SourceOperator; -import org.elasticsearch.compute.operator.TupleBlockSourceOperator; +import org.elasticsearch.compute.operator.TupleAbstractBlockSourceOperator; +import org.elasticsearch.compute.operator.TupleDocLongBlockSourceOperator; +import org.elasticsearch.compute.operator.TupleLongLongBlockSourceOperator; import org.elasticsearch.compute.test.CannedSourceOperator; import org.elasticsearch.compute.test.OperatorTestCase; import org.elasticsearch.compute.test.SequenceLongBlockSourceOperator; import org.elasticsearch.compute.test.TestBlockBuilder; import org.elasticsearch.compute.test.TestBlockFactory; import org.elasticsearch.compute.test.TestDriverFactory; +import org.elasticsearch.core.RefCounted; +import org.elasticsearch.core.SimpleRefCounted; import org.elasticsearch.core.Tuple; import org.elasticsearch.indices.CrankyCircuitBreakerService; import org.elasticsearch.test.ESTestCase; @@ -53,10 +60,12 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.LongStream; +import java.util.stream.Stream; import static java.util.Comparator.naturalOrder; import static java.util.Comparator.reverseOrder; @@ -289,16 +298,19 @@ public class TopNOperatorTests extends OperatorTestCase { boolean ascendingOrder, boolean nullsFirst ) { - return topNTwoColumns( + return topNTwoLongColumns( driverContext, inputValues.stream().map(v -> tuple(v, 0L)).toList(), limit, - List.of(LONG, LONG), List.of(DEFAULT_UNSORTABLE, DEFAULT_UNSORTABLE), List.of(new TopNOperator.SortOrder(0, ascendingOrder, nullsFirst)) ).stream().map(Tuple::v1).toList(); } + private static TupleLongLongBlockSourceOperator longLongSourceOperator(DriverContext driverContext, List> values) { + return new TupleLongLongBlockSourceOperator(driverContext.blockFactory(), values, randomIntBetween(1, 1000)); + } + private List topNLong(List inputValues, int limit, boolean ascendingOrder, boolean nullsFirst) { return topNLong(driverContext(), inputValues, limit, ascendingOrder, nullsFirst); } @@ -465,33 +477,30 @@ public class TopNOperatorTests extends OperatorTestCase { public void testTopNTwoColumns() { List> values = Arrays.asList(tuple(1L, 1L), tuple(1L, 2L), tuple(null, null), tuple(null, 1L), tuple(1L, null)); assertThat( - topNTwoColumns( + topNTwoLongColumns( driverContext(), values, 5, - List.of(LONG, LONG), List.of(TopNEncoder.DEFAULT_SORTABLE, TopNEncoder.DEFAULT_SORTABLE), List.of(new TopNOperator.SortOrder(0, true, false), new TopNOperator.SortOrder(1, true, false)) ), equalTo(List.of(tuple(1L, 1L), tuple(1L, 2L), tuple(1L, null), tuple(null, 1L), tuple(null, null))) ); assertThat( - topNTwoColumns( + topNTwoLongColumns( driverContext(), values, 5, - List.of(LONG, LONG), List.of(TopNEncoder.DEFAULT_SORTABLE, TopNEncoder.DEFAULT_SORTABLE), List.of(new TopNOperator.SortOrder(0, true, true), new TopNOperator.SortOrder(1, true, false)) ), equalTo(List.of(tuple(null, 1L), tuple(null, null), tuple(1L, 1L), tuple(1L, 2L), tuple(1L, null))) ); assertThat( - topNTwoColumns( + topNTwoLongColumns( driverContext(), values, 5, - List.of(LONG, LONG), List.of(TopNEncoder.DEFAULT_SORTABLE, TopNEncoder.DEFAULT_SORTABLE), List.of(new TopNOperator.SortOrder(0, true, false), new TopNOperator.SortOrder(1, true, true)) ), @@ -657,45 +666,82 @@ public class TopNOperatorTests extends OperatorTestCase { assertDriverContext(driverContext); } - private List> topNTwoColumns( + private List> topNTwoLongColumns( DriverContext driverContext, - List> inputValues, + List> values, int limit, - List elementTypes, List encoder, List sortOrders ) { - List> outputValues = new ArrayList<>(); + var page = topNTwoColumns( + driverContext, + new TupleLongLongBlockSourceOperator(driverContext.blockFactory(), values, randomIntBetween(1, 1000)), + limit, + encoder, + sortOrders + ); + var result = pageToTuples( + (block, i) -> block.isNull(i) ? null : ((LongBlock) block).getLong(i), + (block, i) -> block.isNull(i) ? null : ((LongBlock) block).getLong(i), + page + ); + assertThat(result, hasSize(Math.min(limit, values.size()))); + return result; + } + + private List topNTwoColumns( + DriverContext driverContext, + TupleAbstractBlockSourceOperator sourceOperator, + int limit, + List encoder, + List sortOrders + ) { + var pages = new ArrayList(); try ( Driver driver = TestDriverFactory.create( driverContext, - new TupleBlockSourceOperator(driverContext.blockFactory(), inputValues, randomIntBetween(1, 1000)), + sourceOperator, List.of( new TopNOperator( driverContext.blockFactory(), nonBreakingBigArrays().breakerService().getBreaker("request"), limit, - elementTypes, + sourceOperator.elementTypes(), encoder, sortOrders, randomPageSize() ) ), - new PageConsumerOperator(page -> { - LongBlock block1 = page.getBlock(0); - LongBlock block2 = page.getBlock(1); - for (int i = 0; i < block1.getPositionCount(); i++) { - outputValues.add(tuple(block1.isNull(i) ? null : block1.getLong(i), block2.isNull(i) ? null : block2.getLong(i))); - } - page.releaseBlocks(); - }) + new PageConsumerOperator(pages::add) ) ) { runDriver(driver); } - assertThat(outputValues, hasSize(Math.min(limit, inputValues.size()))); assertDriverContext(driverContext); - return outputValues; + return pages; + } + + private static List> pageToTuples( + BiFunction getFirstBlockValue, + BiFunction getSecondBlockValue, + List pages + ) { + var result = new ArrayList>(); + for (Page page : pages) { + var block1 = page.getBlock(0); + var block2 = page.getBlock(1); + for (int i = 0; i < block1.getPositionCount(); i++) { + result.add( + tuple( + block1.isNull(i) ? null : getFirstBlockValue.apply(block1, i), + block2.isNull(i) ? null : getSecondBlockValue.apply(block2, i) + ) + ); + } + page.releaseBlocks(); + } + + return result; } public void testTopNManyDescriptionAndToString() { @@ -1447,6 +1493,53 @@ public class TopNOperatorTests extends OperatorTestCase { } } + public void testShardContextManagement_limitEqualToCount_noShardContextIsReleased() { + topNShardContextManagementAux(4, Stream.generate(() -> true).limit(4).toList()); + } + + public void testShardContextManagement_notAllShardsPassTopN_shardsAreReleased() { + topNShardContextManagementAux(2, List.of(true, false, false, true)); + } + + private void topNShardContextManagementAux(int limit, List expectedOpenAfterTopN) { + List> values = Arrays.asList( + tuple(new BlockUtils.Doc(0, 10, 100), 1L), + tuple(new BlockUtils.Doc(1, 20, 200), 2L), + tuple(new BlockUtils.Doc(2, 30, 300), null), + tuple(new BlockUtils.Doc(3, 40, 400), -3L) + ); + List refCountedList = Stream.generate(() -> new SimpleRefCounted()).limit(4).toList(); + var shardRefCounted = ShardRefCounted.fromList(refCountedList); + + var pages = topNTwoColumns(driverContext(), new TupleDocLongBlockSourceOperator(driverContext().blockFactory(), values) { + @Override + protected Block.Builder firstElementBlockBuilder(int length) { + return DocBlock.newBlockBuilder(blockFactory, length).setShardRefCounted(shardRefCounted); + } + }, + limit, + List.of(TopNEncoder.DEFAULT_UNSORTABLE, TopNEncoder.DEFAULT_SORTABLE), + List.of(new TopNOperator.SortOrder(1, true, false)) + + ); + refCountedList.forEach(RefCounted::decRef); + + assertThat(refCountedList.stream().map(RefCounted::hasReferences).toList(), equalTo(expectedOpenAfterTopN)); + + var expectedValues = values.stream() + .sorted(Comparator.comparingLong(t -> t.v2() == null ? Long.MAX_VALUE : t.v2())) + .limit(limit) + .toList(); + assertThat( + pageToTuples((b, i) -> (BlockUtils.Doc) BlockUtils.toJavaObject(b, i), (b, i) -> ((LongBlock) b).getLong(i), pages), + equalTo(expectedValues) + ); + + for (var rc : refCountedList) { + assertFalse(rc.hasReferences()); + } + } + @SuppressWarnings({ "unchecked", "rawtypes" }) private static void readAsRows(List>> values, Page page) { if (page.getBlockCount() == 0) { diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/TopNRowTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/TopNRowTests.java index fdf62706e210..8171299c4618 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/TopNRowTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/TopNRowTests.java @@ -63,6 +63,7 @@ public class TopNRowTests extends ESTestCase { expected -= RamUsageTester.ramUsed("topn"); // the sort orders are shared expected -= RamUsageTester.ramUsed(sortOrders()); + // expected -= RamUsageTester.ramUsed(row.docVector); return expected; } } diff --git a/x-pack/plugin/esql/compute/test/src/main/java/org/elasticsearch/compute/test/BlockTestUtils.java b/x-pack/plugin/esql/compute/test/src/main/java/org/elasticsearch/compute/test/BlockTestUtils.java index 80f6cbdb81e8..dcfec4b268aa 100644 --- a/x-pack/plugin/esql/compute/test/src/main/java/org/elasticsearch/compute/test/BlockTestUtils.java +++ b/x-pack/plugin/esql/compute/test/src/main/java/org/elasticsearch/compute/test/BlockTestUtils.java @@ -37,6 +37,7 @@ import static org.elasticsearch.test.ESTestCase.randomBoolean; import static org.elasticsearch.test.ESTestCase.randomDouble; import static org.elasticsearch.test.ESTestCase.randomFloat; import static org.elasticsearch.test.ESTestCase.randomInt; +import static org.elasticsearch.test.ESTestCase.randomIntBetween; import static org.elasticsearch.test.ESTestCase.randomLong; import static org.elasticsearch.test.ESTestCase.randomRealisticUnicodeOfCodepointLengthBetween; import static org.hamcrest.Matchers.equalTo; @@ -54,7 +55,11 @@ public class BlockTestUtils { case DOUBLE -> randomDouble(); case BYTES_REF -> new BytesRef(randomRealisticUnicodeOfCodepointLengthBetween(0, 5)); // TODO: also test spatial WKB case BOOLEAN -> randomBoolean(); - case DOC -> new BlockUtils.Doc(randomInt(), randomInt(), between(0, Integer.MAX_VALUE)); + case DOC -> new BlockUtils.Doc( + randomIntBetween(0, 255), // Shard ID should be small and non-negative. + randomInt(), + between(0, Integer.MAX_VALUE) + ); case NULL -> null; case COMPOSITE -> throw new IllegalArgumentException("can't make random values for composite"); case AGGREGATE_METRIC_DOUBLE -> throw new IllegalArgumentException("can't make random values for aggregate_metric_double"); diff --git a/x-pack/plugin/esql/compute/test/src/main/java/org/elasticsearch/compute/test/NoOpReleasable.java b/x-pack/plugin/esql/compute/test/src/main/java/org/elasticsearch/compute/test/NoOpReleasable.java new file mode 100644 index 000000000000..8053685a2fd9 --- /dev/null +++ b/x-pack/plugin/esql/compute/test/src/main/java/org/elasticsearch/compute/test/NoOpReleasable.java @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.test; + +import org.elasticsearch.core.Releasable; + +public class NoOpReleasable implements Releasable { + @Override + public void close() {/* no-op */} +} diff --git a/x-pack/plugin/esql/qa/server/extra-checkers/build.gradle b/x-pack/plugin/esql/qa/server/extra-checkers/build.gradle deleted file mode 100644 index ea6784625e00..000000000000 --- a/x-pack/plugin/esql/qa/server/extra-checkers/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -apply plugin: 'elasticsearch.internal-es-plugin' -apply plugin: 'elasticsearch.internal-java-rest-test' -apply plugin: org.elasticsearch.gradle.internal.precommit.CheckstylePrecommitPlugin -apply plugin: org.elasticsearch.gradle.internal.precommit.ForbiddenApisPrecommitPlugin -apply plugin: org.elasticsearch.gradle.internal.precommit.ForbiddenPatternsPrecommitPlugin -apply plugin: org.elasticsearch.gradle.internal.precommit.FilePermissionsPrecommitPlugin -apply plugin: org.elasticsearch.gradle.internal.precommit.LoggerUsagePrecommitPlugin -apply plugin: org.elasticsearch.gradle.internal.precommit.TestingConventionsPrecommitPlugin - - -esplugin { - name = 'extra-checkers' - description = 'An example plugin disallowing CATEGORIZE' - classname ='org.elasticsearch.xpack.esql.qa.extra.ExtraCheckersPlugin' - extendedPlugins = ['x-pack-esql'] -} - -dependencies { - compileOnly project(':x-pack:plugin:esql') - compileOnly project(':x-pack:plugin:esql-core') - clusterPlugins project(':x-pack:plugin:esql:qa:server:extra-checkers') -} - -tasks.named('javaRestTest') { - usesDefaultDistribution("to be triaged") - maxParallelForks = 1 - jvmArgs('--add-opens=java.base/java.nio=ALL-UNNAMED') -} - diff --git a/x-pack/plugin/esql/qa/server/extra-checkers/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/extra/ExtraCheckersIT.java b/x-pack/plugin/esql/qa/server/extra-checkers/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/extra/ExtraCheckersIT.java deleted file mode 100644 index 6cee56294f7f..000000000000 --- a/x-pack/plugin/esql/qa/server/extra-checkers/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/extra/ExtraCheckersIT.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.esql.qa.extra; - -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; - -import org.apache.http.util.EntityUtils; -import org.elasticsearch.client.Request; -import org.elasticsearch.client.Response; -import org.elasticsearch.client.ResponseException; -import org.elasticsearch.test.TestClustersThreadFilter; -import org.elasticsearch.test.cluster.ElasticsearchCluster; -import org.elasticsearch.test.cluster.local.distribution.DistributionType; -import org.elasticsearch.test.rest.ESRestTestCase; -import org.junit.ClassRule; - -import java.io.IOException; - -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; - -@ThreadLeakFilters(filters = TestClustersThreadFilter.class) -public class ExtraCheckersIT extends ESRestTestCase { - @ClassRule - public static ElasticsearchCluster cluster = ElasticsearchCluster.local() - .distribution(DistributionType.DEFAULT) - .setting("xpack.security.enabled", "false") - .setting("xpack.license.self_generated.type", "trial") - .shared(true) - .plugin("extra-checkers") - .build(); - - public void testWithCategorize() { - ResponseException e = expectThrows(ResponseException.class, () -> runEsql(""" - { - "query": "ROW message=\\"foo bar\\" | STATS COUNT(*) BY CATEGORIZE(message) | LIMIT 1" - }""")); - assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(400)); - assertThat(e.getMessage(), containsString("line 1:43: CATEGORIZE is unsupported")); - } - - public void testWithoutCategorize() throws IOException { - String result = runEsql(""" - { - "query": "ROW message=\\"foo bar\\" | STATS COUNT(*) | LIMIT 1" - }"""); - assertThat(result, containsString(""" - "columns" : [ - { - "name" : "COUNT(*)", - "type" : "long" - } - ], - "values" : [ - [ - 1 - ] - ] - """)); - } - - private String runEsql(String json) throws IOException { - Request request = new Request("POST", "/_query"); - request.setJsonEntity(json); - request.addParameter("error_trace", ""); - request.addParameter("pretty", ""); - Response response = client().performRequest(request); - return EntityUtils.toString(response.getEntity()); - } - - @Override - protected String getTestRestCluster() { - return cluster.getHttpAddresses(); - } -} diff --git a/x-pack/plugin/esql/qa/server/extra-checkers/src/main/java/org/elasticsearch/xpack/esql/qa/extra/DisallowCategorize.java b/x-pack/plugin/esql/qa/server/extra-checkers/src/main/java/org/elasticsearch/xpack/esql/qa/extra/DisallowCategorize.java deleted file mode 100644 index 64775ed73447..000000000000 --- a/x-pack/plugin/esql/qa/server/extra-checkers/src/main/java/org/elasticsearch/xpack/esql/qa/extra/DisallowCategorize.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.esql.qa.extra; - -import org.elasticsearch.xpack.esql.analysis.Verifier; -import org.elasticsearch.xpack.esql.common.Failure; -import org.elasticsearch.xpack.esql.common.Failures; -import org.elasticsearch.xpack.esql.expression.function.grouping.Categorize; -import org.elasticsearch.xpack.esql.plan.logical.Aggregate; -import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; - -import java.util.List; -import java.util.function.BiConsumer; - -public class DisallowCategorize implements Verifier.ExtraCheckers { - @Override - public List> extra() { - return List.of(DisallowCategorize::disallowCategorize); - } - - private static void disallowCategorize(LogicalPlan plan, Failures failures) { - if (plan instanceof Aggregate) { - plan.forEachExpression(Categorize.class, cat -> failures.add(new Failure(cat, "CATEGORIZE is unsupported"))); - } - } -} diff --git a/x-pack/plugin/esql/qa/server/extra-checkers/src/main/java/org/elasticsearch/xpack/esql/qa/extra/ExtraCheckersPlugin.java b/x-pack/plugin/esql/qa/server/extra-checkers/src/main/java/org/elasticsearch/xpack/esql/qa/extra/ExtraCheckersPlugin.java deleted file mode 100644 index da327150d65d..000000000000 --- a/x-pack/plugin/esql/qa/server/extra-checkers/src/main/java/org/elasticsearch/xpack/esql/qa/extra/ExtraCheckersPlugin.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.esql.qa.extra; - -import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.xpack.esql.analysis.Verifier; - -/** - * Marker plugin to enable {@link Verifier.ExtraCheckers}. - */ -public class ExtraCheckersPlugin extends Plugin {} diff --git a/x-pack/plugin/esql/qa/server/extra-checkers/src/main/resources/META-INF/services/org.elasticsearch.xpack.esql.analysis.Verifier$ExtraCheckers b/x-pack/plugin/esql/qa/server/extra-checkers/src/main/resources/META-INF/services/org.elasticsearch.xpack.esql.analysis.Verifier$ExtraCheckers deleted file mode 100644 index 9e206c084354..000000000000 --- a/x-pack/plugin/esql/qa/server/extra-checkers/src/main/resources/META-INF/services/org.elasticsearch.xpack.esql.analysis.Verifier$ExtraCheckers +++ /dev/null @@ -1,8 +0,0 @@ -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License -# 2.0; you may not use this file except in compliance with the Elastic License -# 2.0. -# - -org.elasticsearch.xpack.esql.qa.extra.DisallowCategorize diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java index 1c6bbe449d13..0c29d7a71125 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java @@ -1014,21 +1014,22 @@ public abstract class RestEsqlTestCase extends ESRestTestCase { // Create more than 10 indices to trigger multiple batches of data node execution. // The sort field should be missing on some indices to reproduce NullPointerException caused by duplicated items in layout for (int i = 1; i <= 20; i++) { - createIndex("idx" + i, randomBoolean(), "\"mappings\": {\"properties\" : {\"a\" : {\"type\" : \"keyword\"}}}"); + createIndex("no_sort_field_idx" + i, randomBoolean(), "\"mappings\": {\"properties\" : {\"a\" : {\"type\" : \"keyword\"}}}"); } bulkLoadTestDataLookupMode(10); // lookup join with and without sort for (String sort : List.of("", "| sort integer")) { - var query = requestObjectBuilder().query(format(null, "from * | lookup join {} on integer {}", testIndexName(), sort)); + var query = requestObjectBuilder().query( + format(null, "from {},no_sort_field_idx* | lookup join {} on integer {}", testIndexName(), testIndexName(), sort) + ); Map result = runEsql(query); var columns = as(result.get("columns"), List.class); - assertEquals(22, columns.size()); var values = as(result.get("values"), List.class); assertEquals(10, values.size()); } // clean up for (int i = 1; i <= 20; i++) { - assertThat(deleteIndex("idx" + i).isAcknowledged(), is(true)); + assertThat(deleteIndex("no_sort_field_idx" + i).isAcknowledged(), is(true)); } } @@ -1650,7 +1651,7 @@ public abstract class RestEsqlTestCase extends ESRestTestCase { } private static Request prepareAsyncGetRequest(String id) { - return finishRequest(new Request("GET", "/_query/async/" + id + "?wait_for_completion_timeout=60s")); + return finishRequest(new Request("GET", "/_query/async/" + id + "?wait_for_completion_timeout=6000s")); } private static Request prepareAsyncDeleteRequest(String id) { diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java index d2c887576df9..56fc925ed842 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java @@ -50,6 +50,7 @@ public abstract class GenerativeRestTest extends ESRestTestCase { "Plan \\[ProjectExec\\[\\[.* optimized incorrectly due to missing references", // https://github.com/elastic/elasticsearch/issues/125866 "optimized incorrectly due to missing references", // https://github.com/elastic/elasticsearch/issues/116781 "The incoming YAML document exceeds the limit:", // still to investigate, but it seems to be specific to the test framework + "Data too large", // Circuit breaker exceptions eg. https://github.com/elastic/elasticsearch/issues/130072 // Awaiting fixes for correctness "Expecting the following columns \\[.*\\], got", // https://github.com/elastic/elasticsearch/issues/129000 diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-colors.json b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-colors.json index 24c4102e428f..0d7373e30026 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-colors.json +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-colors.json @@ -13,7 +13,9 @@ "type": "dense_vector", "similarity": "l2_norm", "index_options": { - "type": "hnsw" + "type": "hnsw", + "m": 16, + "ef_construction": 100 } } } diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-dense_vector.json b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-dense_vector.json index 572d9870d09d..9c7d34f0f15e 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-dense_vector.json +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-dense_vector.json @@ -5,7 +5,12 @@ }, "vector": { "type": "dense_vector", - "similarity": "l2_norm" + "similarity": "l2_norm", + "index_options": { + "type": "hnsw", + "m": 16, + "ef_construction": 100 + } } } } diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractPausableIntegTestCase.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractPausableIntegTestCase.java index 86277b1c1cd2..0131e5b81b66 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractPausableIntegTestCase.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractPausableIntegTestCase.java @@ -39,7 +39,11 @@ public abstract class AbstractPausableIntegTestCase extends AbstractEsqlIntegTes @Override protected Collection> nodePlugins() { - return CollectionUtils.appendToCopy(super.nodePlugins(), PausableFieldPlugin.class); + return CollectionUtils.appendToCopy(super.nodePlugins(), pausableFieldPluginClass()); + } + + protected Class pausableFieldPluginClass() { + return PausableFieldPlugin.class; } protected int pageSize() { @@ -56,6 +60,10 @@ public abstract class AbstractPausableIntegTestCase extends AbstractEsqlIntegTes return numberOfDocs; } + protected int shardCount() { + return 1; + } + @Before public void setupIndex() throws IOException { assumeTrue("requires query pragmas", canUseQueryPragmas()); @@ -71,7 +79,7 @@ public abstract class AbstractPausableIntegTestCase extends AbstractEsqlIntegTes mapping.endObject(); } mapping.endObject(); - client().admin().indices().prepareCreate("test").setSettings(indexSettings(1, 0)).setMapping(mapping.endObject()).get(); + client().admin().indices().prepareCreate("test").setSettings(indexSettings(shardCount(), 0)).setMapping(mapping.endObject()).get(); BulkRequestBuilder bulk = client().prepareBulk().setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); for (int i = 0; i < numberOfDocs(); i++) { @@ -89,10 +97,11 @@ public abstract class AbstractPausableIntegTestCase extends AbstractEsqlIntegTes * failed to reduce the index to a single segment and caused this test * to fail in very difficult to debug ways. If it fails again, it'll * trip here. Or maybe it won't! And we'll learn something. Maybe - * it's ghosts. + * it's ghosts. Extending classes can override the shardCount method if + * more than a single segment is expected. */ SegmentsStats stats = client().admin().indices().prepareStats("test").get().getPrimaries().getSegments(); - if (stats.getCount() != 1L) { + if (stats.getCount() != shardCount()) { fail(Strings.toString(stats)); } } diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionBreakerIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionBreakerIT.java index 7a2cccdf680b..a8b687cde48c 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionBreakerIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionBreakerIT.java @@ -87,6 +87,10 @@ public class EsqlActionBreakerIT extends EsqlActionIT { } public static class EsqlTestPluginWithMockBlockFactory extends EsqlPlugin { + public EsqlTestPluginWithMockBlockFactory(Settings settings) { + super(settings); + } + @Override protected BlockFactoryProvider blockFactoryProvider( CircuitBreaker breaker, diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionIT.java index c1a1e04d6385..488c90f77d3e 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionIT.java @@ -1679,6 +1679,39 @@ public class EsqlActionIT extends AbstractEsqlIntegTestCase { } } + public void testGroupingStatsOnMissingFields() { + assertAcked(client().admin().indices().prepareCreate("missing_field_index").setMapping("data", "type=long")); + long oneValue = between(1, 1000); + indexDoc("missing_field_index", "1", "data", oneValue); + refresh("missing_field_index"); + QueryPragmas pragmas = randomPragmas(); + pragmas = new QueryPragmas( + Settings.builder().put(pragmas.getSettings()).put(QueryPragmas.MAX_CONCURRENT_SHARDS_PER_NODE.getKey(), 1).build() + ); + EsqlQueryRequest request = new EsqlQueryRequest(); + request.query("FROM missing_field_index,test | STATS s = sum(data) BY color, tag | SORT color"); + request.pragmas(pragmas); + try (var r = run(request)) { + var rows = getValuesList(r); + assertThat(rows, hasSize(4)); + for (List row : rows) { + assertThat(row, hasSize(3)); + } + assertThat(rows.get(0).get(0), equalTo(20L)); + assertThat(rows.get(0).get(1), equalTo("blue")); + assertNull(rows.get(0).get(2)); + assertThat(rows.get(1).get(0), equalTo(10L)); + assertThat(rows.get(1).get(1), equalTo("green")); + assertNull(rows.get(1).get(2)); + assertThat(rows.get(2).get(0), equalTo(30L)); + assertThat(rows.get(2).get(1), equalTo("red")); + assertNull(rows.get(2).get(2)); + assertThat(rows.get(3).get(0), equalTo(oneValue)); + assertNull(rows.get(3).get(1)); + assertNull(rows.get(3).get(2)); + } + } + private void assertEmptyIndexQueries(String from) { try (EsqlQueryResponse resp = run(from + "METADATA _source | KEEP _source | LIMIT 1")) { assertFalse(resp.values().hasNext()); @@ -1816,6 +1849,8 @@ public class EsqlActionIT extends AbstractEsqlIntegTestCase { "time", "type=long", "color", + "type=keyword", + "tag", "type=keyword" ) ); diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithEnterpriseOrTrialLicense.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithEnterpriseOrTrialLicense.java index 34d09fc54157..79359229e2b1 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithEnterpriseOrTrialLicense.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithEnterpriseOrTrialLicense.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.action; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.License; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.license.internal.XPackLicenseStatus; @@ -19,6 +20,10 @@ import static org.elasticsearch.test.ESTestCase.randomFrom; * that require an Enteprise (or Trial) license. */ public class EsqlPluginWithEnterpriseOrTrialLicense extends EsqlPlugin { + public EsqlPluginWithEnterpriseOrTrialLicense(Settings settings) { + super(settings); + } + protected XPackLicenseState getLicenseState() { License.OperationMode operationMode = randomFrom(License.OperationMode.ENTERPRISE, License.OperationMode.TRIAL); return new XPackLicenseState(() -> System.currentTimeMillis(), new XPackLicenseStatus(operationMode, true, "Test license expired")); diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithNonEnterpriseOrExpiredLicense.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithNonEnterpriseOrExpiredLicense.java index 46c3f3f6204c..4f942173a1b2 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithNonEnterpriseOrExpiredLicense.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithNonEnterpriseOrExpiredLicense.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.action; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.License; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.license.internal.XPackLicenseStatus; @@ -22,6 +23,10 @@ import static org.elasticsearch.test.ESTestCase.randomFrom; * - an expired enterprise or trial license */ public class EsqlPluginWithNonEnterpriseOrExpiredLicense extends EsqlPlugin { + public EsqlPluginWithNonEnterpriseOrExpiredLicense(Settings settings) { + super(settings); + } + protected XPackLicenseState getLicenseState() { License.OperationMode operationMode; boolean active; diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlTopNShardManagementIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlTopNShardManagementIT.java new file mode 100644 index 000000000000..b74b300af68a --- /dev/null +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlTopNShardManagementIT.java @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.action; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.search.MockSearchService; +import org.elasticsearch.search.SearchService; +import org.elasticsearch.search.internal.SearchContext; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.xpack.core.async.GetAsyncResultRequest; +import org.elasticsearch.xpack.esql.plugin.QueryPragmas; +import org.junit.Before; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.elasticsearch.core.TimeValue.timeValueSeconds; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; + +// Verifies that the TopNOperator can release shard contexts as it processes its input. +@ESIntegTestCase.ClusterScope(numDataNodes = 1) +public class EsqlTopNShardManagementIT extends AbstractPausableIntegTestCase { + private static List searchContexts = new ArrayList<>(); + private static final int SHARD_COUNT = 10; + + @Override + protected Class pausableFieldPluginClass() { + return TopNPausableFieldPlugin.class; + } + + @Override + protected int shardCount() { + return SHARD_COUNT; + } + + @Override + protected Collection> nodePlugins() { + return CollectionUtils.appendToCopy(super.nodePlugins(), MockSearchService.TestPlugin.class); + } + + @Before + public void setupMockService() { + searchContexts.clear(); + for (SearchService service : internalCluster().getInstances(SearchService.class)) { + ((MockSearchService) service).setOnCreateSearchContext(ctx -> { + searchContexts.add(ctx); + scriptPermits.release(); + }); + } + } + + public void testTopNOperatorReleasesContexts() throws Exception { + try (var initialResponse = sendAsyncQuery()) { + var getResultsRequest = new GetAsyncResultRequest(initialResponse.asyncExecutionId().get()); + scriptPermits.release(numberOfDocs()); + getResultsRequest.setWaitForCompletionTimeout(timeValueSeconds(10)); + var result = client().execute(EsqlAsyncGetResultAction.INSTANCE, getResultsRequest).get(); + assertThat(result.isRunning(), equalTo(false)); + assertThat(result.isPartial(), equalTo(false)); + result.close(); + } + } + + private static EsqlQueryResponse sendAsyncQuery() { + scriptPermits.drainPermits(); + return EsqlQueryRequestBuilder.newAsyncEsqlQueryRequestBuilder(client()) + // Ensures there is no TopN pushdown to lucene, and that the pause happens after the TopN operator has been applied. + .query("from test | sort foo + 1 | limit 1 | where pause_me + 1 > 42 | stats sum(pause_me)") + .pragmas( + new QueryPragmas( + Settings.builder() + // Configured to ensure that there is only one worker handling all the shards, so that we can assert the correct + // expected behavior. + .put(QueryPragmas.MAX_CONCURRENT_NODES_PER_CLUSTER.getKey(), 1) + .put(QueryPragmas.MAX_CONCURRENT_SHARDS_PER_NODE.getKey(), SHARD_COUNT) + .put(QueryPragmas.TASK_CONCURRENCY.getKey(), 1) + .build() + ) + ) + .execute() + .actionGet(1, TimeUnit.MINUTES); + } + + public static class TopNPausableFieldPlugin extends AbstractPauseFieldPlugin { + @Override + protected boolean onWait() throws InterruptedException { + var acquired = scriptPermits.tryAcquire(SHARD_COUNT, 1, TimeUnit.MINUTES); + assertTrue("Failed to acquire permits", acquired); + int closed = 0; + int open = 0; + for (SearchContext searchContext : searchContexts) { + if (searchContext.isClosed()) { + closed++; + } else { + open++; + } + } + assertThat( + Strings.format("most contexts to be closed, but %d were closed and %d were open", closed, open), + closed, + greaterThanOrEqualTo(open) + ); + return scriptPermits.tryAcquire(1, 1, TimeUnit.MINUTES); + } + } +} diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupFromIndexIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupFromIndexIT.java index c652cb09be6f..1355ffba796a 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupFromIndexIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/LookupFromIndexIT.java @@ -184,6 +184,7 @@ public class LookupFromIndexIT extends AbstractEsqlIntegTestCase { ) { ShardContext esqlContext = new EsPhysicalOperationProviders.DefaultShardContext( 0, + searchContext, searchContext.getSearchExecutionContext(), AliasFilter.EMPTY ); diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/ManyShardsIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/ManyShardsIT.java index e74c47b9f71d..40d88064fa5d 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/ManyShardsIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/ManyShardsIT.java @@ -252,7 +252,7 @@ public class ManyShardsIT extends AbstractEsqlIntegTestCase { for (SearchService searchService : searchServices) { SearchContextCounter counter = new SearchContextCounter(pragmas.maxConcurrentShardsPerNode()); var mockSearchService = (MockSearchService) searchService; - mockSearchService.setOnPutContext(r -> counter.onNewContext()); + mockSearchService.setOnCreateSearchContext(r -> counter.onNewContext()); mockSearchService.setOnRemoveContext(r -> counter.onContextReleased()); } run(syncEsqlQueryRequest().query(q).pragmas(pragmas)).close(); @@ -260,7 +260,7 @@ public class ManyShardsIT extends AbstractEsqlIntegTestCase { } finally { for (SearchService searchService : searchServices) { var mockSearchService = (MockSearchService) searchService; - mockSearchService.setOnPutContext(r -> {}); + mockSearchService.setOnCreateSearchContext(r -> {}); mockSearchService.setOnRemoveContext(r -> {}); } } diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/spatial/SpatialNoLicenseTestCase.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/spatial/SpatialNoLicenseTestCase.java index c3a770ed375e..4ccbf4dd9164 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/spatial/SpatialNoLicenseTestCase.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/spatial/SpatialNoLicenseTestCase.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.spatial; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.License; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.license.internal.XPackLicenseStatus; @@ -49,6 +50,10 @@ public abstract class SpatialNoLicenseTestCase extends ESIntegTestCase { * This is used to test the behavior of spatial functions when no valid license is present. */ public static class TestEsqlPlugin extends EsqlPlugin { + public TestEsqlPlugin(Settings settings) { + super(settings); + } + protected XPackLicenseState getLicenseState() { return SpatialNoLicenseTestCase.getLicenseState(); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponse.java index ac4960ce9a13..93c30470c316 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponse.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponse.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.action; +import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Iterators; @@ -37,6 +38,7 @@ import java.util.Objects; import java.util.Optional; import static org.elasticsearch.TransportVersions.ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED; +import static org.elasticsearch.TransportVersions.ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED_8_19; public class EsqlQueryResponse extends org.elasticsearch.xpack.core.esql.action.EsqlQueryResponse implements @@ -120,8 +122,8 @@ public class EsqlQueryResponse extends org.elasticsearch.xpack.core.esql.action. } List columns = in.readCollectionAsList(ColumnInfoImpl::new); List pages = in.readCollectionAsList(Page::new); - long documentsFound = in.getTransportVersion().onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED) ? in.readVLong() : 0; - long valuesLoaded = in.getTransportVersion().onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED) ? in.readVLong() : 0; + long documentsFound = supportsValuesLoaded(in.getTransportVersion()) ? in.readVLong() : 0; + long valuesLoaded = supportsValuesLoaded(in.getTransportVersion()) ? in.readVLong() : 0; if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_12_0)) { profile = in.readOptionalWriteable(Profile::readFrom); } @@ -153,7 +155,7 @@ public class EsqlQueryResponse extends org.elasticsearch.xpack.core.esql.action. } out.writeCollection(columns); out.writeCollection(pages); - if (out.getTransportVersion().onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED)) { + if (supportsValuesLoaded(out.getTransportVersion())) { out.writeVLong(documentsFound); out.writeVLong(valuesLoaded); } @@ -166,6 +168,11 @@ public class EsqlQueryResponse extends org.elasticsearch.xpack.core.esql.action. } } + private static boolean supportsValuesLoaded(TransportVersion version) { + return version.onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED) + || version.isPatchFrom(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED_8_19); + } + public List columns() { return columns; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java index 0fb7387cfa42..9db9e1cabfa1 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.analysis; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.xpack.esql.LicenseAware; import org.elasticsearch.xpack.esql.capabilities.PostAnalysisPlanVerificationAware; @@ -60,7 +61,7 @@ public class Verifier { * Build a list of checks to perform on the plan. Each one is called once per * {@link LogicalPlan} node in the plan. */ - List> extra(); + List> extra(Settings settings); } /** @@ -69,15 +70,17 @@ public class Verifier { private final List extraCheckers; private final Metrics metrics; private final XPackLicenseState licenseState; + private final Settings settings; public Verifier(Metrics metrics, XPackLicenseState licenseState) { - this(metrics, licenseState, Collections.emptyList()); + this(metrics, licenseState, Collections.emptyList(), Settings.EMPTY); } - public Verifier(Metrics metrics, XPackLicenseState licenseState, List extraCheckers) { + public Verifier(Metrics metrics, XPackLicenseState licenseState, List extraCheckers, Settings settings) { this.metrics = metrics; this.licenseState = licenseState; this.extraCheckers = extraCheckers; + this.settings = settings; } /** @@ -102,7 +105,7 @@ public class Verifier { // collect plan checkers var planCheckers = planCheckers(plan); for (ExtraCheckers e : extraCheckers) { - planCheckers.addAll(e.extra()); + planCheckers.addAll(e.extra(settings)); } // Concrete verifications diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/AbstractLookupService.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/AbstractLookupService.java index ea78252197e3..1c21f9205360 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/AbstractLookupService.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/AbstractLookupService.java @@ -343,7 +343,7 @@ public abstract class AbstractLookupService extraCheckers + List extraCheckers, + Settings settings ) { this.indexResolver = indexResolver; this.preAnalyzer = new PreAnalyzer(); this.functionRegistry = new EsqlFunctionRegistry(); this.mapper = new Mapper(); this.metrics = new Metrics(functionRegistry); - this.verifier = new Verifier(metrics, licenseState, extraCheckers); + this.verifier = new Verifier(metrics, licenseState, extraCheckers, settings); this.planTelemetryManager = new PlanTelemetryManager(meterRegistry); this.queryLog = queryLog; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java index 5f675adbcd50..dab2b35025aa 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java @@ -30,7 +30,8 @@ import static org.elasticsearch.xpack.esql.optimizer.LogicalPlanOptimizer.operat * This class is part of the planner. Data node level logical optimizations. At this point we have access to * {@link org.elasticsearch.xpack.esql.stats.SearchStats} which provides access to metadata about the index. * - *

    NB: This class also reapplies all the rules from {@link LogicalPlanOptimizer#operators()} and {@link LogicalPlanOptimizer#cleanup()} + *

    NB: This class also reapplies all the rules from {@link LogicalPlanOptimizer#operators(boolean)} + * and {@link LogicalPlanOptimizer#cleanup()} */ public class LocalLogicalPlanOptimizer extends ParameterizedRuleExecutor { @@ -58,8 +59,8 @@ public class LocalLogicalPlanOptimizer extends ParameterizedRuleExecutor localOperators() { - var operators = operators(); - var rules = operators().rules(); + var operators = operators(true); + var rules = operators.rules(); List> newRules = new ArrayList<>(rules.length); // apply updates to existing rules that have different applicability locally diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java index f64f4fe38ac0..14a858f85fd2 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java @@ -82,7 +82,7 @@ import java.util.List; *

  1. The {@link LogicalPlanOptimizer#substitutions()} phase rewrites things to expand out shorthand in the syntax. For example, * a nested expression embedded in a stats gets replaced with an eval followed by a stats, followed by another eval. This phase * also applies surrogates, such as replacing an average with a sum divided by a count.
  2. - *
  3. {@link LogicalPlanOptimizer#operators()} (NB: The word "operator" is extremely overloaded and referrers to many different + *
  4. {@link LogicalPlanOptimizer#operators(boolean)} (NB: The word "operator" is extremely overloaded and referrers to many different * things.) transform the tree in various different ways. This includes folding (i.e. computing constant expressions at parse * time), combining expressions, dropping redundant clauses, and some normalization such as putting literals on the right whenever * possible. These rules are run in a loop until none of the rules make any changes to the plan (there is also a safety shut off @@ -90,14 +90,14 @@ import java.util.List; *
  5. {@link LogicalPlanOptimizer#cleanup()} Which can replace sorts+limit with a TopN
  6. * * - *

    Note that the {@link LogicalPlanOptimizer#operators()} and {@link LogicalPlanOptimizer#cleanup()} steps are reapplied at the + *

    Note that the {@link LogicalPlanOptimizer#operators(boolean)} and {@link LogicalPlanOptimizer#cleanup()} steps are reapplied at the * {@link LocalLogicalPlanOptimizer} layer.

    */ public class LogicalPlanOptimizer extends ParameterizedRuleExecutor { private static final List> RULES = List.of( substitutions(), - operators(), + operators(false), new Batch<>("Skip Compute", new SkipQueryOnLimitZero()), cleanup(), new Batch<>("Set as Optimized", Limiter.ONCE, new SetAsOptimized()) @@ -160,10 +160,10 @@ public class LogicalPlanOptimizer extends ParameterizedRuleExecutor operators() { + protected static Batch operators(boolean local) { return new Batch<>( "Operator Optimization", - new CombineProjections(), + new CombineProjections(local), new CombineEvals(), new PruneEmptyPlans(), new PropagateEmptyRelation(), diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/CombineProjections.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/CombineProjections.java index e9ca958c5e97..0a3f0cca0d9e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/CombineProjections.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/CombineProjections.java @@ -18,18 +18,24 @@ import org.elasticsearch.xpack.esql.core.expression.NamedExpression; import org.elasticsearch.xpack.esql.core.expression.ReferenceAttribute; import org.elasticsearch.xpack.esql.expression.function.grouping.GroupingFunction; import org.elasticsearch.xpack.esql.plan.logical.Aggregate; +import org.elasticsearch.xpack.esql.plan.logical.Eval; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.esql.plan.logical.Project; import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; public final class CombineProjections extends OptimizerRules.OptimizerRule { + // don't drop groupings from a local plan, as the layout has already been agreed upon + private final boolean local; - public CombineProjections() { + public CombineProjections(boolean local) { super(OptimizerRules.TransformDirection.UP); + this.local = local; } @Override @@ -60,27 +66,91 @@ public final class CombineProjections extends OptimizerRules.OptimizerRule groupingAttrs = new ArrayList<>(a.groupings().size()); - for (Expression grouping : groupings) { - if (grouping instanceof Attribute attribute) { - groupingAttrs.add(attribute); - } else if (grouping instanceof Alias as && as.child() instanceof GroupingFunction.NonEvaluatableGroupingFunction) { - groupingAttrs.add(as); + if (plan instanceof Aggregate a && child instanceof Project p) { + var groupings = a.groupings(); + + // sanity checks + for (Expression grouping : groupings) { + if ((grouping instanceof Attribute + || grouping instanceof Alias as && as.child() instanceof GroupingFunction.NonEvaluatableGroupingFunction) == false) { + // After applying ReplaceAggregateNestedExpressionWithEval, + // evaluatable groupings can only contain attributes. + throw new EsqlIllegalArgumentException("Expected an attribute or grouping function, got {}", grouping); + } + } + assert groupings.size() <= 1 + || groupings.stream() + .anyMatch(group -> group.anyMatch(expr -> expr instanceof GroupingFunction.NonEvaluatableGroupingFunction)) == false + : "CombineProjections only tested with a single CATEGORIZE with no additional groups"; + + // Collect the alias map for resolving the source (f1 = 1, f2 = f1, etc..) + AttributeMap.Builder aliasesBuilder = AttributeMap.builder(); + for (NamedExpression ne : p.projections()) { + // Record the aliases. + // Projections are just aliases for attributes, so casting is safe. + aliasesBuilder.put(ne.toAttribute(), (Attribute) Alias.unwrap(ne)); + } + var aliases = aliasesBuilder.build(); + + // Propagate any renames from the lower projection into the upper groupings. + List resolvedGroupings = new ArrayList<>(); + for (Expression grouping : groupings) { + Expression transformed = grouping.transformUp(Attribute.class, as -> aliases.resolve(as, as)); + resolvedGroupings.add(transformed); + } + + // This can lead to duplicates in the groupings: e.g. + // | EVAL x = y | STATS ... BY x, y + if (local) { + // On the data node, the groupings must be preserved because they affect the physical output (see + // AbstractPhysicalOperationProviders#intermediateAttributes). + // In case that propagating the lower projection leads to duplicates in the resolved groupings, we'll leave an Eval in place + // of the original projection to create new attributes for the duplicate groups. + Set seenResolvedGroupings = new HashSet<>(resolvedGroupings.size()); + List newGroupings = new ArrayList<>(); + List aliasesAgainstDuplication = new ArrayList<>(); + + for (int i = 0; i < groupings.size(); i++) { + Expression resolvedGrouping = resolvedGroupings.get(i); + if (seenResolvedGroupings.add(resolvedGrouping)) { + newGroupings.add(resolvedGrouping); } else { - // After applying ReplaceAggregateNestedExpressionWithEval, - // evaluatable groupings can only contain attributes. - throw new EsqlIllegalArgumentException("Expected an Attribute, got {}", grouping); + // resolving the renames leads to a duplicate here - we need to alias the underlying attribute this refers to. + // should really only be 1 attribute, anyway, but going via .references() includes the case of a + // GroupingFunction.NonEvaluatableGroupingFunction. + Attribute coreAttribute = resolvedGrouping.references().iterator().next(); + + Alias renameAgainstDuplication = new Alias( + coreAttribute.source(), + TemporaryNameUtils.locallyUniqueTemporaryName(coreAttribute.name()), + coreAttribute + ); + aliasesAgainstDuplication.add(renameAgainstDuplication); + + // propagate the new alias into the new grouping + AttributeMap.Builder resolverBuilder = AttributeMap.builder(); + resolverBuilder.put(coreAttribute, renameAgainstDuplication.toAttribute()); + AttributeMap resolver = resolverBuilder.build(); + + newGroupings.add(resolvedGrouping.transformUp(Attribute.class, attr -> resolver.resolve(attr, attr))); } } - plan = a.with( - p.child(), - combineUpperGroupingsAndLowerProjections(groupingAttrs, p.projections()), - combineProjections(a.aggregates(), p.projections()) - ); + + LogicalPlan newChild = aliasesAgainstDuplication.isEmpty() + ? p.child() + : new Eval(p.source(), p.child(), aliasesAgainstDuplication); + plan = a.with(newChild, newGroupings, combineProjections(a.aggregates(), p.projections())); + } else { + // On the coordinator, we can just discard the duplicates. + // All substitutions happen before; groupings must be attributes at this point except for non-evaluatable groupings which + // will be an alias like `c = CATEGORIZE(attribute)`. + // Due to such aliases, we can't use an AttributeSet to deduplicate. But we can use a regular set to deduplicate based on + // regular equality (i.e. based on names) instead of name ids. + // TODO: The deduplication based on simple equality will be insufficient in case of multiple non-evaluatable groupings, e.g. + // for `| EVAL x = y | STATS ... BY CATEGORIZE(x), CATEGORIZE(y)`. That will require semantic equality instead. Also + // applies in the local case below. + List newGroupings = new ArrayList<>(new LinkedHashSet<>(resolvedGroupings)); + plan = a.with(p.child(), newGroupings, combineProjections(a.aggregates(), p.projections())); } } @@ -143,39 +213,6 @@ public final class CombineProjections extends OptimizerRules.OptimizerRule combineUpperGroupingsAndLowerProjections( - List upperGroupings, - List lowerProjections - ) { - assert upperGroupings.size() <= 1 - || upperGroupings.stream() - .anyMatch(group -> group.anyMatch(expr -> expr instanceof GroupingFunction.NonEvaluatableGroupingFunction)) == false - : "CombineProjections only tested with a single CATEGORIZE with no additional groups"; - // Collect the alias map for resolving the source (f1 = 1, f2 = f1, etc..) - AttributeMap.Builder aliasesBuilder = AttributeMap.builder(); - for (NamedExpression ne : lowerProjections) { - // Record the aliases. - // Projections are just aliases for attributes, so casting is safe. - aliasesBuilder.put(ne.toAttribute(), (Attribute) Alias.unwrap(ne)); - } - var aliases = aliasesBuilder.build(); - - // Propagate any renames from the lower projection into the upper groupings. - // This can lead to duplicates: e.g. - // | EVAL x = y | STATS ... BY x, y - // All substitutions happen before; groupings must be attributes at this point except for non-evaluatable groupings which will be - // an alias like `c = CATEGORIZE(attribute)`. - // Therefore, it is correct to deduplicate based on simple equality (based on names) instead of name ids (Set vs. AttributeSet). - // TODO: The deduplication based on simple equality will be insufficient in case of multiple non-evaluatable groupings, e.g. for - // `| EVAL x = y | STATS ... BY CATEGORIZE(x), CATEGORIZE(y)`. That will require semantic equality instead. - LinkedHashSet resolvedGroupings = new LinkedHashSet<>(); - for (NamedExpression ne : upperGroupings) { - NamedExpression transformed = (NamedExpression) ne.transformUp(Attribute.class, a -> aliases.resolve(a, a)); - resolvedGroupings.add(transformed); - } - return new ArrayList<>(resolvedGroupings); - } - /** * Replace grouping alias previously contained in the aggregations that might have been projected away. */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/IdentifierBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/IdentifierBuilder.java index 91b8606c4030..9268dd08bc7e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/IdentifierBuilder.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/IdentifierBuilder.java @@ -20,17 +20,22 @@ import org.elasticsearch.xpack.esql.parser.EsqlBaseParser.IdentifierContext; import org.elasticsearch.xpack.esql.parser.EsqlBaseParser.IndexStringContext; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.SelectorResolver.SELECTOR_SEPARATOR; import static org.elasticsearch.transport.RemoteClusterAware.REMOTE_CLUSTER_INDEX_SEPARATOR; -import static org.elasticsearch.transport.RemoteClusterAware.isRemoteIndexName; +import static org.elasticsearch.transport.RemoteClusterAware.splitIndexName; import static org.elasticsearch.xpack.esql.core.util.StringUtils.EXCLUSION; import static org.elasticsearch.xpack.esql.core.util.StringUtils.WILDCARD; import static org.elasticsearch.xpack.esql.parser.ParserUtils.source; abstract class IdentifierBuilder extends AbstractBuilder { + private static final String BLANK_INDEX_ERROR_MESSAGE = "Blank index specified in index pattern"; + + private static final String INVALID_ESQL_CHARS = Strings.INVALID_FILENAME_CHARS.replace("'*',", ""); + @Override public String visitIdentifier(IdentifierContext ctx) { return ctx == null ? null : unquoteIdentifier(ctx.QUOTED_IDENTIFIER(), ctx.UNQUOTED_IDENTIFIER()); @@ -88,39 +93,21 @@ abstract class IdentifierBuilder extends AbstractBuilder { String indexPattern = c.unquotedIndexString() != null ? c.unquotedIndexString().getText() : visitIndexString(c.indexString()); String clusterString = visitClusterString(c.clusterString()); String selectorString = visitSelectorString(c.selectorString()); - // skip validating index on remote cluster, because the behavior of remote cluster is not consistent with local cluster - // For example, invalid#index is an invalid index name, however FROM *:invalid#index does not return an error - if (clusterString == null) { - hasSeenStar.set(indexPattern.contains(WILDCARD) || hasSeenStar.get()); - validateIndexPattern(indexPattern, c, hasSeenStar.get()); - // Other instances of Elasticsearch may have differing selectors so only validate selector string if remote cluster - // string is unset - if (selectorString != null) { - try { - // Ensures that the selector provided is one of the valid kinds - IndexNameExpressionResolver.SelectorResolver.validateIndexSelectorString(indexPattern, selectorString); - } catch (InvalidIndexNameException e) { - throw new ParsingException(e, source(c), e.getMessage()); - } - } - } else { - validateClusterString(clusterString, c); - // Do not allow selectors on remote cluster expressions until they are supported - if (selectorString != null) { - throwOnMixingSelectorWithCluster(reassembleIndexName(clusterString, indexPattern, selectorString), c); - } - } + + hasSeenStar.set(hasSeenStar.get() || indexPattern.contains(WILDCARD)); + validate(clusterString, indexPattern, selectorString, c, hasSeenStar.get()); patterns.add(reassembleIndexName(clusterString, indexPattern, selectorString)); }); return Strings.collectionToDelimitedString(patterns, ","); } + private static void throwInvalidIndexNameException(String indexPattern, String message, EsqlBaseParser.IndexPatternContext ctx) { + var ie = new InvalidIndexNameException(indexPattern, message); + throw new ParsingException(ie, source(ctx), ie.getMessage()); + } + private static void throwOnMixingSelectorWithCluster(String indexPattern, EsqlBaseParser.IndexPatternContext c) { - InvalidIndexNameException ie = new InvalidIndexNameException( - indexPattern, - "Selectors are not yet supported on remote cluster patterns" - ); - throw new ParsingException(ie, source(c), ie.getMessage()); + throwInvalidIndexNameException(indexPattern, "Selectors are not yet supported on remote cluster patterns", c); } private static String reassembleIndexName(String clusterString, String indexPattern, String selectorString) { @@ -144,56 +131,192 @@ abstract class IdentifierBuilder extends AbstractBuilder { } } - private static void validateIndexPattern(String indexPattern, EsqlBaseParser.IndexPatternContext ctx, boolean hasSeenStar) { - // multiple index names can be in the same double quote, e.g. indexPattern = "idx1, *, -idx2" - String[] indices = indexPattern.split(","); - boolean hasExclusion = false; - for (String index : indices) { - // Strip spaces off first because validation checks are not written to handle them - index = index.strip(); - if (isRemoteIndexName(index)) { // skip the validation if there is remote cluster - // Ensure that there are no selectors as they are not yet supported - if (index.contains(SELECTOR_SEPARATOR)) { - throwOnMixingSelectorWithCluster(index, ctx); - } - continue; + /** + * Takes the parsed constituent strings and validates them. + * @param clusterString Name of the remote cluster. Can be null. + * @param indexPattern Name of the index or pattern; can also have multiple patterns in case of quoting, + * e.g. {@code FROM """index*,-index1"""}. + * @param selectorString Selector string, i.e. "::data" or "::failures". Can be null. + * @param ctx Index Pattern Context for generating error messages with offsets. + * @param hasSeenStar If we've seen an asterisk so far. + */ + private static void validate( + String clusterString, + String indexPattern, + String selectorString, + EsqlBaseParser.IndexPatternContext ctx, + boolean hasSeenStar + ) { + /* + * At this point, only 3 formats are possible: + * "index_pattern(s)", + * remote:index_pattern, and, + * index_pattern::selector_string. + * + * The grammar prohibits remote:"index_pattern(s)" or "index_pattern(s)"::selector_string as they're + * partially quoted. So if either of cluster string or selector string are present, there's no need + * to split the pattern by comma since comma requires partial quoting. + */ + + String[] patterns; + if (clusterString == null && selectorString == null) { + // Pattern could be quoted or is singular like "index_name". + patterns = indexPattern.split(",", -1); + } else { + // Either of cluster string or selector string is present. Pattern is unquoted. + patterns = new String[] { indexPattern }; + } + + patterns = Arrays.stream(patterns).map(String::strip).toArray(String[]::new); + if (Arrays.stream(patterns).anyMatch(String::isBlank)) { + throwInvalidIndexNameException(indexPattern, BLANK_INDEX_ERROR_MESSAGE, ctx); + } + + // Edge case: happens when all the index names in a pattern are empty like "FROM ",,,,,"". + if (patterns.length == 0) { + throwInvalidIndexNameException(indexPattern, BLANK_INDEX_ERROR_MESSAGE, ctx); + } else if (patterns.length == 1) { + // Pattern is either an unquoted string or a quoted string with a single index (no comma sep). + validateSingleIndexPattern(clusterString, patterns[0], selectorString, ctx, hasSeenStar); + } else { + /* + * Presence of multiple patterns requires a comma and comma requires quoting. If quoting is present, + * cluster string and selector string cannot be present; they need to be attached within the quoting. + * So we attempt to extract them later. + */ + for (String pattern : patterns) { + validateSingleIndexPattern(null, pattern, null, ctx, hasSeenStar); } + } + } + + /** + * Validates the constituent strings. Will extract the cluster string and/or selector string from the index + * name if clubbed together inside a quoted string. + * + * @param clusterString Name of the remote cluster. Can be null. + * @param indexName Name of the index. + * @param selectorString Selector string, i.e. "::data" or "::failures". Can be null. + * @param ctx Index Pattern Context for generating error messages with offsets. + * @param hasSeenStar If we've seen an asterisk so far. + */ + private static void validateSingleIndexPattern( + String clusterString, + String indexName, + String selectorString, + EsqlBaseParser.IndexPatternContext ctx, + boolean hasSeenStar + ) { + indexName = indexName.strip(); + + /* + * Precedence: + * 1. Cannot mix cluster and selector strings. + * 2. Cluster string must be valid. + * 3. Index name must be valid. + * 4. Selector string must be valid. + * + * Since cluster string and/or selector string can be clubbed with the index name, we must try to + * manually extract them before we attempt to do #2, #3, and #4. + */ + + // It is possible to specify a pattern like "remote_cluster:index_name". Try to extract such details from the index string. + if (clusterString == null && selectorString == null) { try { - Tuple splitPattern = IndexNameExpressionResolver.splitSelectorExpression(index); - if (splitPattern.v2() != null) { - index = splitPattern.v1(); + var split = splitIndexName(indexName); + clusterString = split[0]; + indexName = split[1]; + } catch (IllegalArgumentException e) { + throw new ParsingException(e, source(ctx), e.getMessage()); + } + } + + // At the moment, selector strings for remote indices is not allowed. + if (clusterString != null && selectorString != null) { + throwOnMixingSelectorWithCluster(reassembleIndexName(clusterString, indexName, selectorString), ctx); + } + + // Validation in the right precedence. + if (clusterString != null) { + clusterString = clusterString.strip(); + validateClusterString(clusterString, ctx); + } + + /* + * It is possible for selector string to be attached to the index: "index_name::selector_string". + * Try to extract the selector string. + */ + try { + Tuple splitPattern = IndexNameExpressionResolver.splitSelectorExpression(indexName); + if (splitPattern.v2() != null) { + // Cluster string too was clubbed with the index name like selector string. + if (clusterString != null) { + throwOnMixingSelectorWithCluster(reassembleIndexName(clusterString, splitPattern.v1(), splitPattern.v2()), ctx); + } else { + // We've seen a selectorString. Use it. + selectorString = splitPattern.v2(); } + } + + indexName = splitPattern.v1(); + } catch (InvalidIndexNameException e) { + throw new ParsingException(e, source(ctx), e.getMessage()); + } + + resolveAndValidateIndex(indexName, ctx, hasSeenStar); + if (selectorString != null) { + selectorString = selectorString.strip(); + try { + // Ensures that the selector provided is one of the valid kinds. + IndexNameExpressionResolver.SelectorResolver.validateIndexSelectorString(indexName, selectorString); } catch (InvalidIndexNameException e) { - // throws exception if the selector expression is invalid. Selector resolution does not complain about exclusions throw new ParsingException(e, source(ctx), e.getMessage()); } - hasSeenStar = index.contains(WILDCARD) || hasSeenStar; - index = index.replace(WILDCARD, "").strip(); - if (index.isBlank()) { - continue; + } + } + + private static void resolveAndValidateIndex(String index, EsqlBaseParser.IndexPatternContext ctx, boolean hasSeenStar) { + // If index name is blank without any replacements, it was likely blank right from the beginning and is invalid. + if (index.isBlank()) { + throwInvalidIndexNameException(index, BLANK_INDEX_ERROR_MESSAGE, ctx); + } + + hasSeenStar = hasSeenStar || index.contains(WILDCARD); + index = index.replace(WILDCARD, "").strip(); + if (index.isBlank()) { + return; + } + var hasExclusion = index.startsWith(EXCLUSION); + index = removeExclusion(index); + String tempName; + try { + // remove the exclusion outside of <>, from index names with DateMath expression, + // e.g. -<-logstash-{now/d}> becomes <-logstash-{now/d}> before calling resolveDateMathExpression + tempName = IndexNameExpressionResolver.resolveDateMathExpression(index); + } catch (ElasticsearchParseException e) { + // throws exception if the DateMath expression is invalid, resolveDateMathExpression does not complain about exclusions + throw new ParsingException(e, source(ctx), e.getMessage()); + } + hasExclusion = tempName.startsWith(EXCLUSION) || hasExclusion; + index = tempName.equals(index) ? index : removeExclusion(tempName); + try { + MetadataCreateIndexService.validateIndexOrAliasName(index, InvalidIndexNameException::new); + } catch (InvalidIndexNameException e) { + // ignore invalid index name if it has exclusions and there is an index with wildcard before it + if (hasSeenStar && hasExclusion) { + return; } - hasExclusion = index.startsWith(EXCLUSION); - index = removeExclusion(index); - String tempName; - try { - // remove the exclusion outside of <>, from index names with DateMath expression, - // e.g. -<-logstash-{now/d}> becomes <-logstash-{now/d}> before calling resolveDateMathExpression - tempName = IndexNameExpressionResolver.resolveDateMathExpression(index); - } catch (ElasticsearchParseException e) { - // throws exception if the DateMath expression is invalid, resolveDateMathExpression does not complain about exclusions - throw new ParsingException(e, source(ctx), e.getMessage()); - } - hasExclusion = tempName.startsWith(EXCLUSION) || hasExclusion; - index = tempName.equals(index) ? index : removeExclusion(tempName); - try { - MetadataCreateIndexService.validateIndexOrAliasName(index, InvalidIndexNameException::new); - } catch (InvalidIndexNameException e) { - // ignore invalid index name if it has exclusions and there is an index with wildcard before it - if (hasSeenStar && hasExclusion) { - continue; - } - throw new ParsingException(e, source(ctx), e.getMessage()); + + /* + * We only modify this particular message because it mentions '*' as an invalid char. + * However, we do allow asterisk in the index patterns: wildcarded patterns. Let's not + * mislead the user by mentioning this char in the error message. + */ + if (e.getMessage().contains("must not contain the following characters")) { + throwInvalidIndexNameException(index, "must not contain the following characters " + INVALID_ESQL_CHARS, ctx); } + + throw new ParsingException(e, source(ctx), e.getMessage()); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsPhysicalOperationProviders.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsPhysicalOperationProviders.java index 0b2fafcf2df2..acf685f3dcd9 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsPhysicalOperationProviders.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsPhysicalOperationProviders.java @@ -7,6 +7,9 @@ package org.elasticsearch.xpack.esql.planner; +import org.apache.lucene.document.FieldType; +import org.apache.lucene.index.DocValuesType; +import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.SortedSetDocValues; import org.apache.lucene.search.BooleanClause; @@ -14,6 +17,7 @@ import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.elasticsearch.common.logging.HeaderWarning; +import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.compute.aggregation.AggregatorMode; import org.elasticsearch.compute.aggregation.GroupingAggregator; import org.elasticsearch.compute.aggregation.blockhash.BlockHash; @@ -31,7 +35,9 @@ import org.elasticsearch.compute.operator.Operator; import org.elasticsearch.compute.operator.OrdinalsGroupingOperator; import org.elasticsearch.compute.operator.SourceOperator; import org.elasticsearch.compute.operator.TimeSeriesAggregationOperator; +import org.elasticsearch.core.AbstractRefCounted; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Releasable; import org.elasticsearch.index.IndexMode; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.AnalysisRegistry; @@ -76,7 +82,6 @@ import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -90,14 +95,41 @@ public class EsPhysicalOperationProviders extends AbstractPhysicalOperationProvi private static final Logger logger = LogManager.getLogger(EsPhysicalOperationProviders.class); /** - * Context of each shard we're operating against. + * Context of each shard we're operating against. Note these objects are shared across multiple operators as + * {@link org.elasticsearch.core.RefCounted}. */ - public interface ShardContext extends org.elasticsearch.compute.lucene.ShardContext { + public abstract static class ShardContext implements org.elasticsearch.compute.lucene.ShardContext, Releasable { + private final AbstractRefCounted refCounted = new AbstractRefCounted() { + @Override + protected void closeInternal() { + ShardContext.this.close(); + } + }; + + @Override + public void incRef() { + refCounted.incRef(); + } + + @Override + public boolean tryIncRef() { + return refCounted.tryIncRef(); + } + + @Override + public boolean decRef() { + return refCounted.decRef(); + } + + @Override + public boolean hasReferences() { + return refCounted.hasReferences(); + } /** * Convert a {@link QueryBuilder} into a real {@link Query lucene query}. */ - Query toQuery(QueryBuilder queryBuilder); + public abstract Query toQuery(QueryBuilder queryBuilder); /** * Tuning parameter for deciding when to use the "merge" stored field loader. @@ -107,7 +139,7 @@ public class EsPhysicalOperationProviders extends AbstractPhysicalOperationProvi * A value of {@code .2} means we'll use the sequential reader even if we only * need one in ten documents. */ - double storedFieldsSequentialProportion(); + public abstract double storedFieldsSequentialProportion(); } private final List shardContexts; @@ -177,19 +209,39 @@ public class EsPhysicalOperationProviders extends AbstractPhysicalOperationProvi /** A hack to pretend an unmapped field still exists. */ private static class DefaultShardContextForUnmappedField extends DefaultShardContext { + private static final FieldType UNMAPPED_FIELD_TYPE = new FieldType(KeywordFieldMapper.Defaults.FIELD_TYPE); + static { + UNMAPPED_FIELD_TYPE.setDocValuesType(DocValuesType.NONE); + UNMAPPED_FIELD_TYPE.setIndexOptions(IndexOptions.NONE); + UNMAPPED_FIELD_TYPE.setStored(false); + UNMAPPED_FIELD_TYPE.freeze(); + } private final KeywordEsField unmappedEsField; DefaultShardContextForUnmappedField(DefaultShardContext ctx, PotentiallyUnmappedKeywordEsField unmappedEsField) { - super(ctx.index, ctx.ctx, ctx.aliasFilter); + super(ctx.index, ctx.releasable, ctx.ctx, ctx.aliasFilter); this.unmappedEsField = unmappedEsField; } @Override public @Nullable MappedFieldType fieldType(String name) { var superResult = super.fieldType(name); - return superResult == null && name.equals(unmappedEsField.getName()) - ? new KeywordFieldMapper.KeywordFieldType(name, false /* isIndexed */, false /* hasDocValues */, Map.of() /* meta */) - : superResult; + return superResult == null && name.equals(unmappedEsField.getName()) ? createUnmappedFieldType(name, this) : superResult; + } + + static MappedFieldType createUnmappedFieldType(String name, DefaultShardContext context) { + var builder = new KeywordFieldMapper.Builder(name, context.ctx.indexVersionCreated()); + builder.docValues(false); + builder.indexed(false); + return new KeywordFieldMapper.KeywordFieldType( + name, + UNMAPPED_FIELD_TYPE, + Lucene.KEYWORD_ANALYZER, + Lucene.KEYWORD_ANALYZER, + Lucene.KEYWORD_ANALYZER, + builder, + context.ctx.isSourceSynthetic() + ); } } @@ -349,18 +401,24 @@ public class EsPhysicalOperationProviders extends AbstractPhysicalOperationProvi ); } - public static class DefaultShardContext implements ShardContext { + public static class DefaultShardContext extends ShardContext { private final int index; + /** + * In production, this will be a {@link org.elasticsearch.search.internal.SearchContext}, but we don't want to drag that huge + * dependency here. + */ + private final Releasable releasable; private final SearchExecutionContext ctx; private final AliasFilter aliasFilter; private final String shardIdentifier; - public DefaultShardContext(int index, SearchExecutionContext ctx, AliasFilter aliasFilter) { + public DefaultShardContext(int index, Releasable releasable, SearchExecutionContext ctx, AliasFilter aliasFilter) { this.index = index; + this.releasable = releasable; this.ctx = ctx; this.aliasFilter = aliasFilter; // Build the shardIdentifier once up front so we can reuse references to it in many places. - this.shardIdentifier = ctx.getFullyQualifiedIndex().getName() + ":" + ctx.getShardId(); + this.shardIdentifier = this.ctx.getFullyQualifiedIndex().getName() + ":" + this.ctx.getShardId(); } @Override @@ -473,6 +531,11 @@ public class EsPhysicalOperationProviders extends AbstractPhysicalOperationProvi public double storedFieldsSequentialProportion() { return EsqlPlugin.STORED_FIELDS_SEQUENTIAL_PROPORTION.get(ctx.getIndexSettings().getSettings()); } + + @Override + public void close() { + releasable.close(); + } } private static class TypeConvertingBlockLoader implements BlockLoader { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeResponse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeResponse.java index 4f55a2a6e8ce..ac49213d6151 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeResponse.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeResponse.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.plugin; +import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.common.io.stream.StreamInput; @@ -20,6 +21,7 @@ import java.io.IOException; import java.util.List; import static org.elasticsearch.TransportVersions.ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED; +import static org.elasticsearch.TransportVersions.ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED_8_19; /** * The compute result of {@link DataNodeRequest} or {@link ClusterComputeRequest} @@ -58,7 +60,7 @@ final class ComputeResponse extends TransportResponse { } ComputeResponse(StreamInput in) throws IOException { - if (in.getTransportVersion().onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED)) { + if (supportsCompletionInfo(in.getTransportVersion())) { completionInfo = DriverCompletionInfo.readFrom(in); } else if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_12_0)) { if (in.readBoolean()) { @@ -92,7 +94,7 @@ final class ComputeResponse extends TransportResponse { @Override public void writeTo(StreamOutput out) throws IOException { - if (out.getTransportVersion().onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED)) { + if (supportsCompletionInfo(out.getTransportVersion())) { completionInfo.writeTo(out); } else if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_12_0)) { out.writeBoolean(true); @@ -111,6 +113,11 @@ final class ComputeResponse extends TransportResponse { } } + private static boolean supportsCompletionInfo(TransportVersion version) { + return version.onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED) + || version.isPatchFrom(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED_8_19); + } + public DriverCompletionInfo getCompletionInfo() { return completionInfo; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java index 6d15f88a26f1..4adc97d28fee 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java @@ -26,6 +26,7 @@ import org.elasticsearch.compute.operator.exchange.ExchangeService; import org.elasticsearch.compute.operator.exchange.ExchangeSink; import org.elasticsearch.compute.operator.exchange.ExchangeSinkHandler; import org.elasticsearch.compute.operator.exchange.ExchangeSourceHandler; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.Releasables; import org.elasticsearch.core.Tuple; @@ -541,7 +542,12 @@ public class ComputeService { } }; contexts.add( - new EsPhysicalOperationProviders.DefaultShardContext(i, searchExecutionContext, searchContext.request().getAliasFilter()) + new EsPhysicalOperationProviders.DefaultShardContext( + i, + searchContext, + searchExecutionContext, + searchContext.request().getAliasFilter() + ) ); } EsPhysicalOperationProviders physicalOperationProviders = new EsPhysicalOperationProviders( @@ -579,6 +585,9 @@ public class ComputeService { LOGGER.debug("Local execution plan:\n{}", localExecutionPlan.describe()); } var drivers = localExecutionPlan.createDrivers(context.sessionId()); + // After creating the drivers (and therefore, the operators), we can safely decrement the reference count since the operators + // will hold a reference to the contexts where relevant. + contexts.forEach(RefCounted::decRef); if (drivers.isEmpty()) { throw new IllegalStateException("no drivers created"); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/DataNodeComputeResponse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/DataNodeComputeResponse.java index cdf418df1526..e21f6d7e44b5 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/DataNodeComputeResponse.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/DataNodeComputeResponse.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.plugin; +import org.elasticsearch.TransportVersion; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.compute.operator.DriverCompletionInfo; @@ -19,6 +20,7 @@ import java.util.List; import java.util.Map; import static org.elasticsearch.TransportVersions.ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED; +import static org.elasticsearch.TransportVersions.ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED_8_19; /** * The compute result of {@link DataNodeRequest} @@ -33,7 +35,7 @@ final class DataNodeComputeResponse extends TransportResponse { } DataNodeComputeResponse(StreamInput in) throws IOException { - if (in.getTransportVersion().onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED)) { + if (supportsCompletionInfo(in.getTransportVersion())) { this.completionInfo = DriverCompletionInfo.readFrom(in); this.shardLevelFailures = in.readMap(ShardId::new, StreamInput::readException); return; @@ -49,7 +51,7 @@ final class DataNodeComputeResponse extends TransportResponse { @Override public void writeTo(StreamOutput out) throws IOException { - if (out.getTransportVersion().onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED)) { + if (supportsCompletionInfo(out.getTransportVersion())) { completionInfo.writeTo(out); out.writeMap(shardLevelFailures, (o, v) -> v.writeTo(o), StreamOutput::writeException); return; @@ -65,6 +67,11 @@ final class DataNodeComputeResponse extends TransportResponse { new ComputeResponse(completionInfo).writeTo(out); } + private static boolean supportsCompletionInfo(TransportVersion version) { + return version.onOrAfter(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED) + || version.isPatchFrom(ESQL_DOCUMENTS_FOUND_AND_VALUES_LOADED_8_19); + } + public DriverCompletionInfo completionInfo() { return completionInfo; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java index 19f926b36ef2..293a7be6be04 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java @@ -190,6 +190,11 @@ public class EsqlPlugin extends Plugin implements ActionPlugin, ExtensiblePlugin ); private final List extraCheckers = new ArrayList<>(); + private final Settings settings; + + public EsqlPlugin(Settings settings) { + this.settings = settings; + } @Override public Collection createComponents(PluginServices services) { @@ -209,7 +214,8 @@ public class EsqlPlugin extends Plugin implements ActionPlugin, ExtensiblePlugin services.telemetryProvider().getMeterRegistry(), getLicenseState(), new EsqlQueryLog(services.clusterService().getClusterSettings(), services.slowLogFieldProvider()), - extraCheckers + extraCheckers, + settings ), new ExchangeService( services.clusterService().getSettings(), diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/QueryPragmas.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/QueryPragmas.java index 29951070a96c..345bf3b8767e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/QueryPragmas.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/QueryPragmas.java @@ -35,7 +35,7 @@ public final class QueryPragmas implements Writeable { public static final Setting EXCHANGE_CONCURRENT_CLIENTS = Setting.intSetting("exchange_concurrent_clients", 2); public static final Setting ENRICH_MAX_WORKERS = Setting.intSetting("enrich_max_workers", 1); - private static final Setting TASK_CONCURRENCY = Setting.intSetting( + public static final Setting TASK_CONCURRENCY = Setting.intSetting( "task_concurrency", ThreadPool.searchOrGetThreadPoolSize(EsExecutors.allocatedProcessors(Settings.EMPTY)) ); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/TransportEsqlQueryAction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/TransportEsqlQueryAction.java index 4cc928fe07cb..6629b0b09d08 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/TransportEsqlQueryAction.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/TransportEsqlQueryAction.java @@ -349,7 +349,7 @@ public class TransportEsqlQueryAction extends HandledTransportAction throw new IllegalArgumentException(); } - ; return new EsqlQueryResponse(columns, pages, documentsFound, valuesLoaded, profile, columnar, isAsync, executionInfo); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/NamedWriteablesTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/NamedWriteablesTests.java index 1fba8f66d0f1..186120a59d3f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/NamedWriteablesTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/NamedWriteablesTests.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.esql.action; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.compute.operator.Operator; import org.elasticsearch.compute.operator.topn.TopNOperatorStatus; import org.elasticsearch.test.ESTestCase; @@ -18,7 +19,7 @@ import static org.hamcrest.Matchers.equalTo; public class NamedWriteablesTests extends ESTestCase { public void testTopNStatus() throws Exception { - try (EsqlPlugin plugin = new EsqlPlugin()) { + try (EsqlPlugin plugin = new EsqlPlugin(Settings.EMPTY)) { NamedWriteableRegistry registry = new NamedWriteableRegistry(plugin.getNamedWriteables()); TopNOperatorStatus origin = new TopNOperatorStatus( randomNonNegativeInt(), diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexOperatorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexOperatorTests.java index dabcc6cbce89..f3bdf29688b9 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexOperatorTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/LookupFromIndexOperatorTests.java @@ -35,6 +35,7 @@ import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.DriverContext; import org.elasticsearch.compute.operator.Operator; import org.elasticsearch.compute.operator.SourceOperator; +import org.elasticsearch.compute.test.NoOpReleasable; import org.elasticsearch.compute.test.OperatorTestCase; import org.elasticsearch.compute.test.SequenceLongBlockSourceOperator; import org.elasticsearch.core.IOUtils; @@ -246,11 +247,7 @@ public class LookupFromIndexOperatorTests extends OperatorTestCase { }"""); DirectoryReader reader = DirectoryReader.open(lookupIndexDirectory); SearchExecutionContext executionCtx = mapperHelper.createSearchExecutionContext(mapperService, newSearcher(reader)); - EsPhysicalOperationProviders.DefaultShardContext ctx = new EsPhysicalOperationProviders.DefaultShardContext( - 0, - executionCtx, - AliasFilter.EMPTY - ); + var ctx = new EsPhysicalOperationProviders.DefaultShardContext(0, new NoOpReleasable(), executionCtx, AliasFilter.EMPTY); return new AbstractLookupService.LookupShardContext(ctx, executionCtx, () -> { try { IOUtils.close(reader, mapperService); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java index 8251611dcfe4..f2b70c99253b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java @@ -744,6 +744,36 @@ public class LocalLogicalPlanOptimizerTests extends ESTestCase { assertEquals("integer_long_field", unionTypeField.fieldName().string()); } + /** + * \_Aggregate[[first_name{r}#7, $$first_name$temp_name$17{r}#18],[SUM(salary{f}#11,true[BOOLEAN]) AS SUM(salary)#5, first_nam + * e{r}#7, first_name{r}#7 AS last_name#10]] + * \_Eval[[null[KEYWORD] AS first_name#7, null[KEYWORD] AS $$first_name$temp_name$17#18]] + * \_EsRelation[test][_meta_field{f}#12, emp_no{f}#6, first_name{f}#7, ge..] + */ + public void testGroupingByMissingFields() { + var plan = plan("FROM test | STATS SUM(salary) BY first_name, last_name"); + var testStats = statsForMissingField("first_name", "last_name"); + var localPlan = localPlan(plan, testStats); + Limit limit = as(localPlan, Limit.class); + Aggregate aggregate = as(limit.child(), Aggregate.class); + assertThat(aggregate.groupings(), hasSize(2)); + ReferenceAttribute grouping1 = as(aggregate.groupings().get(0), ReferenceAttribute.class); + ReferenceAttribute grouping2 = as(aggregate.groupings().get(1), ReferenceAttribute.class); + Eval eval = as(aggregate.child(), Eval.class); + assertThat(eval.fields(), hasSize(2)); + Alias eval1 = eval.fields().get(0); + Literal literal1 = as(eval1.child(), Literal.class); + assertNull(literal1.value()); + assertThat(literal1.dataType(), is(DataType.KEYWORD)); + Alias eval2 = eval.fields().get(1); + Literal literal2 = as(eval2.child(), Literal.class); + assertNull(literal2.value()); + assertThat(literal2.dataType(), is(DataType.KEYWORD)); + assertThat(grouping1.id(), equalTo(eval1.id())); + assertThat(grouping2.id(), equalTo(eval2.id())); + as(eval.child(), EsRelation.class); + } + private IsNotNull isNotNull(Expression field) { return new IsNotNull(EMPTY, field); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java index cb5c53193949..dc7256e3c452 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java @@ -472,10 +472,12 @@ public class StatementParserTests extends AbstractStatementParserTests { "foo, test-*, abc, xyz", test123 """); assertStringAsIndexPattern("foo,test,xyz", command + " foo, test,xyz"); + assertStringAsIndexPattern("", command + " "); assertStringAsIndexPattern( ",", command + " , \"\"" ); + assertStringAsIndexPattern("", command + " \"\""); assertStringAsIndexPattern( "-,-<-logstash-{now/M{yyyy.MM}}>," + "-,-<-logstash-{now/d{yyyy.MM.dd|+12:00}}>", @@ -494,18 +496,30 @@ public class StatementParserTests extends AbstractStatementParserTests { lineNumber, "mismatched input '\"index|pattern\"' expecting UNQUOTED_SOURCE" ); - assertStringAsIndexPattern("*:index|pattern", command + " \"*:index|pattern\""); + // Entire index pattern is quoted. So it's not a parse error but a semantic error where the index name + // is invalid. + expectError(command + " \"*:index|pattern\"", "Invalid index name [index|pattern], must not contain the following characters"); clusterAndIndexAsIndexPattern(command, "cluster:index"); clusterAndIndexAsIndexPattern(command, "cluster:.index"); clusterAndIndexAsIndexPattern(command, "cluster*:index*"); - clusterAndIndexAsIndexPattern(command, "cluster*:*");// this is not a valid pattern, * should be inside <> - clusterAndIndexAsIndexPattern(command, "cluster*:"); + clusterAndIndexAsIndexPattern(command, "cluster*:*"); clusterAndIndexAsIndexPattern(command, "cluster*:*"); clusterAndIndexAsIndexPattern(command, "*:index*"); clusterAndIndexAsIndexPattern(command, "*:*"); + expectError( + command + " \"cluster:index|pattern\"", + "Invalid index name [index|pattern], must not contain the following characters" + ); + expectError(command + " *:\"index|pattern\"", "expecting UNQUOTED_SOURCE"); if (EsqlCapabilities.Cap.INDEX_COMPONENT_SELECTORS.isEnabled()) { assertStringAsIndexPattern("foo::data", command + " foo::data"); assertStringAsIndexPattern("foo::failures", command + " foo::failures"); + expectErrorWithLineNumber( + command + " *,\"-foo\"::data", + "*,-foo::data", + lineNumber, + "mismatched input '::' expecting {, '|', ',', 'metadata'}" + ); expectErrorWithLineNumber( command + " cluster:\"foo::data\"", " cluster:\"foo::data\"", @@ -585,6 +599,7 @@ public class StatementParserTests extends AbstractStatementParserTests { Map commands = new HashMap<>(); commands.put("FROM {}", "line 1:6: "); if (Build.current().isSnapshot()) { + commands.put("TS {}", "line 1:4: "); commands.put("ROW x = 1 | LOOKUP_🐔 {} ON j", "line 1:22: "); } String lineNumber; @@ -625,7 +640,11 @@ public class StatementParserTests extends AbstractStatementParserTests { expectInvalidIndexNameErrorWithLineNumber(command, "index::failure", lineNumber); // Cluster name cannot be combined with selector yet. - var parseLineNumber = command.contains("FROM") ? 6 : 9; + int parseLineNumber = 6; + if (command.startsWith("TS")) { + parseLineNumber = 4; + } + expectDoubleColonErrorWithLineNumber(command, "cluster:foo::data", parseLineNumber + 11); expectDoubleColonErrorWithLineNumber(command, "cluster:foo::failures", parseLineNumber + 11); @@ -633,19 +652,18 @@ public class StatementParserTests extends AbstractStatementParserTests { expectErrorWithLineNumber( command, "cluster:\"foo\"::data", - "line 1:14: ", + command.startsWith("FROM") ? "line 1:14: " : "line 1:12: ", "mismatched input '\"foo\"' expecting UNQUOTED_SOURCE" ); expectErrorWithLineNumber( command, "cluster:\"foo\"::failures", - "line 1:14: ", + command.startsWith("FROM") ? "line 1:14: " : "line 1:12: ", "mismatched input '\"foo\"' expecting UNQUOTED_SOURCE" ); - // TODO: Edge case that will be invalidated in follow up (https://github.com/elastic/elasticsearch/issues/122651) - // expectDoubleColonErrorWithLineNumber(command, "\"cluster:foo\"::data", parseLineNumber + 13); - // expectDoubleColonErrorWithLineNumber(command, "\"cluster:foo\"::failures", parseLineNumber + 13); + expectDoubleColonErrorWithLineNumber(command, "\"cluster:foo\"::data", parseLineNumber + 13); + expectDoubleColonErrorWithLineNumber(command, "\"cluster:foo\"::failures", parseLineNumber + 13); expectErrorWithLineNumber( command, @@ -689,7 +707,7 @@ public class StatementParserTests extends AbstractStatementParserTests { expectErrorWithLineNumber( command, "cluster:\"index,index2\"::failures", - "line 1:14: ", + command.startsWith("FROM") ? "line 1:14: " : "line 1:12: ", "mismatched input '\"index,index2\"' expecting UNQUOTED_SOURCE" ); } @@ -746,16 +764,33 @@ public class StatementParserTests extends AbstractStatementParserTests { clustersAndIndices(command, "index*", "-index#pattern"); clustersAndIndices(command, "*", "-<--logstash-{now/M{yyyy.MM}}>"); clustersAndIndices(command, "index*", "-<--logstash#-{now/M{yyyy.MM}}>"); + expectInvalidIndexNameErrorWithLineNumber(command, "*, index#pattern", lineNumber, "index#pattern", "must not contain '#'"); + expectInvalidIndexNameErrorWithLineNumber( + command, + "index*, index#pattern", + indexStarLineNumber, + "index#pattern", + "must not contain '#'" + ); + expectDateMathErrorWithLineNumber(command, "cluster*:", commands.get(command), dateMathError); expectDateMathErrorWithLineNumber(command, "*, \"-<-logstash-{now/D}>\"", lineNumber, dateMathError); expectDateMathErrorWithLineNumber(command, "*, -<-logstash-{now/D}>", lineNumber, dateMathError); expectDateMathErrorWithLineNumber(command, "\"*, -<-logstash-{now/D}>\"", commands.get(command), dateMathError); expectDateMathErrorWithLineNumber(command, "\"*, -<-logst:ash-{now/D}>\"", commands.get(command), dateMathError); if (EsqlCapabilities.Cap.INDEX_COMPONENT_SELECTORS.isEnabled()) { - clustersAndIndices(command, "*", "-index#pattern::data"); - clustersAndIndices(command, "*", "-index#pattern::data"); + clustersAndIndices(command, "*", "-index::data"); + clustersAndIndices(command, "*", "-index::failures"); + clustersAndIndices(command, "*", "-index*pattern::data"); + clustersAndIndices(command, "*", "-index*pattern::failures"); + + // This is by existing design: refer to the comment in IdentifierBuilder#resolveAndValidateIndex() in the last + // catch clause. If there's an index with a wildcard before an invalid index, we don't error out. clustersAndIndices(command, "index*", "-index#pattern::data"); clustersAndIndices(command, "*", "-<--logstash-{now/M{yyyy.MM}}>::data"); clustersAndIndices(command, "index*", "-<--logstash#-{now/M{yyyy.MM}}>::data"); + + expectError(command + "index1,", "unit [-] not supported for date math [+-/d]"); + // Throw on invalid date math expectDateMathErrorWithLineNumber( command, @@ -3140,6 +3175,128 @@ public class StatementParserTests extends AbstractStatementParserTests { assertThat(joinType.coreJoin().joinName(), equalTo("LEFT OUTER")); } + public void testInvalidFromPatterns() { + var sourceCommands = Build.current().isSnapshot() ? new String[] { "FROM", "TS" } : new String[] { "FROM" }; + var indexIsBlank = "Blank index specified in index pattern"; + var remoteIsEmpty = "remote part is empty"; + var invalidDoubleColonUsage = "invalid usage of :: separator"; + + expectError(randomFrom(sourceCommands) + " \"\"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \" \"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \",,,\"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \",,, \"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \", , ,,\"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \",,,\",*", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"*,\"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"*,,,\"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"index1,,,,\"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"index1,index2,,\"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"index1,<-+^,index2\",*", "must not contain the following characters"); + expectError(randomFrom(sourceCommands) + " \"\",*", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"*: ,*,\"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"*: ,*,\",validIndexName", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"\", \" \", \" \",validIndexName", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"index1\", \"index2\", \" ,index3,index4\"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"index1,index2,,index3\"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"index1,index2, ,index3\"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"*, \"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"*\", \"\"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"*\", \" \"", indexIsBlank); + expectError(randomFrom(sourceCommands) + " \"*\", \":index1\"", remoteIsEmpty); + expectError(randomFrom(sourceCommands) + " \"index1,*,:index2\"", remoteIsEmpty); + expectError(randomFrom(sourceCommands) + " \"*\", \"::data\"", remoteIsEmpty); + expectError(randomFrom(sourceCommands) + " \"*\", \"::failures\"", remoteIsEmpty); + expectError(randomFrom(sourceCommands) + " \"*,index1::\"", invalidDoubleColonUsage); + expectError(randomFrom(sourceCommands) + " \"*\", index1, index2, \"index3:: \"", invalidDoubleColonUsage); + expectError(randomFrom(sourceCommands) + " \"*,index1::*\"", invalidDoubleColonUsage); + } + + public void testInvalidPatternsWithIntermittentQuotes() { + // There are 3 ways of crafting invalid index patterns that conforms to the grammar defined through ANTLR. + // 1. Not quoting the pattern, + // 2. Quoting individual patterns ("index1", "index2", ...), and, + // 3. Clubbing all the patterns into a single quoted string ("index1,index2,...). + // + // Note that in these tests, we unquote a pattern and then quote it immediately. + // This is because when randomly generating an index pattern, it may look like: "foo"::data. + // To convert it into a quoted string like "foo::data", we need to unquote and then re-quote it. + + // Prohibited char in a quoted cross cluster index pattern should result in an error. + { + var randomIndex = randomIndexPattern(); + // Select an invalid char to sneak in. + // Note: some chars like '|' and '"' are excluded to generate a proper invalid name. + Character[] invalidChars = { ' ', '/', '<', '>', '?' }; + var randomInvalidChar = randomFrom(invalidChars); + + // Construct the new invalid index pattern. + var invalidIndexName = "foo" + randomInvalidChar + "bar"; + var remoteIndexWithInvalidChar = quote(randomIdentifier() + ":" + invalidIndexName); + var query = "FROM " + randomIndex + "," + remoteIndexWithInvalidChar; + expectError( + query, + "Invalid index name [" + + invalidIndexName + + "], must not contain the following characters [' ','\"',',','/','<','>','?','\\','|']" + ); + } + + // Colon outside a quoted string should result in an ANTLR error: a comma is expected. + { + var randomIndex = randomIndexPattern(); + + // In the form of: "*|cluster alias:random string". + var malformedClusterAlias = quote((randomBoolean() ? "*" : randomIdentifier()) + ":" + randomIdentifier()); + + // We do not generate a cross cluster pattern or else we'd be getting a different error (which is tested in + // the next test). + var remoteIndex = quote(unquoteIndexPattern(randomIndexPattern(without(CROSS_CLUSTER)))); + // Format: FROM , "": + var query = "FROM " + randomIndex + "," + malformedClusterAlias + ":" + remoteIndex; + expectError(query, " mismatched input ':'"); + } + + // If an explicit cluster string is present, then we expect an unquoted string next. + { + var randomIndex = randomIndexPattern(); + var remoteClusterAlias = randomBoolean() ? "*" : randomIdentifier(); + // In the form of: random string:random string. + var malformedRemoteIndex = quote(unquoteIndexPattern(randomIndexPattern(CROSS_CLUSTER))); + // Format: FROM , :"random string:random string" + var query = "FROM " + randomIndex + "," + remoteClusterAlias + ":" + malformedRemoteIndex; + // Since "random string:random string" is partially quoted, expect a ANTLR's parse error. + expectError(query, "expecting UNQUOTED_SOURCE"); + } + + if (EsqlCapabilities.Cap.INDEX_COMPONENT_SELECTORS.isEnabled()) { + // If a stream in on a remote and the pattern is entirely quoted, we should be able to validate it. + // Note: invalid selector syntax is covered in a different test. + { + var fromPattern = randomIndexPattern(); + var malformedIndexSelectorPattern = quote( + (randomIdentifier()) + ":" + unquoteIndexPattern(randomIndexPattern(INDEX_SELECTOR, without(CROSS_CLUSTER))) + ); + // Format: FROM , ":::" + var query = "FROM " + fromPattern + "," + malformedIndexSelectorPattern; + expectError(query, "Selectors are not yet supported on remote cluster patterns"); + } + + // If a stream in on a remote and the cluster alias and index pattern are separately quoted, we should + // still be able to validate it. + // Note: invalid selector syntax is covered in a different test. + { + var fromPattern = randomIndexPattern(); + var malformedIndexSelectorPattern = quote(randomIdentifier()) + + ":" + + quote(unquoteIndexPattern(randomIndexPattern(INDEX_SELECTOR, without(CROSS_CLUSTER)))); + // Format: FROM , "":"::" + var query = "FROM " + fromPattern + "," + malformedIndexSelectorPattern; + // Everything after "" is extraneous input and hence ANTLR's error. + expectError(query, "mismatched input ':'"); + } + } + } + public void testInvalidJoinPatterns() { assumeTrue("LOOKUP JOIN requires corresponding capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled()); @@ -3172,6 +3329,18 @@ public class StatementParserTests extends AbstractStatementParserTests { // If one or more patterns participating in LOOKUP JOINs are partially quoted, we expect the partial quoting // error messages to take precedence over any LOOKUP JOIN error messages. + + { + // Generate a syntactically invalid (partial quoted) pattern. + var fromPatterns = quote(randomIdentifier()) + ":" + unquoteIndexPattern(randomIndexPattern(without(CROSS_CLUSTER))); + var joinPattern = randomIndexPattern(); + expectError( + "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + // Since the from pattern is partially quoted, we get an error at the end of the partially quoted string. + " mismatched input ':'" + ); + } + { // Generate a syntactically invalid (partial quoted) pattern. var fromPatterns = randomIdentifier() + ":" + quote(randomIndexPatterns(without(CROSS_CLUSTER))); @@ -3184,6 +3353,17 @@ public class StatementParserTests extends AbstractStatementParserTests { ); } + { + var fromPatterns = randomIndexPattern(); + // Generate a syntactically invalid (partial quoted) pattern. + var joinPattern = quote(randomIdentifier()) + ":" + unquoteIndexPattern(randomIndexPattern(without(CROSS_CLUSTER))); + expectError( + "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + // Since the join pattern is partially quoted, we get an error at the end of the partially quoted string. + "mismatched input ':'" + ); + } + { var fromPatterns = randomIndexPattern(); // Generate a syntactically invalid (partial quoted) pattern. @@ -3251,6 +3431,31 @@ public class StatementParserTests extends AbstractStatementParserTests { + "], index pattern selectors are not supported in LOOKUP JOIN" ); } + + { + // Although we don't support selector strings for remote indices, it's alright. + // The parser error message takes precedence. + var fromPatterns = randomIndexPatterns(); + var joinPattern = quote(randomIdentifier()) + "::" + randomFrom("data", "failures"); + // After the end of the partially quoted string, i.e. the index name, parser now expects "ON..." and not a selector string. + expectError( + "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + "mismatched input ':' expecting 'on'" + ); + } + + { + // Although we don't support selector strings for remote indices, it's alright. + // The parser error message takes precedence. + var fromPatterns = randomIndexPatterns(); + var joinPattern = randomIdentifier() + "::" + quote(randomFrom("data", "failures")); + // After the index name and "::", parser expects an unquoted string, i.e. the selector string should not be + // partially quoted. + expectError( + "FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(), + " mismatched input ':' expecting UNQUOTED_SOURCE" + ); + } } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlannerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlannerTests.java index 0fee0c13178d..9bc2118c0451 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlannerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlannerTests.java @@ -23,14 +23,21 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.compute.lucene.DataPartitioning; import org.elasticsearch.compute.lucene.LuceneSourceOperator; import org.elasticsearch.compute.lucene.LuceneTopNSourceOperator; +import org.elasticsearch.compute.lucene.ValuesSourceReaderOperator; import org.elasticsearch.compute.operator.SourceOperator; +import org.elasticsearch.compute.test.NoOpReleasable; import org.elasticsearch.compute.test.TestBlockFactory; import org.elasticsearch.core.IOUtils; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.Releasables; import org.elasticsearch.index.IndexMode; import org.elasticsearch.index.cache.query.TrivialQueryCachingPolicy; +import org.elasticsearch.index.mapper.BlockLoader; +import org.elasticsearch.index.mapper.BlockSourceReader; +import org.elasticsearch.index.mapper.FallbackSyntheticSourceBlockLoader; +import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperServiceTestCase; +import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.node.Node; import org.elasticsearch.plugins.ExtensiblePlugin; import org.elasticsearch.plugins.Plugin; @@ -42,10 +49,12 @@ import org.elasticsearch.xpack.esql.core.expression.Literal; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.core.type.EsField; +import org.elasticsearch.xpack.esql.core.type.PotentiallyUnmappedKeywordEsField; import org.elasticsearch.xpack.esql.core.util.StringUtils; import org.elasticsearch.xpack.esql.expression.Order; import org.elasticsearch.xpack.esql.index.EsIndex; import org.elasticsearch.xpack.esql.plan.physical.EsQueryExec; +import org.elasticsearch.xpack.esql.plan.physical.FieldExtractExec; import org.elasticsearch.xpack.esql.plan.physical.LimitExec; import org.elasticsearch.xpack.esql.plan.physical.ParallelExec; import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; @@ -64,6 +73,7 @@ import java.util.Map; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.lessThanOrEqualTo; public class LocalExecutionPlannerTests extends MapperServiceTestCase { @@ -84,10 +94,17 @@ public class LocalExecutionPlannerTests extends MapperServiceTestCase { private final ArrayList releasables = new ArrayList<>(); + private Settings settings = SETTINGS; + public LocalExecutionPlannerTests(@Name("estimatedRowSizeIsHuge") boolean estimatedRowSizeIsHuge) { this.estimatedRowSizeIsHuge = estimatedRowSizeIsHuge; } + @Override + protected Settings getIndexSettings() { + return settings; + } + @Override protected Collection getPlugins() { var plugin = new SpatialPlugin(); @@ -229,6 +246,47 @@ public class LocalExecutionPlannerTests extends MapperServiceTestCase { assertThat(plan.driverFactories, hasSize(2)); } + public void testPlanUnmappedFieldExtractStoredSource() throws Exception { + var blockLoader = constructBlockLoader(); + // In case of stored source we expect bytes based block source loader (this loads source from _source) + assertThat(blockLoader, instanceOf(BlockSourceReader.BytesRefsBlockLoader.class)); + } + + public void testPlanUnmappedFieldExtractSyntheticSource() throws Exception { + // Enables synthetic source, so that fallback synthetic source blocker loader is used: + settings = Settings.builder().put(settings).put("index.mapping.source.mode", "synthetic").build(); + + var blockLoader = constructBlockLoader(); + // In case of synthetic source we expect bytes based block source loader (this loads source from _ignored_source) + assertThat(blockLoader, instanceOf(FallbackSyntheticSourceBlockLoader.class)); + } + + private BlockLoader constructBlockLoader() throws IOException { + EsQueryExec queryExec = new EsQueryExec( + Source.EMPTY, + index().name(), + IndexMode.STANDARD, + index().indexNameWithModes(), + List.of(new FieldAttribute(Source.EMPTY, EsQueryExec.DOC_ID_FIELD.getName(), EsQueryExec.DOC_ID_FIELD)), + null, + null, + null, + between(1, 1000) + ); + FieldExtractExec fieldExtractExec = new FieldExtractExec( + Source.EMPTY, + queryExec, + List.of( + new FieldAttribute(Source.EMPTY, "potentially_unmapped", new PotentiallyUnmappedKeywordEsField("potentially_unmapped")) + ), + MappedFieldType.FieldExtractPreference.NONE + ); + LocalExecutionPlanner.LocalExecutionPlan plan = planner().plan("test", FoldContext.small(), fieldExtractExec); + var p = plan.driverFactories.get(0).driverSupplier().physicalOperation(); + var fieldInfo = ((ValuesSourceReaderOperator.Factory) p.intermediateOperatorFactories.get(0)).fields().get(0); + return fieldInfo.blockLoader().apply(0); + } + private int randomEstimatedRowSize(boolean huge) { int hugeBoundary = SourceOperator.MIN_TARGET_PAGE_SIZE * 10; return huge ? between(hugeBoundary, Integer.MAX_VALUE) : between(1, hugeBoundary); @@ -296,10 +354,11 @@ public class LocalExecutionPlannerTests extends MapperServiceTestCase { true ); for (int i = 0; i < numShards; i++) { + SearchExecutionContext searchExecutionContext = createSearchExecutionContext(createMapperService(mapping(b -> { + b.startObject("point").field("type", "geo_point").endObject(); + })), searcher); shardContexts.add( - new EsPhysicalOperationProviders.DefaultShardContext(i, createSearchExecutionContext(createMapperService(mapping(b -> { - b.startObject("point").field("type", "geo_point").endObject(); - })), searcher), AliasFilter.EMPTY) + new EsPhysicalOperationProviders.DefaultShardContext(i, new NoOpReleasable(), searchExecutionContext, AliasFilter.EMPTY) ); } releasables.add(searcher); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/TestPhysicalOperationProviders.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/TestPhysicalOperationProviders.java index d5aa1af7feec..a8916f140ea1 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/TestPhysicalOperationProviders.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/TestPhysicalOperationProviders.java @@ -26,6 +26,7 @@ import org.elasticsearch.compute.data.IntBlock; import org.elasticsearch.compute.data.IntVector; import org.elasticsearch.compute.data.LongBlock; import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.lucene.ShardRefCounted; import org.elasticsearch.compute.operator.DriverContext; import org.elasticsearch.compute.operator.HashAggregationOperator; import org.elasticsearch.compute.operator.Operator; @@ -188,6 +189,7 @@ public class TestPhysicalOperationProviders extends AbstractPhysicalOperationPro var page = pageIndex.page; BlockFactory blockFactory = driverContext.blockFactory(); DocVector docVector = new DocVector( + ShardRefCounted.ALWAYS_REFERENCED, // The shard ID is used to encode the index ID. blockFactory.newConstantIntVector(index, page.getPositionCount()), blockFactory.newConstantIntVector(0, page.getPositionCount()), diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/ClusterRequestTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/ClusterRequestTests.java index 9b7615d0cc37..ababc8ed3765 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/ClusterRequestTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/ClusterRequestTests.java @@ -57,7 +57,7 @@ public class ClusterRequestTests extends AbstractWireSerializingTestCase writeables = new ArrayList<>(); writeables.addAll(new SearchModule(Settings.EMPTY, List.of()).getNamedWriteables()); - writeables.addAll(new EsqlPlugin().getNamedWriteables()); + writeables.addAll(new EsqlPlugin(Settings.EMPTY).getNamedWriteables()); return new NamedWriteableRegistry(writeables); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/DataNodeRequestSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/DataNodeRequestSerializationTests.java index abf9b527f008..1fc481711df9 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/DataNodeRequestSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plugin/DataNodeRequestSerializationTests.java @@ -60,7 +60,7 @@ public class DataNodeRequestSerializationTests extends AbstractWireSerializingTe protected NamedWriteableRegistry getNamedWriteableRegistry() { List writeables = new ArrayList<>(); writeables.addAll(new SearchModule(Settings.EMPTY, List.of()).getNamedWriteables()); - writeables.addAll(new EsqlPlugin().getNamedWriteables()); + writeables.addAll(new EsqlPlugin(Settings.EMPTY).getNamedWriteables()); return new NamedWriteableRegistry(writeables); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/telemetry/PlanExecutorMetricsTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/telemetry/PlanExecutorMetricsTests.java index 752e61c240cd..2d8151d8fc2a 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/telemetry/PlanExecutorMetricsTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/telemetry/PlanExecutorMetricsTests.java @@ -150,7 +150,14 @@ public class PlanExecutorMetricsTests extends ESTestCase { return null; }).when(esqlClient).execute(eq(EsqlResolveFieldsAction.TYPE), any(), any()); - var planExecutor = new PlanExecutor(indexResolver, MeterRegistry.NOOP, new XPackLicenseState(() -> 0L), mockQueryLog(), List.of()); + var planExecutor = new PlanExecutor( + indexResolver, + MeterRegistry.NOOP, + new XPackLicenseState(() -> 0L), + mockQueryLog(), + List.of(), + Settings.EMPTY + ); var enrichResolver = mockEnrichResolver(); var request = new EsqlQueryRequest(); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java index 389bac89aaed..1dae42956dec 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java @@ -18,7 +18,7 @@ import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.ProjectState; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.LifecycleExecutionState; -import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.metadata.ProjectId; import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.cluster.metadata.SingleNodeShutdownMetadata; import org.elasticsearch.cluster.service.ClusterService; @@ -28,7 +28,7 @@ import org.elasticsearch.common.scheduler.SchedulerEngine; import org.elasticsearch.common.scheduler.TimeValueSchedule; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.AbstractRunnable; -import org.elasticsearch.core.FixForMultiProject; +import org.elasticsearch.core.NotMultiProjectCapable; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.TimeValue; @@ -183,9 +183,12 @@ public class IndexLifecycleService void onMaster(ClusterState clusterState) { maybeScheduleJob(); - // TODO multi-project: this probably needs a per-project iteration - @FixForMultiProject - final ProjectState state = clusterState.projectState(Metadata.DEFAULT_PROJECT_ID); + for (var projectId : clusterState.metadata().projects().keySet()) { + onMaster(clusterState.projectState(projectId)); + } + } + + void onMaster(ProjectState state) { final ProjectMetadata projectMetadata = state.metadata(); final IndexLifecycleMetadata currentMetadata = projectMetadata.custom(IndexLifecycleMetadata.TYPE); if (currentMetadata != null) { @@ -260,13 +263,13 @@ public class IndexLifecycleService } if (safeToStop && OperationMode.STOPPING == currentMode) { - stopILM(); + stopILM(state.projectId()); } } } - private void stopILM() { - submitUnbatchedTask("ilm_operation_mode_update[stopped]", OperationModeUpdateTask.ilmMode(OperationMode.STOPPED)); + private void stopILM(ProjectId projectId) { + submitUnbatchedTask("ilm_operation_mode_update[stopped]", OperationModeUpdateTask.ilmMode(projectId, OperationMode.STOPPED)); } @Override @@ -409,6 +412,7 @@ public class IndexLifecycleService }); } + @NotMultiProjectCapable(description = "See comment inside the method") @Override public void applyClusterState(ClusterChangedEvent event) { // only act if we are master, otherwise keep idle until elected @@ -416,20 +420,21 @@ public class IndexLifecycleService return; } - @FixForMultiProject - final IndexLifecycleMetadata ilmMetadata = event.state() - .metadata() - .getProject(Metadata.DEFAULT_PROJECT_ID) - .custom(IndexLifecycleMetadata.TYPE); - if (ilmMetadata == null) { - return; - } - final IndexLifecycleMetadata previousIlmMetadata = event.previousState() - .metadata() - .getProject(Metadata.DEFAULT_PROJECT_ID) - .custom(IndexLifecycleMetadata.TYPE); - if (event.previousState().nodes().isLocalNodeElectedMaster() == false || ilmMetadata != previousIlmMetadata) { - policyRegistry.update(ilmMetadata); + // We're updating the policy registry cache here, which doesn't actually work with multiple projects because the policies from one + // project would overwrite the polices from another project. However, since we're not planning on running ILM in a multi-project + // cluster, we can ignore this. + for (var project : event.state().metadata().projects().values()) { + final IndexLifecycleMetadata ilmMetadata = project.custom(IndexLifecycleMetadata.TYPE); + if (ilmMetadata == null) { + continue; + } + final var previousProject = event.previousState().metadata().projects().get(project.id()); + final IndexLifecycleMetadata previousIlmMetadata = previousProject == null + ? null + : previousProject.custom(IndexLifecycleMetadata.TYPE); + if (event.previousState().nodes().isLocalNodeElectedMaster() == false || ilmMetadata != previousIlmMetadata) { + policyRegistry.update(ilmMetadata); + } } } @@ -461,10 +466,13 @@ public class IndexLifecycleService * @param clusterState the current cluster state * @param fromClusterStateChange whether things are triggered from the cluster-state-listener or the scheduler */ - @FixForMultiProject void triggerPolicies(ClusterState clusterState, boolean fromClusterStateChange) { - @FixForMultiProject - final var state = clusterState.projectState(Metadata.DEFAULT_PROJECT_ID); + for (var projectId : clusterState.metadata().projects().keySet()) { + triggerPolicies(clusterState.projectState(projectId), fromClusterStateChange); + } + } + + void triggerPolicies(ProjectState state, boolean fromClusterStateChange) { final var projectMetadata = state.metadata(); IndexLifecycleMetadata currentMetadata = projectMetadata.custom(IndexLifecycleMetadata.TYPE); @@ -472,7 +480,7 @@ public class IndexLifecycleService if (currentMetadata == null) { if (currentMode == OperationMode.STOPPING) { // There are no policies and ILM is in stopping mode, so stop ILM and get out of here - stopILM(); + stopILM(state.projectId()); } return; } @@ -555,7 +563,7 @@ public class IndexLifecycleService } if (safeToStop && OperationMode.STOPPING == currentMode) { - stopILM(); + stopILM(state.projectId()); } } @@ -585,7 +593,7 @@ public class IndexLifecycleService return policyRegistry; } - static Set indicesOnShuttingDownNodesInDangerousStep(ClusterState state, String nodeId) { + static boolean hasIndicesInDangerousStepForNodeShutdown(ClusterState state, String nodeId) { final Set shutdownNodes = PluginShutdownService.shutdownTypeNodes( state, SingleNodeShutdownMetadata.Type.REMOVE, @@ -593,43 +601,46 @@ public class IndexLifecycleService SingleNodeShutdownMetadata.Type.REPLACE ); if (shutdownNodes.isEmpty()) { - return Set.of(); + return true; } - // Returning a set of strings will cause weird behavior with multiple projects - @FixForMultiProject - Set indicesPreventingShutdown = state.metadata() - .projects() - .values() - .stream() - .flatMap(project -> project.indices().entrySet().stream()) - // Filter out to only consider managed indices - .filter(indexToMetadata -> Strings.hasText(indexToMetadata.getValue().getLifecyclePolicyName())) - // Only look at indices in the shrink action - .filter(indexToMetadata -> ShrinkAction.NAME.equals(indexToMetadata.getValue().getLifecycleExecutionState().action())) - // Only look at indices on a step that may potentially be dangerous if we removed the node - .filter(indexToMetadata -> { - String step = indexToMetadata.getValue().getLifecycleExecutionState().step(); - return SetSingleNodeAllocateStep.NAME.equals(step) - || CheckShrinkReadyStep.NAME.equals(step) - || ShrinkStep.NAME.equals(step) - || ShrunkShardsAllocatedStep.NAME.equals(step); - }) - // Only look at indices where the node picked for the shrink is the node marked as shutting down - .filter(indexToMetadata -> { - String nodePicked = indexToMetadata.getValue() - .getSettings() - .get(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "_id"); - return nodeId.equals(nodePicked); - }) - .map(Map.Entry::getKey) - .collect(Collectors.toSet()); - logger.trace( - "with nodes marked as shutdown for removal {}, indices {} are preventing shutdown", - shutdownNodes, - indicesPreventingShutdown - ); - return indicesPreventingShutdown; + boolean result = true; + for (var project : state.metadata().projects().values()) { + Set indicesPreventingShutdown = project.indices() + .entrySet() + .stream() + // Filter out to only consider managed indices + .filter(indexToMetadata -> Strings.hasText(indexToMetadata.getValue().getLifecyclePolicyName())) + // Only look at indices in the shrink action + .filter(indexToMetadata -> ShrinkAction.NAME.equals(indexToMetadata.getValue().getLifecycleExecutionState().action())) + // Only look at indices on a step that may potentially be dangerous if we removed the node + .filter(indexToMetadata -> { + String step = indexToMetadata.getValue().getLifecycleExecutionState().step(); + return SetSingleNodeAllocateStep.NAME.equals(step) + || CheckShrinkReadyStep.NAME.equals(step) + || ShrinkStep.NAME.equals(step) + || ShrunkShardsAllocatedStep.NAME.equals(step); + }) + // Only look at indices where the node picked for the shrink is the node marked as shutting down + .filter(indexToMetadata -> { + String nodePicked = indexToMetadata.getValue() + .getSettings() + .get(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "_id"); + return nodeId.equals(nodePicked); + }) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + logger.trace( + "with nodes marked as shutdown for removal {}, indices {} in project {} are preventing shutdown", + shutdownNodes, + indicesPreventingShutdown, + project.id() + ); + if (indicesPreventingShutdown.isEmpty() == false) { + result = false; + } + } + return result; } @Override @@ -641,8 +652,7 @@ public class IndexLifecycleService case REPLACE: case REMOVE: case SIGTERM: - Set indices = indicesOnShuttingDownNodesInDangerousStep(clusterService.state(), nodeId); - return indices.isEmpty(); + return hasIndicesInDangerousStepForNodeShutdown(clusterService.state(), nodeId); default: throw new IllegalArgumentException("unknown shutdown type: " + shutdownType); } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportStartILMAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportStartILMAction.java index 5a2ff6d58bfa..d6964fd7c791 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportStartILMAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportStartILMAction.java @@ -15,6 +15,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.block.ClusterBlockLevel; +import org.elasticsearch.cluster.project.ProjectResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.core.SuppressForbidden; @@ -29,12 +30,15 @@ import org.elasticsearch.xpack.core.ilm.action.ILMActions; public class TransportStartILMAction extends AcknowledgedTransportMasterNodeAction { + private final ProjectResolver projectResolver; + @Inject public TransportStartILMAction( TransportService transportService, ClusterService clusterService, ThreadPool threadPool, - ActionFilters actionFilters + ActionFilters actionFilters, + ProjectResolver projectResolver ) { super( ILMActions.START.name(), @@ -45,13 +49,15 @@ public class TransportStartILMAction extends AcknowledgedTransportMasterNodeActi StartILMRequest::new, EsExecutors.DIRECT_EXECUTOR_SERVICE ); + this.projectResolver = projectResolver; } @Override protected void masterOperation(Task task, StartILMRequest request, ClusterState state, ActionListener listener) { + final var projectId = projectResolver.getProjectId(); submitUnbatchedTask( "ilm_operation_mode_update[running]", - OperationModeUpdateTask.wrap(OperationModeUpdateTask.ilmMode(OperationMode.RUNNING), request, listener) + OperationModeUpdateTask.wrap(OperationModeUpdateTask.ilmMode(projectId, OperationMode.RUNNING), request, listener) ); } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportStopILMAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportStopILMAction.java index 1c231da4ec13..fd8736eb2a28 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportStopILMAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportStopILMAction.java @@ -15,6 +15,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.block.ClusterBlockLevel; +import org.elasticsearch.cluster.project.ProjectResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.core.SuppressForbidden; @@ -29,12 +30,15 @@ import org.elasticsearch.xpack.core.ilm.action.ILMActions; public class TransportStopILMAction extends AcknowledgedTransportMasterNodeAction { + private final ProjectResolver projectResolver; + @Inject public TransportStopILMAction( TransportService transportService, ClusterService clusterService, ThreadPool threadPool, - ActionFilters actionFilters + ActionFilters actionFilters, + ProjectResolver projectResolver ) { super( ILMActions.STOP.name(), @@ -45,13 +49,15 @@ public class TransportStopILMAction extends AcknowledgedTransportMasterNodeActio StopILMRequest::new, EsExecutors.DIRECT_EXECUTOR_SERVICE ); + this.projectResolver = projectResolver; } @Override protected void masterOperation(Task task, StopILMRequest request, ClusterState state, ActionListener listener) { + final var projectId = projectResolver.getProjectId(); submitUnbatchedTask( "ilm_operation_mode_update[stopping]", - OperationModeUpdateTask.wrap(OperationModeUpdateTask.ilmMode(OperationMode.STOPPING), request, listener) + OperationModeUpdateTask.wrap(OperationModeUpdateTask.ilmMode(projectId, OperationMode.STOPPING), request, listener) ); } diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java index 68ab9613e9de..61eb44ee5497 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.LifecycleExecutionState; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.NodesShutdownMetadata; +import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.cluster.metadata.SingleNodeShutdownMetadata; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodeUtils; @@ -169,13 +170,11 @@ public class IndexLifecycleServiceTests extends ESTestCase { .numberOfReplicas(randomIntBetween(0, 5)) .build(); Map indices = Map.of(index.getName(), indexMetadata); - Metadata metadata = Metadata.builder() + var project = ProjectMetadata.builder(randomProjectIdOrDefault()) .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata(policyMap, OperationMode.STOPPED)) - .indices(indices) - .persistentSettings(settings(IndexVersion.current()).build()) - .build(); + .indices(indices); ClusterState currentState = ClusterState.builder(ClusterName.DEFAULT) - .metadata(metadata) + .putProjectMetadata(project) .nodes(DiscoveryNodes.builder().localNodeId(nodeId).masterNodeId(nodeId).add(masterNode).build()) .build(); ClusterChangedEvent event = new ClusterChangedEvent("_source", currentState, ClusterState.EMPTY_STATE); @@ -208,13 +207,11 @@ public class IndexLifecycleServiceTests extends ESTestCase { .numberOfReplicas(randomIntBetween(0, 5)) .build(); Map indices = Map.of(index.getName(), indexMetadata); - Metadata metadata = Metadata.builder() + var project = ProjectMetadata.builder(randomProjectIdOrDefault()) .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata(policyMap, OperationMode.STOPPING)) - .indices(indices) - .persistentSettings(settings(IndexVersion.current()).build()) - .build(); + .indices(indices); ClusterState currentState = ClusterState.builder(ClusterName.DEFAULT) - .metadata(metadata) + .putProjectMetadata(project) .nodes(DiscoveryNodes.builder().localNodeId(nodeId).masterNodeId(nodeId).add(masterNode).build()) .build(); @@ -264,13 +261,11 @@ public class IndexLifecycleServiceTests extends ESTestCase { .numberOfReplicas(randomIntBetween(0, 5)) .build(); Map indices = Map.of(index.getName(), indexMetadata); - Metadata metadata = Metadata.builder() + var project = ProjectMetadata.builder(randomProjectIdOrDefault()) .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata(policyMap, OperationMode.STOPPING)) - .indices(indices) - .persistentSettings(settings(IndexVersion.current()).build()) - .build(); + .indices(indices); ClusterState currentState = ClusterState.builder(ClusterName.DEFAULT) - .metadata(metadata) + .putProjectMetadata(project) .nodes(DiscoveryNodes.builder().localNodeId(nodeId).masterNodeId(nodeId).add(masterNode).build()) .build(); @@ -312,13 +307,11 @@ public class IndexLifecycleServiceTests extends ESTestCase { .numberOfReplicas(randomIntBetween(0, 5)) .build(); Map indices = Map.of(index.getName(), indexMetadata); - Metadata metadata = Metadata.builder() + var project = ProjectMetadata.builder(randomProjectIdOrDefault()) .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata(policyMap, OperationMode.STOPPING)) - .indices(indices) - .persistentSettings(settings(IndexVersion.current()).build()) - .build(); + .indices(indices); ClusterState currentState = ClusterState.builder(ClusterName.DEFAULT) - .metadata(metadata) + .putProjectMetadata(project) .nodes(DiscoveryNodes.builder().localNodeId(nodeId).masterNodeId(nodeId).add(masterNode).build()) .build(); @@ -429,11 +422,9 @@ public class IndexLifecycleServiceTests extends ESTestCase { .build(); Map indices = Map.of(index1.getName(), i1indexMetadata, index2.getName(), i2indexMetadata); - Metadata metadata = Metadata.builder() + var project = ProjectMetadata.builder(randomProjectIdOrDefault()) .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata(policyMap, OperationMode.RUNNING)) - .indices(indices) - .persistentSettings(settings(IndexVersion.current()).build()) - .build(); + .indices(indices); Settings settings = Settings.builder().put(LifecycleSettings.LIFECYCLE_POLL_INTERVAL, "1s").build(); var clusterSettings = new ClusterSettings( @@ -443,7 +434,7 @@ public class IndexLifecycleServiceTests extends ESTestCase { ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool, clusterSettings); DiscoveryNode node = clusterService.localNode(); ClusterState currentState = ClusterState.builder(ClusterName.DEFAULT) - .metadata(metadata) + .putProjectMetadata(project) .nodes(DiscoveryNodes.builder().add(node).masterNodeId(node.getId()).localNodeId(node.getId())) .build(); ClusterServiceUtils.setState(clusterService, currentState); @@ -533,15 +524,16 @@ public class IndexLifecycleServiceTests extends ESTestCase { } } - public void testIndicesOnShuttingDownNodesInDangerousStep() { + public void testHasIndicesInDangerousStepForNodeShutdown() { for (SingleNodeShutdownMetadata.Type type : List.of( SingleNodeShutdownMetadata.Type.REMOVE, SingleNodeShutdownMetadata.Type.SIGTERM, SingleNodeShutdownMetadata.Type.REPLACE )) { - ClusterState state = ClusterState.builder(ClusterName.DEFAULT).build(); - assertThat(IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "regular_node"), equalTo(Set.of())); - assertThat(IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "shutdown_node"), equalTo(Set.of())); + final var project = ProjectMetadata.builder(randomProjectIdOrDefault()).build(); + ClusterState state = ClusterState.builder(ClusterName.DEFAULT).putProjectMetadata(project).build(); + assertThat(IndexLifecycleService.hasIndicesInDangerousStepForNodeShutdown(state, "regular_node"), equalTo(true)); + assertThat(IndexLifecycleService.hasIndicesInDangerousStepForNodeShutdown(state, "shutdown_node"), equalTo(true)); IndexMetadata nonDangerousIndex = IndexMetadata.builder("no_danger") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_NAME, "mypolicy")) @@ -583,14 +575,12 @@ public class IndexLifecycleServiceTests extends ESTestCase { .build(); Map indices = Map.of("no_danger", nonDangerousIndex, "danger", dangerousIndex); - Metadata metadata = Metadata.builder() - .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata(Map.of(), OperationMode.RUNNING)) - .indices(indices) - .persistentSettings(settings(IndexVersion.current()).build()) - .build(); - state = ClusterState.builder(ClusterName.DEFAULT) - .metadata(metadata) + .putProjectMetadata( + ProjectMetadata.builder(project) + .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata(Map.of(), OperationMode.RUNNING)) + .indices(indices) + ) .nodes( DiscoveryNodes.builder() .localNodeId(nodeId) @@ -613,8 +603,8 @@ public class IndexLifecycleServiceTests extends ESTestCase { .build(); // No danger yet, because no node is shutting down - assertThat(IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "regular_node"), equalTo(Set.of())); - assertThat(IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "shutdown_node"), equalTo(Set.of())); + assertThat(IndexLifecycleService.hasIndicesInDangerousStepForNodeShutdown(state, "regular_node"), equalTo(true)); + assertThat(IndexLifecycleService.hasIndicesInDangerousStepForNodeShutdown(state, "shutdown_node"), equalTo(true)); state = ClusterState.builder(state) .metadata( @@ -638,12 +628,12 @@ public class IndexLifecycleServiceTests extends ESTestCase { ) .build(); - assertThat(IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "regular_node"), equalTo(Set.of())); + assertThat(IndexLifecycleService.hasIndicesInDangerousStepForNodeShutdown(state, "regular_node"), equalTo(true)); // No danger, because this is a "RESTART" type shutdown assertThat( "restart type shutdowns are not considered dangerous", - IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "shutdown_node"), - equalTo(Set.of()) + IndexLifecycleService.hasIndicesInDangerousStepForNodeShutdown(state, "shutdown_node"), + equalTo(true) ); final String targetNodeName = type == SingleNodeShutdownMetadata.Type.REPLACE ? randomAlphaOfLengthBetween(10, 20) : null; @@ -673,7 +663,7 @@ public class IndexLifecycleServiceTests extends ESTestCase { .build(); // The dangerous index should be calculated as being in danger now - assertThat(IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "shutdown_node"), equalTo(Set.of("danger"))); + assertThat(IndexLifecycleService.hasIndicesInDangerousStepForNodeShutdown(state, "shutdown_node"), equalTo(false)); } } } diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportStopILMActionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportStopILMActionTests.java index 073cb5554443..7569a70155c9 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportStopILMActionTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportStopILMActionTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.cluster.AckedClusterStateUpdateTask; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.project.TestProjectResolvers; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Priority; import org.elasticsearch.tasks.Task; @@ -41,7 +42,8 @@ public class TransportStopILMActionTests extends ESTestCase { transportService, clusterService, threadPool, - mock(ActionFilters.class) + mock(ActionFilters.class), + TestProjectResolvers.singleProject(randomProjectIdOrDefault()) ); Task task = new Task( randomLong(), diff --git a/x-pack/plugin/inference/build.gradle b/x-pack/plugin/inference/build.gradle index b58e1e941b16..9486d239e5de 100644 --- a/x-pack/plugin/inference/build.gradle +++ b/x-pack/plugin/inference/build.gradle @@ -405,3 +405,7 @@ tasks.named("thirdPartyAudit").configure { tasks.named('yamlRestTest') { usesDefaultDistribution("Uses the inference API") } + +artifacts { + restXpackTests(new File(projectDir, "src/yamlRestTest/resources/rest-api-spec/test")) +} diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java index 2bc481cc484d..3d05600709b2 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java @@ -17,6 +17,7 @@ import java.util.Set; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.SEMANTIC_TEXT_EXCLUDE_SUB_FIELDS_FROM_FIELD_CAPS; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.SEMANTIC_TEXT_INDEX_OPTIONS; +import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.SEMANTIC_TEXT_INDEX_OPTIONS_WITH_DEFAULTS; import static org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper.SEMANTIC_TEXT_SUPPORT_CHUNKING_CONFIG; import static org.elasticsearch.xpack.inference.queries.SemanticKnnVectorQueryRewriteInterceptor.SEMANTIC_KNN_FILTER_FIX; import static org.elasticsearch.xpack.inference.queries.SemanticKnnVectorQueryRewriteInterceptor.SEMANTIC_KNN_VECTOR_QUERY_REWRITE_INTERCEPTION_SUPPORTED; @@ -66,7 +67,8 @@ public class InferenceFeatures implements FeatureSpecification { SEMANTIC_TEXT_MATCH_ALL_HIGHLIGHTER, SEMANTIC_TEXT_EXCLUDE_SUB_FIELDS_FROM_FIELD_CAPS, SEMANTIC_TEXT_INDEX_OPTIONS, - COHERE_V2_API + COHERE_V2_API, + SEMANTIC_TEXT_INDEX_OPTIONS_WITH_DEFAULTS ); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java index 5400bf6acc67..fd5f1ce2735a 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java @@ -69,6 +69,7 @@ import org.elasticsearch.inference.ChunkingSettings; import org.elasticsearch.inference.InferenceResults; import org.elasticsearch.inference.MinimalServiceSettings; import org.elasticsearch.inference.SimilarityMeasure; +import org.elasticsearch.inference.TaskType; import org.elasticsearch.search.fetch.StoredFieldsSpec; import org.elasticsearch.search.lookup.Source; import org.elasticsearch.search.vectors.KnnVectorQueryBuilder; @@ -139,6 +140,9 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie "semantic_text.exclude_sub_fields_from_field_caps" ); public static final NodeFeature SEMANTIC_TEXT_INDEX_OPTIONS = new NodeFeature("semantic_text.index_options"); + public static final NodeFeature SEMANTIC_TEXT_INDEX_OPTIONS_WITH_DEFAULTS = new NodeFeature( + "semantic_text.index_options_with_defaults" + ); public static final String CONTENT_TYPE = "semantic_text"; public static final String DEFAULT_ELSER_2_INFERENCE_ID = DEFAULT_ELSER_ID; @@ -166,19 +170,9 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie public static class Builder extends FieldMapper.Builder { private final ModelRegistry modelRegistry; private final boolean useLegacyFormat; + private final IndexVersion indexVersionCreated; - private final Parameter inferenceId = Parameter.stringParam( - INFERENCE_ID_FIELD, - false, - mapper -> ((SemanticTextFieldType) mapper.fieldType()).inferenceId, - DEFAULT_ELSER_2_INFERENCE_ID - ).addValidator(v -> { - if (Strings.isEmpty(v)) { - throw new IllegalArgumentException( - "[" + INFERENCE_ID_FIELD + "] on mapper [" + leafName() + "] of type [" + CONTENT_TYPE + "] must not be empty" - ); - } - }).alwaysSerialize(); + private final Parameter inferenceId; private final Parameter searchInferenceId = Parameter.stringParam( SEARCH_INFERENCE_ID_FIELD, @@ -193,25 +187,9 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie } }); - private final Parameter modelSettings = new Parameter<>( - MODEL_SETTINGS_FIELD, - true, - () -> null, - (n, c, o) -> SemanticTextField.parseModelSettingsFromMap(o), - mapper -> ((SemanticTextFieldType) mapper.fieldType()).modelSettings, - XContentBuilder::field, - Objects::toString - ).acceptsNull().setMergeValidator(SemanticTextFieldMapper::canMergeModelSettings); + private final Parameter modelSettings; - private final Parameter indexOptions = new Parameter<>( - INDEX_OPTIONS_FIELD, - true, - () -> null, - (n, c, o) -> parseIndexOptionsFromMap(n, o, c.indexVersionCreated()), - mapper -> ((SemanticTextFieldType) mapper.fieldType()).indexOptions, - XContentBuilder::field, - Objects::toString - ).acceptsNull(); + private final Parameter indexOptions; @SuppressWarnings("unchecked") private final Parameter chunkingSettings = new Parameter<>( @@ -248,6 +226,50 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie super(name); this.modelRegistry = modelRegistry; this.useLegacyFormat = InferenceMetadataFieldsMapper.isEnabled(indexSettings.getSettings()) == false; + this.indexVersionCreated = indexSettings.getIndexVersionCreated(); + + this.inferenceId = Parameter.stringParam( + INFERENCE_ID_FIELD, + false, + mapper -> ((SemanticTextFieldType) mapper.fieldType()).inferenceId, + DEFAULT_ELSER_2_INFERENCE_ID + ).addValidator(v -> { + if (Strings.isEmpty(v)) { + throw new IllegalArgumentException( + "[" + INFERENCE_ID_FIELD + "] on mapper [" + leafName() + "] of type [" + CONTENT_TYPE + "] must not be empty" + ); + } + }).alwaysSerialize(); + + this.modelSettings = new Parameter<>( + MODEL_SETTINGS_FIELD, + true, + () -> null, + (n, c, o) -> SemanticTextField.parseModelSettingsFromMap(o), + mapper -> ((SemanticTextFieldType) mapper.fieldType()).modelSettings, + XContentBuilder::field, + Objects::toString + ).acceptsNull().setMergeValidator(SemanticTextFieldMapper::canMergeModelSettings); + + this.indexOptions = new Parameter<>( + INDEX_OPTIONS_FIELD, + true, + () -> null, + (n, c, o) -> parseIndexOptionsFromMap(n, o, c.indexVersionCreated()), + mapper -> ((SemanticTextFieldType) mapper.fieldType()).indexOptions, + (b, n, v) -> { + if (v == null) { + MinimalServiceSettings resolvedModelSettings = modelSettings.get() != null + ? modelSettings.get() + : modelRegistry.getMinimalServiceSettings(inferenceId.get()); + b.field(INDEX_OPTIONS_FIELD, defaultIndexOptions(indexVersionCreated, resolvedModelSettings)); + } else { + b.field(INDEX_OPTIONS_FIELD, v); + } + }, + Objects::toString + ).acceptsNull(); + this.inferenceFieldBuilder = c -> { // Resolve the model setting from the registry if it has not been set yet. var resolvedModelSettings = modelSettings.get() != null ? modelSettings.get() : getResolvedModelSettings(c, false); @@ -365,8 +387,11 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie validateServiceSettings(modelSettings.get(), resolvedModelSettings); } - if (context.getMergeReason() != MapperService.MergeReason.MAPPING_RECOVERY && indexOptions.get() != null) { - validateIndexOptions(indexOptions.get(), inferenceId.getValue(), resolvedModelSettings); + // If index_options are specified by the user, we will validate them against the model settings to ensure compatibility. + // We do not serialize or otherwise store model settings at this time, this happens when the underlying vector field is created. + SemanticTextIndexOptions builderIndexOptions = indexOptions.get(); + if (context.getMergeReason() != MapperService.MergeReason.MAPPING_RECOVERY && builderIndexOptions != null) { + validateIndexOptions(builderIndexOptions, inferenceId.getValue(), resolvedModelSettings); } final String fullName = context.buildFullName(leafName()); @@ -1166,6 +1191,9 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie } denseVectorMapperBuilder.dimensions(modelSettings.dimensions()); denseVectorMapperBuilder.elementType(modelSettings.elementType()); + // Here is where we persist index_options. If they are specified by the user, we will use those index_options, + // otherwise we will determine if we can set default index options. If we can't, we won't persist any index_options + // and the field will use the defaults for the dense_vector field. if (indexOptions != null) { DenseVectorFieldMapper.DenseVectorIndexOptions denseVectorIndexOptions = (DenseVectorFieldMapper.DenseVectorIndexOptions) indexOptions.indexOptions(); @@ -1208,7 +1236,6 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie // As embedding models for text perform better with BBQ, we aggressively default semantic_text fields to use optimized index // options if (indexVersionDefaultsToBbqHnsw(indexVersionCreated)) { - DenseVectorFieldMapper.DenseVectorIndexOptions defaultBbqHnswIndexOptions = defaultBbqHnswDenseVectorIndexOptions(); return defaultBbqHnswIndexOptions.validate(modelSettings.elementType(), modelSettings.dimensions(), false) ? defaultBbqHnswIndexOptions @@ -1230,11 +1257,24 @@ public class SemanticTextFieldMapper extends FieldMapper implements InferenceFie return new DenseVectorFieldMapper.BBQHnswIndexOptions(m, efConstruction, rescoreVector); } - static SemanticTextIndexOptions defaultBbqHnswSemanticTextIndexOptions() { - return new SemanticTextIndexOptions( - SemanticTextIndexOptions.SupportedIndexOptions.DENSE_VECTOR, - defaultBbqHnswDenseVectorIndexOptions() - ); + static SemanticTextIndexOptions defaultIndexOptions(IndexVersion indexVersionCreated, MinimalServiceSettings modelSettings) { + + if (modelSettings == null) { + return null; + } + + SemanticTextIndexOptions defaultIndexOptions = null; + if (modelSettings.taskType() == TaskType.TEXT_EMBEDDING) { + DenseVectorFieldMapper.DenseVectorIndexOptions denseVectorIndexOptions = defaultDenseVectorIndexOptions( + indexVersionCreated, + modelSettings + ); + defaultIndexOptions = denseVectorIndexOptions == null + ? null + : new SemanticTextIndexOptions(SemanticTextIndexOptions.SupportedIndexOptions.DENSE_VECTOR, denseVectorIndexOptions); + } + + return defaultIndexOptions; } private static boolean canMergeModelSettings(MinimalServiceSettings previous, MinimalServiceSettings current, Conflicts conflicts) { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextIndexOptions.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextIndexOptions.java index c062adad2f55..db647499f446 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextIndexOptions.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextIndexOptions.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Locale; import java.util.Map; +import java.util.Objects; /** * Represents index options for a semantic_text field. @@ -50,6 +51,25 @@ public class SemanticTextIndexOptions implements ToXContent { return indexOptions; } + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + SemanticTextIndexOptions otherSemanticTextIndexOptions = (SemanticTextIndexOptions) other; + return type == otherSemanticTextIndexOptions.type && Objects.equals(indexOptions, otherSemanticTextIndexOptions.indexOptions); + } + + @Override + public int hashCode() { + return Objects.hash(type, indexOptions); + } + public enum SupportedIndexOptions { DENSE_VECTOR("dense_vector") { @Override diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/amazonbedrock/embeddings/AmazonBedrockEmbeddingsTaskSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/amazonbedrock/embeddings/AmazonBedrockEmbeddingsTaskSettings.java index bb0a8a3348ad..ad06e669ff56 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/amazonbedrock/embeddings/AmazonBedrockEmbeddingsTaskSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/amazonbedrock/embeddings/AmazonBedrockEmbeddingsTaskSettings.java @@ -79,9 +79,16 @@ public record AmazonBedrockEmbeddingsTaskSettings(@Nullable CohereTruncation coh @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.AMAZON_BEDROCK_TASK_SETTINGS; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.AMAZON_BEDROCK_TASK_SETTINGS) + || version.isPatchFrom(TransportVersions.AMAZON_BEDROCK_TASK_SETTINGS_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalEnum(cohereTruncation()); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceSettings.java index 1767653fd1a5..07b81c22c808 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceSettings.java @@ -183,7 +183,7 @@ public class CohereServiceSettings extends FilteredXContentObject implements Ser rateLimitSettings = DEFAULT_RATE_LIMIT_SETTINGS; } if (in.getTransportVersion().onOrAfter(TransportVersions.ML_INFERENCE_COHERE_API_VERSION) - || in.getTransportVersion().isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION)) { + || in.getTransportVersion().isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION_8_19)) { this.apiVersion = in.readEnum(CohereServiceSettings.CohereApiVersion.class); } else { this.apiVersion = CohereServiceSettings.CohereApiVersion.V1; @@ -286,7 +286,7 @@ public class CohereServiceSettings extends FilteredXContentObject implements Ser rateLimitSettings.writeTo(out); } if (out.getTransportVersion().onOrAfter(TransportVersions.ML_INFERENCE_COHERE_API_VERSION) - || out.getTransportVersion().isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION)) { + || out.getTransportVersion().isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION_8_19)) { out.writeEnum(apiVersion); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/completion/CohereCompletionServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/completion/CohereCompletionServiceSettings.java index efe58ed19a00..7f8ef305e5db 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/completion/CohereCompletionServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/completion/CohereCompletionServiceSettings.java @@ -103,7 +103,7 @@ public class CohereCompletionServiceSettings extends FilteredXContentObject impl modelId = in.readOptionalString(); rateLimitSettings = new RateLimitSettings(in); if (in.getTransportVersion().onOrAfter(TransportVersions.ML_INFERENCE_COHERE_API_VERSION) - || in.getTransportVersion().isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION)) { + || in.getTransportVersion().isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION_8_19)) { this.apiVersion = in.readEnum(CohereServiceSettings.CohereApiVersion.class); } else { this.apiVersion = CohereServiceSettings.CohereApiVersion.V1; @@ -156,7 +156,7 @@ public class CohereCompletionServiceSettings extends FilteredXContentObject impl out.writeOptionalString(modelId); rateLimitSettings.writeTo(out); if (out.getTransportVersion().onOrAfter(TransportVersions.ML_INFERENCE_COHERE_API_VERSION) - || out.getTransportVersion().isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION)) { + || out.getTransportVersion().isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION_8_19)) { out.writeEnum(apiVersion); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/rerank/CohereRerankServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/rerank/CohereRerankServiceSettings.java index a17fff7f165c..651b8758c37f 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/rerank/CohereRerankServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/rerank/CohereRerankServiceSettings.java @@ -125,7 +125,7 @@ public class CohereRerankServiceSettings extends FilteredXContentObject implemen } if (in.getTransportVersion().onOrAfter(TransportVersions.ML_INFERENCE_COHERE_API_VERSION) - || in.getTransportVersion().isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION)) { + || in.getTransportVersion().isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION_8_19)) { this.apiVersion = in.readEnum(CohereServiceSettings.CohereApiVersion.class); } else { this.apiVersion = CohereServiceSettings.CohereApiVersion.V1; @@ -207,7 +207,7 @@ public class CohereRerankServiceSettings extends FilteredXContentObject implemen rateLimitSettings.writeTo(out); } if (out.getTransportVersion().onOrAfter(TransportVersions.ML_INFERENCE_COHERE_API_VERSION) - || out.getTransportVersion().isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION)) { + || out.getTransportVersion().isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION_8_19)) { out.writeEnum(apiVersion); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/custom/CustomSecretSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/custom/CustomSecretSettings.java index ac6b7ab10c8b..4c2ff22a5829 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/custom/CustomSecretSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/custom/CustomSecretSettings.java @@ -90,9 +90,16 @@ public class CustomSecretSettings implements SecretSettings { @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.INFERENCE_CUSTOM_SERVICE_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.INFERENCE_CUSTOM_SERVICE_ADDED) + || version.isPatchFrom(TransportVersions.INFERENCE_CUSTOM_SERVICE_ADDED_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeMap(secretParameters, StreamOutput::writeSecureString); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/custom/CustomServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/custom/CustomServiceSettings.java index 83048120bc54..931eb3b79855 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/custom/CustomServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/custom/CustomServiceSettings.java @@ -394,9 +394,16 @@ public class CustomServiceSettings extends FilteredXContentObject implements Ser @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.INFERENCE_CUSTOM_SERVICE_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.INFERENCE_CUSTOM_SERVICE_ADDED) + || version.isPatchFrom(TransportVersions.INFERENCE_CUSTOM_SERVICE_ADDED_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { textEmbeddingSettings.writeTo(out); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/custom/CustomTaskSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/custom/CustomTaskSettings.java index bb665cc196bd..2d43e4278100 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/custom/CustomTaskSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/custom/CustomTaskSettings.java @@ -100,9 +100,16 @@ public class CustomTaskSettings implements TaskSettings { @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.INFERENCE_CUSTOM_SERVICE_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.INFERENCE_CUSTOM_SERVICE_ADDED) + || version.isPatchFrom(TransportVersions.INFERENCE_CUSTOM_SERVICE_ADDED_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeGenericMap(parameters); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/deepseek/DeepSeekChatCompletionModel.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/deepseek/DeepSeekChatCompletionModel.java index 5e9a7e5f93a0..06f21e19a640 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/deepseek/DeepSeekChatCompletionModel.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/deepseek/DeepSeekChatCompletionModel.java @@ -176,9 +176,16 @@ public class DeepSeekChatCompletionModel extends Model { @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_DEEPSEEK; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_DEEPSEEK) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_DEEPSEEK_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(modelId); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/densetextembeddings/ElasticInferenceServiceDenseTextEmbeddingsServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/densetextembeddings/ElasticInferenceServiceDenseTextEmbeddingsServiceSettings.java index 5047f34a1b2e..e8eeee5a34dd 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/densetextembeddings/ElasticInferenceServiceDenseTextEmbeddingsServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/densetextembeddings/ElasticInferenceServiceDenseTextEmbeddingsServiceSettings.java @@ -205,9 +205,16 @@ public class ElasticInferenceServiceDenseTextEmbeddingsServiceSettings extends F @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_ELASTIC_DENSE_TEXT_EMBEDDINGS_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_ELASTIC_DENSE_TEXT_EMBEDDINGS_ADDED) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_ELASTIC_DENSE_TEXT_EMBEDDINGS_ADDED_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(modelId); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/rerank/ElasticInferenceServiceRerankServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/rerank/ElasticInferenceServiceRerankServiceSettings.java index c20846c7fdfc..eff22c277193 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/rerank/ElasticInferenceServiceRerankServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/rerank/ElasticInferenceServiceRerankServiceSettings.java @@ -83,9 +83,16 @@ public class ElasticInferenceServiceRerankServiceSettings extends FilteredXConte @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_ELASTIC_RERANK; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_ELASTIC_RERANK) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_ELASTIC_RERANK_ADDED_8_19); + } + @Override protected XContentBuilder toXContentFragmentOfExposedFields(XContentBuilder builder, Params params) throws IOException { builder.field(MODEL_ID, modelId); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/googlevertexai/completion/GoogleVertexAiChatCompletionServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/googlevertexai/completion/GoogleVertexAiChatCompletionServiceSettings.java index 105d76a9f8cc..a753fc5dc66f 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/googlevertexai/completion/GoogleVertexAiChatCompletionServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/googlevertexai/completion/GoogleVertexAiChatCompletionServiceSettings.java @@ -118,9 +118,16 @@ public class GoogleVertexAiChatCompletionServiceSettings extends FilteredXConten @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_VERTEXAI_CHATCOMPLETION_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_VERTEXAI_CHATCOMPLETION_ADDED) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_VERTEXAI_CHATCOMPLETION_ADDED_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(projectId); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/completion/HuggingFaceChatCompletionServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/completion/HuggingFaceChatCompletionServiceSettings.java index af88316ef516..cdc2529428be 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/completion/HuggingFaceChatCompletionServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/completion/HuggingFaceChatCompletionServiceSettings.java @@ -144,9 +144,16 @@ public class HuggingFaceChatCompletionServiceSettings extends FilteredXContentOb @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_HUGGING_FACE_CHAT_COMPLETION_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_HUGGING_FACE_CHAT_COMPLETION_ADDED) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_HUGGING_FACE_CHAT_COMPLETION_ADDED_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(modelId); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/rerank/HuggingFaceRerankServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/rerank/HuggingFaceRerankServiceSettings.java index 3d4c6aef71e9..b0b21b26395a 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/rerank/HuggingFaceRerankServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/rerank/HuggingFaceRerankServiceSettings.java @@ -115,9 +115,16 @@ public class HuggingFaceRerankServiceSettings extends FilteredXContentObject @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_HUGGING_FACE_RERANK_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_HUGGING_FACE_RERANK_ADDED) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_HUGGING_FACE_RERANK_ADDED_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(uri.toString()); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/rerank/HuggingFaceRerankTaskSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/rerank/HuggingFaceRerankTaskSettings.java index 9f90386edff9..8b9e9113bce1 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/rerank/HuggingFaceRerankTaskSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/rerank/HuggingFaceRerankTaskSettings.java @@ -118,9 +118,16 @@ public class HuggingFaceRerankTaskSettings implements TaskSettings { @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_HUGGING_FACE_RERANK_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_HUGGING_FACE_RERANK_ADDED) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_HUGGING_FACE_RERANK_ADDED_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalVInt(topNDocumentsOnly); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/mistral/completion/MistralChatCompletionServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/mistral/completion/MistralChatCompletionServiceSettings.java index 676653d54a56..89b9475ad65d 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/mistral/completion/MistralChatCompletionServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/mistral/completion/MistralChatCompletionServiceSettings.java @@ -78,9 +78,16 @@ public class MistralChatCompletionServiceSettings extends FilteredXContentObject @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_MISTRAL_CHAT_COMPLETION_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_MISTRAL_CHAT_COMPLETION_ADDED) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_MISTRAL_CHAT_COMPLETION_ADDED_8_19); + } + @Override public String modelId() { return this.modelId; diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/model/SageMakerServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/model/SageMakerServiceSettings.java index 2caf97bdd05b..b7a554d387c8 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/model/SageMakerServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/model/SageMakerServiceSettings.java @@ -111,9 +111,16 @@ record SageMakerServiceSettings( @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_SAGEMAKER; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_SAGEMAKER) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_SAGEMAKER_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(endpointName()); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/model/SageMakerTaskSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/model/SageMakerTaskSettings.java index c1c244cc3705..fd9eb2d20c5d 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/model/SageMakerTaskSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/model/SageMakerTaskSettings.java @@ -101,9 +101,16 @@ record SageMakerTaskSettings( @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_SAGEMAKER; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_SAGEMAKER) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_SAGEMAKER_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(customAttributes); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/SageMakerStoredServiceSchema.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/SageMakerStoredServiceSchema.java index 9fb320a2d364..b3d948a85de9 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/SageMakerStoredServiceSchema.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/SageMakerStoredServiceSchema.java @@ -29,9 +29,16 @@ public interface SageMakerStoredServiceSchema extends ServiceSettings { @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_SAGEMAKER; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_SAGEMAKER) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_SAGEMAKER_8_19); + } + @Override public void writeTo(StreamOutput out) { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/SageMakerStoredTaskSchema.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/SageMakerStoredTaskSchema.java index 2aa2f9556d41..09a73f0f42ea 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/SageMakerStoredTaskSchema.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/SageMakerStoredTaskSchema.java @@ -39,9 +39,16 @@ public interface SageMakerStoredTaskSchema extends TaskSettings { @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_SAGEMAKER; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_SAGEMAKER) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_SAGEMAKER_8_19); + } + @Override public void writeTo(StreamOutput out) {} diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/elastic/ElasticTextEmbeddingPayload.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/elastic/ElasticTextEmbeddingPayload.java index cf9d24a86dcc..6e1407beab1d 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/elastic/ElasticTextEmbeddingPayload.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/elastic/ElasticTextEmbeddingPayload.java @@ -250,9 +250,16 @@ public class ElasticTextEmbeddingPayload implements ElasticPayload { @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_SAGEMAKER_ELASTIC; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_SAGEMAKER_ELASTIC) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_SAGEMAKER_ELASTIC_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalVInt(dimensions); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/elastic/SageMakerElasticTaskSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/elastic/SageMakerElasticTaskSettings.java index 3cdcbb35ffdc..088de2068741 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/elastic/SageMakerElasticTaskSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/elastic/SageMakerElasticTaskSettings.java @@ -50,9 +50,16 @@ record SageMakerElasticTaskSettings(@Nullable Map passthroughSet @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_SAGEMAKER_ELASTIC; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_SAGEMAKER_ELASTIC) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_SAGEMAKER_ELASTIC_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeGenericMap(passthroughSettings); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/openai/OpenAiTextEmbeddingPayload.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/openai/OpenAiTextEmbeddingPayload.java index 276c407d694d..6fcbd309551e 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/openai/OpenAiTextEmbeddingPayload.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/openai/OpenAiTextEmbeddingPayload.java @@ -138,9 +138,16 @@ public class OpenAiTextEmbeddingPayload implements SageMakerSchemaPayload { @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_SAGEMAKER; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_SAGEMAKER) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_SAGEMAKER_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalInt(dimensions); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/openai/SageMakerOpenAiTaskSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/openai/SageMakerOpenAiTaskSettings.java index 4eeba9f69022..b8ce19ba712b 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/openai/SageMakerOpenAiTaskSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/sagemaker/schema/openai/SageMakerOpenAiTaskSettings.java @@ -37,9 +37,16 @@ record SageMakerOpenAiTaskSettings(@Nullable String user) implements SageMakerSt @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.ML_INFERENCE_SAGEMAKER_CHAT_COMPLETION; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.ML_INFERENCE_SAGEMAKER_CHAT_COMPLETION) + || version.isPatchFrom(TransportVersions.ML_INFERENCE_SAGEMAKER_CHAT_COMPLETION_8_19); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(user); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/VoyageAIServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/VoyageAIServiceSettings.java index 75497d1a4b4f..ba7db5bc16f4 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/VoyageAIServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/VoyageAIServiceSettings.java @@ -108,9 +108,16 @@ public class VoyageAIServiceSettings extends FilteredXContentObject implements S @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.VOYAGE_AI_INTEGRATION_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.VOYAGE_AI_INTEGRATION_ADDED) + || version.isPatchFrom(TransportVersions.VOYAGE_AI_INTEGRATION_ADDED_BACKPORT_8_X); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(modelId); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/embeddings/VoyageAIEmbeddingsServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/embeddings/VoyageAIEmbeddingsServiceSettings.java index cc4db278d0e2..a0960fb6f74a 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/embeddings/VoyageAIEmbeddingsServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/embeddings/VoyageAIEmbeddingsServiceSettings.java @@ -226,9 +226,16 @@ public class VoyageAIEmbeddingsServiceSettings extends FilteredXContentObject im @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.VOYAGE_AI_INTEGRATION_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.VOYAGE_AI_INTEGRATION_ADDED) + || version.isPatchFrom(TransportVersions.VOYAGE_AI_INTEGRATION_ADDED_BACKPORT_8_X); + } + @Override public void writeTo(StreamOutput out) throws IOException { commonSettings.writeTo(out); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/embeddings/VoyageAIEmbeddingsTaskSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/embeddings/VoyageAIEmbeddingsTaskSettings.java index 2c6bf3a59c61..11728075fe2b 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/embeddings/VoyageAIEmbeddingsTaskSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/embeddings/VoyageAIEmbeddingsTaskSettings.java @@ -162,9 +162,16 @@ public class VoyageAIEmbeddingsTaskSettings implements TaskSettings { @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.VOYAGE_AI_INTEGRATION_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.VOYAGE_AI_INTEGRATION_ADDED) + || version.isPatchFrom(TransportVersions.VOYAGE_AI_INTEGRATION_ADDED_BACKPORT_8_X); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalEnum(inputType); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/rerank/VoyageAIRerankServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/rerank/VoyageAIRerankServiceSettings.java index 1d3607922c5c..4e23efac2701 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/rerank/VoyageAIRerankServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/rerank/VoyageAIRerankServiceSettings.java @@ -90,9 +90,16 @@ public class VoyageAIRerankServiceSettings extends FilteredXContentObject implem @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.VOYAGE_AI_INTEGRATION_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.VOYAGE_AI_INTEGRATION_ADDED) + || version.isPatchFrom(TransportVersions.VOYAGE_AI_INTEGRATION_ADDED_BACKPORT_8_X); + } + @Override public void writeTo(StreamOutput out) throws IOException { commonSettings.writeTo(out); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/rerank/VoyageAIRerankTaskSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/rerank/VoyageAIRerankTaskSettings.java index a5004fde1e17..9e57b5848767 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/rerank/VoyageAIRerankTaskSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/voyageai/rerank/VoyageAIRerankTaskSettings.java @@ -135,9 +135,16 @@ public class VoyageAIRerankTaskSettings implements TaskSettings { @Override public TransportVersion getMinimalSupportedVersion() { + assert false : "should never be called when supportsVersion is used"; return TransportVersions.VOYAGE_AI_INTEGRATION_ADDED; } + @Override + public boolean supportsVersion(TransportVersion version) { + return version.onOrAfter(TransportVersions.VOYAGE_AI_INTEGRATION_ADDED) + || version.isPatchFrom(TransportVersions.VOYAGE_AI_INTEGRATION_ADDED_BACKPORT_8_X); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalInt(topKDocumentsOnly); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceSettingsTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceSettingsTests.java index 0ce016956cda..cac416fd454a 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceSettingsTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceSettingsTests.java @@ -7,15 +7,17 @@ package org.elasticsearch.xpack.inference.services.cohere; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; import org.elasticsearch.common.Strings; import org.elasticsearch.common.ValidationException; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.core.Nullable; import org.elasticsearch.inference.SimilarityMeasure; -import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xpack.core.ml.AbstractBWCWireSerializationTestCase; import org.elasticsearch.xpack.inference.services.ConfigurationParseContext; import org.elasticsearch.xpack.inference.services.ServiceFields; import org.elasticsearch.xpack.inference.services.ServiceUtils; @@ -30,7 +32,7 @@ import java.util.Map; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; -public class CohereServiceSettingsTests extends AbstractWireSerializingTestCase { +public class CohereServiceSettingsTests extends AbstractBWCWireSerializationTestCase { public static CohereServiceSettings createRandomWithNonNullUrl() { return createRandom(randomAlphaOfLength(15)); @@ -359,4 +361,22 @@ public class CohereServiceSettingsTests extends AbstractWireSerializingTestCase< return map; } + + @Override + protected CohereServiceSettings mutateInstanceForVersion(CohereServiceSettings instance, TransportVersion version) { + if (version.before(TransportVersions.ML_INFERENCE_COHERE_API_VERSION) + && (version.isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION_8_19) == false)) { + return new CohereServiceSettings( + instance.uri(), + instance.similarity(), + instance.dimensions(), + instance.maxInputTokens(), + instance.modelId(), + instance.rateLimitSettings(), + CohereServiceSettings.CohereApiVersion.V1 + ); + } + + return instance; + } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/completion/CohereCompletionServiceSettingsTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/completion/CohereCompletionServiceSettingsTests.java index 06ebdd158b92..92ebb3fdc0a0 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/completion/CohereCompletionServiceSettingsTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/completion/CohereCompletionServiceSettingsTests.java @@ -7,12 +7,14 @@ package org.elasticsearch.xpack.inference.services.cohere.completion; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xpack.core.ml.AbstractBWCWireSerializationTestCase; import org.elasticsearch.xpack.inference.services.ConfigurationParseContext; import org.elasticsearch.xpack.inference.services.ServiceFields; import org.elasticsearch.xpack.inference.services.cohere.CohereServiceSettings; @@ -25,7 +27,7 @@ import java.util.Map; import static org.hamcrest.Matchers.is; -public class CohereCompletionServiceSettingsTests extends AbstractWireSerializingTestCase { +public class CohereCompletionServiceSettingsTests extends AbstractBWCWireSerializationTestCase { public static CohereCompletionServiceSettings createRandom() { return new CohereCompletionServiceSettings( @@ -110,4 +112,19 @@ public class CohereCompletionServiceSettingsTests extends AbstractWireSerializin protected CohereCompletionServiceSettings mutateInstance(CohereCompletionServiceSettings instance) throws IOException { return randomValueOtherThan(instance, this::createTestInstance); } + + @Override + protected CohereCompletionServiceSettings mutateInstanceForVersion(CohereCompletionServiceSettings instance, TransportVersion version) { + if (version.before(TransportVersions.ML_INFERENCE_COHERE_API_VERSION) + && (version.isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION_8_19) == false)) { + return new CohereCompletionServiceSettings( + instance.uri(), + instance.modelId(), + instance.rateLimitSettings(), + CohereServiceSettings.CohereApiVersion.V1 + ); + } + + return instance; + } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/rerank/CohereRerankServiceSettingsTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/rerank/CohereRerankServiceSettingsTests.java index 27a9fc38f392..773ccc5933aa 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/rerank/CohereRerankServiceSettingsTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/rerank/CohereRerankServiceSettingsTests.java @@ -88,14 +88,15 @@ public class CohereRerankServiceSettingsTests extends AbstractBWCWireSerializati CohereServiceSettings.DEFAULT_RATE_LIMIT_SETTINGS, CohereServiceSettings.CohereApiVersion.V1 ); - } else if (version.before(TransportVersions.ML_INFERENCE_COHERE_API_VERSION)) { - return new CohereRerankServiceSettings( - instance.uri(), - instance.modelId(), - instance.rateLimitSettings(), - CohereServiceSettings.CohereApiVersion.V1 - ); - } + } else if (version.before(TransportVersions.ML_INFERENCE_COHERE_API_VERSION) + && version.isPatchFrom(TransportVersions.ML_INFERENCE_COHERE_API_VERSION_8_19) == false) { + return new CohereRerankServiceSettings( + instance.uri(), + instance.modelId(), + instance.rateLimitSettings(), + CohereServiceSettings.CohereApiVersion.V1 + ); + } return instance; } diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml index 5cc0d8368516..637087071b8c 100644 --- a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping.yml @@ -833,3 +833,147 @@ setup: type: int8_flat - match: { status: 400 } + + +--- +"Displaying default index_options with and without include_defaults": + - requires: + cluster_features: "semantic_text.index_options_with_defaults" + reason: Index options defaults support introduced in 9.2.0 + + # Semantic text defaults to BBQ HNSW starting in 8.19.0/9.1.0 + - do: + indices.create: + index: test-index-options-dense + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id-compatible-with-bbq + + - do: + indices.get_mapping: + index: test-index-options-dense + + - not_exists: test-index-options-dense.mappings.properties.semantic_field.index_options + + - do: + indices.get_field_mapping: + index: test-index-options-dense + fields: semantic_field + include_defaults: true + + - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.type": "bbq_hnsw" } + - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.m": 16 } + - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.ef_construction": 100 } + - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.rescore_vector.oversample": 3 } + + # Validate that actually specifying the same values as our defaults will still serialize the user provided index_options + - do: + indices.create: + index: test-index-options-dense2 + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id-compatible-with-bbq + index_options: + dense_vector: + type: bbq_hnsw + m: 16 + ef_construction: 100 + rescore_vector: + oversample: 3 + + - do: + indices.get_mapping: + index: test-index-options-dense2 + + - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.type": "bbq_hnsw" } + - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.m": 16 } + - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.ef_construction": 100 } + - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.rescore_vector.oversample": 3 } + + - do: + indices.get_field_mapping: + index: test-index-options-dense2 + fields: semantic_field + include_defaults: true + + - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.type": "bbq_hnsw" } + - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.m": 16 } + - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.ef_construction": 100 } + - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.rescore_vector.oversample": 3 } + + # Indices not compatible with BBQ for whatever reason will fall back to whatever `dense_vector` defaults are. + - do: + indices.create: + index: test-index-options-dense-no-bbq + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id + + - do: + indices.get_mapping: + index: test-index-options-dense-no-bbq + + - not_exists: test-index-options-dense-no-bbq.mappings.properties.semantic_field.index_options + + - do: + indices.get_field_mapping: + index: test-index-options-dense-no-bbq + fields: semantic_field + include_defaults: true + + - not_exists: test-index-options-dense-no-bbq.mappings.properties.semantic_field.index_options + + # Sparse embeddings models do not have index options for semantic_text in 8.19/9.1. + - do: + indices.create: + index: test-index-options-sparse + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: false + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + + - do: + indices.get_mapping: + index: test-index-options-sparse + + - not_exists: test-index-options-sparse.mappings.properties.semantic_field.index_options + + - do: + indices.get_field_mapping: + index: test-index-options-sparse + fields: semantic_field + include_defaults: true + + - not_exists: test-index-options-sparse.mappings.properties.semantic_field.index_options + diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping_bwc.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping_bwc.yml index b089d8c43933..1121958b39ed 100644 --- a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping_bwc.yml +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_field_mapping_bwc.yml @@ -736,3 +736,146 @@ setup: type: int8_flat - match: { status: 400 } + +--- +"Displaying default index_options with and without include_defaults": + - requires: + cluster_features: "semantic_text.index_options_with_defaults" + reason: Index options defaults support introduced in 9.2.0 + + # Semantic text defaults to BBQ HNSW starting in 8.19.0/9.1.0 + - do: + indices.create: + index: test-index-options-dense + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: true + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id-compatible-with-bbq + + - do: + indices.get_mapping: + index: test-index-options-dense + + - not_exists: test-index-options-dense.mappings.properties.semantic_field.index_options + + - do: + indices.get_field_mapping: + index: test-index-options-dense + fields: semantic_field + include_defaults: true + + - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.type": "bbq_hnsw" } + - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.m": 16 } + - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.ef_construction": 100 } + - match: { "test-index-options-dense.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.rescore_vector.oversample": 3 } + + # Validate that actually specifying the same values as our defaults will still serialize the user provided index_options + - do: + indices.create: + index: test-index-options-dense2 + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: true + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id-compatible-with-bbq + index_options: + dense_vector: + type: bbq_hnsw + m: 16 + ef_construction: 100 + rescore_vector: + oversample: 3 + + - do: + indices.get_mapping: + index: test-index-options-dense2 + + - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.type": "bbq_hnsw" } + - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.m": 16 } + - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.ef_construction": 100 } + - match: { "test-index-options-dense2.mappings.properties.semantic_field.index_options.dense_vector.rescore_vector.oversample": 3 } + + - do: + indices.get_field_mapping: + index: test-index-options-dense2 + fields: semantic_field + include_defaults: true + + - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.type": "bbq_hnsw" } + - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.m": 16 } + - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.ef_construction": 100 } + - match: { "test-index-options-dense2.mappings.semantic_field.mapping.semantic_field.index_options.dense_vector.rescore_vector.oversample": 3 } + + # Indices not compatible with BBQ for whatever reason will fall back to whatever `dense_vector` defaults are. + - do: + indices.create: + index: test-index-options-dense-no-bbq + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: true + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: dense-inference-id + + - do: + indices.get_mapping: + index: test-index-options-dense-no-bbq + + - not_exists: test-index-options-dense-no-bbq.mappings.properties.semantic_field.index_options + + - do: + indices.get_field_mapping: + index: test-index-options-dense-no-bbq + fields: semantic_field + include_defaults: true + + - not_exists: test-index-options-dense-no-bbq.mappings.properties.semantic_field.index_options + + # Sparse embeddings models do not have index options for semantic_text in 8.19/9.1. + - do: + indices.create: + index: test-index-options-sparse + body: + settings: + index: + mapping: + semantic_text: + use_legacy_format: true + mappings: + properties: + semantic_field: + type: semantic_text + inference_id: sparse-inference-id + + - do: + indices.get_mapping: + index: test-index-options-sparse + + - not_exists: test-index-options-sparse.mappings.properties.semantic_field.index_options + + - do: + indices.get_field_mapping: + index: test-index-options-sparse + fields: semantic_field + include_defaults: true + + - not_exists: test-index-options-sparse.mappings.properties.semantic_field.index_options + diff --git a/x-pack/plugin/logsdb/build.gradle b/x-pack/plugin/logsdb/build.gradle index 4496d5843afc..aebb860f9d5c 100644 --- a/x-pack/plugin/logsdb/build.gradle +++ b/x-pack/plugin/logsdb/build.gradle @@ -24,12 +24,13 @@ base { restResources { restApi { - include 'bulk', 'search', '_common', 'indices', 'index', 'cluster', 'data_stream', 'ingest', 'cat', 'capabilities', 'esql.query' + include 'bulk', 'search', '_common', 'indices', 'index', 'cluster', 'data_stream', 'ingest', 'cat', 'capabilities', 'esql.query', 'field_caps' } } dependencies { compileOnly project(path: xpackModule('core')) + implementation project(':modules:mapper-extras') testImplementation project(':modules:data-streams') testImplementation(testArtifact(project(xpackModule('core')))) javaRestTestImplementation(testArtifact(project(xpackModule('spatial')))) diff --git a/x-pack/plugin/logsdb/qa/rolling-upgrade/src/javaRestTest/java/org/elasticsearch/upgrades/MatchOnlyTextRollingUpgradeIT.java b/x-pack/plugin/logsdb/qa/rolling-upgrade/src/javaRestTest/java/org/elasticsearch/upgrades/MatchOnlyTextRollingUpgradeIT.java new file mode 100644 index 000000000000..80a77d76ea16 --- /dev/null +++ b/x-pack/plugin/logsdb/qa/rolling-upgrade/src/javaRestTest/java/org/elasticsearch/upgrades/MatchOnlyTextRollingUpgradeIT.java @@ -0,0 +1,252 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.upgrades; + +import com.carrotsearch.randomizedtesting.annotations.Name; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.network.NetworkAddress; +import org.elasticsearch.common.time.DateFormatter; +import org.elasticsearch.common.time.FormatNames; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.test.rest.ObjectPath; +import org.elasticsearch.xcontent.XContentType; + +import java.io.IOException; +import java.io.InputStream; +import java.time.Instant; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.elasticsearch.upgrades.LogsIndexModeRollingUpgradeIT.enableLogsdbByDefault; +import static org.elasticsearch.upgrades.LogsIndexModeRollingUpgradeIT.getWriteBackingIndex; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.notNullValue; + +public class MatchOnlyTextRollingUpgradeIT extends AbstractRollingUpgradeWithSecurityTestCase { + + static String BULK_ITEM_TEMPLATE = + """ + {"@timestamp": "$now", "host.name": "$host", "method": "$method", "ip": "$ip", "message": "$message", "length": $length, "factor": $factor} + """; + + private static final String TEMPLATE = """ + { + "mappings": { + "properties": { + "@timestamp" : { + "type": "date" + }, + "method": { + "type": "keyword" + }, + "message": { + "type": "match_only_text" + }, + "ip": { + "type": "ip" + }, + "length": { + "type": "long" + }, + "factor": { + "type": "double" + } + } + } + }"""; + + public MatchOnlyTextRollingUpgradeIT(@Name("upgradedNodes") int upgradedNodes) { + super(upgradedNodes); + } + + public void testIndexing() throws Exception { + String dataStreamName = "logs-bwc-test"; + if (isOldCluster()) { + startTrial(); + enableLogsdbByDefault(); + createTemplate(dataStreamName, getClass().getSimpleName().toLowerCase(Locale.ROOT), TEMPLATE); + + Instant startTime = Instant.now().minusSeconds(60 * 60); + bulkIndex(dataStreamName, 4, 1024, startTime); + + String firstBackingIndex = getWriteBackingIndex(client(), dataStreamName, 0); + var settings = (Map) getIndexSettingsWithDefaults(firstBackingIndex).get(firstBackingIndex); + assertThat(((Map) settings.get("settings")).get("index.mode"), equalTo("logsdb")); + assertThat(((Map) settings.get("defaults")).get("index.mapping.source.mode"), equalTo("SYNTHETIC")); + + ensureGreen(dataStreamName); + search(dataStreamName); + query(dataStreamName); + } else if (isMixedCluster()) { + Instant startTime = Instant.now().minusSeconds(60 * 30); + bulkIndex(dataStreamName, 4, 1024, startTime); + + ensureGreen(dataStreamName); + search(dataStreamName); + query(dataStreamName); + } else if (isUpgradedCluster()) { + ensureGreen(dataStreamName); + Instant startTime = Instant.now(); + bulkIndex(dataStreamName, 4, 1024, startTime); + search(dataStreamName); + query(dataStreamName); + + var forceMergeRequest = new Request("POST", "/" + dataStreamName + "/_forcemerge"); + forceMergeRequest.addParameter("max_num_segments", "1"); + assertOK(client().performRequest(forceMergeRequest)); + + ensureGreen(dataStreamName); + search(dataStreamName); + query(dataStreamName); + } + } + + static void createTemplate(String dataStreamName, String id, String template) throws IOException { + final String INDEX_TEMPLATE = """ + { + "index_patterns": ["$DATASTREAM"], + "template": $TEMPLATE, + "data_stream": { + } + }"""; + var putIndexTemplateRequest = new Request("POST", "/_index_template/" + id); + putIndexTemplateRequest.setJsonEntity(INDEX_TEMPLATE.replace("$TEMPLATE", template).replace("$DATASTREAM", dataStreamName)); + assertOK(client().performRequest(putIndexTemplateRequest)); + } + + static String bulkIndex(String dataStreamName, int numRequest, int numDocs, Instant startTime) throws Exception { + String firstIndex = null; + for (int i = 0; i < numRequest; i++) { + var bulkRequest = new Request("POST", "/" + dataStreamName + "/_bulk"); + StringBuilder requestBody = new StringBuilder(); + for (int j = 0; j < numDocs; j++) { + String hostName = "host" + j % 50; // Not realistic, but makes asserting search / query response easier. + String methodName = "method" + j % 5; + String ip = NetworkAddress.format(randomIp(true)); + String param = "chicken" + randomInt(5); + String message = "the quick brown fox jumps over the " + param; + long length = randomLong(); + double factor = randomDouble(); + + requestBody.append("{\"create\": {}}"); + requestBody.append('\n'); + requestBody.append( + BULK_ITEM_TEMPLATE.replace("$now", formatInstant(startTime)) + .replace("$host", hostName) + .replace("$method", methodName) + .replace("$ip", ip) + .replace("$message", message) + .replace("$length", Long.toString(length)) + .replace("$factor", Double.toString(factor)) + ); + requestBody.append('\n'); + + startTime = startTime.plusMillis(1); + } + bulkRequest.setJsonEntity(requestBody.toString()); + bulkRequest.addParameter("refresh", "true"); + var response = client().performRequest(bulkRequest); + assertOK(response); + var responseBody = entityAsMap(response); + assertThat("errors in response:\n " + responseBody, responseBody.get("errors"), equalTo(false)); + if (firstIndex == null) { + firstIndex = (String) ((Map) ((Map) ((List) responseBody.get("items")).get(0)).get("create")).get("_index"); + } + } + return firstIndex; + } + + void search(String dataStreamName) throws Exception { + var searchRequest = new Request("POST", "/" + dataStreamName + "/_search"); + searchRequest.addParameter("pretty", "true"); + searchRequest.setJsonEntity(""" + { + "size": 500, + "query": { + "match_phrase": { + "message": "chicken" + } + } + } + """.replace("chicken", "chicken" + randomInt(5))); + var response = client().performRequest(searchRequest); + assertOK(response); + var responseBody = entityAsMap(response); + logger.info("{}", responseBody); + + Integer totalCount = ObjectPath.evaluate(responseBody, "hits.total.value"); + assertThat(totalCount, greaterThanOrEqualTo(512)); + } + + void query(String dataStreamName) throws Exception { + var queryRequest = new Request("POST", "/_query"); + queryRequest.addParameter("pretty", "true"); + queryRequest.setJsonEntity(""" + { + "query": "FROM $ds | STATS max(length), max(factor) BY message | SORT message | LIMIT 5" + } + """.replace("$ds", dataStreamName)); + var response = client().performRequest(queryRequest); + assertOK(response); + var responseBody = entityAsMap(response); + logger.info("{}", responseBody); + + String column1 = ObjectPath.evaluate(responseBody, "columns.0.name"); + String column2 = ObjectPath.evaluate(responseBody, "columns.1.name"); + String column3 = ObjectPath.evaluate(responseBody, "columns.2.name"); + assertThat(column1, equalTo("max(length)")); + assertThat(column2, equalTo("max(factor)")); + assertThat(column3, equalTo("message")); + + String key = ObjectPath.evaluate(responseBody, "values.0.2"); + assertThat(key, equalTo("the quick brown fox jumps over the chicken0")); + Long maxRx = ObjectPath.evaluate(responseBody, "values.0.0"); + assertThat(maxRx, notNullValue()); + Double maxTx = ObjectPath.evaluate(responseBody, "values.0.1"); + assertThat(maxTx, notNullValue()); + } + + protected static void startTrial() throws IOException { + Request startTrial = new Request("POST", "/_license/start_trial"); + startTrial.addParameter("acknowledge", "true"); + try { + assertOK(client().performRequest(startTrial)); + } catch (ResponseException e) { + var responseBody = entityAsMap(e.getResponse()); + String error = ObjectPath.evaluate(responseBody, "error_message"); + assertThat(error, containsString("Trial was already activated.")); + } + } + + static Map getIndexSettingsWithDefaults(String index) throws IOException { + Request request = new Request("GET", "/" + index + "/_settings"); + request.addParameter("flat_settings", "true"); + request.addParameter("include_defaults", "true"); + Response response = client().performRequest(request); + try (InputStream is = response.getEntity().getContent()) { + return XContentHelper.convertToMap( + XContentType.fromMediaType(response.getEntity().getContentType().getValue()).xContent(), + is, + true + ); + } + } + + static String formatInstant(Instant instant) { + return DateFormatter.forPattern(FormatNames.STRICT_DATE_OPTIONAL_TIME.getName()).format(instant); + } + +} diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsDBPlugin.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsDBPlugin.java index 695406fb9bb3..70236c8e085c 100644 --- a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsDBPlugin.java +++ b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsDBPlugin.java @@ -12,21 +12,27 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.IndexSettingProvider; import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.license.LicenseService; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.plugins.MapperPlugin; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction; import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction; +import org.elasticsearch.xpack.logsdb.patternedtext.PatternedTextFieldMapper; +import org.elasticsearch.xpack.logsdb.patternedtext.PatternedTextFieldType; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; +import static java.util.Collections.singletonMap; import static org.elasticsearch.xpack.logsdb.LogsdbLicenseService.FALLBACK_SETTING; -public class LogsDBPlugin extends Plugin implements ActionPlugin { +public class LogsDBPlugin extends Plugin implements ActionPlugin, MapperPlugin { private final Settings settings; private final LogsdbLicenseService licenseService; @@ -98,6 +104,15 @@ public class LogsDBPlugin extends Plugin implements ActionPlugin { return actions; } + @Override + public Map getMappers() { + if (PatternedTextFieldMapper.PATTERNED_TEXT_MAPPER.isEnabled()) { + return singletonMap(PatternedTextFieldType.CONTENT_TYPE, PatternedTextFieldMapper.PARSER); + } else { + return Map.of(); + } + } + protected XPackLicenseState getLicenseState() { return XPackPlugin.getSharedLicenseState(); } diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextDocValues.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextDocValues.java new file mode 100644 index 000000000000..b7dfdc95683e --- /dev/null +++ b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextDocValues.java @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.logsdb.patternedtext; + +import org.apache.lucene.index.BinaryDocValues; +import org.apache.lucene.index.DocValues; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.SortedSetDocValues; +import org.apache.lucene.util.BytesRef; + +import java.io.IOException; + +public class PatternedTextDocValues extends BinaryDocValues { + private final SortedSetDocValues templateDocValues; + private final SortedSetDocValues argsDocValues; + + PatternedTextDocValues(SortedSetDocValues templateDocValues, SortedSetDocValues argsDocValues) { + this.templateDocValues = templateDocValues; + this.argsDocValues = argsDocValues; + } + + static PatternedTextDocValues from(LeafReader leafReader, String templateFieldName, String argsFieldName) throws IOException { + SortedSetDocValues templateDocValues = DocValues.getSortedSet(leafReader, templateFieldName); + if (templateDocValues.getValueCount() == 0) { + return null; + } + + SortedSetDocValues argsDocValues = DocValues.getSortedSet(leafReader, argsFieldName); + return new PatternedTextDocValues(templateDocValues, argsDocValues); + } + + private String getNextStringValue() throws IOException { + assert templateDocValues.docValueCount() == 1; + String template = templateDocValues.lookupOrd(templateDocValues.nextOrd()).utf8ToString(); + int argsCount = PatternedTextValueProcessor.countArgs(template); + if (argsCount > 0) { + assert argsDocValues.docValueCount() == 1; + var mergedArgs = argsDocValues.lookupOrd(argsDocValues.nextOrd()); + var args = PatternedTextValueProcessor.decodeRemainingArgs(mergedArgs.utf8ToString()); + return PatternedTextValueProcessor.merge(new PatternedTextValueProcessor.Parts(template, args)); + } else { + return template; + } + } + + @Override + public BytesRef binaryValue() throws IOException { + return new BytesRef(getNextStringValue()); + } + + @Override + public boolean advanceExact(int i) throws IOException { + argsDocValues.advanceExact(i); + // If template has a value, then message has a value. We don't have to check args here, since there may not be args for the doc + return templateDocValues.advanceExact(i); + } + + @Override + public int docID() { + return templateDocValues.docID(); + } + + @Override + public int nextDoc() throws IOException { + int templateNext = templateDocValues.nextDoc(); + var argsAdvance = argsDocValues.advance(templateNext); + assert argsAdvance >= templateNext; + return templateNext; + } + + @Override + public int advance(int i) throws IOException { + int templateAdvance = templateDocValues.advance(i); + var argsAdvance = argsDocValues.advance(templateAdvance); + assert argsAdvance >= templateAdvance; + return templateAdvance; + } + + @Override + public long cost() { + return templateDocValues.cost() + argsDocValues.cost(); + } +} diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextFieldMapper.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextFieldMapper.java new file mode 100644 index 000000000000..55f5616f4ac7 --- /dev/null +++ b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextFieldMapper.java @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.logsdb.patternedtext; + +import org.apache.lucene.document.Field; +import org.apache.lucene.document.FieldType; +import org.apache.lucene.document.SortedSetDocValuesField; +import org.apache.lucene.index.IndexOptions; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.util.FeatureFlag; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.analysis.IndexAnalyzers; +import org.elasticsearch.index.analysis.NamedAnalyzer; +import org.elasticsearch.index.mapper.CompositeSyntheticFieldLoader; +import org.elasticsearch.index.mapper.DocumentParserContext; +import org.elasticsearch.index.mapper.FieldMapper; +import org.elasticsearch.index.mapper.MapperBuilderContext; +import org.elasticsearch.index.mapper.TextParams; +import org.elasticsearch.index.mapper.TextSearchInfo; + +import java.io.IOException; +import java.util.Map; + +/** + * A {@link FieldMapper} that assigns every document the same value. + */ +public class PatternedTextFieldMapper extends FieldMapper { + + public static final FeatureFlag PATTERNED_TEXT_MAPPER = new FeatureFlag("patterned_text"); + + public static class Defaults { + public static final FieldType FIELD_TYPE; + + static { + final FieldType ft = new FieldType(); + ft.setTokenized(true); + ft.setStored(false); + ft.setStoreTermVectors(false); + ft.setOmitNorms(true); + ft.setIndexOptions(IndexOptions.DOCS); + FIELD_TYPE = freezeAndDeduplicateFieldType(ft); + } + } + + public static class Builder extends FieldMapper.Builder { + + private final IndexVersion indexCreatedVersion; + + private final Parameter> meta = Parameter.metaParam(); + + private final TextParams.Analyzers analyzers; + + public Builder(String name, IndexVersion indexCreatedVersion, IndexAnalyzers indexAnalyzers) { + super(name); + this.indexCreatedVersion = indexCreatedVersion; + this.analyzers = new TextParams.Analyzers( + indexAnalyzers, + m -> ((PatternedTextFieldMapper) m).indexAnalyzer, + m -> ((PatternedTextFieldMapper) m).positionIncrementGap, + indexCreatedVersion + ); + } + + @Override + protected Parameter[] getParameters() { + return new Parameter[] { meta }; + } + + private PatternedTextFieldType buildFieldType(MapperBuilderContext context) { + NamedAnalyzer searchAnalyzer = analyzers.getSearchAnalyzer(); + NamedAnalyzer searchQuoteAnalyzer = analyzers.getSearchQuoteAnalyzer(); + NamedAnalyzer indexAnalyzer = analyzers.getIndexAnalyzer(); + TextSearchInfo tsi = new TextSearchInfo(Defaults.FIELD_TYPE, null, searchAnalyzer, searchQuoteAnalyzer); + return new PatternedTextFieldType( + context.buildFullName(leafName()), + tsi, + indexAnalyzer, + context.isSourceSynthetic(), + meta.getValue() + ); + } + + @Override + public PatternedTextFieldMapper build(MapperBuilderContext context) { + return new PatternedTextFieldMapper(leafName(), buildFieldType(context), builderParams(this, context), this); + } + } + + public static final TypeParser PARSER = new TypeParser((n, c) -> new Builder(n, c.indexVersionCreated(), c.getIndexAnalyzers())); + + private final IndexVersion indexCreatedVersion; + private final IndexAnalyzers indexAnalyzers; + private final NamedAnalyzer indexAnalyzer; + private final int positionIncrementGap; + private final FieldType fieldType; + + private PatternedTextFieldMapper( + String simpleName, + PatternedTextFieldType mappedFieldPatternedTextFieldType, + BuilderParams builderParams, + Builder builder + ) { + super(simpleName, mappedFieldPatternedTextFieldType, builderParams); + assert mappedFieldPatternedTextFieldType.getTextSearchInfo().isTokenized(); + assert mappedFieldPatternedTextFieldType.hasDocValues() == false; + this.fieldType = Defaults.FIELD_TYPE; + this.indexCreatedVersion = builder.indexCreatedVersion; + this.indexAnalyzers = builder.analyzers.indexAnalyzers; + this.indexAnalyzer = builder.analyzers.getIndexAnalyzer(); + this.positionIncrementGap = builder.analyzers.positionIncrementGap.getValue(); + } + + @Override + public Map indexAnalyzers() { + return Map.of(mappedFieldType.name(), indexAnalyzer); + } + + @Override + public FieldMapper.Builder getMergeBuilder() { + return new Builder(leafName(), indexCreatedVersion, indexAnalyzers).init(this); + } + + @Override + protected void parseCreateField(DocumentParserContext context) throws IOException { + final String value = context.parser().textOrNull(); + if (value == null) { + return; + } + + var existingValue = context.doc().getField(fieldType().name()); + if (existingValue != null) { + throw new IllegalArgumentException("Multiple values are not allowed for field [" + fieldType().name() + "]."); + } + + // Parse template and args. + PatternedTextValueProcessor.Parts parts = PatternedTextValueProcessor.split(value); + + // Add index on original value + context.doc().add(new Field(fieldType().name(), value, fieldType)); + + // Add template doc_values + context.doc().add(new SortedSetDocValuesField(fieldType().templateFieldName(), new BytesRef(parts.template()))); + + // Add args doc_values + if (parts.args().isEmpty() == false) { + String remainingArgs = PatternedTextValueProcessor.encodeRemainingArgs(parts); + context.doc().add(new SortedSetDocValuesField(fieldType().argsFieldName(), new BytesRef(remainingArgs))); + } + } + + @Override + protected String contentType() { + return PatternedTextFieldType.CONTENT_TYPE; + } + + @Override + public PatternedTextFieldType fieldType() { + return (PatternedTextFieldType) super.fieldType(); + } + + @Override + protected SyntheticSourceSupport syntheticSourceSupport() { + return new SyntheticSourceSupport.Native( + () -> new CompositeSyntheticFieldLoader( + leafName(), + fullPath(), + new PatternedTextSyntheticFieldLoaderLayer(fieldType().name(), fieldType().templateFieldName(), fieldType().argsFieldName()) + ) + ); + } +} diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextFieldType.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextFieldType.java new file mode 100644 index 000000000000..4c712d10e0aa --- /dev/null +++ b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextFieldType.java @@ -0,0 +1,270 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.logsdb.patternedtext; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.Term; +import org.apache.lucene.queries.intervals.Intervals; +import org.apache.lucene.queries.intervals.IntervalsSource; +import org.apache.lucene.search.ConstantScoreQuery; +import org.apache.lucene.search.FieldExistsQuery; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.MultiTermQuery; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOFunction; +import org.elasticsearch.common.CheckedIntFunction; +import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.common.unit.Fuzziness; +import org.elasticsearch.index.fielddata.FieldDataContext; +import org.elasticsearch.index.fielddata.IndexFieldData; +import org.elasticsearch.index.fielddata.SourceValueFetcherSortedBinaryIndexFieldData; +import org.elasticsearch.index.mapper.BlockDocValuesReader; +import org.elasticsearch.index.mapper.BlockLoader; +import org.elasticsearch.index.mapper.SourceValueFetcher; +import org.elasticsearch.index.mapper.StringFieldType; +import org.elasticsearch.index.mapper.TextFieldMapper; +import org.elasticsearch.index.mapper.TextSearchInfo; +import org.elasticsearch.index.mapper.ValueFetcher; +import org.elasticsearch.index.mapper.extras.SourceConfirmedTextQuery; +import org.elasticsearch.index.mapper.extras.SourceIntervalsSource; +import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.script.field.KeywordDocValuesField; +import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; +import org.elasticsearch.search.lookup.SourceProvider; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class PatternedTextFieldType extends StringFieldType { + + private static final String TEMPLATE_SUFFIX = ".template"; + private static final String ARGS_SUFFIX = ".args"; + + public static final String CONTENT_TYPE = "patterned_text"; + + private final Analyzer indexAnalyzer; + private final TextFieldMapper.TextFieldType textFieldType; + + PatternedTextFieldType(String name, TextSearchInfo tsi, Analyzer indexAnalyzer, boolean isSyntheticSource, Map meta) { + // Though this type is based on doc_values, hasDocValues is set to false as the patterned_text type is not aggregatable. + // This does not stop its child .template type from being aggregatable. + super(name, true, false, false, tsi, meta); + this.indexAnalyzer = Objects.requireNonNull(indexAnalyzer); + this.textFieldType = new TextFieldMapper.TextFieldType(name, isSyntheticSource); + } + + PatternedTextFieldType(String name) { + this( + name, + new TextSearchInfo(PatternedTextFieldMapper.Defaults.FIELD_TYPE, null, Lucene.STANDARD_ANALYZER, Lucene.STANDARD_ANALYZER), + Lucene.STANDARD_ANALYZER, + false, + Collections.emptyMap() + ); + } + + @Override + public String typeName() { + return CONTENT_TYPE; + } + + @Override + public String familyTypeName() { + return TextFieldMapper.CONTENT_TYPE; + } + + @Override + public ValueFetcher valueFetcher(SearchExecutionContext context, String format) { + return SourceValueFetcher.toString(name(), context, format); + } + + private IOFunction, IOException>> getValueFetcherProvider( + SearchExecutionContext searchExecutionContext + ) { + return context -> { + ValueFetcher valueFetcher = valueFetcher(searchExecutionContext, null); + SourceProvider sourceProvider = searchExecutionContext.lookup(); + valueFetcher.setNextReader(context); + return docID -> { + try { + return valueFetcher.fetchValues(sourceProvider.getSource(context, docID), docID, new ArrayList<>()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + }; + } + + private Query sourceConfirmedQuery(Query query, SearchExecutionContext context) { + // Disable scoring + return new ConstantScoreQuery(new SourceConfirmedTextQuery(query, getValueFetcherProvider(context), indexAnalyzer)); + } + + private IntervalsSource toIntervalsSource(IntervalsSource source, Query approximation, SearchExecutionContext searchExecutionContext) { + return new SourceIntervalsSource(source, approximation, getValueFetcherProvider(searchExecutionContext), indexAnalyzer); + } + + @Override + public Query termQuery(Object query, SearchExecutionContext context) { + // Disable scoring + return new ConstantScoreQuery(super.termQuery(query, context)); + } + + @Override + public Query fuzzyQuery( + Object value, + Fuzziness fuzziness, + int prefixLength, + int maxExpansions, + boolean transpositions, + SearchExecutionContext context, + MultiTermQuery.RewriteMethod rewriteMethod + ) { + // Disable scoring + return new ConstantScoreQuery( + super.fuzzyQuery(value, fuzziness, prefixLength, maxExpansions, transpositions, context, rewriteMethod) + ); + } + + @Override + public Query existsQuery(SearchExecutionContext context) { + return new FieldExistsQuery(templateFieldName()); + } + + @Override + public IntervalsSource termIntervals(BytesRef term, SearchExecutionContext context) { + return toIntervalsSource(Intervals.term(term), new TermQuery(new Term(name(), term)), context); + } + + @Override + public IntervalsSource prefixIntervals(BytesRef term, SearchExecutionContext context) { + return toIntervalsSource( + Intervals.prefix(term, IndexSearcher.getMaxClauseCount()), + new PrefixQuery(new Term(name(), term)), + context + ); + } + + @Override + public IntervalsSource fuzzyIntervals( + String term, + int maxDistance, + int prefixLength, + boolean transpositions, + SearchExecutionContext context + ) { + FuzzyQuery fuzzyQuery = new FuzzyQuery( + new Term(name(), term), + maxDistance, + prefixLength, + IndexSearcher.getMaxClauseCount(), + transpositions, + MultiTermQuery.CONSTANT_SCORE_BLENDED_REWRITE + ); + IntervalsSource fuzzyIntervals = Intervals.multiterm(fuzzyQuery.getAutomata(), IndexSearcher.getMaxClauseCount(), term); + return toIntervalsSource(fuzzyIntervals, fuzzyQuery, context); + } + + @Override + public IntervalsSource wildcardIntervals(BytesRef pattern, SearchExecutionContext context) { + return toIntervalsSource( + Intervals.wildcard(pattern, IndexSearcher.getMaxClauseCount()), + new MatchAllDocsQuery(), // wildcard queries can be expensive, what should the approximation be? + context + ); + } + + @Override + public IntervalsSource regexpIntervals(BytesRef pattern, SearchExecutionContext context) { + return toIntervalsSource( + Intervals.regexp(pattern, IndexSearcher.getMaxClauseCount()), + new MatchAllDocsQuery(), // regexp queries can be expensive, what should the approximation be? + context + ); + } + + @Override + public IntervalsSource rangeIntervals( + BytesRef lowerTerm, + BytesRef upperTerm, + boolean includeLower, + boolean includeUpper, + SearchExecutionContext context + ) { + return toIntervalsSource( + Intervals.range(lowerTerm, upperTerm, includeLower, includeUpper, IndexSearcher.getMaxClauseCount()), + new MatchAllDocsQuery(), // range queries can be expensive, what should the approximation be? + context + ); + } + + @Override + public Query phraseQuery(TokenStream stream, int slop, boolean enablePosIncrements, SearchExecutionContext queryShardContext) + throws IOException { + final Query textQuery = textFieldType.phraseQuery(stream, slop, enablePosIncrements, queryShardContext); + return sourceConfirmedQuery(textQuery, queryShardContext); + } + + @Override + public Query multiPhraseQuery(TokenStream stream, int slop, boolean enablePositionIncrements, SearchExecutionContext queryShardContext) + throws IOException { + final Query textQuery = textFieldType.multiPhraseQuery(stream, slop, enablePositionIncrements, queryShardContext); + return sourceConfirmedQuery(textQuery, queryShardContext); + } + + @Override + public Query phrasePrefixQuery(TokenStream stream, int slop, int maxExpansions, SearchExecutionContext queryShardContext) + throws IOException { + final Query textQuery = textFieldType.phrasePrefixQuery(stream, slop, maxExpansions, queryShardContext); + return sourceConfirmedQuery(textQuery, queryShardContext); + } + + @Override + public BlockLoader blockLoader(BlockLoaderContext blContext) { + return new BlockDocValuesReader.BytesRefsFromBinaryBlockLoader(name()); + } + + @Override + public IndexFieldData.Builder fielddataBuilder(FieldDataContext fieldDataContext) { + if (fieldDataContext.fielddataOperation() != FielddataOperation.SCRIPT) { + throw new IllegalArgumentException(CONTENT_TYPE + " fields do not support sorting and aggregations"); + } + if (textFieldType.isSyntheticSource()) { + return new PatternedTextIndexFieldData.Builder(this); + } + return new SourceValueFetcherSortedBinaryIndexFieldData.Builder( + name(), + CoreValuesSourceType.KEYWORD, + SourceValueFetcher.toString(fieldDataContext.sourcePathsLookup().apply(name())), + fieldDataContext.lookupSupplier().get(), + KeywordDocValuesField::new + ); + + } + + String templateFieldName() { + return name() + TEMPLATE_SUFFIX; + } + + String argsFieldName() { + return name() + ARGS_SUFFIX; + } + +} diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextIndexFieldData.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextIndexFieldData.java new file mode 100644 index 000000000000..8e532a9dd5a3 --- /dev/null +++ b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextIndexFieldData.java @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.logsdb.patternedtext; + +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.SortField; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.index.fielddata.IndexFieldData; +import org.elasticsearch.index.fielddata.IndexFieldDataCache; +import org.elasticsearch.index.fielddata.LeafFieldData; +import org.elasticsearch.index.fielddata.SortedBinaryDocValues; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.script.field.DocValuesScriptFieldFactory; +import org.elasticsearch.script.field.KeywordDocValuesField; +import org.elasticsearch.script.field.ToScriptFieldFactory; +import org.elasticsearch.search.DocValueFormat; +import org.elasticsearch.search.MultiValueMode; +import org.elasticsearch.search.aggregations.support.ValuesSourceType; +import org.elasticsearch.search.sort.BucketedSort; +import org.elasticsearch.search.sort.SortOrder; + +import java.io.IOException; +import java.io.UncheckedIOException; + +public class PatternedTextIndexFieldData implements IndexFieldData { + + private final PatternedTextFieldType fieldType; + + static class Builder implements IndexFieldData.Builder { + + final PatternedTextFieldType fieldType; + + Builder(PatternedTextFieldType fieldType) { + this.fieldType = fieldType; + } + + public PatternedTextIndexFieldData build(IndexFieldDataCache cache, CircuitBreakerService breakerService) { + return new PatternedTextIndexFieldData(fieldType); + } + } + + PatternedTextIndexFieldData(PatternedTextFieldType fieldType) { + this.fieldType = fieldType; + } + + @Override + public String getFieldName() { + return fieldType.name(); + } + + @Override + public ValuesSourceType getValuesSourceType() { + return null; + } + + @Override + public LeafFieldData load(LeafReaderContext context) { + try { + return loadDirect(context); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public LeafFieldData loadDirect(LeafReaderContext context) throws IOException { + LeafReader leafReader = context.reader(); + PatternedTextDocValues docValues = PatternedTextDocValues.from( + leafReader, + fieldType.templateFieldName(), + fieldType.argsFieldName() + ); + return new LeafFieldData() { + + final ToScriptFieldFactory factory = KeywordDocValuesField::new; + + @Override + public DocValuesScriptFieldFactory getScriptFieldFactory(String name) { + return factory.getScriptFieldFactory(getBytesValues(), name); + } + + @Override + public SortedBinaryDocValues getBytesValues() { + return new SortedBinaryDocValues() { + @Override + public boolean advanceExact(int doc) throws IOException { + return docValues.advanceExact(doc); + } + + @Override + public int docValueCount() { + return 1; + } + + @Override + public BytesRef nextValue() throws IOException { + return docValues.binaryValue(); + } + }; + } + + @Override + public long ramBytesUsed() { + return 1L; + } + }; + } + + @Override + public SortField sortField(Object missingValue, MultiValueMode sortMode, XFieldComparatorSource.Nested nested, boolean reverse) { + throw new IllegalArgumentException("not supported for source patterned text field type"); + } + + @Override + public BucketedSort newBucketedSort( + BigArrays bigArrays, + Object missingValue, + MultiValueMode sortMode, + XFieldComparatorSource.Nested nested, + SortOrder sortOrder, + DocValueFormat format, + int bucketSize, + BucketedSort.ExtraData extra + ) { + throw new IllegalArgumentException("only supported on numeric fields"); + } +} diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextSyntheticFieldLoaderLayer.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextSyntheticFieldLoaderLayer.java new file mode 100644 index 000000000000..f05fa31671cd --- /dev/null +++ b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextSyntheticFieldLoaderLayer.java @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.logsdb.patternedtext; + +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.search.DocIdSetIterator; +import org.elasticsearch.index.mapper.CompositeSyntheticFieldLoader; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; + +class PatternedTextSyntheticFieldLoaderLayer implements CompositeSyntheticFieldLoader.DocValuesLayer { + + private final String name; + private final String templateFieldName; + private final String argsFieldName; + private PatternedTextSyntheticFieldLoader loader; + + PatternedTextSyntheticFieldLoaderLayer(String name, String templateFieldName, String argsFieldName) { + this.name = name; + this.templateFieldName = templateFieldName; + this.argsFieldName = argsFieldName; + } + + @Override + public long valueCount() { + return loader != null && loader.hasValue() ? 1 : 0; + } + + @Override + public DocValuesLoader docValuesLoader(LeafReader leafReader, int[] docIdsInLeaf) throws IOException { + var docValues = PatternedTextDocValues.from(leafReader, templateFieldName, argsFieldName); + if (docValues == null) { + return null; + } + loader = new PatternedTextSyntheticFieldLoader(docValues); + return loader; + } + + @Override + public boolean hasValue() { + return loader != null && loader.hasValue(); + } + + @Override + public void write(XContentBuilder b) throws IOException { + if (loader != null) { + loader.write(b); + } + } + + @Override + public String fieldName() { + return name; + } + + private static class PatternedTextSyntheticFieldLoader implements DocValuesLoader { + private final PatternedTextDocValues docValues; + private boolean hasValue = false; + + PatternedTextSyntheticFieldLoader(PatternedTextDocValues docValues) { + this.docValues = docValues; + } + + public boolean hasValue() { + assert docValues.docID() != DocIdSetIterator.NO_MORE_DOCS; + return hasValue; + } + + @Override + public boolean advanceToDoc(int docId) throws IOException { + return hasValue = docValues.advanceExact(docId); + } + + public void write(XContentBuilder b) throws IOException { + if (hasValue) { + b.value(docValues.binaryValue().utf8ToString()); + } + } + } +} diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextValueProcessor.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextValueProcessor.java new file mode 100644 index 000000000000..c4551777c319 --- /dev/null +++ b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextValueProcessor.java @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.logsdb.patternedtext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class PatternedTextValueProcessor { + private static final String TEXT_ARG_PLACEHOLDER = "%W"; + private static final String DELIMITER = "[\\s\\[\\]]"; + private static final String SPACE = " "; + + record Parts(String template, List args) {} + + static Parts split(String text) { + StringBuilder template = new StringBuilder(); + List args = new ArrayList<>(); + String[] tokens = text.split(DELIMITER); + int textIndex = 0; + for (String token : tokens) { + if (token.isEmpty()) { + if (textIndex < text.length() - 1) { + template.append(text.charAt(textIndex++)); + } + continue; + } + if (isArg(token)) { + args.add(token); + template.append(TEXT_ARG_PLACEHOLDER); + } else { + template.append(token); + } + textIndex += token.length(); + if (textIndex < text.length()) { + template.append(text.charAt(textIndex++)); + } + } + while (textIndex < text.length()) { + template.append(text.charAt(textIndex++)); + } + return new Parts(template.toString(), args); + } + + private static boolean isArg(String text) { + for (int i = 0; i < text.length(); i++) { + if (Character.isDigit(text.charAt(i))) { + return true; + } + } + return false; + } + + static String merge(Parts parts) { + StringBuilder builder = new StringBuilder(); + String[] templateParts = parts.template.split(DELIMITER); + int i = 0; + int templateIndex = 0; + for (String part : templateParts) { + if (part.equals(TEXT_ARG_PLACEHOLDER)) { + builder.append(parts.args.get(i++)); + templateIndex += TEXT_ARG_PLACEHOLDER.length(); + } else if (part.isEmpty() == false) { + builder.append(part); + templateIndex += part.length(); + } + if (templateIndex < parts.template.length()) { + builder.append(parts.template.charAt(templateIndex++)); + } + } + assert i == parts.args.size() : "expected " + i + " but got " + parts.args.size(); + assert builder.toString().contains(TEXT_ARG_PLACEHOLDER) == false : builder.toString(); + while (templateIndex < parts.template.length()) { + builder.append(parts.template.charAt(templateIndex++)); + } + return builder.toString(); + } + + static String encodeRemainingArgs(Parts parts) { + return String.join(SPACE, parts.args); + } + + static List decodeRemainingArgs(String mergedArgs) { + return Arrays.asList(mergedArgs.split(SPACE)); + } + + static int countArgs(String template) { + int count = 0; + for (int i = 0; i < template.length() - 1; i++) { + if (template.charAt(i) == '%') { + char next = template.charAt(i + 1); + if (next == 'W') { + count++; + i++; + } + } + } + return count; + } +} diff --git a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternTextDocValuesTests.java b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternTextDocValuesTests.java new file mode 100644 index 000000000000..85eeac12abfb --- /dev/null +++ b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternTextDocValuesTests.java @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.logsdb.patternedtext; + +import org.apache.lucene.index.SortedSetDocValues; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS; + +public class PatternTextDocValuesTests extends ESTestCase { + + private static PatternedTextDocValues makeDocValueSparseArgs() { + var template = new SimpleSortedSetDocValues("%W dog", "cat", "%W mouse %W", "hat %W"); + var args = new SimpleSortedSetDocValues("1", null, "2 3", "4"); + return new PatternedTextDocValues(template, args); + } + + private static PatternedTextDocValues makeDocValuesDenseArgs() { + var template = new SimpleSortedSetDocValues("%W moose", "%W goose %W", "%W mouse %W", "%W house"); + var args = new SimpleSortedSetDocValues("1", "4 5", "2 3", "7"); + return new PatternedTextDocValues(template, args); + } + + private static PatternedTextDocValues makeDocValueMissingValues() { + var template = new SimpleSortedSetDocValues("%W cheddar", "cat", null, "%W cheese"); + var args = new SimpleSortedSetDocValues("1", null, null, "4"); + return new PatternedTextDocValues(template, args); + } + + public void testNextDoc() throws IOException { + var docValues = randomBoolean() ? makeDocValueSparseArgs() : makeDocValuesDenseArgs(); + assertEquals(-1, docValues.docID()); + assertEquals(0, docValues.nextDoc()); + assertEquals(1, docValues.nextDoc()); + assertEquals(2, docValues.nextDoc()); + assertEquals(3, docValues.nextDoc()); + assertEquals(NO_MORE_DOCS, docValues.nextDoc()); + } + + public void testNextDocMissing() throws IOException { + var docValues = makeDocValueMissingValues(); + assertEquals(-1, docValues.docID()); + assertEquals(0, docValues.nextDoc()); + assertEquals(1, docValues.nextDoc()); + assertEquals(3, docValues.nextDoc()); + assertEquals(NO_MORE_DOCS, docValues.nextDoc()); + } + + public void testAdvance1() throws IOException { + var docValues = randomBoolean() ? makeDocValueSparseArgs() : makeDocValuesDenseArgs(); + assertEquals(-1, docValues.docID()); + assertEquals(0, docValues.nextDoc()); + assertEquals(1, docValues.advance(1)); + assertEquals(2, docValues.advance(2)); + assertEquals(3, docValues.advance(3)); + assertEquals(NO_MORE_DOCS, docValues.advance(4)); + } + + public void testAdvanceFarther() throws IOException { + var docValues = randomBoolean() ? makeDocValueSparseArgs() : makeDocValuesDenseArgs(); + assertEquals(2, docValues.advance(2)); + // repeats says on value + assertEquals(2, docValues.advance(2)); + } + + public void testAdvanceSkipsValuesIfMissing() throws IOException { + var docValues = makeDocValueMissingValues(); + assertEquals(3, docValues.advance(2)); + } + + public void testAdvanceExactMissing() throws IOException { + var docValues = makeDocValueMissingValues(); + assertTrue(docValues.advanceExact(1)); + assertFalse(docValues.advanceExact(2)); + assertEquals(3, docValues.docID()); + } + + public void testValueAll() throws IOException { + var docValues = makeDocValuesDenseArgs(); + assertEquals(0, docValues.nextDoc()); + assertEquals("1 moose", docValues.binaryValue().utf8ToString()); + assertEquals(1, docValues.nextDoc()); + assertEquals("4 goose 5", docValues.binaryValue().utf8ToString()); + assertEquals(2, docValues.nextDoc()); + assertEquals("2 mouse 3", docValues.binaryValue().utf8ToString()); + assertEquals(3, docValues.nextDoc()); + assertEquals("7 house", docValues.binaryValue().utf8ToString()); + } + + public void testValueMissing() throws IOException { + var docValues = makeDocValueMissingValues(); + assertEquals(0, docValues.nextDoc()); + assertEquals("1 cheddar", docValues.binaryValue().utf8ToString()); + assertEquals(1, docValues.nextDoc()); + assertEquals("cat", docValues.binaryValue().utf8ToString()); + assertEquals(3, docValues.nextDoc()); + assertEquals("4 cheese", docValues.binaryValue().utf8ToString()); + } + + static class SimpleSortedSetDocValues extends SortedSetDocValues { + + private final List ordToValues; + private final List docToOrds; + private int currDoc = -1; + + // Single value for each docId, null if no value for a docId + SimpleSortedSetDocValues(String... docIdToValue) { + ordToValues = Arrays.stream(docIdToValue).filter(Objects::nonNull).collect(Collectors.toSet()).stream().sorted().toList(); + docToOrds = Arrays.stream(docIdToValue).map(v -> v == null ? null : ordToValues.indexOf(v)).toList(); + } + + @Override + public long nextOrd() { + return docToOrds.get(currDoc); + } + + @Override + public int docValueCount() { + return 1; + } + + @Override + public BytesRef lookupOrd(long ord) { + return new BytesRef(ordToValues.get((int) ord)); + } + + @Override + public long getValueCount() { + return ordToValues.size(); + } + + @Override + public boolean advanceExact(int target) { + return advance(target) == target; + } + + @Override + public int docID() { + return currDoc >= docToOrds.size() ? NO_MORE_DOCS : currDoc; + } + + @Override + public int nextDoc() throws IOException { + return advance(currDoc + 1); + } + + @Override + public int advance(int target) { + for (currDoc = target; currDoc < docToOrds.size(); currDoc++) { + if (docToOrds.get(currDoc) != null) { + return currDoc; + } + } + return NO_MORE_DOCS; + } + + @Override + public long cost() { + return 1; + } + } +} diff --git a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextFieldMapperTests.java b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextFieldMapperTests.java new file mode 100644 index 000000000000..2a707eafa285 --- /dev/null +++ b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextFieldMapperTests.java @@ -0,0 +1,284 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.logsdb.patternedtext; + +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.DocValuesType; +import org.apache.lucene.index.IndexOptions; +import org.apache.lucene.index.IndexableField; +import org.apache.lucene.index.IndexableFieldType; +import org.apache.lucene.search.FieldExistsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.TotalHits; +import org.apache.lucene.store.Directory; +import org.apache.lucene.tests.analysis.CannedTokenStream; +import org.apache.lucene.tests.analysis.Token; +import org.apache.lucene.tests.index.RandomIndexWriter; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.index.mapper.DateFieldMapper; +import org.elasticsearch.index.mapper.DocumentMapper; +import org.elasticsearch.index.mapper.KeywordFieldMapper; +import org.elasticsearch.index.mapper.LuceneDocument; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.MapperParsingException; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.MapperTestCase; +import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.query.MatchPhraseQueryBuilder; +import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xpack.logsdb.LogsDBPlugin; +import org.junit.AssumptionViolatedException; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.startsWith; + +public class PatternedTextFieldMapperTests extends MapperTestCase { + + @Override + protected Collection getPlugins() { + return List.of(new LogsDBPlugin(Settings.EMPTY)); + } + + @Override + protected Object getSampleValueForDocument() { + return "value"; + } + + @Override + protected void assertExistsQuery(MappedFieldType fieldType, Query query, LuceneDocument fields) { + assertThat(query, instanceOf(FieldExistsQuery.class)); + FieldExistsQuery fieldExistsQuery = (FieldExistsQuery) query; + assertThat(fieldExistsQuery.getField(), startsWith("field")); + assertNoFieldNamesField(fields); + } + + public void testExistsStandardSource() throws IOException { + assertExistsQuery(createMapperService(fieldMapping(b -> b.field("type", "patterned_text")))); + } + + public void testExistsSyntheticSource() throws IOException { + assertExistsQuery(createSytheticSourceMapperService(fieldMapping(b -> b.field("type", "patterned_text")))); + } + + public void testPhraseQueryStandardSource() throws IOException { + assertPhraseQuery(createMapperService(fieldMapping(b -> b.field("type", "patterned_text")))); + } + + public void testPhraseQuerySyntheticSource() throws IOException { + assertPhraseQuery(createSytheticSourceMapperService(fieldMapping(b -> b.field("type", "patterned_text")))); + } + + private void assertPhraseQuery(MapperService mapperService) throws IOException { + try (Directory directory = newDirectory()) { + RandomIndexWriter iw = new RandomIndexWriter(random(), directory); + LuceneDocument doc = mapperService.documentMapper().parse(source(b -> b.field("field", "the quick brown fox 1"))).rootDoc(); + iw.addDocument(doc); + iw.close(); + try (DirectoryReader reader = DirectoryReader.open(directory)) { + SearchExecutionContext context = createSearchExecutionContext(mapperService, newSearcher(reader)); + MatchPhraseQueryBuilder queryBuilder = new MatchPhraseQueryBuilder("field", "brown fox 1"); + TopDocs docs = context.searcher().search(queryBuilder.toQuery(context), 1); + assertThat(docs.totalHits.value(), equalTo(1L)); + assertThat(docs.totalHits.relation(), equalTo(TotalHits.Relation.EQUAL_TO)); + assertThat(docs.scoreDocs[0].doc, equalTo(0)); + } + } + } + + @Override + protected void registerParameters(ParameterChecker checker) throws IOException { + checker.registerUpdateCheck( + b -> { b.field("meta", Collections.singletonMap("format", "mysql.access")); }, + m -> assertEquals(Collections.singletonMap("format", "mysql.access"), m.fieldType().meta()) + ); + } + + @Override + protected void minimalMapping(XContentBuilder b) throws IOException { + b.field("type", "patterned_text"); + } + + @Override + protected void minimalStoreMapping(XContentBuilder b) throws IOException { + // 'store' is always true + minimalMapping(b); + } + + public void testDefaults() throws IOException { + DocumentMapper mapper = createDocumentMapper(fieldMapping(this::minimalMapping)); + assertEquals(Strings.toString(fieldMapping(this::minimalMapping)), mapper.mappingSource().toString()); + + ParsedDocument doc = mapper.parse(source(b -> b.field("field", "1234"))); + List fields = doc.rootDoc().getFields("field"); + assertEquals(1, fields.size()); + assertEquals("1234", fields.get(0).stringValue()); + IndexableFieldType fieldType = fields.get(0).fieldType(); + assertThat(fieldType.omitNorms(), equalTo(true)); + assertTrue(fieldType.tokenized()); + assertFalse(fieldType.stored()); + assertThat(fieldType.indexOptions(), equalTo(IndexOptions.DOCS)); + assertThat(fieldType.storeTermVectors(), equalTo(false)); + assertThat(fieldType.storeTermVectorOffsets(), equalTo(false)); + assertThat(fieldType.storeTermVectorPositions(), equalTo(false)); + assertThat(fieldType.storeTermVectorPayloads(), equalTo(false)); + assertEquals(DocValuesType.NONE, fieldType.docValuesType()); + } + + public void testNullConfigValuesFail() throws MapperParsingException { + Exception e = expectThrows( + MapperParsingException.class, + () -> createDocumentMapper(fieldMapping(b -> b.field("type", "patterned_text").field("meta", (String) null))) + ); + assertThat(e.getMessage(), containsString("[meta] on mapper [field] of type [patterned_text] must not have a [null] value")); + } + + public void testSimpleMerge() throws IOException { + XContentBuilder startingMapping = fieldMapping(b -> b.field("type", "patterned_text")); + MapperService mapperService = createMapperService(startingMapping); + assertThat(mapperService.documentMapper().mappers().getMapper("field"), instanceOf(PatternedTextFieldMapper.class)); + + merge(mapperService, startingMapping); + assertThat(mapperService.documentMapper().mappers().getMapper("field"), instanceOf(PatternedTextFieldMapper.class)); + + XContentBuilder newField = mapping(b -> { + b.startObject("field").field("type", "patterned_text").startObject("meta").field("key", "value").endObject().endObject(); + b.startObject("other_field").field("type", "keyword").endObject(); + }); + merge(mapperService, newField); + assertThat(mapperService.documentMapper().mappers().getMapper("field"), instanceOf(PatternedTextFieldMapper.class)); + assertThat(mapperService.documentMapper().mappers().getMapper("other_field"), instanceOf(KeywordFieldMapper.class)); + } + + public void testDisabledSource() throws IOException { + XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("_doc"); + { + mapping.startObject("properties"); + { + mapping.startObject("foo"); + { + mapping.field("type", "patterned_text"); + } + mapping.endObject(); + } + mapping.endObject(); + + mapping.startObject("_source"); + { + mapping.field("enabled", false); + } + mapping.endObject(); + } + mapping.endObject().endObject(); + + MapperService mapperService = createMapperService(mapping); + MappedFieldType ft = mapperService.fieldType("foo"); + SearchExecutionContext context = createSearchExecutionContext(mapperService); + TokenStream ts = new CannedTokenStream(new Token("a", 0, 3), new Token("b", 4, 7)); + + // Allowed even if source is disabled. + ft.phraseQuery(ts, 0, true, context); + ft.termQuery("a", context); + } + + @Override + protected Object generateRandomInputValue(MappedFieldType ft) { + assumeFalse("We don't have a way to assert things here", true); + return null; + } + + @Override + protected void randomFetchTestFieldConfig(XContentBuilder b) throws IOException { + assumeFalse("We don't have a way to assert things here", true); + } + + @Override + protected boolean supportsIgnoreMalformed() { + return false; + } + + @Override + protected SyntheticSourceSupport syntheticSourceSupport(boolean ignoreMalformed) { + assertFalse("patterned_text doesn't support ignoreMalformed", ignoreMalformed); + return new PatternedTextSyntheticSourceSupport(); + } + + static class PatternedTextSyntheticSourceSupport implements SyntheticSourceSupport { + @Override + public SyntheticSourceExample example(int maxValues) { + Tuple v = generateValue(); + return new SyntheticSourceExample(v.v1(), v.v2(), this::mapping); + } + + private Tuple generateValue() { + StringBuilder builder = new StringBuilder(); + if (randomBoolean()) { + builder.append(randomAlphaOfLength(5)); + } else { + String timestamp = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(System.currentTimeMillis()); + builder.append(timestamp); + } + for (int i = 0; i < randomIntBetween(0, 9); i++) { + builder.append(" "); + int rand = randomIntBetween(0, 4); + switch (rand) { + case 0 -> builder.append(randomAlphaOfLength(5)); + case 1 -> builder.append(randomAlphanumericOfLength(5)); + case 2 -> builder.append(UUID.randomUUID()); + case 3 -> builder.append(randomIp(true)); + case 4 -> builder.append(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(randomMillisUpToYear9999())); + } + } + String value = builder.toString(); + return Tuple.tuple(value, value); + } + + private void mapping(XContentBuilder b) throws IOException { + b.field("type", "patterned_text"); + } + + @Override + public List invalidExample() throws IOException { + return List.of(); + } + } + + public void testDocValues() throws IOException { + MapperService mapper = createMapperService(fieldMapping(b -> b.field("type", "patterned_text"))); + assertScriptDocValues(mapper, "foo", equalTo(List.of("foo"))); + } + + public void testDocValuesSynthetic() throws IOException { + MapperService mapper = createSytheticSourceMapperService(fieldMapping(b -> b.field("type", "patterned_text"))); + assertScriptDocValues(mapper, "foo", equalTo(List.of("foo"))); + } + + @Override + public void testSyntheticSourceKeepArrays() { + // This mapper does not allow arrays + } + + @Override + protected IngestScriptSupport ingestScriptSupport() { + throw new AssumptionViolatedException("not supported"); + } +} diff --git a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextFieldTypeTests.java b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextFieldTypeTests.java new file mode 100644 index 000000000000..2e07c4c0d839 --- /dev/null +++ b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextFieldTypeTests.java @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.logsdb.patternedtext; + +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.index.Term; +import org.apache.lucene.queries.intervals.Intervals; +import org.apache.lucene.queries.intervals.IntervalsSource; +import org.apache.lucene.search.ConstantScoreQuery; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MultiPhraseQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.RegexpQuery; +import org.apache.lucene.search.TermInSetQuery; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TermRangeQuery; +import org.apache.lucene.tests.analysis.CannedTokenStream; +import org.apache.lucene.tests.analysis.Token; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.lucene.BytesRefs; +import org.elasticsearch.common.lucene.search.AutomatonQueries; +import org.elasticsearch.common.lucene.search.MultiPhrasePrefixQuery; +import org.elasticsearch.common.unit.Fuzziness; +import org.elasticsearch.index.mapper.FieldTypeTestCase; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.extras.SourceIntervalsSource; +import org.hamcrest.Matchers; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class PatternedTextFieldTypeTests extends FieldTypeTestCase { + + public void testTermQuery() { + MappedFieldType ft = new PatternedTextFieldType("field"); + assertEquals(new ConstantScoreQuery(new TermQuery(new Term("field", "foo"))), ft.termQuery("foo", null)); + assertEquals(AutomatonQueries.caseInsensitiveTermQuery(new Term("field", "fOo")), ft.termQueryCaseInsensitive("fOo", null)); + } + + public void testTermsQuery() { + MappedFieldType ft = new PatternedTextFieldType("field"); + List terms = new ArrayList<>(); + terms.add(new BytesRef("foo")); + terms.add(new BytesRef("123")); + assertEquals(new TermInSetQuery("field", terms), ft.termsQuery(Arrays.asList("foo", "123"), null)); + } + + public void testRangeQuery() { + MappedFieldType ft = new PatternedTextFieldType("field"); + assertEquals( + new TermRangeQuery("field", BytesRefs.toBytesRef("foo"), BytesRefs.toBytesRef("bar"), true, false), + ft.rangeQuery("foo", "bar", true, false, null, null, null, MOCK_CONTEXT) + ); + + ElasticsearchException ee = expectThrows( + ElasticsearchException.class, + () -> ft.rangeQuery("foo", "bar", true, false, null, null, null, MOCK_CONTEXT_DISALLOW_EXPENSIVE) + ); + assertEquals( + "[range] queries on [text] or [keyword] fields cannot be executed when " + "'search.allow_expensive_queries' is set to false.", + ee.getMessage() + ); + } + + public void testRegexpQuery() { + MappedFieldType ft = new PatternedTextFieldType("field"); + assertEquals(new RegexpQuery(new Term("field", "foo.*")), ft.regexpQuery("foo.*", 0, 0, 10, null, MOCK_CONTEXT)); + + ElasticsearchException ee = expectThrows( + ElasticsearchException.class, + () -> ft.regexpQuery("foo.*", randomInt(10), 0, randomInt(10) + 1, null, MOCK_CONTEXT_DISALLOW_EXPENSIVE) + ); + assertEquals("[regexp] queries cannot be executed when 'search.allow_expensive_queries' is set to false.", ee.getMessage()); + } + + public void testFuzzyQuery() { + MappedFieldType ft = new PatternedTextFieldType("field"); + assertEquals( + new ConstantScoreQuery(new FuzzyQuery(new Term("field", "foo"), 2, 1, 50, true)), + ft.fuzzyQuery("foo", Fuzziness.fromEdits(2), 1, 50, true, MOCK_CONTEXT) + ); + + ElasticsearchException ee = expectThrows( + ElasticsearchException.class, + () -> ft.fuzzyQuery( + "foo", + Fuzziness.AUTO, + randomInt(10) + 1, + randomInt(10) + 1, + randomBoolean(), + MOCK_CONTEXT_DISALLOW_EXPENSIVE + ) + ); + assertEquals("[fuzzy] queries cannot be executed when 'search.allow_expensive_queries' is set to false.", ee.getMessage()); + } + + private Query unwrapPositionalQuery(Query query) { + query = ((ConstantScoreQuery) query).getQuery(); + return query; + } + + public void testPhraseQuery() throws IOException { + MappedFieldType ft = new PatternedTextFieldType("field"); + TokenStream ts = new CannedTokenStream(new Token("a", 0, 3), new Token("1", 4, 7)); + Query query = ft.phraseQuery(ts, 0, true, MOCK_CONTEXT); + Query delegate = unwrapPositionalQuery(query); + assertEquals(new PhraseQuery("field", "a", "1").toString(), delegate.toString()); + } + + public void testMultiPhraseQuery() throws IOException { + MappedFieldType ft = new PatternedTextFieldType("field"); + TokenStream ts = new CannedTokenStream(new Token("a", 0, 3), new Token("2", 0, 0, 3), new Token("c", 4, 7)); + Query query = ft.multiPhraseQuery(ts, 0, true, MOCK_CONTEXT); + Query delegate = unwrapPositionalQuery(query); + Query expected = new MultiPhraseQuery.Builder().add(new Term[] { new Term("field", "a"), new Term("field", "2") }) + .add(new Term("field", "c")) + .build(); + assertEquals(expected.toString(), delegate.toString()); + } + + public void testPhrasePrefixQuery() throws IOException { + MappedFieldType ft = new PatternedTextFieldType("field"); + TokenStream ts = new CannedTokenStream(new Token("a", 0, 3), new Token("b", 0, 0, 3), new Token("c", 4, 7)); + Query query = ft.phrasePrefixQuery(ts, 0, 10, MOCK_CONTEXT); + Query delegate = unwrapPositionalQuery(query); + MultiPhrasePrefixQuery expected = new MultiPhrasePrefixQuery("field"); + expected.add(new Term[] { new Term("field", "a"), new Term("field", "b") }); + expected.add(new Term("field", "c")); + assertEquals(expected.toString(), delegate.toString()); + } + + public void testTermIntervals() { + MappedFieldType ft = new PatternedTextFieldType("field"); + IntervalsSource termIntervals = ft.termIntervals(new BytesRef("foo"), MOCK_CONTEXT); + assertThat(termIntervals, Matchers.instanceOf(SourceIntervalsSource.class)); + assertEquals(Intervals.term(new BytesRef("foo")), ((SourceIntervalsSource) termIntervals).getIntervalsSource()); + } + + public void testPrefixIntervals() { + MappedFieldType ft = new PatternedTextFieldType("field"); + IntervalsSource prefixIntervals = ft.prefixIntervals(new BytesRef("foo"), MOCK_CONTEXT); + assertThat(prefixIntervals, Matchers.instanceOf(SourceIntervalsSource.class)); + assertEquals( + Intervals.prefix(new BytesRef("foo"), IndexSearcher.getMaxClauseCount()), + ((SourceIntervalsSource) prefixIntervals).getIntervalsSource() + ); + } + + public void testWildcardIntervals() { + MappedFieldType ft = new PatternedTextFieldType("field"); + IntervalsSource wildcardIntervals = ft.wildcardIntervals(new BytesRef("foo"), MOCK_CONTEXT); + assertThat(wildcardIntervals, Matchers.instanceOf(SourceIntervalsSource.class)); + assertEquals( + Intervals.wildcard(new BytesRef("foo"), IndexSearcher.getMaxClauseCount()), + ((SourceIntervalsSource) wildcardIntervals).getIntervalsSource() + ); + } + + public void testRegexpIntervals() { + MappedFieldType ft = new PatternedTextFieldType("field"); + IntervalsSource regexpIntervals = ft.regexpIntervals(new BytesRef("foo"), MOCK_CONTEXT); + assertThat(regexpIntervals, Matchers.instanceOf(SourceIntervalsSource.class)); + assertEquals( + Intervals.regexp(new BytesRef("foo"), IndexSearcher.getMaxClauseCount()), + ((SourceIntervalsSource) regexpIntervals).getIntervalsSource() + ); + } + + public void testFuzzyIntervals() { + MappedFieldType ft = new PatternedTextFieldType("field"); + IntervalsSource fuzzyIntervals = ft.fuzzyIntervals("foo", 1, 2, true, MOCK_CONTEXT); + assertThat(fuzzyIntervals, Matchers.instanceOf(SourceIntervalsSource.class)); + } + + public void testRangeIntervals() { + MappedFieldType ft = new PatternedTextFieldType("field"); + IntervalsSource rangeIntervals = ft.rangeIntervals(new BytesRef("foo"), new BytesRef("foo1"), true, true, MOCK_CONTEXT); + assertThat(rangeIntervals, Matchers.instanceOf(SourceIntervalsSource.class)); + assertEquals( + Intervals.range(new BytesRef("foo"), new BytesRef("foo1"), true, true, IndexSearcher.getMaxClauseCount()), + ((SourceIntervalsSource) rangeIntervals).getIntervalsSource() + ); + } +} diff --git a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextValueProcessorTests.java b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextValueProcessorTests.java new file mode 100644 index 000000000000..58266b3dae1e --- /dev/null +++ b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/patternedtext/PatternedTextValueProcessorTests.java @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.logsdb.patternedtext; + +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; + +public class PatternedTextValueProcessorTests extends ESTestCase { + + public void testEmpty() { + String text = ""; + PatternedTextValueProcessor.Parts parts = PatternedTextValueProcessor.split(text); + assertEquals(text, parts.template()); + assertTrue(parts.args().isEmpty()); + assertEquals(text, PatternedTextValueProcessor.merge(parts)); + } + + public void testWhitespace() { + String text = " "; + PatternedTextValueProcessor.Parts parts = PatternedTextValueProcessor.split(text); + assertEquals(text, parts.template()); + assertTrue(parts.args().isEmpty()); + assertEquals(text, PatternedTextValueProcessor.merge(parts)); + } + + public void testWithoutTimestamp() { + String text = " some text with arg1 and 2arg2 and 333 "; + PatternedTextValueProcessor.Parts parts = PatternedTextValueProcessor.split(text); + assertEquals(" some text with %W and %W and %W ", parts.template()); + assertThat(parts.args(), Matchers.contains("arg1", "2arg2", "333")); + assertEquals(text, PatternedTextValueProcessor.merge(parts)); + } + + public void testWithTimestamp() { + String text = " 2021-04-13T13:51:38.000Z some text with arg1 and arg2 and arg3"; + PatternedTextValueProcessor.Parts parts = PatternedTextValueProcessor.split(text); + assertEquals(" %W some text with %W and %W and %W", parts.template()); + assertThat(parts.args(), Matchers.contains("2021-04-13T13:51:38.000Z", "arg1", "arg2", "arg3")); + assertEquals(text, PatternedTextValueProcessor.merge(parts)); + } + + public void testWithDateSpaceTime() { + String text = " 2021-04-13 13:51:38 some text with arg1 and arg2 and arg3"; + PatternedTextValueProcessor.Parts parts = PatternedTextValueProcessor.split(text); + assertEquals(" %W %W some text with %W and %W and %W", parts.template()); + assertThat(parts.args(), Matchers.contains("2021-04-13", "13:51:38", "arg1", "arg2", "arg3")); + assertEquals(text, PatternedTextValueProcessor.merge(parts)); + } + + public void testMalformedDate() { + String text = "2020/09/06 10:11:38 Using namespace: kubernetes-dashboard' | HTTP status: 400, message: [1:395]"; + PatternedTextValueProcessor.Parts parts = PatternedTextValueProcessor.split(text); + assertEquals("%W %W Using namespace: kubernetes-dashboard' | HTTP status: %W message: [%W]", parts.template()); + assertThat(parts.args(), Matchers.contains("2020/09/06", "10:11:38", "400,", "1:395")); + assertEquals(text, PatternedTextValueProcessor.merge(parts)); + } + + public void testUUID() { + String text = "[2020-08-18T00:58:56.751+00:00][15][2354][action_controller][INFO]: [18be2355-6306-4a00-9db9-f0696aa1a225] " + + "some text with arg1 and arg2"; + PatternedTextValueProcessor.Parts parts = PatternedTextValueProcessor.split(text); + assertEquals("[%W][%W][%W][action_controller][INFO]: [%W] some text with %W and %W", parts.template()); + assertThat( + parts.args(), + Matchers.contains("2020-08-18T00:58:56.751+00:00", "15", "2354", "18be2355-6306-4a00-9db9-f0696aa1a225", "arg1", "arg2") + ); + assertEquals(text, PatternedTextValueProcessor.merge(parts)); + } + + public void testIP() { + String text = "[2020-08-18T00:58:56.751+00:00][15][2354][action_controller][INFO]: from 94.168.152.150 and arg1"; + PatternedTextValueProcessor.Parts parts = PatternedTextValueProcessor.split(text); + assertEquals("[%W][%W][%W][action_controller][INFO]: from %W and %W", parts.template()); + assertThat(parts.args(), Matchers.contains("2020-08-18T00:58:56.751+00:00", "15", "2354", "94.168.152.150", "arg1")); + assertEquals(text, PatternedTextValueProcessor.merge(parts)); + } + + public void testSecondDate() { + String text = "[2020-08-18T00:58:56.751+00:00][15][2354][action_controller][INFO]: at 2020-08-18 00:58:56 +0000 and arg1"; + PatternedTextValueProcessor.Parts parts = PatternedTextValueProcessor.split(text); + assertEquals("[%W][%W][%W][action_controller][INFO]: at %W %W %W and %W", parts.template()); + assertThat( + parts.args(), + Matchers.contains("2020-08-18T00:58:56.751+00:00", "15", "2354", "2020-08-18", "00:58:56", "+0000", "arg1") + ); + assertEquals(text, PatternedTextValueProcessor.merge(parts)); + } + + public void testWithTimestamp1() { + String text = "[2020-08-18T00:58:56] Found 123 errors for service [cheddar1]"; + PatternedTextValueProcessor.Parts parts = PatternedTextValueProcessor.split(text); + assertEquals("[%W] Found %W errors for service [%W]", parts.template()); + assertThat(parts.args(), Matchers.contains("2020-08-18T00:58:56", "123", "cheddar1")); + assertEquals(text, PatternedTextValueProcessor.merge(parts)); + } +} diff --git a/x-pack/plugin/logsdb/src/yamlRestTest/java/org/elasticsearch/xpack/logsdb/LogsdbTestSuiteIT.java b/x-pack/plugin/logsdb/src/yamlRestTest/java/org/elasticsearch/xpack/logsdb/LogsdbTestSuiteIT.java index 4d7935f7d6bb..acb146d2af54 100644 --- a/x-pack/plugin/logsdb/src/yamlRestTest/java/org/elasticsearch/xpack/logsdb/LogsdbTestSuiteIT.java +++ b/x-pack/plugin/logsdb/src/yamlRestTest/java/org/elasticsearch/xpack/logsdb/LogsdbTestSuiteIT.java @@ -27,6 +27,7 @@ public class LogsdbTestSuiteIT extends ESClientYamlSuiteTestCase { @ClassRule public static final ElasticsearchCluster cluster = ElasticsearchCluster.local() + .module("logsdb") .distribution(DistributionType.DEFAULT) .user(USER, PASS, "superuser", false) .setting("xpack.security.autoconfiguration.enabled", "false") diff --git a/x-pack/plugin/logsdb/src/yamlRestTest/resources/rest-api-spec/test/52_esql_insist_operator_synthetic_source.yml b/x-pack/plugin/logsdb/src/yamlRestTest/resources/rest-api-spec/test/52_esql_insist_operator_synthetic_source.yml new file mode 100644 index 000000000000..36fee80877ae --- /dev/null +++ b/x-pack/plugin/logsdb/src/yamlRestTest/resources/rest-api-spec/test/52_esql_insist_operator_synthetic_source.yml @@ -0,0 +1,94 @@ +--- +setup: + - do: + indices.create: + index: my-index + body: + settings: + index: + mode: logsdb + mappings: + dynamic: false + properties: + "@timestamp": + type: date + message: + type: text + + - do: + bulk: + index: my-index + refresh: true + body: + - { "index": { } } + - { "@timestamp": "2024-02-12T10:30:00Z", "host.name": "foo", "agent_id": "darth-vader", "process_id": 101, "http_method": "GET", "is_https": false, "location": {"lat" : 40.7128, "lon" : -74.0060}, "message": "No, I am your father." } + - { "index": { } } + - { "@timestamp": "2024-02-12T10:31:00Z", "host.name": "bar", "agent_id": "yoda", "process_id": 102, "http_method": "PUT", "is_https": false, "location": {"lat" : 40.7128, "lon" : -74.0060}, "message": "Do. Or do not. There is no try." } + - { "index": { } } + - { "@timestamp": "2024-02-12T10:32:00Z", "host.name": "foo", "agent_id": "obi-wan", "process_id": 103, "http_method": "GET", "is_https": false, "location": {"lat" : 40.7128, "lon" : -74.0060}, "message": "May the force be with you." } + - { "index": { } } + - { "@timestamp": "2024-02-12T10:33:00Z", "host.name": "baz", "agent_id": "darth-vader", "process_id": 102, "http_method": "POST", "is_https": true, "location": {"lat" : 40.7128, "lon" : -74.0060}, "message": "I find your lack of faith disturbing." } + - { "index": { } } + - { "@timestamp": "2024-02-12T10:34:00Z", "host.name": "baz", "agent_id": "yoda", "process_id": 104, "http_method": "POST", "is_https": false, "location": {"lat" : 40.7128, "lon" : -74.0060}, "message": "Wars not make one great." } + - { "index": { } } + - { "@timestamp": "2024-02-12T10:35:00Z", "host.name": "foo", "agent_id": "obi-wan", "process_id": 105, "http_method": "GET", "is_https": false, "location": {"lat" : 40.7128, "lon" : -74.0060}, "message": "That's no moon. It's a space station." } + +--- +teardown: + - do: + indices.delete: + index: my-index + +--- +"Simple from": + - do: + esql.query: + body: + query: 'FROM my-index | SORT @timestamp | LIMIT 1' + + - match: {columns.0.name: "@timestamp"} + - match: {columns.0.type: "date"} + - match: {columns.1.name: "message"} + - match: {columns.1.type: "text"} + + - match: {values.0.0: "2024-02-12T10:30:00.000Z"} + - match: {values.0.1: "No, I am your father."} + +--- +"FROM with INSIST_🐔and LIMIT 1": + - do: + esql.query: + body: + query: 'FROM my-index | INSIST_🐔 host.name, agent_id, http_method | SORT @timestamp | KEEP host.name, agent_id, http_method | LIMIT 1' + + - match: {columns.0.name: "host.name"} + - match: {columns.0.type: "keyword"} + - match: {columns.1.name: "agent_id"} + - match: {columns.1.type: "keyword"} + - match: {columns.2.name: "http_method"} + - match: {columns.2.type: "keyword"} + + - match: {values.0.0: "foo"} + - match: {values.0.1: "darth-vader"} + - match: {values.0.2: "GET"} + +--- +"FROM with INSIST_🐔": + - requires: + test_runner_features: allowed_warnings_regex + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'FROM my-index | INSIST_🐔 agent_id | SORT @timestamp | KEEP agent_id' + + - match: {columns.0.name: "agent_id"} + - match: {columns.0.type: "keyword"} + + - match: {values.0.0: "darth-vader"} + - match: {values.1.0: "yoda"} + - match: {values.2.0: "obi-wan"} + - match: {values.3.0: "darth-vader"} + - match: {values.4.0: "yoda"} + - match: {values.5.0: "obi-wan"} diff --git a/x-pack/plugin/logsdb/src/yamlRestTest/resources/rest-api-spec/test/patternedtext/10_basic.yml b/x-pack/plugin/logsdb/src/yamlRestTest/resources/rest-api-spec/test/patternedtext/10_basic.yml new file mode 100644 index 000000000000..e25a2d2e76a7 --- /dev/null +++ b/x-pack/plugin/logsdb/src/yamlRestTest/resources/rest-api-spec/test/patternedtext/10_basic.yml @@ -0,0 +1,333 @@ +setup: + + - do: + indices.create: + index: test + body: + mappings: + properties: + foo: + type: patterned_text + + - do: + index: + index: test + id: "1" + body: {} + + - do: + index: + index: test + id: "2" + body: { "foo": "Found 5 errors for service [cheddar1]" } + + - do: + index: + index: test + id: "3" + body: { "foo": "[2020-08-18T00:58:56] Found 123 errors for service [cheddar1]" } + + - do: + index: + index: test + id: "4" + body: { "foo": "Found some errors for cheddar data service" } + + - do: + indices.refresh: {} + +--- +Field caps: + + - do: + field_caps: + index: test + fields: [ foo ] + + - match: { fields.foo.text.searchable: true } + - match: { fields.foo.text.aggregatable: false } + +--- +Exist query: + + - do: + search: + index: test + body: + query: + exists: + field: foo + + - match: { "hits.total.value": 3 } + - match: { "hits.hits.0._score": 1.0 } + +--- +Match query: + + - do: + search: + index: test + body: + query: + match: + foo: 5 + + - match: { "hits.total.value": 1 } + - match: { "hits.hits.0._score": 1.0 } + +--- +Match Phrase query: + + - do: + search: + index: test + body: + query: + match_phrase: + foo: "5 errors" + + - match: { "hits.total.value": 1 } + - match: { "hits.hits.0._score": 1.0 } + +--- +Match Phrase Prefix query: + + - do: + search: + index: test + body: + query: + match_phrase_prefix: + foo: "5 err" + + - match: { "hits.total.value": 1 } + - match: { "hits.hits.0._score": 1.0 } + + +--- +Query String query with phrase: + + - do: + search: + index: test + body: + query: + query_string: + query: '"5 errors"' + default_field: "foo" + + - match: { "hits.total.value": 1 } + - match: { "hits.hits.0._score": 1.0 } + + +--- +Regexp query: + + - do: + search: + index: test + body: + query: + regexp: + foo: "ser.*ce" + + - match: { "hits.total.value": 3 } + - match: { "hits.hits.0._score": 1.0 } + +--- +Wildcard query: + + - do: + search: + index: test + body: + query: + wildcard: + foo: "ser*ce" + + - match: { "hits.total.value": 3 } + - match: { "hits.hits.0._score": 1.0 } + +--- +Prefix query: + + - do: + search: + index: test + body: + query: + prefix: + foo: "ser" + + - match: { "hits.total.value": 3 } + - match: { "hits.hits.0._score": 1.0 } + +--- +Fuzzy query: + + - do: + search: + index: test + body: + query: + fuzzy: + foo: "errars" + + - match: { "hits.total.value": 3 } + - match: { "hits.hits.0._score": 1.0 } + +--- +Span query: + + - do: + catch: bad_request + search: + index: test + body: + query: + span_term: + foo: errors + +--- +Term intervals query: + + - do: + search: + index: test + body: + query: + intervals: + foo: + match: + query: "for service" + max_gaps: 1 + + - match: { "hits.total.value": 2 } + +--- +Prefix intervals query: + + - do: + search: + index: test + body: + query: + intervals: + foo: + prefix: + prefix: "ser" + + - match: { "hits.total.value": 3 } + +--- +Wildcard intervals query: + + - do: + search: + index: test + body: + query: + intervals: + foo: + wildcard: + pattern: "*edda*" + + - match: { "hits.total.value": 3 } + +--- +Fuzzy intervals query: + + - do: + search: + index: test + body: + query: + intervals: + foo: + fuzzy: + term: "servace" + + - match: { "hits.total.value": 3 } + +--- +Wildcard highlighting: + + - do: + search: + index: test + body: + query: + match: + foo: "5" + highlight: + fields: + "*": {} + + - match: { hits.total.value: 1 } + - match: { hits.hits.0._source.foo: "Found 5 errors for service [cheddar1]" } + - match: { hits.hits.0.highlight.foo.0: "Found 5 errors for service [cheddar1]" } + +--- +tsdb: + + - do: + indices.create: + index: tsdb_test + body: + settings: + index: + mode: time_series + routing_path: [ dimension ] + time_series: + start_time: 2000-01-01T00:00:00Z + end_time: 2099-12-31T23:59:59Z + mappings: + properties: + dimension: + type: keyword + time_series_dimension: true + foo: + type: patterned_text + + - do: + index: + index: tsdb_test + refresh: true + body: + "@timestamp": "2000-01-01T00:00:00Z" + dimension: "a" + foo: "Apache Lucene powers Elasticsearch" + + - do: + search: + index: tsdb_test + - match: { "hits.total.value": 1 } + - match: + hits.hits.0._source: + "@timestamp" : "2000-01-01T00:00:00.000Z" + "dimension" : "a" + foo: "Apache Lucene powers Elasticsearch" + +--- +Multiple values: + - do: + indices.create: + index: test1 + body: + mappings: + properties: + foo: + type: patterned_text + - do: + catch: bad_request + index: + index: test1 + id: "1" + body: { + "foo": [ + "Found 5 errors for service [cheddar1]", + "[2020-08-18T00:58:56] Found 123 errors for service [cheddar1]" + ] + } + + diff --git a/x-pack/plugin/logsdb/src/yamlRestTest/resources/rest-api-spec/test/patternedtext/20_synthetic_source.yml b/x-pack/plugin/logsdb/src/yamlRestTest/resources/rest-api-spec/test/patternedtext/20_synthetic_source.yml new file mode 100644 index 000000000000..a21ee18ac642 --- /dev/null +++ b/x-pack/plugin/logsdb/src/yamlRestTest/resources/rest-api-spec/test/patternedtext/20_synthetic_source.yml @@ -0,0 +1,76 @@ +simple: + - do: + indices.create: + index: test + body: + settings: + index: + mapping.source.mode: synthetic + mappings: + properties: + id: + type: integer + message: + type: patterned_text + + - do: + bulk: + index: test + refresh: true + body: + - '{ "create": { } }' + - '{ "id": 1, "message": "some log message with no arg" }' + - '{ "create": { } }' + - '{ "id": 2, "message": "another log message with arg 1234 and arg 5678 and a mixed one ABCD9" }' + - '{ "create": { } }' + - '{ "id": 3, "message": "some log message with no arg" }' + - '{ "create": { } }' + - '{ "id": 4, "message": "another log message with arg 1234 and arg 8765 and a mixed one ABCD1" }' + + - do: + search: + index: test + sort: id + + - match: { hits.hits.0._source.message: "some log message with no arg" } + - match: { hits.hits.1._source.message: "another log message with arg 1234 and arg 5678 and a mixed one ABCD9" } + - match: { hits.hits.2._source.message: "some log message with no arg" } + - match: { hits.hits.3._source.message: "another log message with arg 1234 and arg 8765 and a mixed one ABCD1" } + +--- +synthetic_source with copy_to: + + - do: + indices.create: + index: synthetic_source_test + body: + settings: + index: + mapping.source.mode: synthetic + mappings: + properties: + foo: + type: patterned_text + copy_to: copy + copy: + type: keyword + + - do: + index: + index: synthetic_source_test + id: "1" + refresh: true + body: + foo: "another log message with arg 1234 and arg 5678 and a mixed one ABCD9" + + - do: + search: + index: synthetic_source_test + body: + fields: ["copy"] + + - match: { "hits.total.value": 1 } + - match: + hits.hits.0._source.foo: "another log message with arg 1234 and arg 5678 and a mixed one ABCD9" + - match: + hits.hits.0.fields.copy.0: "another log message with arg 1234 and arg 5678 and a mixed one ABCD9" diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleter.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleter.java index 8abcb6b3f8d8..923135fe6b23 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleter.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobDataDeleter.java @@ -109,15 +109,7 @@ public class JobDataDeleter { */ public void deleteModelSnapshots(List modelSnapshots, ActionListener listener) { if (modelSnapshots.isEmpty()) { - listener.onResponse( - new BulkByScrollResponse( - TimeValue.ZERO, - new BulkByScrollTask.Status(Collections.emptyList(), null), - Collections.emptyList(), - Collections.emptyList(), - false - ) - ); + listener.onResponse(emptyBulkByScrollResponse()); return; } @@ -134,7 +126,12 @@ public class JobDataDeleter { indices.add(AnomalyDetectorsIndex.jobResultsAliasedName(modelSnapshot.getJobId())); } - String[] indicesToQuery = removeReadOnlyIndices(new ArrayList<>(indices), listener, "model snapshots", null); + String[] indicesToQuery = removeReadOnlyIndices( + new ArrayList<>(indices), + listener, + "model snapshots", + () -> listener.onResponse(emptyBulkByScrollResponse()) + ); if (indicesToQuery.length == 0) return; DeleteByQueryRequest deleteByQueryRequest = new DeleteByQueryRequest(indicesToQuery).setRefresh(true) @@ -147,6 +144,16 @@ public class JobDataDeleter { executeAsyncWithOrigin(client, ML_ORIGIN, DeleteByQueryAction.INSTANCE, deleteByQueryRequest, listener); } + private static BulkByScrollResponse emptyBulkByScrollResponse() { + return new BulkByScrollResponse( + TimeValue.ZERO, + new BulkByScrollTask.Status(Collections.emptyList(), null), + Collections.emptyList(), + Collections.emptyList(), + false + ); + } + /** * Asynchronously delete the annotations * If the deleteUserAnnotations field is set to true then all @@ -311,7 +318,7 @@ public class JobDataDeleter { List.of(AnomalyDetectorsIndex.jobResultsAliasedName(jobId)), listener, "datafeed timing stats", - null + () -> listener.onResponse(emptyBulkByScrollResponse()) ); if (indicesToQuery.length == 0) return; DeleteByQueryRequest deleteByQueryRequest = new DeleteByQueryRequest(indicesToQuery).setRefresh(true) @@ -504,7 +511,12 @@ public class JobDataDeleter { ActionListener refreshListener = ActionListener.wrap(refreshResponse -> { logger.info("[{}] running delete by query on [{}]", jobId, String.join(", ", indices)); ConstantScoreQueryBuilder query = new ConstantScoreQueryBuilder(new TermQueryBuilder(Job.ID.getPreferredName(), jobId)); - String[] indicesToQuery = removeReadOnlyIndices(List.of(indices), listener, "results", null); + String[] indicesToQuery = removeReadOnlyIndices( + List.of(indices), + listener, + "results", + () -> listener.onResponse(emptyBulkByScrollResponse()) + ); if (indicesToQuery.length == 0) return; DeleteByQueryRequest request = new DeleteByQueryRequest(indicesToQuery).setQuery(query) .setIndicesOptions(MlIndicesUtils.addIgnoreUnavailable(IndicesOptions.lenientExpandOpenHidden())) diff --git a/x-pack/qa/multi-project/core-rest-tests-with-multiple-projects/src/yamlRestTest/java/org/elasticsearch/multiproject/test/CoreWithMultipleProjectsClientYamlTestSuiteIT.java b/x-pack/qa/multi-project/core-rest-tests-with-multiple-projects/src/yamlRestTest/java/org/elasticsearch/multiproject/test/CoreWithMultipleProjectsClientYamlTestSuiteIT.java index da0432a3e3c5..305af0860740 100644 --- a/x-pack/qa/multi-project/core-rest-tests-with-multiple-projects/src/yamlRestTest/java/org/elasticsearch/multiproject/test/CoreWithMultipleProjectsClientYamlTestSuiteIT.java +++ b/x-pack/qa/multi-project/core-rest-tests-with-multiple-projects/src/yamlRestTest/java/org/elasticsearch/multiproject/test/CoreWithMultipleProjectsClientYamlTestSuiteIT.java @@ -18,7 +18,7 @@ import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; import org.junit.ClassRule; -@TimeoutSuite(millis = 30 * TimeUnits.MINUTE) +@TimeoutSuite(millis = 45 * TimeUnits.MINUTE) public class CoreWithMultipleProjectsClientYamlTestSuiteIT extends MultipleProjectsClientYamlSuiteTestCase { @ClassRule diff --git a/x-pack/qa/multi-project/xpack-rest-tests-with-multiple-projects/build.gradle b/x-pack/qa/multi-project/xpack-rest-tests-with-multiple-projects/build.gradle index b27413681518..70ddda657fae 100644 --- a/x-pack/qa/multi-project/xpack-rest-tests-with-multiple-projects/build.gradle +++ b/x-pack/qa/multi-project/xpack-rest-tests-with-multiple-projects/build.gradle @@ -8,7 +8,6 @@ dependencies { testImplementation project(':test:yaml-rest-runner') testImplementation project(':x-pack:qa:multi-project:yaml-test-framework') testImplementation(testArtifact(project(":x-pack:plugin:security:qa:service-account"), "javaRestTest")) - restXpackTestConfig project(path: ':x-pack:plugin:ilm:qa:rest', configuration: "basicRestSpecs") restXpackTestConfig project(path: ':x-pack:plugin:downsample:qa:rest', configuration: "basicRestSpecs") restXpackTestConfig project(path: ':x-pack:plugin:stack', configuration: "basicRestSpecs") } @@ -47,12 +46,6 @@ tasks.named("yamlRestTest").configure { '^esql/191_lookup_join_text/*', '^esql/192_lookup_join_on_aliases/*', '^health/10_usage/*', - '^ilm/10_basic/Test Undeletable Policy In Use', - '^ilm/20_move_to_step/*', - '^ilm/30_retry/*', - '^ilm/60_operation_mode/*', - '^ilm/60_remove_policy_for_index/*', - '^ilm/70_downsampling/*', '^ilm/80_health/*', '^logsdb/10_usage/*', '^migrate/10_reindex/*', diff --git a/x-pack/rest-resources-zip/build.gradle b/x-pack/rest-resources-zip/build.gradle index a613d91d8e9f..6cc39aa168e9 100644 --- a/x-pack/rest-resources-zip/build.gradle +++ b/x-pack/rest-resources-zip/build.gradle @@ -27,6 +27,7 @@ dependencies { platinumTests project(path: ':x-pack:plugin', configuration: 'restXpackTests') platinumTests project(path: ':x-pack:plugin:eql:qa:rest', configuration: 'restXpackTests') platinumTests project(path: ':x-pack:plugin:ent-search', configuration: 'restXpackTests') + platinumTests project(path: ':x-pack:plugin:inference', configuration: 'restXpackTests') platinumCompatTests project(path: ':x-pack:plugin', configuration: 'restCompatTests') platinumCompatTests project(path: ':x-pack:plugin:eql:qa:rest', configuration: 'restCompatTests') }