1
0
mirror of synced 2026-07-05 17:50:00 +00:00

Compare commits

..

53 Commits

Author SHA1 Message Date
Mark Paluch 5706fc0d3f Release version 6.1 RC1 (2026.0.0).
See #3258
2026-04-17 17:12:04 +02:00
Mark Paluch ad68771825 Prepare 6.1 RC1 (2026.0.0).
See #3258
2026-04-17 17:11:42 +02:00
Peter-Josef Meisch eb5c6872d9 Upgrade Elasticsearch to 9.3.3/9.3.4
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-04-16 23:34:49 +02:00
dependabot[bot] 4ee59bb52f Bump org.apache.logging.log4j:log4j-core from 2.25.3 to 2.25.4
Bumps org.apache.logging.log4j:log4j-core from 2.25.3 to 2.25.4.

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.25.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 12:35:30 +02:00
Ralph Ursprung ffdbea4dba Make AOT hints for ELC optional.
see also opensearch-project/spring-data-opensearch#441

Signed-off-by: Ralph Ursprung <Ralph.Ursprung@avaloq.com>
2026-03-30 20:16:14 +02:00
Peter-Josef Meisch 7c1bc087ba Polishing
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-03-29 18:39:28 +02:00
rafareborn 475df8ef80 Clarify that interface-based projections are not supported.
Closes #3261

Signed-off-by: hxreborn <hxreborn@duck.com>
2026-03-29 18:37:31 +02:00
Peter-Josef Meisch 78bef3105c Upgrade Elasticsearch client to 9.3.3.
Closes #3262

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-03-22 16:54:55 +01:00
Peter-Josef Meisch 81d40611b9 Upgrade Elasticsearch client to 9.3.2.
Closes #3259

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-03-14 13:29:57 +01:00
Peter-Josef Meisch 446869154f Polishing 2026-03-14 13:10:51 +01:00
Andriy Redko 836187c170 Allow to customize mappings parameters.
Signed-off-by: Andriy Redko <drreta@gmail.com>
2026-03-14 13:07:01 +01:00
Christoph Strobl 87427d96a2 After release cleanups.
See #3244
2026-03-13 14:23:21 +01:00
Christoph Strobl 4803ac2f98 Prepare next development iteration.
See #3244
2026-03-13 14:23:19 +01:00
Christoph Strobl da059770ba Release version 6.1 M2 (2026.0.0).
See #3244
2026-03-13 14:19:17 +01:00
Christoph Strobl 36056d1b63 Prepare 6.1 M2 (2026.0.0).
See #3244
2026-03-13 14:18:39 +01:00
Peter-Josef Meisch 2c01e1ee1a Polishing 2026-03-07 18:25:44 +01:00
Maryanto 9852589000 Add Micrometer Observation support. (#3249)
Signed-off-by: Maryanto <54889592+maryantocinn@users.noreply.github.com>
2026-03-07 18:20:30 +01:00
Peter-Josef Meisch 6e86bb2892 Upgrade to Elasticsearch 9.3.1.
Closes #3250

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-03-01 15:50:06 +01:00
Peter-Josef Meisch d9a70f8fb1 Polishing 2026-02-22 12:13:40 +01:00
Veljko Potparic 56ff6fcce2 Add support for includeNamedQueriesScore in Query
* Add support for includeNamedQueriesScore in Query
* Add integration tests in NativeQueryIntegrationTest class

Signed-off-by: veljko.potparic <veljko_velid@yahoo.com>
2026-02-22 12:12:00 +01:00
Mark Paluch 6f4a50c437 Update GitHub action branch triggers.
See #3244
2026-02-20 17:50:22 +01:00
Mark Paluch f2d8c71e64 Refine Antora-build.
See spring-projects/spring-data-build#2797
2026-02-20 17:17:25 +01:00
Mark Paluch 29997d114d Update GitHub action branch triggers.
See #3244
2026-02-19 14:47:21 +01:00
Christoph Strobl 2c77df67c3 Remove obsolete CI configuration.
See spring-projects/spring-data-build#2764
2026-02-16 15:03:57 +01:00
Mark Paluch 672315475d Add GitHub actions.
See spring-projects/spring-data-build#2764
2026-02-16 15:03:55 +01:00
noel1155 0c1f5369df Fix error propagation in AbstractReactiveElasticsearchTemplate:save()
Previously, errors occurring during the saveAll operation within the reactive save method were swallowed because the inner subscriber did not have an error handler. This caused the Flux to hang indefinitely instead of terminating with an error.

This commit adds an error handler to the inner subscriber that:
1. Cancels the upstream subscription to prevent further processing.
2. Propagates the error to the sink, allowing the caller to receive the error signal.
3. Updates the map operation to return the entity for better debugging capability.

Signed-off-by: Noel F <noel@Noels-MacBook-Pro.local>

* Add test for error propagation in reactive Flux save operations

This test verifies that errors occurring during saveAll operations
with a Flux are properly propagated to the subscriber instead of
being swallowed. The test creates a Flux that emits valid entities
followed by an error, and confirms the error reaches the caller.

Signed-off-by: Noel F <noel@Noels-MacBook-Pro.local>

* undo format fixes

Signed-off-by: Noel F <noel@Noels-MacBook-Pro.local>

* Update error propagation test: expect 0 entities before error due to race condition

The manual subscriber's onError fires before in-flight saveAll can push
results through tryEmitNext, so the caller sees 0 entities before the error.
Updated test expectation and added clarifying comment.

Signed-off-by: Noel F <noel@Noels-MacBook-Pro.local>

---------

Signed-off-by: Noel F <noel@Noels-MacBook-Pro.local>
Co-authored-by: xylos19 <noel@Noels-MacBook-Pro.local>

Closes #3233
2026-02-15 08:39:08 +01:00
Mark Paluch 0d688ac728 After release cleanups.
See #3198
2026-02-13 10:23:53 +01:00
Mark Paluch 0dddc9e952 Prepare next development iteration.
See #3198
2026-02-13 10:23:52 +01:00
Mark Paluch cf8d803877 Release version 6.1 M1 (2026.0.0).
See #3198
2026-02-13 10:21:30 +01:00
Mark Paluch a994520a38 Prepare 6.1 M1 (2026.0.0).
See #3198
2026-02-13 10:21:04 +01:00
Peter-Josef Meisch 2952389f26 Polishing
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-02-10 20:14:53 +01:00
Peter-Josef Meisch 548841cc22 Enable IndexCoordinates a parameter in repository search queries.
Closes #2506

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
Co-authored-by: Urs Keller <urs.keller@lightspeedhq.com>
2026-02-09 21:30:39 +01:00
Peter-Josef Meisch 2278d075a7 Use jackson3 version from the parent bom.
Closes #3239

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-02-09 21:18:58 +01:00
Peter-Josef Meisch 9a66c77b06 Upgrade to Elasticsearch 9.2.5.
Closes #3235

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-02-08 18:17:16 +01:00
Peter-Josef Meisch ead1926d13 Fix setting script id in UpdateQuery request.
Closes #3231

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-02-01 13:37:42 +01:00
Mark Paluch c5043d6544 Update CI Properties.
See #3198
2026-01-28 10:40:10 +01:00
Peter-Josef Meisch bccaa5c0ab Upgrade to Elasticsearch 9.2.4.
Closes #3227

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-01-23 20:50:35 +01:00
Mark Paluch 0a65d36040 Add Readme templates.
See spring-projects/spring-data-build#2758
2026-01-12 15:27:32 +01:00
Peter-Josef Meisch f5e9558f3c Remove deprecated code.
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-01-06 14:29:48 +01:00
Mark Paluch c118bb3b84 Extend license header copyright years to present.
See #3220
2026-01-05 08:45:17 +01:00
dependabot[bot] a7871385bf Bump org.apache.logging.log4j:log4j-core from 2.25.1 to 2.25.3 (#3215)
Bumps org.apache.logging.log4j:log4j-core from 2.25.1 to 2.25.3.

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.25.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-24 16:58:24 +01:00
Peter-Josef Meisch cf9de2fc7d Upgrade to Elasticsearch 9.2.3.
Closes #3216

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-12-24 15:55:20 +01:00
Mark Paluch e0199e3ed4 Polishing.
Simplify test dependency setup, remove no longer required servlet/xbean dependencies, exclude commons-lang3 in favor of the Testcontainers variant. Remove outdated commons-codec dependency in favor of the Testcontainers variant.

See #3212
2025-12-11 09:02:33 +01:00
Mark Paluch 1e5341ad07 Add @ContextConfiguration(…) to test classes that use @SpringIntegrationTest on super classes.
Closes #3212
2025-12-11 08:59:58 +01:00
Mark Paluch 6c7c323246 Update CI Properties.
See #3198
2025-12-10 08:35:42 +01:00
Mark Paluch d623f228a0 Remove project.yml. 2025-12-10 08:34:04 +01:00
Peter-Josef Meisch 8c0e62c9d8 Upgrade to Elasticsearch 9.2.2.
Closes #3207

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-12-04 23:41:51 +01:00
Peter-Josef Meisch 69746441e1 Fix UpdateQuery.Builder to allow only a scriptname to be set.
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-12-04 23:24:35 +01:00
Peter-Josef Meisch e31b66768b Adjust aot hints for Elasticsearch 9 client.
The hints for the old httpclient are only needed when the old library is on the classpath, in case a user still uses the old RestClient. For the new Elasticsearch client there are no aot hints required.

Closes: #3203

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-11-25 20:09:58 +01:00
Peter-Josef Meisch e7e0ac89e2 Migrate to Jackson3.
Jackson2 ist still needed as runtime optional dependency for users that still use the old RestClient.

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-11-14 20:14:40 +01:00
Peter-Josef Meisch 5821a81db9 Fix documentation.
Closes #3199

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2025-11-14 19:01:33 +01:00
Mark Paluch eabfd95497 After release cleanups.
See #3186
2025-11-14 13:56:11 +01:00
Mark Paluch b712cb8829 Prepare next development iteration.
See #3186
2025-11-14 13:56:10 +01:00
58 changed files with 2204 additions and 380 deletions
+3 -5
View File
@@ -3,7 +3,7 @@ name: CI Build
on:
workflow_dispatch:
push:
branches: [ 6.0.x, 'issue/6.0.x/**' ]
branches: [ main, 'issue/**' ]
permissions: read-all
@@ -17,13 +17,11 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup Java and Maven
uses: spring-projects/spring-data-build/actions/setup-maven@4.0.x
uses: spring-projects/spring-data-build/actions/setup-maven@main
with:
java-version: ${{ matrix.java-version }}
develocity-access-key: '${{ secrets.DEVELOCITY_ACCESS_KEY }}'
- name: Build
uses: spring-projects/spring-data-build/actions/maven-build@4.0.x
uses: spring-projects/spring-data-build/actions/maven-build@main
env:
TESTCONTAINERS_REUSE_ENABLE: true
with:
settings-xml: '${{ vars.SETTINGS_XML }}'
-45
View File
@@ -1,45 +0,0 @@
# GitHub Actions to automate GitHub issues for Spring Data Project Management
name: Spring Data GitHub Issues
on:
issues:
types: [opened, edited, reopened]
issue_comment:
types: [created]
pull_request_target:
types: [opened, edited, reopened]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
Inbox:
runs-on: ubuntu-latest
if: vars.PROJECT_CARDS_ENABLED == 'true' && github.repository_owner == 'spring-projects' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request == null && !contains(join(github.event.issue.labels.*.name, ', '), 'dependency-upgrade') && !contains(github.event.issue.title, 'Release ')
steps:
- name: Create or Update Issue Card
uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/spring-projects/projects/25
github-token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }}
Pull-Request:
runs-on: ubuntu-latest
if: vars.PROJECT_CARDS_ENABLED == 'true' && github.repository_owner == 'spring-projects' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request != null
steps:
- name: Create or Update Pull Request Card
uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/spring-projects/projects/25
github-token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }}
Feedback-Provided:
runs-on: ubuntu-latest
if: vars.PROJECT_CARDS_ENABLED == 'true' && github.repository_owner == 'spring-projects' && github.event_name == 'issue_comment' && github.event.action == 'created' && github.actor != 'spring-projects-issues' && github.event.pull_request == null && github.event.issue.state == 'open' && contains(toJSON(github.event.issue.labels), 'waiting-for-feedback')
steps:
- name: Update Project Card
uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/spring-projects/projects/25
github-token: ${{ secrets.GH_ISSUES_TOKEN_SPRING_DATA }}
+3 -7
View File
@@ -3,7 +3,7 @@ name: Snapshots
on:
workflow_dispatch:
push:
branches: [ 6.0.x, 'issue/6.0.x/**' ]
branches: [ main, 'issue/**' ]
permissions: read-all
@@ -15,18 +15,14 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup Java and Maven
uses: spring-projects/spring-data-build/actions/setup-maven@4.0.x
uses: spring-projects/spring-data-build/actions/setup-maven@main
with:
develocity-access-key: '${{ secrets.DEVELOCITY_ACCESS_KEY }}'
- name: Deploy to Artifactory
uses: spring-projects/spring-data-build/actions/maven-artifactory-deploy@4.0.x
uses: spring-projects/spring-data-build/actions/maven-artifactory-deploy@main
env:
TESTCONTAINERS_REUSE_ENABLE: true
with:
build-name: 'spring-data-elasticsearch'
username: '${{ secrets.ARTIFACTORY_USERNAME }}'
password: '${{ secrets.ARTIFACTORY_PASSWORD }}'
context-url: '${{ vars.ARTIFACTORY_CONTEXT_URL }}'
repository: '${{ vars.ARTIFACTORY_REPOSITORY }}'
project: '${{ vars.ARTIFACTORY_PROJECT }}'
settings-xml: '${{ vars.SETTINGS_XML }}'
+2 -2
View File
@@ -1,3 +1,3 @@
#Tue Jun 02 14:59:45 CEST 2026
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
#Thu Jul 17 13:59:56 CEST 2025
wrapperUrl=https\://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
+29 -9
View File
@@ -5,12 +5,12 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>6.0.6</version>
<version>6.1.0-RC1</version>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>4.0.6</version>
<version>4.1.0-RC1</version>
</parent>
<name>Spring Data Elasticsearch</name>
@@ -18,14 +18,14 @@
<url>https://github.com/spring-projects/spring-data-elasticsearch</url>
<properties>
<springdata.commons>4.0.6</springdata.commons>
<springdata.commons>4.1.0-RC1</springdata.commons>
<!-- version of the ElasticsearchClient -->
<elasticsearch-java>9.2.8</elasticsearch-java>
<elasticsearch-rest-client>9.2.8</elasticsearch-rest-client>
<elasticsearch-java>9.3.4</elasticsearch-java>
<elasticsearch-rest-client>9.3.3</elasticsearch-rest-client>
<hoverfly>0.20.2</hoverfly>
<log4j>2.25.1</log4j>
<log4j>2.25.4</log4j>
<jsonassert>1.5.3</jsonassert>
<wiremock>3.9.2</wiremock>
@@ -82,7 +82,8 @@
<scm>
<url>https://github.com/spring-projects/spring-data-elasticsearch</url>
<connection>scm:git:git://github.com/spring-projects/spring-data-elasticsearch.git</connection>
<developerConnection>scm:git:ssh://git@github.com/spring-projects/spring-data-elasticsearch.git</developerConnection>
<developerConnection>scm:git:ssh://git@github.com/spring-projects/spring-data-elasticsearch.git
</developerConnection>
</scm>
<issueManagement>
@@ -178,17 +179,30 @@
</dependency>
<!-- Jackson JSON Mapper -->
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Version 2 to use with the legacy RestClient -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<!-- CDI -->
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
@@ -341,7 +355,13 @@
<scope>test</scope>
</dependency>
</dependencies>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
+29
View File
@@ -0,0 +1,29 @@
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>spring-plugins-release</id>
<username>${env.ARTIFACTORY_USR}</username>
<password>${env.ARTIFACTORY_PSW}</password>
</server>
<server>
<id>spring-libs-snapshot</id>
<username>${env.ARTIFACTORY_USR}</username>
<password>${env.ARTIFACTORY_PSW}</password>
</server>
<server>
<id>spring-libs-milestone</id>
<username>${env.ARTIFACTORY_USR}</username>
<password>${env.ARTIFACTORY_PSW}</password>
</server>
<server>
<id>spring-libs-release</id>
<username>${env.ARTIFACTORY_USR}</username>
<password>${env.ARTIFACTORY_PSW}</password>
</server>
</servers>
</settings>
@@ -1,12 +1,20 @@
[[new-features]]
= What's new
[[new-features.6-1-0]]
== New in Spring Data Elasticsearch 6.1
* Upgrade to Elasticsearch 9.3.3/ Client 9.3.4
* Add support to use `IndexCoordinates` as repository query parameter
* Add support for includeNamedQueriesScore in Query
* Add support for Micrometer observation.
[[new-features.6-0-0]]
== New in Spring Data Elasticsearch 6.0
* Upgrade to Spring 7
* Switch to jspecify nullability annotations
* Upgrade to Elasticsearch 9.2.2
* Upgrade to Elasticsearch 9.2.1
* Use the new Elasticsearch Rest5Client as default
* Add support for SpEL expressions in the `settingPath` parameter of the `@Setting` annotation
@@ -6,7 +6,8 @@ The following table shows the Elasticsearch and Spring versions that are used by
[cols="^,^,^,^",options="header"]
|===
| Spring Data Release Train | Spring Data Elasticsearch | Elasticsearch | Spring Framework
| 2025.1 | 6.0.x | 9.2.8 | 7.0.x
| 2026.0 | 6.1.x | 9.3.4 | 7.0.x
| 2025.1 | 6.0.x | 9.2.2 | 7.0.x
| 2025.0 | 5.5.x | 8.18.1 | 6.2.x
| 2024.1 | 5.4.xfootnote:oom[Out of maintenance] | 8.15.5 | 6.1.x
| 2024.0 | 5.3.xfootnote:oom[] | 8.13.4 | 6.1.x
@@ -1,4 +1,13 @@
[[elasticsearch.projections]]
= Projections
[[elasticsearch.projections.limitations]]
== Spring Data Elasticsearch Projection Limitations
This chapter is pulled in from the Spring Data Commons documentation, but does not apply to Spring Data Elasticsearch.
IMPORTANT: Interface-based projections are not supported in Spring Data Elasticsearch repository query methods.
To limit the fields returned from Elasticsearch, use the xref:elasticsearch/repositories/elasticsearch-repositories.adoc#elasticsearch.repositories.annotations.sourcefilters[`@SourceFilters`] annotation on your repository methods instead.
include::{commons}@data-commons::page$repositories/projections.adoc[leveloffset=+1]
@@ -0,0 +1,70 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.client.elc;
import io.micrometer.common.KeyValues;
/**
* Default {@link ElasticsearchObservationConvention} implementation.
*
* @author maryantocinn
* @since 6.1
*/
public class DefaultElasticsearchObservationConvention implements ElasticsearchObservationConvention {
public static final DefaultElasticsearchObservationConvention INSTANCE = new DefaultElasticsearchObservationConvention();
@Override
public String getName() {
return ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.getName();
}
@Override
public String getContextualName(ElasticsearchObservationContext context) {
String indexName = context.getIndexName();
if (indexName != null) {
return context.getOperationName().getValue() + " " + indexName;
}
return context.getOperationName().getValue();
}
@Override
public KeyValues getLowCardinalityKeyValues(ElasticsearchObservationContext context) {
KeyValues keyValues = KeyValues.of(
ElasticsearchObservation.LowCardinalityKeyNames.OPERATION.withValue(context.getOperationName().getValue()));
String indexName = context.getIndexName();
if (indexName != null) {
keyValues = keyValues.and(ElasticsearchObservation.LowCardinalityKeyNames.COLLECTION.withValue(indexName));
}
return keyValues;
}
@Override
public KeyValues getHighCardinalityKeyValues(ElasticsearchObservationContext context) {
Integer batchSize = context.getBatchSize();
if (batchSize != null) {
return KeyValues.of(
ElasticsearchObservation.HighCardinalityKeyNames.BATCH_SIZE.withValue(String.valueOf(batchSize)));
}
return KeyValues.empty();
}
}
@@ -17,12 +17,14 @@ package org.springframework.data.elasticsearch.client.elc;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.json.jackson.Jackson3JsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.rest5_client.Rest5ClientOptions;
import co.elastic.clients.transport.rest5_client.low_level.RequestOptions;
import co.elastic.clients.transport.rest5_client.low_level.Rest5Client;
import tools.jackson.databind.cfg.JsonNodeFeature;
import tools.jackson.databind.json.JsonMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
@@ -32,10 +34,6 @@ import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* Base class for a @{@link org.springframework.context.annotation.Configuration} class to set up the Elasticsearch
* connection using the Elasticsearch Client. This class exposes different parts of the setup as Spring beans. Deriving
@@ -128,10 +126,11 @@ public abstract class ElasticsearchConfiguration extends ElasticsearchConfigurat
// we need to create our own objectMapper that keeps null values in order to provide the storeNullValue
// functionality. The one Elasticsearch would provide removes the nulls. We remove unwanted nulls before they get
// into this mapper, so we can safely keep them here.
var objectMapper = (new ObjectMapper())
.configure(SerializationFeature.INDENT_OUTPUT, false)
.setSerializationInclusion(JsonInclude.Include.ALWAYS);
return new JacksonJsonpMapper(objectMapper);
JsonMapper jsonMapper = JsonMapper.builder()
.enable(JsonNodeFeature.WRITE_NULL_PROPERTIES)
.enable(JsonNodeFeature.READ_NULL_PROPERTIES)
.build();
return new Jackson3JsonpMapper(jsonMapper);
}
/**
@@ -47,7 +47,7 @@ import com.fasterxml.jackson.databind.SerializationFeature;
* @since 4.4
* @deprecated since 6.0, use {@link ElasticsearchConfiguration}
*/
@Deprecated(since = "6.0", forRemoval=true)
@Deprecated(since = "6.0", forRemoval = true)
public abstract class ElasticsearchLegacyRestClientConfiguration extends ElasticsearchConfigurationSupport {
/**
@@ -0,0 +1,99 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.client.elc;
import io.micrometer.common.docs.KeyName;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;
import io.micrometer.observation.docs.ObservationDocumentation;
/**
* {@link ObservationDocumentation} for Spring Data Elasticsearch template operations.
*
* @author maryantocinn
* @since 6.1
*/
public enum ElasticsearchObservation implements ObservationDocumentation {
/**
* Timer created around a Spring Data Elasticsearch template operation.
*/
ELASTICSEARCH_COMMAND_OBSERVATION {
@Override
public String getName() {
return "spring.data.elasticsearch.command";
}
@Override
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
return DefaultElasticsearchObservationConvention.class;
}
@Override
public KeyName[] getLowCardinalityKeyNames() {
return LowCardinalityKeyNames.values();
}
@Override
public KeyName[] getHighCardinalityKeyNames() {
return HighCardinalityKeyNames.values();
}
};
/**
* Low cardinality key names for Spring Data Elasticsearch observations. These become metric dimensions and MUST be
* present on every observation to satisfy backends like Prometheus that require consistent tag key sets.
*/
enum LowCardinalityKeyNames implements KeyName {
/**
* The Spring Data operation being performed (e.g., save, search, delete, bulk).
*/
OPERATION {
@Override
public String asString() {
return "spring.data.operation";
}
},
/**
* The target collection (index) name. Only present when the operation targets a specific index.
*/
COLLECTION {
@Override
public String asString() {
return "spring.data.collection";
}
}
}
/**
* High cardinality key names for Spring Data Elasticsearch observations. These appear only on traces/spans, not on
* metrics, because their values are unbounded or optional per operation.
*/
enum HighCardinalityKeyNames implements KeyName {
/**
* The number of operations included in a batch (bulk) request. Only present for bulk operations.
*/
BATCH_SIZE {
@Override
public String asString() {
return "spring.data.batch.size";
}
}
}
}
@@ -0,0 +1,81 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.client.elc;
import io.micrometer.observation.Observation;
import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
/**
* {@link Observation.Context} for Spring Data Elasticsearch operations. One instance is created per observed operation.
* It carries contextual data that conventions use to produce observation names and key-values.
*
* @author maryantocinn
* @since 6.1
*/
public class ElasticsearchObservationContext extends Observation.Context {
private final ElasticsearchOperationName operationName;
@Nullable private final IndexCoordinates indexCoordinates;
@Nullable private Integer batchSize;
public ElasticsearchObservationContext(ElasticsearchOperationName operationName,
@Nullable IndexCoordinates indexCoordinates) {
this.operationName = operationName;
this.indexCoordinates = indexCoordinates;
}
/**
* @return the Spring Data operation being performed.
*/
public ElasticsearchOperationName getOperationName() {
return operationName;
}
/**
* @return the target index coordinates, or {@literal null} if the operation is not index-specific.
*/
@Nullable
public IndexCoordinates getIndexCoordinates() {
return indexCoordinates;
}
/**
* @return the comma-joined index name(s), or {@literal null} if no index coordinates are set.
*/
@Nullable
public String getIndexName() {
return indexCoordinates != null ? String.join(",", indexCoordinates.getIndexNames()) : null;
}
/**
* @return the batch size, or {@literal null} if not a batch operation.
*/
@Nullable
public Integer getBatchSize() {
return batchSize;
}
/**
* Set the number of operations included in a batch (bulk) request.
*
* @param batchSize the batch size, can be {@literal null}
*/
public void setBatchSize(@Nullable Integer batchSize) {
this.batchSize = batchSize;
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.client.elc;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;
/**
* {@link ObservationConvention} for Spring Data Elasticsearch operations. Implement this interface and register it as a
* bean to customize observation names and key-values.
*
* @author maryantocinn
* @since 6.1
*/
public interface ElasticsearchObservationConvention extends ObservationConvention<ElasticsearchObservationContext> {
@Override
default boolean supportsContext(Observation.Context context) {
return context instanceof ElasticsearchObservationContext;
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.client.elc;
/**
* Enumeration of Spring Data Elasticsearch operation names used in observations.
*
* @author maryantocinn
* @since 6.1
*/
public enum ElasticsearchOperationName {
SAVE("save"), //
INDEX("index"), //
GET("get"), //
MULTI_GET("multiGet"), //
EXISTS("exists"), //
DELETE("delete"), //
DELETE_BY_QUERY("deleteByQuery"), //
BULK("bulk"), //
UPDATE("update"), //
UPDATE_BY_QUERY("updateByQuery"), //
COUNT("count"), //
SEARCH("search");
private final String value;
ElasticsearchOperationName(String value) {
this.value = value;
}
/**
* @return the operation name as a string value used in observation key values.
*/
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
}
}
@@ -17,30 +17,11 @@ package org.springframework.data.elasticsearch.client.elc;
import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;
import co.elastic.clients.elasticsearch.core.msearch.MultiSearchResponseItem;
import co.elastic.clients.elasticsearch.core.search.ResponseBody;
import co.elastic.clients.elasticsearch.sql.ElasticsearchSqlClient;
import co.elastic.clients.elasticsearch.sql.QueryResponse;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.transport.Version;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.data.elasticsearch.BulkFailureException;
import org.springframework.data.elasticsearch.client.UnsupportedBackendOperation;
import org.springframework.data.elasticsearch.core.AbstractElasticsearchTemplate;
@@ -62,6 +43,30 @@ import org.springframework.data.elasticsearch.core.script.Script;
import org.springframework.data.elasticsearch.core.sql.SqlResponse;
import org.springframework.util.Assert;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;
import co.elastic.clients.elasticsearch.core.msearch.MultiSearchResponseItem;
import co.elastic.clients.elasticsearch.core.search.ResponseBody;
import co.elastic.clients.elasticsearch.sql.ElasticsearchSqlClient;
import co.elastic.clients.elasticsearch.sql.QueryResponse;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.transport.Version;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
/**
* Implementation of {@link org.springframework.data.elasticsearch.core.ElasticsearchOperations} using the new
* Elasticsearch client.
@@ -70,12 +75,15 @@ import org.springframework.util.Assert;
* @author Hamid Rahimi
* @author Illia Ulianov
* @author Haibo Liu
* @author maryantocinn
* @since 4.4
*/
public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
private static final Log LOGGER = LogFactory.getLog(ElasticsearchTemplate.class);
@Nullable private ElasticsearchObservationConvention observationConvention;
private final ElasticsearchClient client;
private final ElasticsearchSqlClient sqlClient;
private final RequestConverter requestConverter;
@@ -113,6 +121,61 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
protected AbstractElasticsearchTemplate doCopy() {
return new ElasticsearchTemplate(client, elasticsearchConverter);
}
@Override
protected void customizeCopy(AbstractElasticsearchTemplate copy) {
if (copy instanceof ElasticsearchTemplate elasticsearchTemplate) {
elasticsearchTemplate.observationConvention = this.observationConvention;
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
super.setApplicationContext(applicationContext);
if (observationRegistry == ObservationRegistry.NOOP) {
applicationContext.getBeanProvider(ObservationRegistry.class).ifAvailable(this::setObservationRegistry);
}
if (observationConvention == null) {
applicationContext.getBeanProvider(ElasticsearchObservationConvention.class)
.ifAvailable(this::setObservationConvention);
}
}
/**
* Set a custom {@link ElasticsearchObservationConvention} to override the default convention.
*
* @param observationConvention can be {@literal null}.
* @since 6.1
*/
public void setObservationConvention(@Nullable ElasticsearchObservationConvention observationConvention) {
this.observationConvention = observationConvention;
}
private <T> T observe(ElasticsearchOperationName operationName, @Nullable IndexCoordinates index,
Supplier<T> action) {
Observation observation = createObservation(operationName, index, null);
return observation.observe(action);
}
private <T> T observe(ElasticsearchOperationName operationName, @Nullable IndexCoordinates index, int batchSize,
Supplier<T> action) {
Observation observation = createObservation(operationName, index, batchSize);
return observation.observe(action);
}
private Observation createObservation(ElasticsearchOperationName operationName, @Nullable IndexCoordinates index,
@Nullable Integer batchSize) {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(operationName, index);
context.setBatchSize(batchSize);
return ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.observation(observationConvention,
DefaultElasticsearchObservationConvention.INSTANCE, () -> context, observationRegistry);
}
// endregion
// region child templates
@@ -137,16 +200,45 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
// endregion
// region document operations
@Override
public <T> T save(T entity, IndexCoordinates index) {
return observe(ElasticsearchOperationName.SAVE, index, () -> super.save(entity, index));
}
@Override
public String index(IndexQuery query, IndexCoordinates index) {
return observe(ElasticsearchOperationName.INDEX, index, () -> super.index(query, index));
}
@Override
public boolean exists(String id, IndexCoordinates index) {
return observe(ElasticsearchOperationName.EXISTS, index, () -> super.exists(id, index));
}
@Override
public String delete(String id, IndexCoordinates index) {
return observe(ElasticsearchOperationName.DELETE, index, () -> super.delete(id, index));
}
@Override
public List<IndexedObjectInformation> bulkOperation(List<?> queries, BulkOptions bulkOptions,
IndexCoordinates index) {
return observe(ElasticsearchOperationName.BULK, index, queries.size(),
() -> super.bulkOperation(queries, bulkOptions, index));
}
@Override
@Nullable
public <T> T get(String id, Class<T> clazz, IndexCoordinates index) {
GetRequest getRequest = requestConverter.documentGetRequest(elasticsearchConverter.convertId(id),
routingResolver.getRouting(), index);
GetResponse<EntityAsMap> getResponse = execute(client -> client.get(getRequest, EntityAsMap.class));
return observe(ElasticsearchOperationName.GET, index, () -> {
GetRequest getRequest = requestConverter.documentGetRequest(elasticsearchConverter.convertId(id),
routingResolver.getRouting(), index);
GetResponse<EntityAsMap> getResponse = execute(client -> client.get(getRequest, EntityAsMap.class));
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
return callback.doWith(DocumentAdapters.from(getResponse));
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
return callback.doWith(DocumentAdapters.from(getResponse));
});
}
@Override
@@ -155,15 +247,17 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(query, "query must not be null");
Assert.notNull(clazz, "clazz must not be null");
MgetRequest request = requestConverter.documentMgetRequest(query, clazz, index);
MgetResponse<EntityAsMap> result = execute(client -> client.mget(request, EntityAsMap.class));
return observe(ElasticsearchOperationName.MULTI_GET, index, () -> {
MgetRequest request = requestConverter.documentMgetRequest(query, clazz, index);
MgetResponse<EntityAsMap> result = execute(client -> client.mget(request, EntityAsMap.class));
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(elasticsearchConverter, clazz, index);
return DocumentAdapters.from(result).stream() //
.map(multiGetItem -> MultiGetItem.of( //
multiGetItem.isFailed() ? null : callback.doWith(multiGetItem.getItem()), multiGetItem.getFailure())) //
.collect(Collectors.toList());
return DocumentAdapters.from(result).stream() //
.map(multiGetItem -> MultiGetItem.of( //
multiGetItem.isFailed() ? null : callback.doWith(multiGetItem.getItem()), multiGetItem.getFailure())) //
.collect(Collectors.toList());
});
}
@Override
@@ -185,22 +279,26 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
public ByQueryResponse delete(DeleteQuery query, Class<?> clazz, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(),
clazz, index, getRefreshPolicy());
return observe(ElasticsearchOperationName.DELETE_BY_QUERY, index, () -> {
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(),
clazz, index, getRefreshPolicy());
DeleteByQueryResponse response = execute(client -> client.deleteByQuery(request));
DeleteByQueryResponse response = execute(client -> client.deleteByQuery(request));
return responseConverter.byQueryResponse(response);
return responseConverter.byQueryResponse(response);
});
}
@Override
public UpdateResponse update(UpdateQuery updateQuery, IndexCoordinates index) {
UpdateRequest<Document, ?> request = requestConverter.documentUpdateRequest(updateQuery, index, getRefreshPolicy(),
routingResolver.getRouting());
co.elastic.clients.elasticsearch.core.UpdateResponse<Document> response = execute(
client -> client.update(request, Document.class));
return UpdateResponse.of(result(response.result()));
return observe(ElasticsearchOperationName.UPDATE, index, () -> {
UpdateRequest<Document, ?> request = requestConverter.documentUpdateRequest(updateQuery, index,
getRefreshPolicy(), routingResolver.getRouting());
co.elastic.clients.elasticsearch.core.UpdateResponse<Document> response = execute(
client -> client.update(request, Document.class));
return UpdateResponse.of(result(response.result()));
});
}
@Override
@@ -209,11 +307,13 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(updateQuery, "updateQuery must not be null");
Assert.notNull(index, "index must not be null");
UpdateByQueryRequest request = requestConverter.documentUpdateByQueryRequest(updateQuery, index,
getRefreshPolicy());
return observe(ElasticsearchOperationName.UPDATE_BY_QUERY, index, () -> {
UpdateByQueryRequest request = requestConverter.documentUpdateByQueryRequest(updateQuery, index,
getRefreshPolicy());
UpdateByQueryResponse byQueryResponse = execute(client -> client.updateByQuery(request));
return responseConverter.byQueryResponse(byQueryResponse);
UpdateByQueryResponse byQueryResponse = execute(client -> client.updateByQuery(request));
return responseConverter.byQueryResponse(byQueryResponse);
});
}
@Override
@@ -328,12 +428,14 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(query, "query must not be null");
Assert.notNull(index, "index must not be null");
SearchRequest searchRequest = requestConverter.searchRequest(query, routingResolver.getRouting(), clazz, index,
true);
return observe(ElasticsearchOperationName.COUNT, index, () -> {
SearchRequest searchRequest = requestConverter.searchRequest(query, routingResolver.getRouting(), clazz, index,
true);
SearchResponse<EntityAsMap> searchResponse = execute(client -> client.search(searchRequest, EntityAsMap.class));
SearchResponse<EntityAsMap> searchResponse = execute(client -> client.search(searchRequest, EntityAsMap.class));
return searchResponse.hits().total().value();
return searchResponse.hits().total().value();
});
}
@Override
@@ -343,11 +445,13 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate {
Assert.notNull(clazz, "clazz must not be null");
Assert.notNull(index, "index must not be null");
if (query instanceof SearchTemplateQuery searchTemplateQuery) {
return doSearch(searchTemplateQuery, clazz, index);
} else {
return doSearch(query, clazz, index);
}
return observe(ElasticsearchOperationName.SEARCH, index, () -> {
if (query instanceof SearchTemplateQuery searchTemplateQuery) {
return doSearch(searchTemplateQuery, clazz, index);
} else {
return doSearch(query, clazz, index);
}
});
}
protected <T> SearchHits<T> doSearch(Query query, Class<T> clazz, IndexCoordinates index) {
@@ -16,12 +16,14 @@
package org.springframework.data.elasticsearch.client.elc;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.json.jackson.Jackson3JsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.rest5_client.Rest5ClientOptions;
import co.elastic.clients.transport.rest5_client.low_level.RequestOptions;
import co.elastic.clients.transport.rest5_client.low_level.Rest5Client;
import tools.jackson.databind.cfg.JsonNodeFeature;
import tools.jackson.databind.json.JsonMapper;
import org.elasticsearch.client.RestClient;
import org.springframework.context.annotation.Bean;
@@ -32,10 +34,6 @@ import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperatio
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* Base class for a @{@link org.springframework.context.annotation.Configuration} class to set up the Elasticsearch
* connection using the {@link ReactiveElasticsearchClient}. This class exposes different parts of the setup as Spring
@@ -127,10 +125,11 @@ public abstract class ReactiveElasticsearchConfiguration extends ElasticsearchCo
// we need to create our own objectMapper that keeps null values in order to provide the storeNullValue
// functionality. The one Elasticsearch would provide removes the nulls. We remove unwanted nulls before they get
// into this mapper, so we can safely keep them here.
var objectMapper = (new ObjectMapper())
.configure(SerializationFeature.INDENT_OUTPUT, false)
.setSerializationInclusion(JsonInclude.Include.ALWAYS);
return new JacksonJsonpMapper(objectMapper);
JsonMapper jsonMapper = JsonMapper.builder()
.enable(JsonNodeFeature.WRITE_NULL_PROPERTIES)
.enable(JsonNodeFeature.READ_NULL_PROPERTIES)
.build();
return new Jackson3JsonpMapper(jsonMapper);
}
/**
@@ -18,31 +18,13 @@ package org.springframework.data.elasticsearch.client.elc;
import static co.elastic.clients.util.ApiTypeHelper.*;
import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*;
import co.elastic.clients.elasticsearch._types.Result;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;
import co.elastic.clients.elasticsearch.core.search.ResponseBody;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.transport.Version;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import org.jspecify.annotations.NonNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import java.time.Duration;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.BulkFailureException;
import org.springframework.data.elasticsearch.NoSuchIndexException;
@@ -53,6 +35,7 @@ import org.springframework.data.elasticsearch.core.AggregationContainer;
import org.springframework.data.elasticsearch.core.IndexedObjectInformation;
import org.springframework.data.elasticsearch.core.MultiGetItem;
import org.springframework.data.elasticsearch.core.ReactiveIndexOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.cluster.ReactiveClusterOperations;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.document.Document;
@@ -69,6 +52,28 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import co.elastic.clients.elasticsearch._types.Result;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;
import co.elastic.clients.elasticsearch.core.search.ResponseBody;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.transport.Version;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
/**
* Implementation of {@link org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations} using the new
* Elasticsearch client.
@@ -76,12 +81,15 @@ import org.springframework.util.StringUtils;
* @author Peter-Josef Meisch
* @author Illia Ulianov
* @author Junghoon Ban
* @author maryantocinn
* @since 4.4
*/
public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearchTemplate {
private static final Log LOGGER = LogFactory.getLog(ReactiveElasticsearchTemplate.class);
@Nullable private ElasticsearchObservationConvention observationConvention;
private final ReactiveElasticsearchClient client;
private final ReactiveElasticsearchSqlClient sqlClient;
private final RequestConverter requestConverter;
@@ -102,7 +110,105 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
exceptionTranslator = new ElasticsearchExceptionTranslator(jsonpMapper);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
super.setApplicationContext(applicationContext);
if (observationRegistry == ObservationRegistry.NOOP) {
applicationContext.getBeanProvider(ObservationRegistry.class).ifAvailable(this::setObservationRegistry);
}
if (observationConvention == null) {
applicationContext.getBeanProvider(ElasticsearchObservationConvention.class)
.ifAvailable(this::setObservationConvention);
}
}
/**
* Set a custom {@link ElasticsearchObservationConvention} to override the default convention.
*
* @param observationConvention can be {@literal null}.
* @since 6.1
*/
public void setObservationConvention(@Nullable ElasticsearchObservationConvention observationConvention) {
this.observationConvention = observationConvention;
}
private <T> Mono<T> observeMono(ElasticsearchOperationName operationName, @Nullable IndexCoordinates index,
Mono<T> mono) {
return Mono.defer(() -> {
Observation observation = createObservation(operationName, index, null);
return mono.doOnError(observation::error) //
.doFinally(signalType -> observation.stop())
.contextWrite(context -> context.put(Observation.class, observation))
.doOnSubscribe(subscription -> observation.start());
});
}
private <T> Flux<T> observeFlux(ElasticsearchOperationName operationName, @Nullable IndexCoordinates index,
Flux<T> flux) {
return Flux.defer(() -> {
Observation observation = createObservation(operationName, index, null);
return flux.doOnError(observation::error) //
.doFinally(signalType -> observation.stop())
.contextWrite(context -> context.put(Observation.class, observation))
.doOnSubscribe(subscription -> observation.start());
});
}
private <T> Mono<T> observeMono(ElasticsearchOperationName operationName, @Nullable IndexCoordinates index,
int batchSize, Mono<T> mono) {
return Mono.defer(() -> {
Observation observation = createObservation(operationName, index, batchSize);
return mono.doOnError(observation::error) //
.doFinally(signalType -> observation.stop())
.contextWrite(context -> context.put(Observation.class, observation))
.doOnSubscribe(subscription -> observation.start());
});
}
private Observation createObservation(ElasticsearchOperationName operationName, @Nullable IndexCoordinates index,
@Nullable Integer batchSize) {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(operationName, index);
context.setBatchSize(batchSize);
return ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.observation(observationConvention,
DefaultElasticsearchObservationConvention.INSTANCE, () -> context, observationRegistry);
}
// region Document operations
@Override
public <T> Mono<T> save(T entity, IndexCoordinates index) {
return observeMono(ElasticsearchOperationName.SAVE, index, super.save(entity, index));
}
@Override
public Mono<Boolean> exists(String id, IndexCoordinates index) {
return observeMono(ElasticsearchOperationName.EXISTS, index, super.exists(id, index));
}
@Override
public Mono<String> delete(Object entity, IndexCoordinates index) {
return observeMono(ElasticsearchOperationName.DELETE, index, super.delete(entity, index));
}
@Override
public Mono<String> delete(String id, IndexCoordinates index) {
return observeMono(ElasticsearchOperationName.DELETE, index, super.delete(id, index));
}
@Override
public <T> Flux<SearchHit<T>> search(Query query, Class<?> entityType, Class<T> resultType, IndexCoordinates index) {
return observeFlux(ElasticsearchOperationName.SEARCH, index, super.search(query, entityType, resultType, index));
}
@Override
public Mono<Long> count(Query query, Class<?> entityType, IndexCoordinates index) {
return observeMono(ElasticsearchOperationName.COUNT, index, super.count(query, entityType, index));
}
@Override
protected <T> Mono<Tuple2<T, IndexResponseMetaData>> doIndex(T entity, IndexCoordinates index) {
@@ -124,7 +230,7 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
Assert.notNull(entitiesPublisher, "entitiesPublisher must not be null!");
return entitiesPublisher //
return observeFlux(ElasticsearchOperationName.BULK, index, entitiesPublisher //
.flatMapMany(entities -> Flux.fromIterable(entities) //
.concatMap(entity -> maybeCallbackBeforeConvert(entity, index)) //
).collectList() //
@@ -146,12 +252,12 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
response.index(), //
response.seqNo(), //
response.primaryTerm(), //
response.version()),
converter,
response.version()), //
converter, //
routingResolver);
return maybeCallbackAfterSave(updatedEntity, index);
});
});
}));
}
@Override
@@ -172,9 +278,11 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
public Mono<ByQueryResponse> delete(DeleteQuery query, Class<?> entityType, IndexCoordinates index) {
Assert.notNull(query, "query must not be null");
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(),
entityType, index, getRefreshPolicy());
return Mono.from(execute(client -> client.deleteByQuery(request))).map(responseConverter::byQueryResponse);
return observeMono(ElasticsearchOperationName.DELETE_BY_QUERY, index, Mono.defer(() -> {
DeleteByQueryRequest request = requestConverter.documentDeleteByQueryRequest(query, routingResolver.getRouting(),
entityType, index, getRefreshPolicy());
return Mono.from(execute(client -> client.deleteByQuery(request))).map(responseConverter::byQueryResponse);
}));
}
@Override
@@ -184,13 +292,15 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
Assert.notNull(entityType, "entityType must not be null");
Assert.notNull(index, "index must not be null");
GetRequest getRequest = requestConverter.documentGetRequest(id, routingResolver.getRouting(), index);
return observeMono(ElasticsearchOperationName.GET, index, Mono.defer(() -> {
GetRequest getRequest = requestConverter.documentGetRequest(id, routingResolver.getRouting(), index);
Mono<GetResponse<EntityAsMap>> getResponse = Mono
.from(execute(client -> client.get(getRequest, EntityAsMap.class)));
Mono<GetResponse<EntityAsMap>> getResponse = Mono //
.from(execute(client -> client.get(getRequest, EntityAsMap.class)));
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(converter, entityType, index);
return getResponse.flatMap(response -> callback.toEntity(DocumentAdapters.from(response)));
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(converter, entityType, index);
return getResponse.flatMap(response -> callback.toEntity(DocumentAdapters.from(response)));
}));
}
@Override
@@ -227,13 +337,15 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
Assert.notNull(updateQuery, "UpdateQuery must not be null");
Assert.notNull(index, "Index must not be null");
UpdateRequest<Document, ?> request = requestConverter.documentUpdateRequest(updateQuery, index, getRefreshPolicy(),
routingResolver.getRouting());
return observeMono(ElasticsearchOperationName.UPDATE, index, Mono.defer(() -> {
UpdateRequest<Document, ?> request = requestConverter.documentUpdateRequest(updateQuery, index,
getRefreshPolicy(), routingResolver.getRouting());
return Mono.from(execute(client -> client.update(request, Document.class))).flatMap(response -> {
UpdateResponse.Result result = result(response.result());
return result == null ? Mono.empty() : Mono.just(UpdateResponse.of(result));
});
return Mono.from(execute(client -> client.update(request, Document.class))).flatMap(response -> {
UpdateResponse.Result result = result(response.result());
return result == null ? Mono.empty() : Mono.just(UpdateResponse.of(result));
});
}));
}
@Override
@@ -248,7 +360,8 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
Assert.notNull(bulkOptions, "BulkOptions must not be null");
Assert.notNull(index, "Index must not be null");
return doBulkOperation(queries, bulkOptions, index).then();
return observeMono(ElasticsearchOperationName.BULK, index, queries.size(),
doBulkOperation(queries, bulkOptions, index).then());
}
private Flux<BulkResponseItem> doBulkOperation(List<?> queries, BulkOptions bulkOptions, IndexCoordinates index) {
@@ -311,22 +424,24 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
Assert.notNull(query, "query must not be null");
Assert.notNull(clazz, "clazz must not be null");
MgetRequest request = requestConverter.documentMgetRequest(query, clazz, index);
return observeFlux(ElasticsearchOperationName.MULTI_GET, index, Flux.defer(() -> {
MgetRequest request = requestConverter.documentMgetRequest(query, clazz, index);
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(converter, clazz, index);
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(converter, clazz, index);
Publisher<MgetResponse<EntityAsMap>> response = execute(client -> client.mget(request, EntityAsMap.class));
Publisher<MgetResponse<EntityAsMap>> response = execute(client -> client.mget(request, EntityAsMap.class));
return Mono.from(response)//
.flatMapMany(it -> Flux.fromIterable(DocumentAdapters.from(it))) //
.flatMap(multiGetItem -> {
if (multiGetItem.isFailed()) {
return Mono.just(MultiGetItem.of(null, multiGetItem.getFailure()));
} else {
return callback.toEntity(multiGetItem.getItem()) //
.map(t -> MultiGetItem.of(t, multiGetItem.getFailure()));
}
});
return Mono.from(response)//
.flatMapMany(it -> Flux.fromIterable(DocumentAdapters.from(it))) //
.flatMap(multiGetItem -> {
if (multiGetItem.isFailed()) {
return Mono.just(MultiGetItem.of(null, multiGetItem.getFailure()));
} else {
return callback.toEntity(multiGetItem.getItem()) //
.map(t -> MultiGetItem.of(t, multiGetItem.getFailure()));
}
});
}));
}
// endregion
@@ -336,6 +451,14 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch
return new ReactiveElasticsearchTemplate(client, converter);
}
@Override
protected void customizeCopy(AbstractReactiveElasticsearchTemplate copy) {
if (copy instanceof ReactiveElasticsearchTemplate reactiveTemplate) {
reactiveTemplate.observationConvention = this.observationConvention;
}
}
// region search operations
@Override
@@ -900,8 +900,17 @@ class RequestConverter extends AbstractQueryProcessor {
}
SourceFilter sourceFilter = source.getSourceFilter();
if (sourceFilter != null && sourceFilter.getIncludes() != null) {
s.sourceFields(Arrays.asList(sourceFilter.getIncludes()));
if (sourceFilter != null && (sourceFilter.getIncludes() != null || sourceFilter.getExcludes() != null)) {
s.sourceFields(cfg -> cfg
.filter(f -> {
if (sourceFilter.getIncludes() != null) {
f.includes(Arrays.asList(sourceFilter.getIncludes()));
}
if (sourceFilter.getExcludes() != null) {
f.excludes(Arrays.asList(sourceFilter.getExcludes()));
}
return f;
}));
}
return s;
}) //
@@ -1426,6 +1435,7 @@ class RequestConverter extends AbstractQueryProcessor {
.searchType(searchType) //
.timeout(timeStringMs(query.getTimeout())) //
.requestCache(query.getRequestCache()) //
.includeNamedQueriesScore(query.getIncludeNamedQueriesScore()) //
;
var pointInTime = query.getPointInTime();
@@ -15,6 +15,8 @@
*/
package org.springframework.data.elasticsearch.core;
import io.micrometer.observation.ObservationRegistry;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
@@ -43,7 +45,6 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersiste
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.BulkOptions;
import org.springframework.data.elasticsearch.core.query.ByQueryResponse;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder;
import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery;
@@ -85,6 +86,7 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
@Nullable protected EntityCallbacks entityCallbacks;
@Nullable protected RefreshPolicy refreshPolicy;
protected RoutingResolver routingResolver;
protected ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
public AbstractElasticsearchTemplate() {
this(null);
@@ -117,6 +119,8 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
copy.setRoutingResolver(routingResolver);
copy.setRefreshPolicy(refreshPolicy);
copy.setObservationRegistry(observationRegistry);
customizeCopy(copy);
return copy;
}
@@ -171,6 +175,27 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper
return refreshPolicy;
}
/**
* Set the {@link ObservationRegistry} to use for recording observations.
*
* @param observationRegistry must not be {@literal null}.
* @since 6.1
*/
public void setObservationRegistry(ObservationRegistry observationRegistry) {
Assert.notNull(observationRegistry, "observationRegistry must not be null");
this.observationRegistry = observationRegistry;
}
/**
* Hook for subclasses to copy additional state during {@link #copy()}. Called after all common fields have been
* copied. The default implementation does nothing.
*
* @param copy the new template instance to customize
*/
protected void customizeCopy(AbstractElasticsearchTemplate copy) {}
/**
* logs the versions of the different Elasticsearch components.
*
@@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.core;
import io.micrometer.observation.ObservationRegistry;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
@@ -77,6 +78,7 @@ abstract public class AbstractReactiveElasticsearchTemplate
protected RoutingResolver routingResolver;
protected @Nullable ReactiveEntityCallbacks entityCallbacks;
protected ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
// region Initialization
protected AbstractReactiveElasticsearchTemplate(@Nullable ElasticsearchConverter converter) {
@@ -109,6 +111,8 @@ abstract public class AbstractReactiveElasticsearchTemplate
}
copy.setRoutingResolver(routingResolver);
copy.setObservationRegistry(observationRegistry);
customizeCopy(copy);
return copy;
}
@@ -162,6 +166,27 @@ abstract public class AbstractReactiveElasticsearchTemplate
this.entityCallbacks = entityCallbacks;
}
/**
* Set the {@link ObservationRegistry} to use for recording observations.
*
* @param observationRegistry must not be {@literal null}.
* @since 6.1
*/
public void setObservationRegistry(ObservationRegistry observationRegistry) {
Assert.notNull(observationRegistry, "observationRegistry must not be null");
this.observationRegistry = observationRegistry;
}
/**
* Hook for subclasses to copy additional state during {@link #copy()}. Called after all common fields have been
* copied. The default implementation does nothing.
*
* @param copy the new template instance to customize
*/
protected void customizeCopy(AbstractReactiveElasticsearchTemplate copy) {}
/**
* logs the versions of the different Elasticsearch components.
*
@@ -259,8 +284,7 @@ abstract public class AbstractReactiveElasticsearchTemplate
sink.tryEmitComplete();
}
})
.subscribe(v -> {
}, error -> {
.subscribe(v -> {}, error -> {
if (subscription != null) {
subscription.cancel();
}
@@ -15,7 +15,8 @@
*/
package org.springframework.data.elasticsearch.core.document;
import java.io.IOException;
import tools.jackson.core.JacksonException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
@@ -87,7 +88,7 @@ public interface Document extends StringObjectMap<Document> {
clear();
try {
putAll(MapDocument.OBJECT_MAPPER.readerFor(Map.class).readValue(json));
} catch (IOException e) {
} catch (JacksonException e) {
throw new ConversionException("Cannot parse JSON", e);
}
return this;
@@ -15,6 +15,9 @@
*/
package org.springframework.data.elasticsearch.core.document;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -25,9 +28,6 @@ import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.support.DefaultStringObjectMap;
import org.springframework.data.mapping.MappingException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* {@link Document} implementation backed by a {@link LinkedHashMap}.
*
@@ -344,7 +344,7 @@ class MapDocument implements Document {
public String toJson() {
try {
return OBJECT_MAPPER.writeValueAsString(this);
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
throw new MappingException("Cannot render document to JSON", e);
}
}
@@ -1,26 +1,25 @@
package org.springframework.data.elasticsearch.core.geo;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.core.JsonParser;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.ValueSerializer;
import org.springframework.data.geo.Point;
class PointSerializer extends JsonSerializer<Point> {
class PointSerializer extends ValueSerializer<Point> {
@Override
public void serialize(Point value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeObject(GeoPoint.fromPoint(value));
public void serialize(Point value, JsonGenerator gen, SerializationContext serializers) throws JacksonException {
gen.writePOJO(GeoPoint.fromPoint(value));
}
}
class PointDeserializer extends JsonDeserializer<Point> {
class PointDeserializer extends ValueDeserializer<Point> {
@Override
public Point deserialize(JsonParser p, DeserializationContext context) throws IOException {
public Point deserialize(JsonParser p, DeserializationContext context) throws JacksonException {
return GeoPoint.toPoint(p.readValueAs(GeoPoint.class));
}
}
@@ -15,14 +15,14 @@
*/
package org.springframework.data.elasticsearch.core.index;
import tools.jackson.databind.node.ObjectNode;
import java.io.IOException;
import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.annotations.GeoShapeField;
import org.springframework.util.Assert;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* @author Peter-Josef Meisch
*/
@@ -18,6 +18,13 @@ package org.springframework.data.elasticsearch.core.index;
import static org.springframework.data.elasticsearch.core.index.MappingParameters.*;
import static org.springframework.util.StringUtils.*;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.node.StringNode;
import tools.jackson.databind.util.RawValue;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.nio.charset.Charset;
@@ -31,7 +38,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.annotation.Transient;
import org.springframework.data.core.TypeInformation;
@@ -48,13 +54,6 @@ import org.springframework.data.mapping.PropertyHandler;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.fasterxml.jackson.databind.util.RawValue;
/**
* @author Rizwan Idrees
* @author Mohsin Husen
@@ -112,9 +111,15 @@ public class MappingBuilder {
protected final ElasticsearchConverter elasticsearchConverter;
private final ObjectMapper objectMapper = new ObjectMapper();
private final MappingParametersCustomizer customizer;
public MappingBuilder(ElasticsearchConverter elasticsearchConverter) {
this(elasticsearchConverter, MappingParameters::from);
}
public MappingBuilder(ElasticsearchConverter elasticsearchConverter, MappingParametersCustomizer customizer) {
this.elasticsearchConverter = elasticsearchConverter;
this.customizer = customizer;
}
/**
@@ -184,7 +189,7 @@ public class MappingBuilder {
if (!excludeFromSource.isEmpty()) {
ObjectNode sourceNode = objectNode.putObject(SOURCE);
ArrayNode excludes = sourceNode.putArray(SOURCE_EXCLUDES);
excludeFromSource.stream().map(TextNode::new).forEach(excludes::add);
excludeFromSource.stream().map(StringNode::new).forEach(excludes::add);
}
return objectMapper.writer().writeValueAsString(objectNode);
@@ -238,7 +243,7 @@ public class MappingBuilder {
if (mappingAnnotation.dynamicDateFormats().length > 0) {
objectNode.putArray(DYNAMIC_DATE_FORMATS).addAll(Arrays.stream(mappingAnnotation.dynamicDateFormats())
.map(TextNode::valueOf).collect(Collectors.toList()));
.map(StringNode::valueOf).collect(Collectors.toList()));
}
if (runtimeFields != null) {
@@ -546,7 +551,7 @@ public class MappingBuilder {
if (children.length > 1) {
relationsNode.putArray(parent)
.addAll(Arrays.stream(children).map(TextNode::valueOf).collect(Collectors.toList()));
.addAll(Arrays.stream(children).map(StringNode::valueOf).collect(Collectors.toList()));
} else if (children.length == 1) {
relationsNode.put(parent, children[0]);
}
@@ -589,7 +594,7 @@ public class MappingBuilder {
private void addFieldMappingParameters(ObjectNode fieldNode, Annotation annotation, boolean nestedOrObjectField)
throws IOException {
MappingParameters mappingParameters = MappingParameters.from(annotation);
MappingParameters mappingParameters = customizer.from(annotation);
if (!nestedOrObjectField && mappingParameters.isStore()) {
fieldNode.put(FIELD_PARAM_STORE, true);
@@ -15,6 +15,9 @@
*/
package org.springframework.data.elasticsearch.core.index;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.node.StringNode;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
@@ -28,13 +31,12 @@ import org.springframework.data.elasticsearch.annotations.*;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
/**
* A class to hold the mapping parameters that might be set on
* {@link org.springframework.data.elasticsearch.annotations.Field } or
* {@link org.springframework.data.elasticsearch.annotations.InnerField} annotation.
* {@link org.springframework.data.elasticsearch.annotations.InnerField} annotation. The class allows extensibility
* (non-final) to simplify mapping parameters customization, provided by
* {@link org.springframework.data.elasticsearch.core.index.MappingParametersCustomizer}.
*
* @author Peter-Josef Meisch
* @author Aleksei Arsenev
@@ -42,9 +44,10 @@ import com.fasterxml.jackson.databind.node.TextNode;
* @author Morgan Lutz
* @author Sascha Woo
* @author Haibo Liu
* @author Andriy Redko
* @since 4.0
*/
public final class MappingParameters {
public class MappingParameters {
static final String FIELD_PARAM_COERCE = "coerce";
static final String FIELD_PARAM_COPY_TO = "copy_to";
@@ -137,7 +140,7 @@ public final class MappingParameters {
}
}
private MappingParameters(Field field) {
protected MappingParameters(Field field) {
index = field.index();
store = field.store();
fielddata = field.fielddata();
@@ -184,7 +187,7 @@ public final class MappingParameters {
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
}
private MappingParameters(InnerField field) {
protected MappingParameters(InnerField field) {
index = field.index();
store = field.store();
fielddata = field.fielddata();
@@ -285,7 +288,7 @@ public final class MappingParameters {
if (copyTo != null && copyTo.length > 0) {
objectNode.putArray(FIELD_PARAM_COPY_TO)
.addAll(Arrays.stream(copyTo).map(TextNode::valueOf).collect(Collectors.toList()));
.addAll(Arrays.stream(copyTo).map(StringNode::valueOf).collect(Collectors.toList()));
}
if (ignoreAbove != null) {
@@ -417,4 +420,132 @@ public final class MappingParameters {
objectNode.put(FIELD_PARAM_EAGER_GLOBAL_ORDINALS, eagerGlobalOrdinals);
}
}
protected String analyzer() {
return analyzer;
}
protected boolean coerce() {
return coerce;
}
protected String[] copyTo() {
return copyTo;
}
protected DateFormat[] dateFormats() {
return dateFormats;
}
protected String[] dateFormatPatterns() {
return dateFormatPatterns;
}
protected boolean hasDocValues() {
return docValues;
}
protected boolean hasEagerGlobalOrdinals() {
return eagerGlobalOrdinals;
}
protected boolean isEnabled() {
return enabled;
}
protected boolean hasFielddata() {
return fielddata;
}
protected Integer ignoreAbove() {
return ignoreAbove;
}
protected boolean isIgnoreMalformed() {
return ignoreMalformed;
}
protected boolean isIndex() {
return index;
}
protected IndexOptions indexOptions() {
return indexOptions;
}
protected boolean isIndexPhrases() {
return indexPhrases;
}
protected IndexPrefixes indexPrefixes() {
return indexPrefixes;
}
protected String normalizer() {
return normalizer;
}
protected boolean hasNorms() {
return norms;
}
protected Integer maxShingleSize() {
return maxShingleSize;
}
protected String nullValue() {
return nullValue;
}
protected NullValueType nullValueType() {
return nullValueType;
}
protected Integer positionIncrementGap() {
return positionIncrementGap;
}
protected boolean positiveScoreImpact() {
return positiveScoreImpact;
}
protected Integer dims() {
return dims;
}
protected String elementType() {
return elementType;
}
protected KnnSimilarity knnSimilarity() {
return knnSimilarity;
}
protected KnnIndexOptions knnIndexOptions() {
return knnIndexOptions;
}
protected String searchAnalyzer() {
return searchAnalyzer;
}
protected double scalingFactor() {
return scalingFactor;
}
protected String similarity() {
return similarity;
}
protected TermVector termVector() {
return termVector;
}
protected FieldType type() {
return type;
}
protected String mappedTypeName() {
return mappedTypeName;
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.index;
import java.lang.annotation.Annotation;
import org.jspecify.annotations.NonNull;
/**
* Allows to customize {@link org.springframework.data.elasticsearch.core.index.MappingParameters} that are being
* emitted for each supported annotation. Needed by dependent projects like Spring-Data-Opensearch.
*
* @author Andriy Redko
* @since 6.1.0
*/
public interface MappingParametersCustomizer {
/**
* Customize @link org.springframework.data.elasticsearch.core.index.MappingParameters} for each supported annotation.
*
* @param annotation supported annotation
* @return customized @link org.springframework.data.elasticsearch.core.index.MappingParameters}
*/
@NonNull
MappingParameters from(@NonNull Annotation annotation);
}
@@ -31,6 +31,7 @@ import org.springframework.data.mapping.MappingException;
* Subclass of {@link MappingBuilder} with specialized methods To inhibit blocking calls
*
* @author Peter-Josef Meisch
* @author Andriy Redko
* @since 4.3
*/
public class ReactiveMappingBuilder extends MappingBuilder {
@@ -39,6 +40,10 @@ public class ReactiveMappingBuilder extends MappingBuilder {
super(elasticsearchConverter);
}
public ReactiveMappingBuilder(ElasticsearchConverter elasticsearchConverter, MappingParametersCustomizer customizer) {
super(elasticsearchConverter, customizer);
}
@Override
public String buildPropertyMapping(Class<?> clazz) throws MappingException {
throw new UnsupportedOperationException(
@@ -81,6 +81,7 @@ public class BaseQuery implements Query {
protected List<IdWithRouting> idsWithRouting = Collections.emptyList();
protected List<RuntimeField> runtimeFields = new ArrayList<>();
@Nullable protected PointInTime pointInTime;
@Nullable protected Boolean includeNamedQueriesScore;
private boolean queryIsUpdatedByConverter = false;
@Nullable private Integer reactiveBatchSize = null;
@Nullable private Boolean allowNoIndices = null;
@@ -123,6 +124,7 @@ public class BaseQuery implements Query {
this.docValueFields = builder.getDocValueFields();
this.scriptedFields = builder.getScriptedFields();
this.runtimeFields = builder.getRuntimeFields();
this.includeNamedQueriesScore = builder.getIncludeNamedQueriesScore();
}
/**
@@ -455,6 +457,14 @@ public class BaseQuery implements Query {
this.requestCache = value;
}
/**
* @since 6.1
*/
@Override
public void setIncludeNamedQueriesScore(@Nullable Boolean value) {
this.includeNamedQueriesScore = value;
}
@Override
@Nullable
public Boolean getRequestCache() {
@@ -480,6 +490,14 @@ public class BaseQuery implements Query {
return indicesBoost;
}
/**
* @since 6.1
*/
@Override
public @Nullable Boolean getIncludeNamedQueriesScore() {
return this.includeNamedQueriesScore;
}
/**
* @since 5.0
*/
@@ -71,6 +71,7 @@ public abstract class BaseQueryBuilder<Q extends BaseQuery, SELF extends BaseQue
@Nullable Integer reactiveBatchSize;
private final List<DocValueField> docValueFields = new ArrayList<>();
private final List<ScriptedField> scriptedFields = new ArrayList<>();
@Nullable private Boolean includeNamedQueryScore;
@Nullable
public Sort getSort() {
@@ -177,6 +178,14 @@ public abstract class BaseQueryBuilder<Q extends BaseQuery, SELF extends BaseQue
return requestCache;
}
/**
* @since 6.1
*/
@Nullable
public Boolean getIncludeNamedQueriesScore(){
return includeNamedQueryScore;
}
public List<Query.IdWithRouting> getIdsWithRouting() {
return idsWithRouting;
}
@@ -381,6 +390,14 @@ public abstract class BaseQueryBuilder<Q extends BaseQuery, SELF extends BaseQue
return self();
}
/**
* @since 6.1
*/
public SELF withIncludeNamedQueryScore (@Nullable Boolean namedQueryScore) {
this.includeNamedQueryScore = namedQueryScore;
return self();
}
/**
* Set Ids with routing values for a multi-get request run with this query. Not used in any other searches.
*
@@ -491,6 +491,21 @@ public interface Query {
*/
public Integer getRequestSize();
/**
* Sets the include_named_queries_score value for the query. If true, the response includes the score contribution
* from any named queries.
*
* @param value new value
* @since 6.1
*/
void setIncludeNamedQueriesScore(@Nullable Boolean value);
/**
* @return the include_named_queries_score value for this query.
*/
@Nullable
Boolean getIncludeNamedQueriesScore();
/**
* @since 4.3
*/
@@ -90,7 +90,8 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository
Query query = createQuery(parameters);
IndexCoordinates index = elasticsearchOperations.getIndexCoordinatesFor(clazz);
IndexCoordinates index = parameterAccessor
.getIndexCoordinates(elasticsearchOperations.getIndexCoordinatesFor(clazz));
Object result = null;
@@ -110,7 +110,7 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor
evaluationContextProvider);
String indexName = queryMethod.getEntityInformation().getIndexName();
IndexCoordinates index = IndexCoordinates.of(indexName);
IndexCoordinates index = parameterAccessor.getIndexCoordinates(IndexCoordinates.of(indexName));
ReactiveElasticsearchQueryExecution execution = getExecution(parameterAccessor,
new ResultProcessingConverter(processor));
@@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.repository.query;
import org.springframework.core.MethodParameter;
import org.springframework.data.core.TypeInformation;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.RuntimeField;
import org.springframework.data.elasticsearch.core.query.ScriptedField;
import org.springframework.data.repository.query.Parameter;
@@ -42,7 +43,8 @@ public class ElasticsearchParameter extends Parameter {
@Override
public boolean isSpecialParameter() {
return super.isSpecialParameter() || isScriptedFieldParameter() || isRuntimeFieldParameter();
return super.isSpecialParameter() || isScriptedFieldParameter() || isRuntimeFieldParameter()
|| isIndexCoordinatesParameter();
}
public Boolean isScriptedFieldParameter() {
@@ -52,4 +54,8 @@ public class ElasticsearchParameter extends Parameter {
public Boolean isRuntimeFieldParameter() {
return RuntimeField.class.isAssignableFrom(getType());
}
public Boolean isIndexCoordinatesParameter() {
return IndexCoordinates.class.isAssignableFrom(getType());
}
}
@@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.repository.query.ParameterAccessor;
/**
@@ -29,4 +30,11 @@ public interface ElasticsearchParameterAccessor extends ParameterAccessor {
* @return
*/
Object[] getValues();
/**
* If there is a parameter of type IndexCoordinates, this parameter value is returned, otherwise the defaults value
*
* @param defaults default value
*/
IndexCoordinates getIndexCoordinates(IndexCoordinates defaults);
}
@@ -29,10 +29,11 @@ import org.springframework.data.repository.query.ParametersSource;
* @since 3.2
*/
public class ElasticsearchParameters extends Parameters<ElasticsearchParameters, ElasticsearchParameter> {
private final List<ElasticsearchParameter> scriptedFields = new ArrayList<>();
private final List<ElasticsearchParameter> runtimeFields = new ArrayList<>();
private final int indexCoordinatesIndex;
public ElasticsearchParameters(ParametersSource parametersSource) {
super(parametersSource,
@@ -53,6 +54,23 @@ public class ElasticsearchParameters extends Parameters<ElasticsearchParameters,
runtimeFields.add(parameter);
}
}
this.indexCoordinatesIndex = initIndexCoordinatesIndex();
}
private int initIndexCoordinatesIndex() {
int indexCoordinatesIndex = -1;
int index = 0;
for (ElasticsearchParameter parameter : this) {
if (parameter.isIndexCoordinatesParameter()) {
if (indexCoordinatesIndex != -1) {
throw new IllegalArgumentException(this + " can only contain at most one IndexCoordinates parameter.");
} else {
indexCoordinatesIndex = index;
}
}
index++;
}
return indexCoordinatesIndex;
}
private ElasticsearchParameter parameterFactory(MethodParameter methodParameter, TypeInformation<?> domainType) {
@@ -61,6 +79,7 @@ public class ElasticsearchParameters extends Parameters<ElasticsearchParameters,
private ElasticsearchParameters(List<ElasticsearchParameter> parameters) {
super(parameters);
this.indexCoordinatesIndex = initIndexCoordinatesIndex();
}
@Override
@@ -75,4 +94,12 @@ public class ElasticsearchParameters extends Parameters<ElasticsearchParameters,
List<ElasticsearchParameter> getRuntimeFields() {
return runtimeFields;
}
public boolean hasIndexCoordinatesParameter() {
return this.indexCoordinatesIndex != -1;
}
public int getIndexCoordinatesIndex() {
return indexCoordinatesIndex;
}
}
@@ -15,6 +15,7 @@
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.repository.query.ParametersParameterAccessor;
/**
@@ -25,6 +26,7 @@ public class ElasticsearchParametersParameterAccessor extends ParametersParamete
implements ElasticsearchParameterAccessor {
private final Object[] values;
private final ElasticsearchParameters eleasticSearchParameters;
/**
* Creates a new {@link ElasticsearchParametersParameterAccessor}.
@@ -36,10 +38,19 @@ public class ElasticsearchParametersParameterAccessor extends ParametersParamete
super(method.getParameters(), values);
this.values = values;
this.eleasticSearchParameters = method.getParameters();
}
@Override
public Object[] getValues() {
return values;
}
@Override
public IndexCoordinates getIndexCoordinates(IndexCoordinates defaults) {
if (!eleasticSearchParameters.hasIndexCoordinatesParameter()) {
return defaults;
}
return (IndexCoordinates) getValues()[eleasticSearchParameters.getIndexCoordinatesIndex()];
}
}
@@ -1,39 +0,0 @@
/*
* Copyright 2013-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.repository.query.ValueExpressionDelegate;
/**
* ElasticsearchPartQuery
*
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Kevin Leturc
* @author Mark Paluch
* @author Rasmus Faber-Espensen
* @author Peter-Josef Meisch
* @author Haibo Liu
* @deprecated since 5.5, use {@link RepositoryPartQuery} instead
*/
@Deprecated(forRemoval = true)
public class ElasticsearchPartQuery extends RepositoryPartQuery {
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations,
ValueExpressionDelegate valueExpressionDelegate) {
super(method, elasticsearchOperations, valueExpressionDelegate);
}
}
@@ -373,6 +373,11 @@ public class ElasticsearchQueryMethod extends QueryMethod {
return fieldNames.toArray(new String[0]);
}
@Override
public ElasticsearchParameters getParameters() {
return (ElasticsearchParameters) super.getParameters();
}
// region Copied from QueryMethod base class
/*
* Copied from the QueryMethod class adding support for collections of SearchHit instances. No static method here.
@@ -1,38 +0,0 @@
/*
* Copyright 2013-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.repository.query.ValueExpressionDelegate;
/**
* ElasticsearchStringQuery
*
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Mark Paluch
* @author Taylor Ono
* @author Peter-Josef Meisch
* @author Haibo Liu
* @deprecated since 5.5, use {@link RepositoryStringQuery}
*/
@Deprecated(since = "5.5", forRemoval = true)
public class ElasticsearchStringQuery extends RepositoryStringQuery {
public ElasticsearchStringQuery(ElasticsearchQueryMethod queryMethod, ElasticsearchOperations elasticsearchOperations,
String queryString, ValueExpressionDelegate valueExpressionDelegate) {
super(queryMethod, elasticsearchOperations, queryString, valueExpressionDelegate);
}
}
@@ -136,11 +136,6 @@ public class ReactiveElasticsearchQueryMethod extends ElasticsearchQueryMethod {
return true;
}
@Override
public ElasticsearchParameters getParameters() {
return (ElasticsearchParameters) super.getParameters();
}
@Override
protected boolean isAllowedGenericType(ParameterizedType methodGenericReturnType) {
return super.isAllowedGenericType(methodGenericReturnType)
@@ -1,40 +0,0 @@
/*
* Copyright 2019-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.repository.query;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.repository.query.ValueExpressionDelegate;
/**
* @author Christoph Strobl
* @author Taylor Ono
* @author Haibo Liu
* @since 3.2
* @deprecated since 5.5, use {@link ReactiveRepositoryStringQuery}
*/
@Deprecated(since = "5.5", forRemoval = true)
public class ReactiveElasticsearchStringQuery extends ReactiveRepositoryStringQuery {
public ReactiveElasticsearchStringQuery(ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, ValueExpressionDelegate valueExpressionDelegate) {
super(queryMethod, operations, valueExpressionDelegate);
}
public ReactiveElasticsearchStringQuery(String query, ReactiveElasticsearchQueryMethod queryMethod,
ReactiveElasticsearchOperations operations, ValueExpressionDelegate valueExpressionDelegate) {
super(query, queryMethod, operations, valueExpressionDelegate);
}
}
@@ -15,7 +15,9 @@
*/
package org.springframework.data.elasticsearch.support;
import java.io.IOException;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -25,9 +27,6 @@ import java.util.function.BiConsumer;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Peter-Josef Meisch
*/
@@ -48,7 +47,7 @@ public class DefaultStringObjectMap<T extends StringObjectMap<T>> implements Str
public String toJson() {
try {
return OBJECT_MAPPER.writeValueAsString(this);
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
throw new IllegalArgumentException("Cannot render document to JSON", e);
}
}
@@ -61,7 +60,7 @@ public class DefaultStringObjectMap<T extends StringObjectMap<T>> implements Str
delegate.clear();
try {
delegate.putAll(OBJECT_MAPPER.readerFor(Map.class).readValue(json));
} catch (IOException e) {
} catch (JacksonException e) {
throw new IllegalArgumentException("Cannot parse JSON", e);
}
return (T) this;
+1 -3
View File
@@ -1,4 +1,4 @@
Spring Data Elasticsearch 6.0.6 (2025.1.6)
Spring Data Elasticsearch 6.1 RC1 (2026.0.0)
Copyright (c) [2013-2022] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").
@@ -19,5 +19,3 @@ conditions of the subcomponent's license, as noted in the LICENSE file.
@@ -0,0 +1,202 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.client.elc;
import static org.assertj.core.api.Assertions.*;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import io.micrometer.observation.Observation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
/**
* Unit tests for {@link DefaultElasticsearchObservationConvention}.
*
* @author maryantocinn
*/
class DefaultElasticsearchObservationConventionTests {
private final DefaultElasticsearchObservationConvention convention = DefaultElasticsearchObservationConvention.INSTANCE;
@Test
@DisplayName("should return observation name matching ObservationDocumentation")
void shouldReturnCorrectName() {
assertThat(convention.getName()).isEqualTo("spring.data.elasticsearch.command");
}
@Test
@DisplayName("should support ElasticsearchObservationContext")
void shouldSupportElasticsearchContext() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
IndexCoordinates.of("my-index"));
assertThat(convention.supportsContext(context)).isTrue();
}
@Test
@DisplayName("should not support unrelated context")
void shouldNotSupportUnrelatedContext() {
Observation.Context otherContext = new Observation.Context();
assertThat(convention.supportsContext(otherContext)).isFalse();
}
// region contextual name
@Test
@DisplayName("contextual name should be 'operation index' when index is present")
void contextualNameWithIndex() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
IndexCoordinates.of("products"));
assertThat(convention.getContextualName(context)).isEqualTo("search products");
}
@Test
@DisplayName("contextual name should be just the operation when index is null")
void contextualNameWithoutIndex() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
null);
assertThat(convention.getContextualName(context)).isEqualTo("search");
}
@Test
@DisplayName("contextual name should include comma-joined indices for multi-index operations")
void contextualNameWithMultipleIndices() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
IndexCoordinates.of("index-a", "index-b"));
assertThat(convention.getContextualName(context)).isEqualTo("search index-a,index-b");
}
// endregion
// region low cardinality key values
@Test
@DisplayName("should always include spring.data.operation")
void shouldIncludeRequiredKeyValues() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.GET, null);
KeyValues keyValues = convention.getLowCardinalityKeyValues(context);
assertThat(keyValues).contains(KeyValue.of("spring.data.operation", "get"));
}
@Test
@DisplayName("should include spring.data.collection when index is present")
void shouldIncludeCollectionWhenIndexPresent() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
IndexCoordinates.of("products"));
KeyValues keyValues = convention.getLowCardinalityKeyValues(context);
assertThat(keyValues).contains(KeyValue.of("spring.data.collection", "products"));
}
@Test
@DisplayName("should not include spring.data.collection when index is null")
void shouldNotIncludeCollectionWhenNull() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
null);
KeyValues keyValues = convention.getLowCardinalityKeyValues(context);
assertThat(keyValues.stream().map(KeyValue::getKey)).doesNotContain("spring.data.collection");
}
@Test
@DisplayName("should include spring.data.batch.size as high cardinality when batch size is set")
void shouldIncludeBatchSizeAsHighCardinality() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.BULK,
IndexCoordinates.of("products"));
context.setBatchSize(5);
KeyValues highCardValues = convention.getHighCardinalityKeyValues(context);
assertThat(highCardValues).contains(KeyValue.of("spring.data.batch.size", "5"));
}
@Test
@DisplayName("should not include spring.data.batch.size in low cardinality key values")
void shouldNotIncludeBatchSizeInLowCardinality() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.BULK,
IndexCoordinates.of("products"));
context.setBatchSize(5);
KeyValues keyValues = convention.getLowCardinalityKeyValues(context);
assertThat(keyValues.stream().map(KeyValue::getKey)).doesNotContain("spring.data.batch.size");
}
@Test
@DisplayName("should return empty high cardinality key values when batch size is null")
void shouldReturnEmptyHighCardinalityWhenNoBatchSize() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
IndexCoordinates.of("products"));
KeyValues highCardValues = convention.getHighCardinalityKeyValues(context);
assertThat(highCardValues.stream().map(KeyValue::getKey)).doesNotContain("spring.data.batch.size");
}
@Test
@DisplayName("should produce correct key values for a full bulk operation")
void shouldProduceCorrectKeyValuesForBulk() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.BULK,
IndexCoordinates.of("orders"));
context.setBatchSize(100);
KeyValues lowCardValues = convention.getLowCardinalityKeyValues(context);
KeyValues highCardValues = convention.getHighCardinalityKeyValues(context);
assertThat(lowCardValues).contains(KeyValue.of("spring.data.operation", "bulk"),
KeyValue.of("spring.data.collection", "orders"));
assertThat(highCardValues).contains(KeyValue.of("spring.data.batch.size", "100"));
}
@Test
@DisplayName("should produce correct key values for each operation name")
void shouldProduceCorrectOperationNames() {
for (ElasticsearchOperationName operationName : ElasticsearchOperationName.values()) {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(operationName,
IndexCoordinates.of("test-index"));
KeyValues keyValues = convention.getLowCardinalityKeyValues(context);
assertThat(keyValues).contains(KeyValue.of("spring.data.operation", operationName.getValue()));
}
}
// endregion
}
@@ -0,0 +1,91 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.client.elc;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
/**
* Unit tests for {@link ElasticsearchObservationContext}.
*
* @author maryantocinn
*/
class ElasticsearchObservationContextTests {
@Test
@DisplayName("should carry operation name and index coordinates")
void shouldCarryOperationNameAndIndex() {
IndexCoordinates index = IndexCoordinates.of("my-index");
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
index);
assertThat(context.getOperationName()).isEqualTo(ElasticsearchOperationName.SEARCH);
assertThat(context.getIndexCoordinates()).isEqualTo(index);
assertThat(context.getIndexName()).isEqualTo("my-index");
}
@Test
@DisplayName("should return null index name when index coordinates are null")
void shouldReturnNullIndexNameWhenNull() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
null);
assertThat(context.getIndexCoordinates()).isNull();
assertThat(context.getIndexName()).isNull();
}
@Test
@DisplayName("should join multiple index names with comma")
void shouldJoinMultipleIndexNames() {
IndexCoordinates index = IndexCoordinates.of("index-a", "index-b", "index-c");
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
index);
assertThat(context.getIndexName()).isEqualTo("index-a,index-b,index-c");
}
@Test
@DisplayName("should store and retrieve batch size")
void shouldStoreAndRetrieveBatchSize() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.BULK,
IndexCoordinates.of("my-index"));
assertThat(context.getBatchSize()).isNull();
context.setBatchSize(42);
assertThat(context.getBatchSize()).isEqualTo(42);
}
@Test
@DisplayName("should allow null batch size")
void shouldAllowNullBatchSize() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.BULK,
IndexCoordinates.of("my-index"));
context.setBatchSize(10);
context.setBatchSize(null);
assertThat(context.getBatchSize()).isNull();
}
}
@@ -0,0 +1,207 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.client.elc;
import static org.assertj.core.api.Assertions.*;
import io.micrometer.common.docs.KeyName;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
/**
* Tests for {@link ElasticsearchObservation} and the end-to-end observation lifecycle.
*
* @author maryantocinn
*/
class ElasticsearchObservationDocumentationTests {
@Test
@DisplayName("observation name should be 'spring.data.elasticsearch.command'")
void shouldHaveCorrectObservationName() {
assertThat(ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.getName()).isEqualTo(
"spring.data.elasticsearch.command");
}
@Test
@DisplayName("default convention should be DefaultElasticsearchObservationConvention")
void shouldHaveCorrectDefaultConvention() {
assertThat(ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.getDefaultConvention()).isEqualTo(
DefaultElasticsearchObservationConvention.class);
}
@Test
@DisplayName("should declare all expected low cardinality key names")
void shouldDeclareExpectedLowCardinalityKeyNames() {
KeyName[] keyNames = ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.getLowCardinalityKeyNames();
List<String> keyStrings = Arrays.stream(keyNames).map(KeyName::asString).collect(Collectors.toList());
assertThat(keyStrings).containsExactlyInAnyOrder("spring.data.operation", "spring.data.collection");
}
@Test
@DisplayName("should declare spring.data.batch.size as high cardinality key name")
void shouldDeclareHighCardinalityKeyNames() {
KeyName[] keyNames = ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.getHighCardinalityKeyNames();
List<String> keyStrings = Arrays.stream(keyNames).map(KeyName::asString).collect(Collectors.toList());
assertThat(keyStrings).containsExactly("spring.data.batch.size");
}
@Test
@DisplayName("should record observation with correct key values using TestObservationRegistry")
void shouldRecordObservationWithKeyValues() {
TestObservationRegistry registry = TestObservationRegistry.create();
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
IndexCoordinates.of("products"));
Observation observation = ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.observation(null,
DefaultElasticsearchObservationConvention.INSTANCE, () -> context, registry);
observation.start();
observation.stop();
TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyRemainingCurrentObservation()
.hasObservationWithNameEqualTo("spring.data.elasticsearch.command").that()
.hasLowCardinalityKeyValue("spring.data.operation", "search")
.hasLowCardinalityKeyValue("spring.data.collection", "products").hasContextualNameEqualTo("search products");
}
@Test
@DisplayName("should record observation without index when null")
void shouldRecordObservationWithoutIndex() {
TestObservationRegistry registry = TestObservationRegistry.create();
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.DELETE,
null);
Observation observation = ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.observation(null,
DefaultElasticsearchObservationConvention.INSTANCE, () -> context, registry);
observation.start();
observation.stop();
TestObservationRegistryAssert.assertThat(registry)
.hasObservationWithNameEqualTo("spring.data.elasticsearch.command").that()
.hasLowCardinalityKeyValue("spring.data.operation", "delete")
.doesNotHaveLowCardinalityKeyValue("spring.data.collection", "products").hasContextualNameEqualTo("delete");
}
@Test
@DisplayName("should record observation with batch size for bulk operations")
void shouldRecordObservationWithBatchSize() {
TestObservationRegistry registry = TestObservationRegistry.create();
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.BULK,
IndexCoordinates.of("logs"));
context.setBatchSize(25);
Observation observation = ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.observation(null,
DefaultElasticsearchObservationConvention.INSTANCE, () -> context, registry);
observation.start();
observation.stop();
TestObservationRegistryAssert.assertThat(registry)
.hasObservationWithNameEqualTo("spring.data.elasticsearch.command").that()
.hasLowCardinalityKeyValue("spring.data.operation", "bulk")
.hasHighCardinalityKeyValue("spring.data.batch.size", "25").hasContextualNameEqualTo("bulk logs");
}
@Test
@DisplayName("should record error on observation")
void shouldRecordErrorOnObservation() {
TestObservationRegistry registry = TestObservationRegistry.create();
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
IndexCoordinates.of("products"));
Observation observation = ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.observation(null,
DefaultElasticsearchObservationConvention.INSTANCE, () -> context, registry);
RuntimeException error = new RuntimeException("connection refused");
observation.start();
observation.error(error);
observation.stop();
TestObservationRegistryAssert.assertThat(registry)
.hasObservationWithNameEqualTo("spring.data.elasticsearch.command").that()
.hasLowCardinalityKeyValue("spring.data.operation", "search").hasBeenStopped();
}
@Test
@DisplayName("NOOP registry should not record any observations")
void noopRegistryShouldNotRecord() {
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
IndexCoordinates.of("products"));
Observation observation = ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.observation(null,
DefaultElasticsearchObservationConvention.INSTANCE, () -> context, ObservationRegistry.NOOP);
observation.start();
observation.stop();
assertThat(observation.isNoop()).isTrue();
}
@Test
@DisplayName("custom convention should override default key values")
void customConventionShouldOverride() {
TestObservationRegistry registry = TestObservationRegistry.create();
ElasticsearchObservationConvention customConvention = new ElasticsearchObservationConvention() {
@Override
public String getName() {
return "custom.elasticsearch.command";
}
@Override
public String getContextualName(ElasticsearchObservationContext context) {
return "custom " + context.getOperationName().getValue();
}
};
ElasticsearchObservationContext context = new ElasticsearchObservationContext(ElasticsearchOperationName.SEARCH,
IndexCoordinates.of("products"));
Observation observation = ElasticsearchObservation.ELASTICSEARCH_COMMAND_OBSERVATION.observation(customConvention,
DefaultElasticsearchObservationConvention.INSTANCE, () -> context, registry);
observation.start();
observation.stop();
TestObservationRegistryAssert.assertThat(registry).hasObservationWithNameEqualTo("custom.elasticsearch.command")
.that().hasContextualNameEqualTo("custom search");
}
}
@@ -0,0 +1,166 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.client.elc;
import static org.assertj.core.api.Assertions.*;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration;
/**
* Integration tests verifying that Micrometer observations are recorded for Spring Data Elasticsearch template
* operations when a {@link TestObservationRegistry} is wired into the application context.
*
* @author maryantocinn
* @since 6.1
*/
@SpringIntegrationTest
@ContextConfiguration(classes = { ObservabilityIntegrationTests.Config.class })
@DisplayName("Observability Integration Tests")
class ObservabilityIntegrationTests {
@Configuration
@Import({ ElasticsearchTemplateConfiguration.class })
static class Config {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("observability-it");
}
@Bean
TestObservationRegistry observationRegistry() {
return TestObservationRegistry.create();
}
}
@Autowired private ElasticsearchOperations operations;
@Autowired private IndexNameProvider indexNameProvider;
@Autowired private TestObservationRegistry observationRegistry;
@BeforeEach
void setUp() {
indexNameProvider.increment();
operations.indexOps(SampleEntity.class).createWithMapping();
observationRegistry.clear();
}
@Test
@Order(Integer.MAX_VALUE)
void cleanup() {
operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete();
}
@Test
@DisplayName("should record observation for save operation")
void shouldRecordObservationForSave() {
SampleEntity entity = new SampleEntity();
entity.setId("1");
entity.setMessage("hello");
operations.save(entity);
TestObservationRegistryAssert.assertThat(observationRegistry)
.hasObservationWithNameEqualTo("spring.data.elasticsearch.command")
.that()
.hasLowCardinalityKeyValue("spring.data.operation", "save")
.hasBeenStopped();
}
@Test
@DisplayName("should record observation for search operation")
void shouldRecordObservationForSearch() {
SampleEntity entity = new SampleEntity();
entity.setId("1");
entity.setMessage("hello");
operations.save(entity);
observationRegistry.clear();
SearchHits<SampleEntity> hits = operations.search(operations.matchAllQuery(), SampleEntity.class);
assertThat(hits.getTotalHits()).isGreaterThanOrEqualTo(1);
TestObservationRegistryAssert.assertThat(observationRegistry)
.hasObservationWithNameEqualTo("spring.data.elasticsearch.command")
.that()
.hasLowCardinalityKeyValue("spring.data.operation", "search")
.hasBeenStopped();
}
@Test
@DisplayName("should record observation with collection name")
void shouldRecordObservationWithCollectionName() {
SampleEntity entity = new SampleEntity();
entity.setId("1");
entity.setMessage("hello");
operations.save(entity);
TestObservationRegistryAssert.assertThat(observationRegistry)
.hasObservationWithNameEqualTo("spring.data.elasticsearch.command")
.that()
.hasLowCardinalityKeyValue("spring.data.collection", indexNameProvider.indexName());
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class SampleEntity {
@Nullable
@Id private String id;
@Nullable
@Field(type = FieldType.Text) private String message;
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
@Nullable
public String getMessage() {
return message;
}
public void setMessage(@Nullable String message) {
this.message = message;
}
}
}
@@ -19,11 +19,13 @@ import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationS
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.index.MappingBuilder;
import org.springframework.data.elasticsearch.core.index.MappingParametersCustomizer;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.util.Lazy;
/**
* @author Peter-Josef Meisch
* @author Andriy Redko
*/
public abstract class MappingContextBaseTests {
@@ -42,6 +44,10 @@ public abstract class MappingContextBaseTests {
return mappingContext;
}
final protected MappingBuilder getMappingBuilder(MappingParametersCustomizer customizer) {
return new MappingBuilder(elasticsearchConverter.get(), customizer);
}
final protected MappingBuilder getMappingBuilder() {
return new MappingBuilder(elasticsearchConverter.get());
}
@@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
@@ -49,6 +50,8 @@ import org.springframework.data.geo.Point;
import org.springframework.data.geo.Polygon;
import org.springframework.data.mapping.MappingException;
import tools.jackson.databind.node.ObjectNode;
/**
* @author Stuart Stevenson
* @author Jakub Vavrik
@@ -1339,6 +1342,35 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
assertEquals(expected, result, true);
}
@Test // #1700
@DisplayName("should allow mapping parameters tion")
void shouldAllowMappingParametersCustomization() throws JSONException {
String expected = """
{
"properties": {
"my_vector": {
"type": "dense_vector",
"dimensions": 16
}
}
}
""";
final MappingParametersCustomizer customizer = annotation -> new MappingParameters((Field) annotation) {
@Override
public void writeTypeAndParametersTo(ObjectNode objectNode) throws IOException {
if (type() == FieldType.Dense_Vector) {
objectNode.put(FIELD_PARAM_TYPE, mappedTypeName());
objectNode.put("dimensions", dims());
}
}
};
String mapping = getMappingBuilder(customizer)
.buildPropertyMapping(DenseVectorEntity.class);
assertEquals(expected, mapping, false);
}
// region entities
@Document(indexName = "ignore-above-index")
@@ -19,9 +19,11 @@ import static org.assertj.core.api.Assertions.*;
import co.elastic.clients.elasticsearch._types.GeoHashPrecision;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregation;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery;
import java.util.List;
import org.assertj.core.data.Offset;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@@ -172,6 +174,68 @@ public abstract class NativeQueryIntegrationTests {
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo(entity.getId());
}
@Test // #3248
@DisplayName("should be able to use includeNamedQueriesScore in a NativeQuery")
void shouldBeAbleToUseIncludeQueriesScoreInANativeQuery() {
var entity = new SampleEntity();
entity.setId("7");
entity.setText("seven");
operations.save(entity);
entity = new SampleEntity();
entity.setId("42");
entity.setText("matched");
operations.save(entity);
var matchQuery = MatchQuery.of(m -> m.field("text")
.query("matched")
.queryName("namedQuery"));
var nativeQuery = NativeQuery.builder()
.withQuery(matchQuery._toQuery())
.withIncludeNamedQueryScore(true)
.build();
var searchHits = operations.search(nativeQuery, SampleEntity.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1);
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo(entity.getId());
assertThat(searchHits.getSearchHit(0).getMatchedQueries()).containsKey("namedQuery");
assertThat(searchHits.getSearchHit(0).getMatchedQueries().get("namedQuery")).isGreaterThan(0.0);
assertThat(searchHits.getSearchHit(0).getMatchedQueries().get("namedQuery"))
.isCloseTo(searchHits.getMaxScore(), Offset.offset(0.01));
}
@Test // #3248
@DisplayName("should not be able to use named queries score in a NativeQuery when disabled")
void shouldNotBeAbleToUseNamedQueriesScoreInANativeQueryWhenDisabled() {
var entity = new SampleEntity();
entity.setId("7");
entity.setText("seven");
operations.save(entity);
entity = new SampleEntity();
entity.setId("42");
entity.setText("matched");
operations.save(entity);
var matchQuery = MatchQuery.of(m -> m.field("text")
.query("matched")
.queryName("namedQuery"));
var nativeQuery = NativeQuery.builder()
.withQuery(matchQuery._toQuery())
.withIncludeNamedQueryScore(false)
.build();
var searchHits = operations.search(nativeQuery, SampleEntity.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1);
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo(entity.getId());
assertThat(searchHits.getSearchHit(0).getMatchedQueries()).containsKey("namedQuery");
assertThat(searchHits.getSearchHit(0).getMatchedQueries().get("namedQuery")).isNull();
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class SampleEntity {
@@ -0,0 +1,23 @@
package org.springframework.data.elasticsearch.repository.query.indexcoordinates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration(classes = { IndexCoordinatesParameterELCIntegrationTests.Config.class })
public class IndexCoordinatesParameterELCIntegrationTests extends IndexCoordinatesParameterIntegrationTests {
@Configuration
@Import({ ElasticsearchTemplateConfiguration.class })
@EnableElasticsearchRepositories(considerNestedRepositories = true)
static class Config {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("query-index-coordinates");
}
}
}
@@ -0,0 +1,98 @@
package org.springframework.data.elasticsearch.repository.query.indexcoordinates;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
@SpringIntegrationTest
abstract class IndexCoordinatesParameterIntegrationTests {
@Autowired ElasticsearchOperations operations;
@Autowired IndexNameProvider indexNameProvider;
@Autowired RecordRepository recordRepository;
@BeforeEach
public void before() {
indexNameProvider.increment();
}
@Test
@Order(Integer.MAX_VALUE)
void cleanup() {
operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete();
}
@Test // #2506
@DisplayName("should use indexcoordinates passes as repository query argument")
void shouldUseIndexCoordinatesPassesAsRepositoryQueryArgument() {
var record1 = new Record("1", "one");
var indexName1 = indexNameProvider.indexName();
var indexCoordinates1 = IndexCoordinates.of(indexName1);
operations.save(record1, indexCoordinates1);
var record2 = new Record("2", "two");
var indexName2 = indexName1 + "second";
var indexCoordinates2 = IndexCoordinates.of(indexName2);
operations.save(record2, indexCoordinates2);
// search for record1
var searchHits = recordRepository.findByText("one");
assert searchHits.getTotalHits() == 1;
searchHits = recordRepository.findByText("one", indexCoordinates2);
assert searchHits.getTotalHits() == 0;
// search for record2
searchHits = recordRepository.findByText("two");
assert searchHits.getTotalHits() == 0;
searchHits = recordRepository.findByText("two", indexCoordinates2);
assert searchHits.getTotalHits() == 1;
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class Record {
@Nullable
@Id private String id;
@Nullable
@Field(type = FieldType.Keyword) private String text;
public Record(@Nullable String id, @Nullable String text) {
this.id = id;
this.text = text;
}
public @Nullable String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
public @Nullable String getText() {
return text;
}
public void setText(@Nullable String text) {
this.text = text;
}
}
interface RecordRepository extends ElasticsearchRepository<Record, String> {
SearchHits<Record> findByText(String text);
SearchHits<Record> findByText(String text, IndexCoordinates index);
}
}
@@ -0,0 +1,24 @@
package org.springframework.data.elasticsearch.repository.query.indexcoordinates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration(classes = { ReactiveIndexCoordinatesParameterELCIntegrationTests.Config.class })
public class ReactiveIndexCoordinatesParameterELCIntegrationTests
extends ReactiveIndexCoordinatesParameterIntegrationTests {
@Configuration
@Import({ ReactiveElasticsearchTemplateConfiguration.class })
@EnableReactiveElasticsearchRepositories(considerNestedRepositories = true)
static class Config {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("reactive-query-index-coordinates");
}
}
}
@@ -0,0 +1,109 @@
package org.springframework.data.elasticsearch.repository.query.indexcoordinates;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
@SpringIntegrationTest
abstract class ReactiveIndexCoordinatesParameterIntegrationTests {
@Autowired ReactiveElasticsearchOperations operations;
@Autowired IndexNameProvider indexNameProvider;
@Autowired RecordRepository recordRepository;
@BeforeEach
public void before() {
indexNameProvider.increment();
}
@Test
@Order(Integer.MAX_VALUE)
void cleanup() {
operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete().block();
}
@Test // #2506
@DisplayName("should use indexcoordinates passes as repository query argument")
void shouldUseIndexCoordinatesPassesAsRepositoryQueryArgument() {
var record1 = new Record("1", "one");
var indexName1 = indexNameProvider.indexName();
var indexCoordinates1 = IndexCoordinates.of(indexName1);
operations.save(record1, indexCoordinates1).block();
var record2 = new Record("2", "two");
var indexName2 = indexName1 + "second";
var indexCoordinates2 = IndexCoordinates.of(indexName2);
operations.save(record2, indexCoordinates2).block();
// search for record1
recordRepository.findByText("one")
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
recordRepository.findByText("one", indexCoordinates2)
.as(StepVerifier::create)
.expectNextCount(0)
.verifyComplete();
// search for record2
recordRepository.findByText("two")
.as(StepVerifier::create)
.expectNextCount(0)
.verifyComplete();
recordRepository.findByText("two", indexCoordinates2)
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class Record {
@Nullable
@Id private String id;
@Nullable
@Field(type = FieldType.Keyword) private String text;
public Record(@Nullable String id, @Nullable String text) {
this.id = id;
this.text = text;
}
public @Nullable String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
public @Nullable String getText() {
return text;
}
public void setText(@Nullable String text) {
this.text = text;
}
}
interface RecordRepository extends ReactiveElasticsearchRepository<Record, String> {
Flux<SearchHit<Record>> findByText(String text);
Flux<SearchHit<Record>> findByText(String text, IndexCoordinates index);
}
}
@@ -15,7 +15,7 @@
#
#
sde.testcontainers.image-name=docker.elastic.co/elasticsearch/elasticsearch
sde.testcontainers.image-version=9.2.8
sde.testcontainers.image-version=9.3.3
#
#
# needed as we do a DELETE /* at the end of the tests, will be required from 8.0 on, produces a warning since 7.13